Expose similar interface to computeWithDatabase on web (#3672)

This commit is contained in:
Cunibon
2025-10-23 23:34:13 +02:00
committed by GitHub
parent 0f7f493279
commit d4ca33c6e9
6 changed files with 167 additions and 70 deletions
-69
View File
@@ -266,75 +266,6 @@ extension ComputeWithDriftIsolate<DB extends DatabaseConnectionUser> on DB {
);
}
}
/// Spawns a short-lived isolate to run the [computation] with a drift
/// database.
///
/// Essentially, this is a variant of [Isolate.run] for computations that also
/// need to share a drift database between them. As drift databases are
/// stateful objects, they can't be send across isolates (and thus used in
/// [Isolate.run] or Flutter's `compute`) without special setup.
///
/// This method will extract the underlying database connection of `this`
/// database into a form that can be serialized across isolates. Then,
/// [Isolate.run] will be called to invoke [computation]. The [connect]
/// function is responsible for creating an instance of your database class
/// from the low-level connection.
///
/// As an example, consider a database class:
///
/// ```dart
/// class MyDatabase extends $MyDatabase {
/// MyDatabase(QueryExecutor executor): super(executor);
/// }
/// ```
///
/// [computeWithDatabase] can then be used to access an instance of
/// `MyDatabase` in a new isolate, even though `MyDatabase` is not generally
/// sharable between isolates:
///
/// ```dart
/// Future<void> loadBulkData(MyDatabase db) async {
/// await db.computeWithDatabase(
/// connect: MyDatabase.new,
/// computation: (db) async {
/// // This computation has access to a second `db` that is internally
/// // linked to the original database.
/// final data = await fetchRowsFromNetwork();
/// await db.batch((batch) {
/// // More expensive work like inserting data
/// });
/// },
/// );
/// }
/// ```
///
/// Note that with the recommended setup of `NativeDatabase.createInBackground`,
/// drift will already use an isolate to run your SQL statements. Using
/// [computeWithDatabase] is beneficial when an an expensive work unit needs
/// to use the database, or when creating the SQL statements itself is
/// expensive.
/// In particular, note that [computeWithDatabase] does not create a second
/// database connection to sqlite3 - the current one is re-used. So if you're
/// using a synchronous database connection, using this method is unlikely to
/// take significant loads off the main isolate. For that reason, the use of
/// `NativeDatabase.createInBackground` is encouraged.
@experimental
Future<Ret> computeWithDatabase<Ret>({
required FutureOr<Ret> Function(DB) computation,
required DB Function(DatabaseConnection) connect,
}) async {
final connection = await serializableConnection();
return await Isolate.run(() async {
final database = connect(await connection.connect());
try {
return await computation(database);
} finally {
await database.close();
}
});
}
}
/// Creates a [RunningDriftServer] and sends a [SendPort] that can be used to
@@ -0,0 +1,25 @@
import 'dart:async';
import 'dart:isolate';
import 'package:drift/isolate.dart';
import 'package:drift/src/runtime/api/runtime_api.dart';
/// Spawns a short-lived isolate to run the [computation] with a drift
/// database.
Future<Ret>
computeWithDatabaseImplementation<Ret, DB extends GeneratedDatabase>({
required FutureOr<Ret> Function(DB) computation,
required DB Function(DatabaseConnection) connect,
required DB database,
}) async {
final connection = await database.serializableConnection();
return await Isolate.run(() async {
final database = connect(await connection.connect());
try {
return await computation(database);
} finally {
await database.close();
}
});
}
@@ -0,0 +1,13 @@
import 'dart:async';
import 'package:drift/src/runtime/api/runtime_api.dart';
/// This will directly execute the passed computation as a future with the
/// current Database.
Future<Ret>
computeWithDatabaseImplementation<Ret, DB extends GeneratedDatabase>({
required FutureOr<Ret> Function(DB) computation,
required DB Function(DatabaseConnection) connect,
required DB database,
}) async =>
await computation(database);
+60
View File
@@ -151,4 +151,64 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
return true;
}());
}
/// On native platforms this spawns a short-lived isolate to run the [computation] with a drift
/// database.
/// On web platforms, this will run the [computation] on the current JavaScript context.
///
/// Essentially, this is a variant of [Isolate.run] for computations that also
/// need to share a drift database between them. As drift databases are
/// stateful objects, they can't be send across isolates (and thus used in
/// [Isolate.run] or Flutter's `compute`) without special setup.
///
/// This method will extract the underlying database connection of `this`
/// database into a form that can be serialized across isolates. Then,
/// [Isolate.run] will be called to invoke [computation]. The [connect]
/// function is responsible for creating an instance of your database class
/// from the low-level connection.
///
/// As an example, consider a database class:
///
/// ```dart
/// class MyDatabase extends $MyDatabase {
/// MyDatabase(QueryExecutor executor): super(executor);
/// }
/// ```
///
/// [computeWithDatabase] can then be used to access an instance of
/// `MyDatabase` in a new isolate, even though `MyDatabase` is not generally
/// sharable between isolates:
///
/// ```dart
/// Future<void> loadBulkData(MyDatabase db) async {
/// await db.computeWithDatabase(
/// connect: MyDatabase.new,
/// computation: (db) async {
/// // This computation has access to a second `db` that is internally
/// // linked to the original database.
/// final data = await fetchRowsFromNetwork();
/// await db.batch((batch) {
/// // More expensive work like inserting data
/// });
/// },
/// );
/// }
/// ```
///
/// Note that with the recommended setup of `NativeDatabase.createInBackground`,
/// drift will already use an isolate to run your SQL statements. Using
/// [computeWithDatabase] is beneficial when an an expensive work unit needs
/// to use the database, or when creating the SQL statements itself is
/// expensive.
/// In particular, note that [computeWithDatabase] does not create a second
/// database connection to sqlite3 - the current one is re-used. So if you're
/// using a synchronous database connection, using this method is unlikely to
/// take significant loads off the main isolate. For that reason, the use of
/// `NativeDatabase.createInBackground` is encouraged.
Future<Ret> computeWithDatabase<Ret, DB extends GeneratedDatabase>({
required FutureOr<Ret> Function(DB) computation,
required DB Function(DatabaseConnection) connect,
}) =>
computeWithDatabaseImplementation(
computation: computation, connect: connect, database: this as DB);
}
@@ -8,6 +8,8 @@ import 'package:meta/meta.dart';
import '../devtools/devtools.dart' as devtools;
import '../query_builder/query_builder.dart';
import 'compute_with_database_implementation/compute_with_database_native.dart'
if (dart.library.js_interop) 'compute_with_database_implementation/compute_with_database_unsupported.dart';
part 'batch.dart';
part 'connection.dart';
+67 -1
View File
@@ -315,7 +315,7 @@ class _FakeMigrator_26 extends _i1.SmartFake implements _i2.Migrator {
);
}
class _FakeFuture_27<T1> extends _i1.SmartFake implements _i5.Future<T1> {
class _FakeFuture_27<T> extends _i1.SmartFake implements _i5.Future<T> {
_FakeFuture_27(
Object parent,
Invocation parentInvocation,
@@ -965,6 +965,72 @@ class MockTodoDb extends _i1.Mock implements _i3.TodoDb {
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override
_i5.Future<Ret> computeWithDatabase<Ret, DB extends _i2.GeneratedDatabase>({
required _i5.FutureOr<Ret> Function(DB)? computation,
required DB Function(_i2.DatabaseConnection)? connect,
}) =>
(super.noSuchMethod(
Invocation.method(
#computeWithDatabase,
[],
{
#computation: computation,
#connect: connect,
},
),
returnValue: _i6.ifNotNull(
_i6.dummyValueOrNull<Ret>(
this,
Invocation.method(
#computeWithDatabase,
[],
{
#computation: computation,
#connect: connect,
},
),
),
(Ret v) => _i5.Future<Ret>.value(v),
) ??
_FakeFuture_27<Ret>(
this,
Invocation.method(
#computeWithDatabase,
[],
{
#computation: computation,
#connect: connect,
},
),
),
returnValueForMissingStub: _i6.ifNotNull(
_i6.dummyValueOrNull<Ret>(
this,
Invocation.method(
#computeWithDatabase,
[],
{
#computation: computation,
#connect: connect,
},
),
),
(Ret v) => _i5.Future<Ret>.value(v),
) ??
_FakeFuture_27<Ret>(
this,
Invocation.method(
#computeWithDatabase,
[],
{
#computation: computation,
#connect: connect,
},
),
),
) as _i5.Future<Ret>);
@override
_i5.Stream<T> createStream<T extends Object>(
_i4.QueryStreamFetcher<T>? stmt) =>