247 lines
7.8 KiB
Dart
247 lines
7.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:html';
|
|
import 'dart:indexed_db';
|
|
|
|
import 'package:matrix/src/database/zone_transaction_mixin.dart';
|
|
|
|
/// 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 with ZoneTransactionMixin {
|
|
final Database _db;
|
|
final Set<String> boxNames;
|
|
final String name;
|
|
|
|
BoxCollection(this._db, this.boxNames, this.name);
|
|
|
|
static Future<BoxCollection> open(
|
|
String name,
|
|
Set<String> boxNames, {
|
|
Object? sqfliteDatabase,
|
|
Object? sqfliteFactory,
|
|
IdbFactory? idbFactory,
|
|
int version = 1,
|
|
}) async {
|
|
idbFactory ??= window.indexedDB!;
|
|
final db = await idbFactory.open(
|
|
name,
|
|
version: version,
|
|
onUpgradeNeeded: (VersionChangeEvent event) {
|
|
final db = event.target.result;
|
|
for (final name in boxNames) {
|
|
if (db.objectStoreNames.contains(name)) continue;
|
|
|
|
db.createObjectStore(name, autoIncrement: true);
|
|
}
|
|
},
|
|
);
|
|
return BoxCollection(db, boxNames, name);
|
|
}
|
|
|
|
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,
|
|
}) =>
|
|
zoneTransaction(() async {
|
|
boxNames ??= _db.objectStoreNames!.toList();
|
|
final txnCache = _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) {
|
|
// The IDB methods return a Future in Dart but must not be awaited in
|
|
// order to have an actual transaction. They must only be performed and
|
|
// then the transaction object must call `txn.completed;` which then
|
|
// returns the actual future.
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction
|
|
unawaited(fun(txn));
|
|
}
|
|
await txn.completed;
|
|
return;
|
|
});
|
|
|
|
Future<void> clear() async {
|
|
final txn = _db.transaction(boxNames.toList(), 'readwrite');
|
|
for (final name in boxNames) {
|
|
unawaited(txn.objectStore(name).clear());
|
|
}
|
|
await txn.completed;
|
|
}
|
|
|
|
Future<void> close() async {
|
|
assert(_txnCache == null, 'Database closed while in transaction!');
|
|
// Note, zoneTransaction and txnCache are different kinds of transactions.
|
|
return zoneTransaction(() async => _db.close());
|
|
}
|
|
|
|
@Deprecated('use collection.deleteDatabase now')
|
|
static Future<void> delete(String name, [dynamic factory]) =>
|
|
(factory ?? window.indexedDB!).deleteDatabase(name);
|
|
|
|
Future<void> deleteDatabase(String name, [dynamic factory]) async {
|
|
await close();
|
|
await (factory ?? window.indexedDB).deleteDatabase(name);
|
|
}
|
|
}
|
|
|
|
class Box<V> {
|
|
final String name;
|
|
final BoxCollection boxCollection;
|
|
final Map<String, V?> _quickAccessCache = {};
|
|
|
|
/// _quickAccessCachedKeys is only used to make sure that if you fetch all keys from a
|
|
/// box, you do not need to have an expensive read operation twice. There is
|
|
/// no other usage for this at the moment. So the cache is never partial.
|
|
/// Once the keys are cached, they need to be updated when changed in put and
|
|
/// delete* so that the cache does not become outdated.
|
|
Set<String>? _quickAccessCachedKeys;
|
|
|
|
Box(this.name, this.boxCollection);
|
|
|
|
Future<List<String>> getAllKeys([Transaction? txn]) async {
|
|
if (_quickAccessCachedKeys != null) return _quickAccessCachedKeys!.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>();
|
|
_quickAccessCachedKeys = keys.toSet();
|
|
return keys;
|
|
}
|
|
|
|
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] = _fromValue(cursor.value) as V;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
Future<V?> get(String key, [Transaction? txn]) async {
|
|
if (_quickAccessCache.containsKey(key)) return _quickAccessCache[key];
|
|
txn ??= boxCollection._db.transaction(name, 'readonly');
|
|
final store = txn.objectStore(name);
|
|
_quickAccessCache[key] = await store.getObject(key).then(_fromValue);
|
|
return _quickAccessCache[key];
|
|
}
|
|
|
|
Future<List<V?>> getAll(List<String> keys, [Transaction? txn]) async {
|
|
if (keys.every((key) => _quickAccessCache.containsKey(key))) {
|
|
return keys.map((key) => _quickAccessCache[key]).toList();
|
|
}
|
|
txn ??= boxCollection._db.transaction(name, 'readonly');
|
|
final store = txn.objectStore(name);
|
|
final list = await Future.wait(
|
|
keys.map((key) => store.getObject(key).then(_fromValue)),
|
|
);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
_quickAccessCache[keys[i]] = list[i];
|
|
}
|
|
return list;
|
|
}
|
|
|
|
Future<void> put(String key, V val, [Transaction? txn]) async {
|
|
if (boxCollection._txnCache != null) {
|
|
boxCollection._txnCache!.add((txn) => put(key, val, txn));
|
|
_quickAccessCache[key] = val;
|
|
_quickAccessCachedKeys?.add(key);
|
|
return;
|
|
}
|
|
|
|
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
|
final store = txn.objectStore(name);
|
|
await store.put(val as Object, key);
|
|
_quickAccessCache[key] = val;
|
|
_quickAccessCachedKeys?.add(key);
|
|
return;
|
|
}
|
|
|
|
Future<void> delete(String key, [Transaction? txn]) async {
|
|
if (boxCollection._txnCache != null) {
|
|
boxCollection._txnCache!.add((txn) => delete(key, txn));
|
|
_quickAccessCache[key] = null;
|
|
_quickAccessCachedKeys?.remove(key);
|
|
return;
|
|
}
|
|
|
|
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
|
final store = txn.objectStore(name);
|
|
await store.delete(key);
|
|
|
|
// Set to null instead remove() so that inside of transactions null is
|
|
// returned.
|
|
_quickAccessCache[key] = null;
|
|
_quickAccessCachedKeys?.remove(key);
|
|
return;
|
|
}
|
|
|
|
Future<void> deleteAll(List<String> keys, [Transaction? txn]) async {
|
|
if (boxCollection._txnCache != null) {
|
|
boxCollection._txnCache!.add((txn) => deleteAll(keys, txn));
|
|
for (final key in keys) {
|
|
_quickAccessCache[key] = null;
|
|
}
|
|
_quickAccessCachedKeys?.removeAll(keys);
|
|
return;
|
|
}
|
|
|
|
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
|
final store = txn.objectStore(name);
|
|
for (final key in keys) {
|
|
await store.delete(key);
|
|
_quickAccessCache[key] = null;
|
|
_quickAccessCachedKeys?.remove(key);
|
|
}
|
|
return;
|
|
}
|
|
|
|
void clearQuickAccessCache() {
|
|
_quickAccessCache.clear();
|
|
_quickAccessCachedKeys = null;
|
|
}
|
|
|
|
Future<void> clear([Transaction? txn]) async {
|
|
if (boxCollection._txnCache != null) {
|
|
boxCollection._txnCache!.add((txn) => clear(txn));
|
|
} else {
|
|
txn ??= boxCollection._db.transaction(name, 'readwrite');
|
|
final store = txn.objectStore(name);
|
|
await store.clear();
|
|
}
|
|
|
|
clearQuickAccessCache();
|
|
}
|
|
|
|
V? _fromValue(Object? value) {
|
|
if (value == null) return null;
|
|
switch (V) {
|
|
case const (List<dynamic>):
|
|
return List.unmodifiable(value as List) as V;
|
|
case const (Map<dynamic, dynamic>):
|
|
return Map.unmodifiable(value as Map) as V;
|
|
case const (int):
|
|
case const (double):
|
|
case const (bool):
|
|
case const (String):
|
|
default:
|
|
return value as V;
|
|
}
|
|
}
|
|
}
|