Skip to content

Commit 637036f

Browse files
Add Store.attach.
1 parent 47b71d3 commit 637036f

File tree

2 files changed

+126
-48
lines changed

2 files changed

+126
-48
lines changed

objectbox/lib/src/native/store.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,59 @@ class Store {
221221
}
222222
}
223223

224+
/// Create a Dart store instance from an existing store openend in the same
225+
/// path.
226+
///
227+
/// Use this if you want to access the same store from multiple isolates.
228+
/// This results in two (or more) isolates having access to the same
229+
/// underlying native store. Concurrent access is ensured using implicit or
230+
/// explicit transactions.
231+
///
232+
/// When using from an isolate, make sure to [close] the store before the
233+
/// isolate exits.
234+
Store.attach(this._defs, String? directoryOrNull,
235+
{bool queriesCaseSensitiveDefault = true})
236+
: _weak = false,
237+
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
238+
_dbDir = path.context.canonicalize(
239+
(directoryOrNull == null || directoryOrNull.isEmpty)
240+
? 'objectbox'
241+
: directoryOrNull) {
242+
// FIXME Refactor/combine with default constructor.
243+
try {
244+
final directory = (directoryOrNull == null || directoryOrNull.isEmpty)
245+
? 'objectbox'
246+
: directoryOrNull;
247+
final cStr = directory.toNativeUtf8();
248+
try {
249+
_cStore = C.store_attach(cStr.cast());
250+
} finally {
251+
malloc.free(cStr);
252+
}
253+
254+
try {
255+
checkObxPtr(_cStore, 'failed to create store');
256+
} on ObjectBoxException catch (e) {
257+
// Recognize common problems when trying to open/create a database
258+
// 10199 = OBX_ERROR_STORAGE_GENERAL
259+
// 13 = permissions denied, 30 = read-only filesystem
260+
if (e.message.contains(OBX_ERROR_STORAGE_GENERAL.toString()) &&
261+
e.message.contains('Dir does not exist') &&
262+
(e.message.endsWith(' (13)') || e.message.endsWith(' (30)'))) {
263+
throw ObjectBoxException(e.message +
264+
' - this usually indicates a problem with permissions; '
265+
"if you're using Flutter you may need to use "
266+
'getApplicationDocumentsDirectory() from the path_provider '
267+
'package, see example/README.md');
268+
}
269+
rethrow;
270+
}
271+
} catch (e) {
272+
_reader.clear();
273+
rethrow;
274+
}
275+
}
276+
224277
/// Returns a store reference you can use to create a new store instance with
225278
/// a single underlying native store. See [Store.fromReference] for more details.
226279
ByteData get reference => _reference;

objectbox/test/isolates_test.dart

Lines changed: 73 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -46,60 +46,84 @@ void main() {
4646
receivePort.close();
4747
});
4848

