From 480315c1be75364ba04d1985fb6749657daafc22 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 29 Apr 2024 12:37:12 +0100 Subject: [PATCH] Fixed issue #150, by better supporting background/multiple `FlutterEngine`s --- .../impls/objectbox/backend/backend.dart | 7 ++++ .../impls/objectbox/backend/internal.dart | 36 ++++++++++--------- .../internal_workers/standard/worker.dart | 24 +++++++------ .../backend/internal_workers/thread_safe.dart | 8 ++--- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/lib/src/backend/impls/objectbox/backend/backend.dart b/lib/src/backend/impls/objectbox/backend/backend.dart index 6fe31a25..fc70cc64 100644 --- a/lib/src/backend/impls/objectbox/backend/backend.dart +++ b/lib/src/backend/impls/objectbox/backend/backend.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; @@ -44,12 +45,17 @@ final class FMTCObjectBoxBackend implements FMTCBackend { /// [the ObjectBox docs](https://docs.objectbox.io/getting-started) for /// details. /// + /// [rootIsolateToken] should only be used in exceptional circumstances where + /// this backend is being initialised in a seperate isolate (or background) + /// thread. + /// /// Avoid using [useInMemoryDatabase] outside of testing purposes. @override Future initialise({ String? rootDirectory, int maxDatabaseSize = 10000000, String? macosApplicationGroup, + RootIsolateToken? rootIsolateToken, @visibleForTesting bool useInMemoryDatabase = false, }) => FMTCObjectBoxBackendInternal._instance.initialise( @@ -57,6 +63,7 @@ final class FMTCObjectBoxBackend implements FMTCBackend { maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, useInMemoryDatabase: useInMemoryDatabase, + rootIsolateToken: rootIsolateToken, ); /// {@macro fmtc.backend.uninitialise} diff --git a/lib/src/backend/impls/objectbox/backend/internal.dart b/lib/src/backend/impls/objectbox/backend/internal.dart index 9cdcac45..154e928b 100644 --- a/lib/src/backend/impls/objectbox/backend/internal.dart +++ b/lib/src/backend/impls/objectbox/backend/internal.dart @@ -118,9 +118,19 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { required int maxDatabaseSize, required String? macosApplicationGroup, required bool useInMemoryDatabase, + required RootIsolateToken? rootIsolateToken, }) async { if (_sendPort != null) throw RootAlreadyInitialised(); + // Obtain the `RootIsolateToken` to enable the worker isolate to use + // ObjectBox + rootIsolateToken ??= ServicesBinding.rootIsolateToken ?? + (throw StateError( + 'Unable to start FMTC in a background thread without access to a ' + '`RootIsolateToken`', + )); + + // Construct the root directory path if (useInMemoryDatabase) { this.rootDirectory = Store.inMemoryPrefix + (rootDirectory ?? 'fmtc'); } else { @@ -137,16 +147,11 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerResOneShot[0] = Completer(); final workerInitialRes = _workerResOneShot[0]! .future // Completed directly by handler below - .then<({ByteData? storeRef, Object? err, StackTrace? stackTrace})>( + .then<({Object err, StackTrace stackTrace})?>( (res) { _workerResOneShot.remove(0); _sendPort = res!['sendPort']; - - return ( - storeRef: res['storeReference'] as ByteData, - err: null, - stackTrace: null, - ); + return null; }, onError: (err, stackTrace) { _workerHandler.cancel(); @@ -156,7 +161,7 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { _workerResOneShot.clear(); _workerResStreamed.clear(); - return (storeRef: null, err: err, stackTrace: stackTrace); + return (err: err, stackTrace: stackTrace); }, ); @@ -209,22 +214,19 @@ class _ObjectBoxBackendImpl implements FMTCObjectBoxBackendInternal { rootDirectory: this.rootDirectory, maxDatabaseSize: maxDatabaseSize, macosApplicationGroup: macosApplicationGroup, - rootIsolateToken: ServicesBinding.rootIsolateToken!, + rootIsolateToken: rootIsolateToken, ), onExit: receivePort.sendPort, debugName: '[FMTC] ObjectBox Backend Worker', ); - // Wait for initial response from isolate - final initResult = await workerInitialRes; - - // Check whether initialisation was successful - if (initResult.storeRef case final storeRef?) { + // Check whether initialisation was successful after initial response + if (await workerInitialRes case (:final err, :final stackTrace)) { + Error.throwWithStackTrace(err, stackTrace); + } else { FMTCBackendAccess.internal = this; FMTCBackendAccessThreadSafe.internal = - _ObjectBoxBackendThreadSafeImpl._(storeReference: storeRef); - } else { - Error.throwWithStackTrace(initResult.err!, initResult.stackTrace!); + _ObjectBoxBackendThreadSafeImpl._(rootDirectory: this.rootDirectory); } } diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart index a5134af7..37f9e17c 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/standard/worker.dart @@ -25,11 +25,18 @@ Future _worker( // Open database, kill self if failed late final Store root; try { - root = await openStore( - directory: input.rootDirectory, - maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB - macosApplicationGroup: input.macosApplicationGroup, - ); + // If already opened, attach existing instance + // This can occur when a background `FlutterEngine` is in use, keeping the + // database open + if (Store.isOpen(input.rootDirectory)) { + root = Store.attach(getObjectBoxModel(), input.rootDirectory); + } else { + root = await openStore( + directory: input.rootDirectory, + maxDBSizeInKB: input.maxDatabaseSize, // Defaults to 10 GB + macosApplicationGroup: input.macosApplicationGroup, + ); + } // If the database is new, create the root statistics object final rootBox = root.box(); @@ -47,7 +54,7 @@ Future _worker( // Respond with comms channel for future cmds sendRes( id: 0, - data: {'sendPort': receivePort.sendPort, 'storeReference': root.reference}, + data: {'sendPort': receivePort.sendPort}, ); //! UTIL METHODS !// @@ -58,8 +65,6 @@ Future _worker( /// Should be run within a transaction. /// /// Specified values may be negative. - /// - /// Handles cases where there is no root statistics object yet. void updateRootStatistics({int deltaLength = 0, int deltaSize = 0}) => root.box().put( root.box().get(1)! @@ -836,8 +841,7 @@ Future _worker( (numExportedTiles) { if (numExportedTiles == 0) { throw ArgumentError( - 'must include at least one tile in any of the specified ' - 'stores', + 'Specified stores must include at least one tile total', 'storeNames', ); } diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart index 297b2580..25f20b0f 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/thread_safe.dart @@ -5,20 +5,20 @@ part of '../backend.dart'; class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { _ObjectBoxBackendThreadSafeImpl._({ - required this.storeReference, + required this.rootDirectory, }); @override String get friendlyIdentifier => 'ObjectBox'; - final ByteData storeReference; + final String rootDirectory; Store get expectInitialisedRoot => _root ?? (throw RootUnavailable()); Store? _root; @override void initialise() { if (_root != null) throw RootAlreadyInitialised(); - _root = Store.fromReference(getObjectBoxModel(), storeReference); + _root = Store.attach(getObjectBoxModel(), rootDirectory); } @override @@ -29,7 +29,7 @@ class _ObjectBoxBackendThreadSafeImpl implements FMTCBackendInternalThreadSafe { @override _ObjectBoxBackendThreadSafeImpl duplicate() => - _ObjectBoxBackendThreadSafeImpl._(storeReference: storeReference); + _ObjectBoxBackendThreadSafeImpl._(rootDirectory: rootDirectory); @override Future readTile({