/// Contains utils to run drift databases in a background isolate. /// /// Please note that this API is not supported on the web. library; import 'dart:async'; import 'dart:isolate'; import 'package:stream_channel/stream_channel.dart'; import 'drift.dart'; import 'remote.dart'; import 'src/isolate.dart'; import 'src/remote/protocol.dart'; export 'remote.dart' show DriftRemoteException; /// Signature of a function that opens a database connection. typedef DatabaseOpener = QueryExecutor Function(); /// Defines utilities to run drift in a background isolate. In the operation /// mode created by these utilities, there's a single background isolate doing /// all the work. Any other isolate can use the [connect] method to obtain an /// instance of a [GeneratedDatabase] class that will delegate its work onto a /// background isolate. Auto-updating queries, and transactions work across /// isolates, and the user facing api is exactly the same. /// /// Please note that, while running drift in a background isolate can reduce /// lags in foreground isolates (thus removing UI jank), the overall database /// performance will be worse. This is because result data is not available /// directly and instead needs to be copied from the database isolate. Thanks /// to recent improvements like isolate groups in the Dart VM, this overhead is /// fairly small and using isolates to run drift queries is recommended where /// possible. /// /// The easiest way to use drift isolates is to use /// `NativeDatabase.createInBackground`, which is a drop-in replacement for /// `NativeDatabase` that uses a [DriftIsolate] under the hood. /// /// Also, be aware that this api is not available on the web. /// /// See also: /// - [Isolate], for general information on multi threading in Dart. /// - The [detailed documentation](https://drift.simonbinder.eu/docs/advanced-features/isolates), /// which provides example codes on how to use this api. class DriftIsolate { /// The underlying port used to establish a connection with this /// [DriftIsolate]. /// /// This [SendPort] can safely be sent over isolates. The receiving isolate /// can reconstruct a [DriftIsolate] by using [DriftIsolate.fromConnectPort]. final SendPort connectPort; /// The flag indicating whether messages between this [DriftIsolate] /// and the [DriftServer] should be serialized. /// /// When null, drift will try to send a test message to infer whether the /// connection to [connectPort] requires serialization. final bool? serialize; /// Creates a [DriftIsolate] talking to another isolate by using the /// [connectPort]. /// /// {@template drift_isolate_serialize} /// Internally, drift uses ports from `dart:isolate` to send commands to an /// internal server dispatching database actions. /// In most setups, those ports can send and receive almost any Dart object. /// In special cases though, the platform only supports sending simple types /// across send types. In particular, isolates across different Flutter /// engines (such as the ones spawned by the `workmanager` package) are /// unable to handle most objects. /// To support these setups, drift can serialize the objects sent to the /// background isolates into simple lists. This is adds considerable overhead, /// but is necessary to make two independent isolates talk to each other. /// /// The [serialize] parameter can be used to explicitly enable or disable this /// behavior. By default, drift will attempt to send a test message to infer /// whether serialization is necessary or not. /// {@endtemplate} DriftIsolate.fromConnectPort(this.connectPort, {this.serialize}); Future<(StreamChannel, bool)> _open(Duration? timeout) { return connectToServer(connectPort, serialize, timeout); } /// Connects to this [DriftIsolate] from another isolate. /// /// All operations on the returned [DatabaseConnection] will be executed on a /// background isolate. /// /// When [singleClientMode] is enabled (it defaults to `false`), drift assumes /// that the isolate will only be connected to once. In this mode, drift will /// shutdown the remote isolate once the returned [DatabaseConnection] is /// closed. /// Also, stream queries are more efficient when this mode is enables since we /// don't have to synchronize table updates to other clients (since there are /// none). /// /// Setting the [isolateDebugLog] is only helpful when debugging drift itself. /// It will print messages exchanged between the two isolates. Future connect({ bool isolateDebugLog = false, bool singleClientMode = false, Duration? connectTimeout, }) async { final (channel, serialize) = await _open(connectTimeout); final connection = await connectToRemoteAndInitialize( channel, debugLog: isolateDebugLog, serialize: serialize, singleClientMode: singleClientMode, ); return DatabaseConnection(connection.executor, streamQueries: connection.streamQueries, connectionData: this); } /// Stops the background isolate and disconnects all [DatabaseConnection]s /// created. /// If you only want to disconnect a database connection created via /// [connect], use [GeneratedDatabase.close] instead. Future shutdownAll() async { final (channel, serialize) = await _open(null); return await shutdown(channel, serialize: serialize); } /// Creates a new [DriftIsolate] on a background thread. /// /// The [opener] function will be used to open the [DatabaseConnection] used /// by the isolate. /// /// To close the isolate later, use [shutdownAll]. Or, if you know that only /// a single client will connect, set `singleClientMode: true` in [connect]. /// That way, the drift isolate will shutdown when the client is closed. /// /// The optional [isolateSpawn] parameter can be used to make drift use /// something else instead of [Isolate.spawn] to spawn the isolate. This may /// be useful if you want to set additional options on the isolate or /// otherwise need a reference to it. /// /// {@macro drift_isolate_serialize} static Future spawn( DatabaseOpener opener, { bool serialize = false, Future Function(void Function(T), T) isolateSpawn = Isolate.spawn, }) async { final receiveServer = ReceivePort('drift isolate connect'); final keyFuture = receiveServer.first; await isolateSpawn(_startDriftIsolate, [receiveServer.sendPort, opener]); final key = await keyFuture as SendPort; return DriftIsolate.fromConnectPort(key, serialize: serialize); } /// Creates a [DriftIsolate] in the [Isolate.current] isolate. The returned /// [DriftIsolate] is an object than can be sent across isolates - any other /// isolate can then use [DriftIsolate.connect] to obtain a special database /// connection which operations are all executed on this isolate. /// /// When [killIsolateWhenDone] is enabled (it defaults to `false`) and /// [shutdownAll] is called on the returned [DriftIsolate], the isolate used /// to call [DriftIsolate.inCurrent] will be killed. /// /// {@macro drift_isolate_serialize} factory DriftIsolate.inCurrent( DatabaseOpener opener, { bool killIsolateWhenDone = false, bool serialize = false, bool shutdownAfterLastDisconnect = false, ReceivePort? port, void Function()? beforeShutdown, }) { final server = RunningDriftServer( Isolate.current, opener(), killIsolateWhenDone: killIsolateWhenDone, port: port, beforeShutdown: beforeShutdown, shutDownAfterLastDisconnect: shutdownAfterLastDisconnect, ); return DriftIsolate.fromConnectPort( server.portToOpenConnection, serialize: serialize, ); } } /// Experimental methods to connect to an existing drift database from different /// isolates. extension ComputeWithDriftIsolate on DB { /// Creates a [DriftIsolate] that, when connected to, will run queries on the /// database already opened by `this`. /// /// This can be used to share existing database across isolates, as instances /// of generated database classes can't be sent across isolates by default. A /// [DriftIsolate] can be sent over ports though, which enables a concise way /// to open a temporary isolate that is using an existing database: /// /// ```dart /// Future main() async { /// final database = MyDatabase(...); /// /// // This is illegal - MyDatabase is not serializable /// await Isolate.run(() async { /// await database.batch(...); /// }); /// /// // This will work. Only the `connection` is sent to the new isolate. By /// // creating a new database instance based on the connection, the same /// // logical database can be shared across isolates. /// final connection = await database.serializableConnection(); /// await Isolate.run(() async { /// final database = MyDatabase(await connection.connect()); /// await database.batch(...); /// }); /// } /// ``` /// /// The example of running a short-lived database for a single task unit /// requiring a database is also available through [computeWithDatabase]. Future serializableConnection() async { final currentlyInRootConnection = resolvedEngine is GeneratedDatabase; // ignore: invalid_use_of_protected_member final localConnection = resolvedEngine.connection; final data = await localConnection.connectionData; // If we're connected to an isolate already, we can use that one directly // instead of starting a short-lived drift server. // However, this does not work if [serializableConnection] is called in a // transaction zone, since the top-level connection could be blocked waiting // for the transaction (as transactions can't be concurrent in sqlite3). if (data is DriftIsolate && currentlyInRootConnection) { return data; } else { // Set up a drift server acting as a proxy to the existing database // connection. final server = RunningDriftServer( Isolate.current, localConnection, onlyAcceptSingleConnection: true, closeConnectionAfterShutdown: false, killIsolateWhenDone: false, ); // Since the existing database didn't use an isolate server, we need to // manually forward stream query updates. final forwardToServer = tableUpdates().listen((localUpdates) { server.server.dispatchTableUpdateNotification( NotifyTablesUpdated(localUpdates.toList())); }); final forwardToLocal = server.server.tableUpdateNotifications.listen((remoteUpdates) { notifyUpdates(remoteUpdates.updates.toSet()); }); server.server.done.whenComplete(() { forwardToServer.cancel(); forwardToLocal.cancel(); }); return DriftIsolate.fromConnectPort( server.portToOpenConnection, serialize: false, ); } } } /// Creates a [RunningDriftServer] and sends a [SendPort] that can be used to /// establish connections. /// /// Te [args] list must contain two elements. The first one is the [SendPort] /// that [_startDriftIsolate] will use to send the new [SendPort] used to /// establish further connections. The second element is a [DatabaseOpener] /// used to open the underlying database connection. void _startDriftIsolate(List args) { final sendPort = args[0] as SendPort; final opener = args[1] as DatabaseOpener; final server = RunningDriftServer(Isolate.current, opener()); sendPort.send(server.portToOpenConnection); }