49-
/// Work with a single store across multiple isolates.
50-
test('single store in multiple isolates', () async {
51-
final receivePort = ReceivePort();
52-
final isolate =
53-
await Isolate.spawn(createDataIsolate, receivePort.sendPort);
54-
55-
final sendPortCompleter = Completer<SendPort>();
56-
late Completer<dynamic> responseCompleter;
57-
receivePort.listen((dynamic data) {
58-
if (data is SendPort) {
59-
sendPortCompleter.complete(data);
60-
} else {
61-
print('Main received: $data');
62-
responseCompleter.complete(data);
63-
}
64-
});
49+
/// Work with a single store across multiple isolates using
50+
/// the legacy way of passing a pointer reference to the isolate.
51+
test('single store using reference', () async {
52+
final storeCreator = (dynamic msg) =>
53+
Store.fromReference(getObjectBoxModel(), msg as ByteData);
54+
await testUsingStoreFromIsolate(storeCreator, (env) => env.store.reference);
55+
});
6556

66-
// Receive the SendPort from the Isolate
67-
SendPort sendPort = await sendPortCompleter.future;
57+
/// Work with a single store across multiple isolates using
58+
/// the directory path to attach to an existing store.
59+
test('single store using attach', () async {
60+
final storeCreator =
61+
(dynamic msg) => Store.attach(getObjectBoxModel(), msg as String);
62+
await testUsingStoreFromIsolate(storeCreator, (env) => env.dir.path);
63+
});
64+
}
6865

69-
final call = (dynamic message) {
70-
responseCompleter = Completer<dynamic>();
71-
sendPort.send(message);
72-
return responseCompleter.future;
73-
};
66+
class IsolateInitMessage {
67+
SendPort sendPort;
68+
Store Function(dynamic) storeCreator;
7469

75-
// Pass the store to the isolate
76-
final env = TestEnv('isolates');
77-
expect(await call(env.store.reference), equals('store set'));
70+
IsolateInitMessage(this.sendPort, this.storeCreator);
71+
}
7872

79-
{
80-
// check simple box operations
81-
expect(env.box.isEmpty(), isTrue);
82-
expect(await call(['put', 'Foo']), equals(1)); // returns inserted id = 1
83-
expect(env.box.get(1)!.tString, equals('Foo'));
73+
Future<void> testUsingStoreFromIsolate(Store Function(dynamic) storeCreator,
74+
dynamic Function(TestEnv) storeRefGetter) async {
75+
final receivePort = ReceivePort();
76+
final initMessage = IsolateInitMessage(receivePort.sendPort, storeCreator);
77+
final isolate = await Isolate.spawn(createDataIsolate, initMessage);
78+
79+
final sendPortCompleter = Completer<SendPort>();
80+
late Completer<dynamic> responseCompleter;
81+
receivePort.listen((dynamic data) {
82+
if (data is SendPort) {
83+
sendPortCompleter.complete(data);
84+
} else {
85+
print('Main received: $data');
86+
responseCompleter.complete(data);
8487
}
88+
});
8589

86-
{
87-
// verify that query streams (using observers) work fine across isolates
88-
final queryStream = env.box.query().watch();
89-
// starts a subscription
90-
final futureFirst = queryStream.map((q) => q.find()).first;
91-
expect(await call(['put', 'Bar']), equals(2));
92-
List<TestEntity> found = await futureFirst.timeout(defaultTimeout);
93-
expect(found.length, equals(2));
94-
expect(found.last.tString, equals('Bar'));
95-
}
90+
// Receive the SendPort from the Isolate
91+
SendPort sendPort = await sendPortCompleter.future;
92+
93+
final call = (dynamic message) {
94+
responseCompleter = Completer<dynamic>();
95+
sendPort.send(message);
96+
return responseCompleter.future;
97+
};
98+
99+
// FIXME Send path instead of reference when using attach.
100+
// Pass the store to the isolate
101+
final env = TestEnv('isolates');
102+
expect(await call(storeRefGetter(env)), equals('store set'));
103+
104+
{
105+
// check simple box operations
106+
expect(env.box.isEmpty(), isTrue);
107+
expect(await call(['put', 'Foo']), equals(1)); // returns inserted id = 1
108+
expect(env.box.get(1)!.tString, equals('Foo'));
109+
}
110+
111+
{
112+
// verify that query streams (using observers) work fine across isolates
113+
final queryStream = env.box.query().watch();
114+
// starts a subscription
115+
final futureFirst = queryStream.map((q) => q.find()).first;
116+
expect(await call(['put', 'Bar']), equals(2));
117+
List<TestEntity> found = await futureFirst.timeout(defaultTimeout);
118+
expect(found.length, equals(2));
119+
expect(found.last.tString, equals('Bar'));
120+
}
96121

97-
expect(await call(['close']), equals('done'));
122+
expect(await call(['close']), equals('done'));
98123

99-
isolate.kill();
100-
receivePort.close();
101-
env.close();
102-
});
124+
isolate.kill();
125+
receivePort.close();
126+
env.close();
103127
}
104128

105129
// Echoes back any received message.
@@ -119,19 +143,20 @@ void echoIsolate(SendPort sendPort) async {
119143
}
120144

121145
// Creates data in the background, in the [Store] received as the first message.
122-
void createDataIsolate(SendPort sendPort) async {
146+
void createDataIsolate(IsolateInitMessage initMessage) async {
123147
// Open the ReceivePort to listen for incoming messages
124148
final port = ReceivePort();
125149

126150
// Send the port where the main isolate can contact us
151+
final sendPort = initMessage.sendPort;
127152
sendPort.send(port.sendPort);
128153

129154
Store? store;
130155
// Listen for messages
131156
await for (final msg in port) {
132157
if (store == null) {
133158
// first message data is Store's C pointer address
134-
store = Store.fromReference(getObjectBoxModel(), msg as ByteData);
159+
store = initMessage.storeCreator(msg);
135160
sendPort.send('store set');
136161
} else {
137162
print('Isolate received: $msg');

0 commit comments

Comments
 (0)