Skip to content

Commit b406487

Browse files
greenrobotgreenrobot-team
authored andcommitted
Add Store.runIsolated.
1 parent 44d201b commit b406487

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

objectbox/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
* Support [ObjectBox Admin](https://docs.objectbox.io/data-browser) for Android apps to browse
44
the database. #148
5+
* Add `Store.runIsolated` to run database operations (asynchronous) in the background. It spawns an
6+
isolate, runs the given callback in that isolate with its own Store and returns the result of the
7+
callback. This is similar to Flutters compute, but with the callback having access to a Store.
58
* Add `Store.attach` to attach to a Store opened in a directory. This is an improved replacement for
69
`Store.fromReference` to share a Store across isolates. It is no longer required to pass a
710
Store reference and the underlying Store remains open until the last instance is closed. #376

objectbox/lib/src/native/store.dart

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,39 @@ class Store {
382382
return _runInTransaction(mode, (tx) => fn());
383383
}
384384

385+
// Isolate entry point must be static or top-level.
386+
static void _callFunctionWithStoreInIsolate<P, R>(IsoPass<P, R> isoPass) {
387+
final store = Store.attach(isoPass.model, isoPass.dbDirectoryPath,
388+
queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault);
389+
final result = isoPass.runFn(store);
390+
store.close();
391+
// Note: maybe replace with Isolate.exit once min Dart SDK 2.15.
392+
isoPass.resultPort?.send(result);
393+
}
394+
395+
/// Spawns an isolate, runs [callback] in that isolate passing it [param] with
396+
/// its own Store and returns the result of callback.
397+
///
398+
/// Instances of [callback] must be top-level functions or static methods
399+
/// of classes, not closures or instance methods of objects.
400+
Future<R> runIsolated<P, R>(
401+
TxMode mode, R Function(Store, P) callback, P param) async {
402+
final resultPort = ReceivePort();
403+
// Await isolate spawn to avoid waiting forever if it fails to spawn.
404+
await Isolate.spawn(
405+
_callFunctionWithStoreInIsolate,
406+
IsoPass(_defs, directoryPath, _queriesCaseSensitiveDefault,
407+
resultPort.sendPort, callback, param));
408+
// Use Completer to return result so type is not lost.
409+
final result = Completer<R>();
410+
resultPort.listen((dynamic message) {
411+
result.complete(message as R);
412+
});
413+
await result.future;
414+
resultPort.close();
415+
return result.future;
416+
}
417+
385418
/// Internal only - bypasses the main checks for async functions, you may
386419
/// only pass synchronous callbacks!
387420
R _runInTransaction<R>(TxMode mode, R Function(Transaction) fn) {
@@ -495,3 +528,40 @@ final _openStoreDirectories = HashSet<String>();
495528
/// Otherwise, it's we can distinguish at runtime whether a function is async.
496529
final _nullSafetyEnabled = _nullReturningFn is! Future Function();
497530
final _nullReturningFn = () => null;
531+
532+
/// Captures everything required to create a "copy" of a store in an isolate
533+
/// and run user code.
534+
@immutable
535+
class IsoPass<P, R> {
536+
///
537+
final ModelDefinition model;
538+
539+
/// Used to attach to store in separate isolate
540+
/// (may be replaced in the future).
541+
final String dbDirectoryPath;
542+
543+
/// Config
544+
final bool queriesCaseSensitiveDefault;
545+
546+
/// Non-void functions can use this port to receive the result
547+
final SendPort? resultPort;
548+
549+
/// Parameter passed to the function
550+
final P param;
551+
552+
/// Function to be called in isolate
553+
final R Function(Store, P) fn;
554+
555+
/// creates everything that needs to be passed to the isolate.
556+
const IsoPass(
557+
this.model,
558+
this.dbDirectoryPath,
559+
// ignore: avoid_positional_boolean_parameters
560+
this.queriesCaseSensitiveDefault,
561+
this.resultPort,
562+
this.fn,
563+
this.param);
564+
565+
/// Called inside this class so types are not lost (dynamic instead of P and R).
566+
R runFn(Store store) => fn(store, param);
567+
}

objectbox/test/basics_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:async';
12
import 'dart:ffi' as ffi;
23
import 'dart:io';
34
import 'dart:isolate';
@@ -192,6 +193,29 @@ void main() {
192193
store.close();
193194
Directory('basics').deleteSync(recursive: true);
194195
});
196+
197+
test('store_runInIsolatedTx', () async {
198+
final env = TestEnv('basics');
199+
final id = env.box.put(TestEntity(tString: 'foo'));
200+
final futureResult =
201+
env.store.runIsolated(TxMode.write, readStringAndRemove, id);
202+
print('Count in main isolate: ${env.box.count()}');
203+
final x = await futureResult;
204+
expect(x, 'foo!');
205+
expect(env.box.count(), 0); // Must be removed once awaited
206+
env.closeAndDelete();
207+
});
208+
}
209+
210+
String readStringAndRemove(Store store, int id) {
211+
var box = store.box<TestEntity>();
212+
var testEntity = box.get(id);
213+
final result = testEntity!.tString! + '!';
214+
print('Result in 2nd isolate: $result');
215+
final removed = box.remove(id);
216+
print('Removed in 2nd isolate: $removed');
217+
print('Count in 2nd isolate after remove: ${box.count()}');
218+
return result;
195219
}
196220

197221
class StoreAttachIsolateInit {

0 commit comments

Comments
 (0)