Skip to content

Commit 37f8089

Browse files
Merge branch '19-run-in-tx-async' into 'main'
Add async variant of runInTransaction Closes #19 See merge request objectbox/objectbox-dart!5
2 parents 2a8b410 + 70cc9dd commit 37f8089

File tree

9 files changed

+387
-54
lines changed

9 files changed

+387
-54
lines changed

objectbox/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
## latest
2+
* Add `Store.runInTransactionAsync` to run database operations asynchronously in the background
3+
(requires Flutter 2.8.0/Dart 2.15.0 or newer).
4+
* Rename `Store.runIsolated` to `runAsync`, drop unused `mode` parameter, propagate errors and
5+
handle premature isolate exit.
26

37
* The native ObjectBox library is also searched for in the `lib` subfolder on desktop OS (macOS,
48
Linux, Windows). This is where the [`install.sh`](/install.sh) script downloads it by default.

objectbox/example/flutter/objectbox_demo/lib/main.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ class _MyHomePageState extends State<MyHomePage> {
4444
final _noteInputController = TextEditingController();
4545
final _listController = StreamController<List<Note>>(sync: true);
4646

47-
void _addNote() {
47+
Future<void> _addNote() async {
4848
if (_noteInputController.text.isEmpty) return;
49-
objectbox.noteBox.put(Note(_noteInputController.text));
49+
await objectbox.addNote(_noteInputController.text);
5050
_noteInputController.text = '';
5151
}
5252

objectbox/example/flutter/objectbox_demo/lib/objectbox.dart

+23-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,28 @@ class ObjectBox {
4040
Note('Delete notes by tapping on one'),
4141
Note('Write a demo app for ObjectBox')
4242
];
43-
noteBox.putMany(demoNotes);
43+
store.runInTransactionAsync(TxMode.write, _putNotesInTx, demoNotes);
44+
}
45+
46+
static void _putNotesInTx(Store store, List<Note> notes) =>
47+
store.box<Note>().putMany(notes);
48+
49+
/// Add a note within a transaction.
50+
///
51+
/// To avoid frame drops, run ObjectBox operations that take longer than a
52+
/// few milliseconds, e.g. putting many objects, in an isolate with its
53+
/// own Store instance.
54+
/// For this example only a single object is put which would also be fine if
55+
/// done here directly.
56+
Future<void> addNote(String text) =>
57+
store.runInTransactionAsync(TxMode.write, _addNoteInTx, text);
58+
59+
/// Note: due to [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983)
60+
/// not using a closure as it may capture more objects than expected.
61+
/// These might not be send-able to an isolate. See Store.runAsync for details.
62+
static void _addNoteInTx(Store store, String text) {
63+
// Perform ObjectBox operations that take longer than a few milliseconds
64+
// here. To keep it simple, this example just puts a single object.
65+
store.box<Note>().put(Note(text));
4466
}
4567
}

objectbox/example/flutter/objectbox_demo_sync/lib/main.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ class _MyHomePageState extends State<MyHomePage> {
4444
final _noteInputController = TextEditingController();
4545
final _listController = StreamController<List<Note>>(sync: true);
4646

47-
void _addNote() {
47+
Future<void> _addNote() async {
4848
if (_noteInputController.text.isEmpty) return;
49-
objectbox.noteBox.put(Note(_noteInputController.text));
49+
await objectbox.addNote(_noteInputController.text);
5050
_noteInputController.text = '';
5151
}
5252

objectbox/example/flutter/objectbox_demo_sync/lib/objectbox.dart

+19
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,23 @@ class ObjectBox {
4242
);
4343
return ObjectBox._create(store);
4444
}
45+
46+
/// Add a note within a transaction.
47+
///
48+
/// To avoid frame drops, run ObjectBox operations that take longer than a
49+
/// few milliseconds, e.g. putting many objects, in an isolate with its
50+
/// own Store instance.
51+
/// For this example only a single object is put which would also be fine if
52+
/// done here directly.
53+
Future<void> addNote(String text) =>
54+
store.runInTransactionAsync(TxMode.write, _addNoteInTx, text);
55+
56+
/// Note: due to [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/36983)
57+
/// not using a closure as it may capture more objects than expected.
58+
/// These might not be send-able to an isolate. See Store.runAsync for details.
59+
static void _addNoteInTx(Store store, String text) {
60+
// Perform ObjectBox operations that take longer than a few milliseconds
61+
// here. To keep it simple, this example just puts a single object.
62+
store.box<Note>().put(Note(text));
63+
}
4564
}

objectbox/lib/src/native/store.dart

+177-30
Original file line numberDiff line numberDiff line change
@@ -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>();
571691
final _nullSafetyEnabled = _nullReturningFn is! Future Function();
572692
final _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

Comments
 (0)