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 boxNames;
final String name;
BoxCollection(this._db, this.boxNames, this.name);
static Future open(
String name,
Set 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 openBox(String name) {
if (!boxNames.contains(name)) {
throw ('Box with name $name is not in the known box names of this collection.');
}
return Box(name, this);
}
List Function(Transaction txn)>? _txnCache;
Future transaction(
Future Function() action, {
List? boxNames,
bool readOnly = false,
}) =>
zoneTransaction(() async {
boxNames ??= _db.objectStoreNames!.toList();
final txnCache = _txnCache = [];
await action();
final cache =
List 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 clear() async {
final txn = _db.transaction(boxNames.toList(), 'readwrite');
for (final name in boxNames) {
unawaited(txn.objectStore(name).clear());
}
await txn.completed;
}
Future close() async {
assert(_txnCache == null, 'Database closed while in transaction!');
return _db.close();
}
@Deprecated('use collection.deleteDatabase now')
static Future delete(String name, [dynamic factory]) =>
(factory ?? window.indexedDB!).deleteDatabase(name);
Future deleteDatabase(String name, [dynamic factory]) async {
await close();
await (factory ?? window.indexedDB).deleteDatabase(name);
}
}
class Box {
final String name;
final BoxCollection boxCollection;
final Map _cache = {};
/// _cachedKeys 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? _cachedKeys;
bool get _keysCached => _cachedKeys != null;
Box(this.name, this.boxCollection);
Future> 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();
_cachedKeys = keys.toSet();
return keys;
}
Future