@@ -412,45 +412,165 @@ class Store {
412412 return _runInTransaction (mode, (tx) => fn ());
413413 }
414414
415- // Isolate entry point must be static or top-level.
415+ /// Like [runAsync] , but executes [callback] within a read or write
416+ /// transaction depending on [mode] .
417+ ///
418+ /// See the documentation on [runAsync] for important usage details.
419+ ///
420+ /// The following example gets the name of a User object, deletes the object
421+ /// and returns the name within a write transaction:
422+ /// ```dart
423+ /// String? readNameAndRemove(Store store, int objectId) {
424+ /// var box = store.box<User>();
425+ /// final nameOrNull = box.get(objectId)?.name;
426+ /// box.remove(objectId);
427+ /// return nameOrNull;
428+ /// }
429+ /// await store.runInTransactionAsync(TxMode.write, readNameAndRemove, objectId);
430+ /// ```
431+ Future <R > runInTransactionAsync <R , P >(
432+ TxMode mode, TxAsyncCallback <R , P > callback, P param) =>
433+ runAsync (
434+ (Store store, P p) =>
435+ store.runInTransaction (mode, () => callback (store, p)),
436+ param);
437+
438+ // Isolate entry point must be able to be sent via SendPort.send.
439+ // Must guarantee only a single result event is sent.
440+ // runAsync only handles a single event, any sent afterwards are ignored. E.g.
441+ // in case [Error] or [Exception] are thrown after the result is sent.
416442 static Future <void > _callFunctionWithStoreInIsolate <P , R >(
417- _IsoPass <P , R > isoPass) async {
443+ _RunAsyncIsolateConfig <P , R > isoPass) async {
418444 final store = Store .attach (isoPass.model, isoPass.dbDirectoryPath,
419445 queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault);
420- final result = await isoPass.runFn (store);
421- store.close ();
422- // Note: maybe replace with Isolate.exit (and remove kill call in
423- // runIsolated) once min Dart SDK 2.15.
424- isoPass.resultPort? .send (result);
446+ dynamic result;
447+ try {
448+ final callbackResult = await isoPass.runCallback (store);
449+ result = _RunAsyncResult (callbackResult);
450+ } catch (error, stack) {
451+ result = _RunAsyncError (error, stack);
452+ } finally {
453+ store.close ();
454+ }
455+
456+ // Note: maybe replace with Isolate.exit (and remove kill() call in caller)
457+ // once min Dart SDK 2.15.
458+ isoPass.resultPort.send (result);
425459 }
426460
427461 /// Spawns an isolate, runs [callback] in that isolate passing it [param] with
428462 /// its own Store and returns the result of callback.
429463 ///
430- /// Instances of [callback] must be top-level functions or static methods
431- /// of classes, not closures or instance methods of objects.
464+ /// This is useful for ObjectBox operations that take longer than a few
465+ /// milliseconds, e.g. putting many objects, which would cause frame drops.
466+ /// If all operations can execute within a single transaction, prefer to use
467+ /// [runInTransactionAsync] .
468+ ///
469+ /// The following example gets the name of a User object, deletes the object
470+ /// and returns the name:
471+ /// ```dart
472+ /// String? readNameAndRemove(Store store, int objectId) {
473+ /// var box = store.box<User>();
474+ /// final nameOrNull = box.get(objectId)?.name;
475+ /// box.remove(objectId);
476+ /// return nameOrNull;
477+ /// }
478+ /// await store.runAsync(readNameAndRemove, objectId);
479+ /// ```
480+ ///
481+ /// The [callback] must be a function that can be sent to an isolate: either a
482+ /// top-level function, static method or a closure that only captures objects
483+ /// that can be sent to an isolate.
484+ ///
485+ /// Warning: Due to
486+ /// [dart-lang/sdk#36983] (https://github.com/dart-lang/sdk/issues/36983) a
487+ /// closure may capture more objects than expected, even if they are not
488+ /// directly used in the closure itself.
489+ ///
490+ /// The types `P` (type of the parameter to be passed to the callback) and
491+ /// `R` (type of the result returned by the callback) must be able to be sent
492+ /// to or received from an isolate. The same applies to errors originating
493+ /// from the callback.
494+ ///
495+ /// See [SendPort.send] for a discussion on which values can be sent to and
496+ /// received from isolates.
432497 ///
433498 /// Note: this requires Dart 2.15.0 or newer
434499 /// (shipped with Flutter 2.8.0 or newer).
435- Future <R > runIsolated <P , R >(
436- TxMode mode, FutureOr <R > Function (Store , P ) callback, P param) async {
437- final resultPort = ReceivePort ();
438- // Await isolate spawn to avoid waiting forever if it fails to spawn.
439- final isolate = await Isolate .spawn (
440- _callFunctionWithStoreInIsolate,
441- _IsoPass (_defs, directoryPath, _queriesCaseSensitiveDefault,
442- resultPort.sendPort, callback, param));
443- // Use Completer to return result so type is not lost.
444- final result = Completer <R >();
445- resultPort.listen ((dynamic message) {
446- result.complete (message as R );
447- });
448- await result.future;
449- resultPort.close ();
500+ Future <R > runAsync <P , R >(RunAsyncCallback <P , R > callback, P param) async {
501+ final port = RawReceivePort ();
502+ final completer = Completer <dynamic >();
503+
504+ void _cleanup () {
505+ port.close ();
506+ }
507+
508+ port.handler = (dynamic message) {
509+ _cleanup ();
510+ completer.complete (message);
511+ };
512+
513+ final Isolate isolate;
514+ try {
515+ // Await isolate spawn to avoid waiting forever if it fails to spawn.
516+ isolate = await Isolate .spawn (
517+ _callFunctionWithStoreInIsolate,
518+ _RunAsyncIsolateConfig (_defs, directoryPath,
519+ _queriesCaseSensitiveDefault, port.sendPort, callback, param),
520+ errorsAreFatal: true ,
521+ onError: port.sendPort,
522+ onExit: port.sendPort);
523+ } on Object {
524+ _cleanup ();
525+ rethrow ;
526+ }
527+
528+ final dynamic response = await completer.future;
529+ // Replace with Isolate.exit in _callFunctionWithStoreInIsolate
530+ // once min SDK 2.15.
450531 isolate.kill ();
451- return result.future;
532+
533+ if (response == null ) {
534+ throw RemoteError ('Isolate exited without result or error.' , '' );
535+ }
536+
537+ if (response is _RunAsyncResult ) {
538+ // Success, return result.
539+ return response.result as R ;
540+ } else if (response is List <dynamic >) {
541+ // See isolate.addErrorListener docs for message structure.
542+ assert (response.length == 2 );
543+ await Future <Never >.error (RemoteError (
544+ response[0 ] as String ,
545+ response[1 ] as String ,
546+ ));
547+ } else {
548+ // Error thrown by callback.
549+ assert (response is _RunAsyncError );
550+ response as _RunAsyncError ;
551+
552+ await Future <Never >.error (
553+ response.error,
554+ response.stack,
555+ );
556+ }
452557 }
453558
559+ /// Deprecated. Use [runAsync] instead. Will be removed in a future release.
560+ ///
561+ /// Spawns an isolate, runs [callback] in that isolate passing it [param] with
562+ /// its own Store and returns the result of callback.
563+ ///
564+ /// Instances of [callback] must be top-level functions or static methods
565+ /// of classes, not closures or instance methods of objects.
566+ ///
567+ /// Note: this requires Dart 2.15.0 or newer
568+ /// (shipped with Flutter 2.8.0 or newer).
569+ @Deprecated ('Use `runAsync` instead. Will be removed in a future release.' )
570+ Future <R > runIsolated <P , R >(TxMode mode,
571+ FutureOr <R > Function (Store , P ) callback, P param) async =>
572+ runAsync (callback, param);
573+
454574 /// Internal only - bypasses the main checks for async functions, you may
455575 /// only pass synchronous callbacks!
456576 R _runInTransaction <R >(TxMode mode, R Function (Transaction ) fn) {
@@ -571,10 +691,16 @@ final _openStoreDirectories = HashSet<String>();
571691final _nullSafetyEnabled = _nullReturningFn is ! Future Function ();
572692final _nullReturningFn = () => null ;
573693
694+ // Define type so IDE generates named parameters.
695+ /// Signature for the callback passed to [Store.runAsync] .
696+ ///
697+ /// Instances must be functions that can be sent to an isolate.
698+ typedef RunAsyncCallback <P , R > = FutureOr <R > Function (Store store, P parameter);
699+
574700/// Captures everything required to create a "copy" of a store in an isolate
575701/// and run user code.
576702@immutable
577- class _IsoPass <P , R > {
703+ class _RunAsyncIsolateConfig <P , R > {
578704 final ModelDefinition model;
579705
580706 /// Used to attach to store in separate isolate
@@ -584,15 +710,15 @@ class _IsoPass<P, R> {
584710 final bool queriesCaseSensitiveDefault;
585711
586712 /// Non-void functions can use this port to receive the result.
587- final SendPort ? resultPort;
713+ final SendPort resultPort;
588714
589715 /// Parameter passed to [callback] .
590716 final P param;
591717
592718 /// To be called in isolate.
593- final FutureOr < R > Function ( Store , P ) callback;
719+ final RunAsyncCallback < P , R > callback;
594720
595- const _IsoPass (
721+ const _RunAsyncIsolateConfig (
596722 this .model,
597723 this .dbDirectoryPath,
598724 // ignore: avoid_positional_boolean_parameters
@@ -603,5 +729,26 @@ class _IsoPass<P, R> {
603729
604730 /// Calls [callback] inside this class so types are not lost
605731 /// (if called in isolate types would be dynamic instead of P and R).
606- FutureOr <R > runFn (Store store) => callback (store, param);
732+ FutureOr <R > runCallback (Store store) => callback (store, param);
733+ }
734+
735+ @immutable
736+ class _RunAsyncResult <R > {
737+ final R result;
738+
739+ const _RunAsyncResult (this .result);
740+ }
741+
742+ @immutable
743+ class _RunAsyncError {
744+ final Object error;
745+ final StackTrace stack;
746+
747+ const _RunAsyncError (this .error, this .stack);
607748}
749+
750+ // Specify so IDE generates named parameters.
751+ /// Signature for callback passed to [Store.runInTransactionAsync] .
752+ ///
753+ /// Instances must be functions that can be sent to an isolate.
754+ typedef TxAsyncCallback <R , P > = R Function (Store store, P parameter);
0 commit comments