Skip to content

Commit bf3005f

Browse files
committed
improve concurrency, misc fixes, tests
1 parent acc4f05 commit bf3005f

8 files changed

Lines changed: 98 additions & 24 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
## Persistent reactive models in Flutter with zero boilerplate
1111

12-
Flutter Data is an offline-first data framework with a customizable REST client and powerful model relationships, built on Riverpod.
12+
Flutter Data is a [local-first](https://www.inkandswitch.com/local-first/) data framework with a customizable REST client and powerful model relationships, built on Riverpod.
1313

1414
<small>Inspired by [Ember Data](https://github.com/emberjs/data) and [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html).</small>
1515

lib/src/adapter/adapter.dart

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ abstract class _BaseAdapter<T extends DataModelMixin<T>> with _Lifecycle {
175175
if (model._key == null) {
176176
throw Exception("Model must be initialized:\n\n$model");
177177
}
178-
final key = model._key!.detypifyKey()!;
178+
final key = model._key!.detypifyKey();
179179
final map = serializeLocal(model, withRelationships: false);
180180
final data = jsonEncode(map);
181181
db.execute(
@@ -192,6 +192,8 @@ abstract class _BaseAdapter<T extends DataModelMixin<T>> with _Lifecycle {
192192
final internalProvidersMap = _internalProvidersMap!;
193193
final _internalType = internalType;
194194

195+
final _refProvider = Provider((ref) => ref);
196+
195197
return await Isolate.run(() async {
196198
late final ProviderContainer container;
197199
try {
@@ -203,8 +205,18 @@ abstract class _BaseAdapter<T extends DataModelMixin<T>> with _Lifecycle {
203205
],
204206
);
205207

206-
await container
207-
.read(initializeFlutterData(internalProvidersMap).future);
208+
final _ref = container.read(_refProvider);
209+
_internalProvidersMap = internalProvidersMap;
210+
_internalAdaptersMap = internalProvidersMap
211+
.map((key, value) => MapEntry(key, _ref.read(value)));
212+
213+
await container.read(localStorageProvider).initialize(inIsolate: true);
214+
215+
// initialize and register
216+
for (final adapter in _internalAdaptersMap!.values) {
217+
adapter.dispose();
218+
await adapter.initialize(ref: _ref);
219+
}
208220

209221
final adapter = internalProvidersMap[_internalType]!;
210222
return fn(container.read(adapter));
@@ -220,24 +232,43 @@ abstract class _BaseAdapter<T extends DataModelMixin<T>> with _Lifecycle {
220232
List<String> _saveManyLocal(
221233
Adapter adapter, Iterable<DataModelMixin> models) {
222234
final db = adapter.db;
223-
final savedKeys = <String>[];
224235

236+
final savedKeys = <int>[];
237+
final pssMap = <PreparedStatement, List<(int, String)>>{};
238+
final keyMap = <int, String>{};
239+
240+
// per adapter, create prepared statements for each type and serialize models
225241
final grouped = models.groupSetsBy((e) => e._adapter);
226242
for (final e in grouped.entries) {
227243
final adapter = e.key;
244+
228245
final ps = db.prepare(
229246
'REPLACE INTO ${adapter.internalType} (key, data) VALUES (?, ?) RETURNING key;');
247+
pssMap[ps] = [];
248+
230249
for (final model in e.value) {
231-
final key = model._key!.detypifyKey();
250+
final [type, _key] = model._key!.split('#');
251+
final intKey = int.parse(_key);
252+
keyMap[intKey] = type;
232253
final map = adapter.serializeLocal(model, withRelationships: false);
233254
final data = jsonEncode(map);
255+
pssMap[ps]!.add((intKey, data));
256+
}
257+
}
258+
259+
// with everything ready, execute transaction
260+
db.execute('BEGIN');
261+
for (final MapEntry(key: ps, value: record) in pssMap.entries) {
262+
for (final (key, data) in record) {
234263
final result = ps.select([key, data]);
235-
savedKeys
236-
.add((result.first['key'] as int).typifyWith(adapter.internalType));
264+
savedKeys.add(result.first['key'] as int);
237265
}
238266
ps.dispose();
239267
}
240-
return savedKeys;
268+
db.execute('COMMIT');
269+
270+
// read keys returned by queries and typify with their original type
271+
return savedKeys.map((key) => key.typifyWith(keyMap[key]!)).toList();
241272
}
242273

243274
Future<List<String>?> saveManyLocal(Iterable<DataModelMixin> models,
@@ -268,7 +299,7 @@ abstract class _BaseAdapter<T extends DataModelMixin<T>> with _Lifecycle {
268299

269300
/// Deletes models with [keys] from local storage.
270301
void deleteLocalByKeys(Iterable<String> keys, {bool notify = true}) {
271-
final intKeys = keys.map((k) => k.detypifyKey()!).toList();
302+
final intKeys = keys.map((k) => k.detypifyKey()).toList();
272303
db.execute(
273304
'DELETE FROM $internalType WHERE key IN (${keys.map((_) => '?').join(', ')})',
274305
intKeys);

lib/src/core/core_notifier.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ class CoreNotifier extends DelayedStateNotifier<DataGraphEvent> {
3838
/// Finds an ID, given a [key].
3939
Object? getIdForKey(String key) {
4040
final intKey = key.detypifyKey();
41-
if (intKey == null) {
42-
return null;
43-
}
4441
final result = storage.db
4542
.select('SELECT id, is_int FROM _keys WHERE key = ?', [intKey]);
4643
if (result.isEmpty) {
@@ -56,7 +53,7 @@ class CoreNotifier extends DelayedStateNotifier<DataGraphEvent> {
5653
@protected
5754
Future<void> deleteKeysWithEdges(Iterable<String> keys) async {
5855
final params = keys.map((_) => '?').join(', ');
59-
final intKeys = keys.map((k) => k.detypifyKey()!).toList();
56+
final intKeys = keys.map((k) => k.detypifyKey()).toList();
6057

6158
storage.db.execute('BEGIN');
6259
storage.db.execute('DELETE FROM _keys WHERE key IN ($params);', intKeys);

lib/src/model/relationship/has_many.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ class HasMany<E extends DataModelMixin<E>> extends Relationship<E, Set<E>> {
4444
}
4545

4646
void addAll(Iterable<E> values) {
47+
db.execute('BEGIN');
4748
_addAll(values.map((e) => e._key!).toSet());
49+
db.execute('COMMIT');
4850
}
4951

5052
bool contains(E element) {

lib/src/model/relationship/relationship.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ sealed class Relationship<E extends DataModelMixin<E>, N> with EquatableMixin {
5353

5454
// setting up from scratch, remove all and add keys
5555

56-
db.execute('BEGIN');
5756
final existingKeys = keys;
5857
final keysToAdd = _uninitializedKeys!.difference(existingKeys);
5958
final keysToRemove = existingKeys.difference(_uninitializedKeys!);
59+
db.execute('BEGIN');
6060
_removeAll(keysToRemove);
6161
_addAll(keysToAdd);
6262
db.execute('COMMIT');

lib/src/storage/local_storage.dart

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ class LocalStorage {
44
LocalStorage({
55
required this.baseDirFn,
66
LocalStorageClearStrategy? clear,
7+
this.busyTimeout = 5000,
78
}) : clear = clear ?? LocalStorageClearStrategy.never;
89

910
var isInitialized = false;
1011

1112
final FutureOr<String> Function() baseDirFn;
1213
final LocalStorageClearStrategy clear;
14+
final int busyTimeout;
1315

1416
late final String path;
17+
18+
@protected
1519
late final Database db;
1620

17-
Future<LocalStorage> initialize() async {
21+
Future<LocalStorage> initialize({bool inIsolate = false}) async {
1822
if (isInitialized) return this;
1923

2024
final baseDirPath = await baseDirFn();
@@ -25,12 +29,19 @@ class LocalStorage {
2529
await destroy();
2630
}
2731

28-
db = sqlite3.open(path);
32+
db = sqlite3.open(path, mutex: false);
2933

30-
db.execute('''
34+
if (inIsolate) {
35+
db.execute('''
36+
PRAGMA journal_mode = WAL;
37+
PRAGMA busy_timeout = $busyTimeout;
38+
''');
39+
} else {
40+
db.execute('''
3141
PRAGMA journal_mode = WAL;
42+
PRAGMA busy_timeout = $busyTimeout;
3243
VACUUM;
33-
44+
3445
CREATE TABLE IF NOT EXISTS _edges (
3546
key_ INTEGER NOT NULL,
3647
name_ TEXT,
@@ -59,6 +70,7 @@ class LocalStorage {
5970
key TEXT
6071
);
6172
''');
73+
}
6274
} catch (e, stackTrace) {
6375
print('[flutter_data] Failed to open:\n$e\n$stackTrace');
6476
if (clear == LocalStorageClearStrategy.whenError) {

lib/src/utils/extensions.dart

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,8 @@ extension StringUtilsX on String {
6565
}
6666

6767
/// Returns key as int
68-
int? detypifyKey() {
68+
int detypifyKey() {
6969
final [_, key] = split('#');
70-
if (key.isEmpty) {
71-
// enters here if there is only a type, e.g. `people`
72-
return null;
73-
}
7470
return int.parse(key);
7571
}
7672

test/repository/remote_adapter_serialization_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,40 @@ void main() async {
245245
'number_of_sales': 0,
246246
});
247247
});
248+
249+
test('deserialize async', () async {
250+
final data = await container.people.deserializeAsync([
251+
{'_id': '23', 'name': 'Ko', 'age': 24}
252+
]);
253+
254+
expect(data.models, [Person(id: '23', name: 'Ko', age: 24)]);
255+
256+
final data2 = await container.people.deserializeAsync([
257+
{'_id': '26', 'name': 'Ze', 'age': 58}
258+
]);
259+
260+
expect(data2.models, [Person(id: '26', name: 'Ze', age: 58)]);
261+
});
262+
263+
test('deserialize async concurrent', () async {
264+
final p1 = container.people.deserializeAsync(
265+
List.generate(500, (i) {
266+
return {'_id': '$i', 'name': 'Tom $i', 'age': 18 + i};
267+
}),
268+
save: true);
269+
final p2 = container.people.deserializeAsync(
270+
List.generate(500, (i) {
271+
final j = i + 500;
272+
return {'_id': '$j', 'name': 'Jack $j', 'age': 18 + i};
273+
}),
274+
save: true);
275+
final p3 = container.people.deserializeAsync(
276+
List.generate(500, (i) {
277+
final j = i + 1000;
278+
return {'_id': '$j', 'name': 'Rob $j', 'age': 18 + i};
279+
}),
280+
save: true);
281+
await Future.wait([p1, p2, p3]);
282+
expect(container.people.countLocal, 1500);
283+
});
248284
}

0 commit comments

Comments
 (0)