Skip to content

Commit 0da66a2

Browse files
Merge branch 'runInIsolatedTx'
2 parents 573bbf4 + 153d5bb commit 0da66a2

File tree

3 files changed

+132
-10
lines changed

3 files changed

+132
-10
lines changed

objectbox/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
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
6+
(requires Flutter 2.8.0/Dart 2.15.0 or newer). It spawns an isolate, runs the given callback in that
7+
isolate with its own Store and returns the result of the callback. This is similar to Flutters
8+
compute, but with the callback having access to a Store. #384
59
* Add `Store.attach` to attach to a Store opened in a directory. This is an improved replacement for
610
`Store.fromReference` to share a Store across isolates. It is no longer required to pass a
711
Store reference and the underlying Store remains open until the last instance is closed. #376

objectbox/lib/src/native/store.dart

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class Store {
4444
final _reader = ReaderWithCBuffer();
4545
Transaction? _tx;
4646

47+
/// Path to the database directory.
48+
final String directoryPath;
49+
4750
/// Absolute path to the database directory, used for open check.
4851
final String _absoluteDirectoryPath;
4952

@@ -88,6 +91,7 @@ class Store {
8891
String? macosApplicationGroup})
8992
: _weak = false,
9093
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
94+
directoryPath = _safeDirectoryPath(directory),
9195
_absoluteDirectoryPath =
9296
path.context.canonicalize(_safeDirectoryPath(directory)) {
9397
try {
@@ -114,13 +118,11 @@ class Store {
114118

115119
try {
116120
checkObx(C.opt_model(opt, model.ptr));
117-
if (directory != null && directory.isNotEmpty) {
118-
final cStr = directory.toNativeUtf8();
119-
try {
120-
checkObx(C.opt_directory(opt, cStr.cast()));
121-
} finally {
122-
malloc.free(cStr);
123-
}
121+
final cStr = directoryPath.toNativeUtf8();
122+
try {
123+
checkObx(C.opt_directory(opt, cStr.cast()));
124+
} finally {
125+
malloc.free(cStr);
124126
}
125127
if (maxDBSizeInKB != null && maxDBSizeInKB > 0) {
126128
C.opt_max_db_size_in_kb(opt, maxDBSizeInKB);
@@ -199,6 +201,7 @@ class Store {
199201
{bool queriesCaseSensitiveDefault = true})
200202
// must not close the same native store twice so [_weak]=true
201203
: _weak = true,
204+
directoryPath = '',
202205
_absoluteDirectoryPath = '',
203206
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault {
204207
// see [reference] for serialization order
@@ -231,6 +234,7 @@ class Store {
231234
// _weak = false so store can be closed.
232235
: _weak = false,
233236
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
237+
directoryPath = _safeDirectoryPath(directoryPath),
234238
_absoluteDirectoryPath =
235239
path.context.canonicalize(_safeDirectoryPath(directoryPath)) {
236240
try {
@@ -240,12 +244,12 @@ class Store {
240244
// overlap.
241245
_checkStoreDirectoryNotOpen();
242246

243-
final path = _safeDirectoryPath(directoryPath);
244-
final pathCStr = path.toNativeUtf8();
247+
final pathCStr = this.directoryPath.toNativeUtf8();
245248
try {
246249
if (debugLogs) {
247250
final isOpen = C.store_is_open(pathCStr.cast());
248-
print('Attaching to store... path=$path isOpen=$isOpen');
251+
print(
252+
'Attaching to store... path=${this.directoryPath} isOpen=$isOpen');
249253
}
250254
_cStore = C.store_attach(pathCStr.cast());
251255
} finally {
@@ -378,6 +382,45 @@ class Store {
378382
return _runInTransaction(mode, (tx) => fn());
379383
}
380384

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

objectbox/test/basics_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import 'dart:async';
12
import 'dart:ffi' as ffi;
23
import 'dart:io';
34
import 'dart:isolate';
45

56
import 'package:async/async.dart';
7+
import 'package:meta/meta.dart';
68
import 'package:objectbox/internal.dart';
79
import 'package:objectbox/src/native/bindings/bindings.dart';
810
import 'package:objectbox/src/native/bindings/helpers.dart';
@@ -192,6 +194,44 @@ void main() {
192194
store.close();
193195
Directory('basics').deleteSync(recursive: true);
194196
});
197+
198+
test('store_runInIsolatedTx', () async {
199+
final env = TestEnv('basics');
200+
final id = env.box.put(TestEntity(tString: 'foo'));
201+
final futureResult =
202+
env.store.runIsolated(TxMode.write, readStringAndRemove, id);
203+
print('Count in main isolate: ${env.box.count()}');
204+
final String x;
205+
try {
206+
x = await futureResult;
207+
} catch (e) {
208+
final dartVersion = RegExp('([0-9]+).([0-9]+).([0-9]+)')
209+
.firstMatch(Platform.version)
210+
?.group(0);
211+
if (dartVersion != null && dartVersion.compareTo('2.15.0') < 0) {
212+
print('runIsolated requires Dart 2.15, ignoring error.');
213+
env.closeAndDelete();
214+
return;
215+
} else {
216+
rethrow;
217+
}
218+
}
219+
expect(x, 'foo!');
220+
expect(env.box.count(), 0); // Must be removed once awaited
221+
env.closeAndDelete();
222+
});
223+
}
224+
225+
Future<String> readStringAndRemove(Store store, int id) async {
226+
var box = store.box<TestEntity>();
227+
var testEntity = box.get(id);
228+
final result = testEntity!.tString! + '!';
229+
print('Result in 2nd isolate: $result');
230+
final removed = box.remove(id);
231+
print('Removed in 2nd isolate: $removed');
232+
print('Count in 2nd isolate after remove: ${box.count()}');
233+
// Pointless Future to test async functions are supported.
234+
return await Future.delayed(const Duration(milliseconds: 10), () => result);
195235
}
196236

197237
class StoreAttachIsolateInit {

0 commit comments

Comments
 (0)