Skip to content

Commit

Permalink
feat: Add native sqflite indexeddb database
Browse files Browse the repository at this point in the history
  • Loading branch information
krille-chan committed Dec 6, 2023
1 parent c0895ac commit cf071e3
Show file tree
Hide file tree
Showing 9 changed files with 576 additions and 22 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- uses: actions/checkout@v3
- name: Run tests
run: |
apt-get update && apt-get install --no-install-recommends --no-install-suggests -y curl lcov python3 python3-distutils
apt-get update && apt-get install --no-install-recommends --no-install-suggests -y curl lcov python3 python3-distutils libsqlite3-dev
curl -o /bin/lcov_cobertura.py https://raw.githubusercontent.com/eriwen/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py && sed 's/env python/env python3/' -i /bin/lcov_cobertura.py && chmod +x /bin/lcov_cobertura.py
dart pub get
./scripts/test.sh
Expand Down
2 changes: 1 addition & 1 deletion lib/matrix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export 'package:matrix_api_lite/matrix_api_lite.dart';
export 'src/client.dart';
export 'src/database/database_api.dart';
export 'src/database/hive_database.dart';
export 'src/database/fluffybox_database.dart';
export 'src/database/matrix_sdk_database.dart';
export 'src/database/hive_collections_database.dart';
export 'src/event.dart';
export 'src/presence.dart';
Expand Down
194 changes: 194 additions & 0 deletions lib/src/database/indexeddb_box.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import 'dart:async';
import 'dart:html';
import 'dart:indexed_db';

/// Key-Value store abstraction over IndexedDB so that the sdk database can use
/// a single interface for all platforms. API is inspired by Hive.
class BoxCollection {
final Database _db;
final Set<String> boxNames;

BoxCollection(this._db, this.boxNames);

static Future<BoxCollection> open(
String name,
Set<String> boxNames, {
int version = 1,
Object? sqfliteDatabase,
IdbFactory? idbFactory,
}) async {
idbFactory ??= window.indexedDB!;
final db = await idbFactory.open(name, version: 1,
onUpgradeNeeded: (VersionChangeEvent event) {
final db = event.target.result;
for (final name in boxNames) {
db.createObjectStore(name, autoIncrement: true);
}
});
return BoxCollection(db, boxNames);
}

Box<V> openBox<V>(String name) {
if (!boxNames.contains(name)) {
throw ('Box with name $name is not in the known box names of this collection.');
}
return Box<V>(name, this);
}

List<Future<void> Function(Transaction txn)>? _txnCache;

Future<void> transaction(
Future<void> Function() action, {
List<String>? boxNames,
bool readOnly = false,
}) async {
boxNames ??= _db.objectStoreNames!.toList();
_txnCache = [];
await action();
final cache = List<Future<void> Function(Transaction txn)>.from(_txnCache!);
_txnCache = null;
if (cache.isEmpty) return;
final txn = _db.transaction(boxNames, readOnly ? 'readonly' : 'readwrite');
for (final fun in cache) {
unawaited(fun(txn));
}
await txn.completed;
return;
}

Future<void> clear() async {
for (final name in boxNames) {
_db.deleteObjectStore(name);
}
}

Future<void> close() async => _db.close();
}

class Box<V> {
final String name;
final BoxCollection boxCollection;
final Map<String, V?> _cache = {};
Set<String>? _cachedKeys;
bool get _keysCached => _cachedKeys != null;

Box(this.name, this.boxCollection);

Future<List<String>> getAllKeys([Transaction? txn]) async {
if (_keysCached) return _cachedKeys!.toList();
txn ??= boxCollection._db.transaction(name, 'readonly');
final store = txn.objectStore(name);
final request = store.getAllKeys(null);
await request.onSuccess.first;
final keys = request.result.cast<String>();
_cachedKeys = keys.toSet();
return keys;
}

Future<V?> getWhere(String indexName, String pattern,
[Transaction? txn]) async {
txn ??= boxCollection._db.transaction(name, 'readonly');
final store = txn.objectStore(name);
final index = store.index(indexName);
final value = await index.get(pattern) as V?;
return value;
}

Future<Map<String, V>> getAllValues([Transaction? txn]) async {
txn ??= boxCollection._db.transaction(name, 'readonly');
final store = txn.objectStore(name);
final map = <String, V>{};
final cursorStream = store.openCursor(autoAdvance: true);
await for (final cursor in cursorStream) {
map[cursor.key as String] = cursor.value as V;
}
return map;
}

Future<V?> get(String key, [Transaction? txn]) async {
if (_cache.containsKey(key)) return _cache[key];
txn ??= boxCollection._db.transaction(name, 'readonly');
final store = txn.objectStore(name);
_cache[key] = await store.getObject(key) as V?;
return _cache[key];
}

Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
if (!keys.any((key) => !_cache.containsKey(key))) {
return keys.map((key) => _cache[key]).toList();
}
txn ??= boxCollection._db.transaction(name, 'readonly');
final store = txn.objectStore(name);
final list = await Future.wait(keys.map((key) => store.getObject(key)));
for (var i = 0; i < keys.length; i++) {
_cache[keys[i]] = list[i] as V?;
}
return list.cast<V?>();
}

Future<void> put(String key, V val, [Transaction? txn]) async {
if (boxCollection._txnCache != null) {
boxCollection._txnCache!.add((txn) => put(key, val, txn));
_cache[key] = val;
_cachedKeys?.add(key);
return;
}

txn ??= boxCollection._db.transaction(name, 'readwrite');
final store = txn.objectStore(name);
await store.put(val as Object, key);
_cache[key] = val;
_cachedKeys?.add(key);
return;
}

Future<void> delete(String key, [Transaction? txn]) async {
if (boxCollection._txnCache != null) {
boxCollection._txnCache!.add((txn) => delete(key, txn));
_cache.remove(key);
_cachedKeys?.remove(key);
return;
}

txn ??= boxCollection._db.transaction(name, 'readwrite');
final store = txn.objectStore(name);
await store.delete(key);
_cache.remove(key);
_cachedKeys?.remove(key);
return;
}

Future<void> deleteAll(List<String> keys, [Transaction? txn]) async {
if (boxCollection._txnCache != null) {
boxCollection._txnCache!.add((txn) => deleteAll(keys, txn));
keys.forEach(_cache.remove);
_cachedKeys?.removeAll(keys);
return;
}

txn ??= boxCollection._db.transaction(name, 'readwrite');
final store = txn.objectStore(name);
for (final key in keys) {
await store.delete(key);
_cache.remove(key);
_cachedKeys?.removeAll(keys);
}
return;
}

Future<void> clear([Transaction? txn]) async {
if (boxCollection._txnCache != null) {
boxCollection._txnCache!.add((txn) => clear(txn));
_cache.clear();
_cachedKeys = null;
return;
}

txn ??= boxCollection._db.transaction(name, 'readwrite');
final store = txn.objectStore(name);
await store.clear();
_cache.clear();
_cachedKeys = null;
return;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:fluffybox/fluffybox.dart';
import 'package:sqflite_common/sqflite.dart';

import 'package:matrix/encryption/utils/olm_session.dart';
Expand All @@ -33,7 +32,10 @@ import 'package:matrix/matrix.dart';
import 'package:matrix/src/utils/queued_to_device_event.dart';
import 'package:matrix/src/utils/run_benchmarked.dart';

class FluffyBoxDatabase extends DatabaseApi {
import 'package:matrix/src/database/indexeddb_box.dart'
if (dart.library.io) 'package:matrix/src/database/sqflite_box.dart';

class MatrixSdkDatabase extends DatabaseApi {
static const int version = 6;
final String name;
late BoxCollection _collection;
Expand Down Expand Up @@ -122,16 +124,22 @@ class FluffyBoxDatabase extends DatabaseApi {

String get _seenDeviceKeysBoxName => 'box_seen_device_keys';

FluffyBoxDatabase(
Database? database;

/// Custom IdbFactory used to create the indexedDB. On IO platforms it would
/// lead to an error to import "dart:indexed_db" so this is dynamically
/// typed.
final dynamic idbFactory;

MatrixSdkDatabase(
this.name, {
this.database,
this.idbFactory,
this.maxFileSize = 0,
this.fileStoragePath,
this.deleteFilesAfterDuration,
});

Database? database;

Future<void> open() async {
_collection = await BoxCollection.open(
name,
Expand All @@ -157,6 +165,7 @@ class FluffyBoxDatabase extends DatabaseApi {
_seenDeviceKeysBoxName,
},
sqfliteDatabase: database,
idbFactory: idbFactory,
);
_clientBox = _collection.openBox<String>(
_clientBoxName,
Expand Down Expand Up @@ -234,9 +243,7 @@ class FluffyBoxDatabase extends DatabaseApi {
}

@override
Future<void> clear() async {
// TODO: Implement clear
}
Future<void> clear() => _collection.clear();

@override
Future<void> clearCache() => transaction(() async {
Expand Down Expand Up @@ -681,9 +688,9 @@ class FluffyBoxDatabase extends DatabaseApi {
.toList();
final states = await _roomMembersBox.getAll(keys);
states.removeWhere((state) => state == null);
states.forEach(
(state) => users.add(Event.fromJson(copyMap(state!), room).asUser),
);
for (final state in states) {
users.add(Event.fromJson(copyMap(state!), room).asUser);
}

return users;
}
Expand Down
Loading

0 comments on commit cf071e3

Please sign in to comment.