-
Notifications
You must be signed in to change notification settings - Fork 409
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- add `BoxCollection` - prepare support for other web backends Fixes: #939 Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
- Loading branch information
TheOneWithTheBraid
committed
Apr 29, 2022
1 parent
0651452
commit d34462c
Showing
17 changed files
with
962 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
!lib/src/backend/js/web_worker/web_worker.dart.js* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,18 @@ | ||
import 'dart:html'; | ||
import 'dart:indexed_db'; | ||
import 'dart:js' as js; | ||
import 'package:hive/hive.dart'; | ||
import 'package:hive/src/backend/js/storage_backend_js.dart'; | ||
import 'package:hive/src/backend/storage_backend.dart'; | ||
|
||
/// Opens IndexedDB databases | ||
class BackendManager implements BackendManagerInterface { | ||
IdbFactory? get indexedDB => js.context.hasProperty('window') | ||
? window.indexedDB | ||
: WorkerGlobalScope.instance.indexedDB; | ||
|
||
@override | ||
Future<StorageBackend> open( | ||
String name, String? path, bool crashRecovery, HiveCipher? cipher) async { | ||
var db = await indexedDB!.open(name, version: 1, onUpgradeNeeded: (e) { | ||
var db = e.target.result as Database; | ||
if (!db.objectStoreNames!.contains('box')) { | ||
db.createObjectStore('box'); | ||
} | ||
}); | ||
|
||
return StorageBackendJs(db, cipher); | ||
} | ||
import 'native/backend_manager.dart' as native; | ||
|
||
@override | ||
Future<void> deleteBox(String name, String? path) { | ||
return indexedDB!.deleteDatabase(name); | ||
} | ||
/// Opens IndexedDB databases | ||
abstract class BackendManager { | ||
BackendManager._(); | ||
|
||
@override | ||
Future<bool> boxExists(String name, String? path) async { | ||
// https://stackoverflow.com/a/17473952 | ||
try { | ||
var _exists = true; | ||
await indexedDB!.open(name, version: 1, onUpgradeNeeded: (e) { | ||
e.target.transaction!.abort(); | ||
_exists = false; | ||
}); | ||
return _exists; | ||
} catch (error) { | ||
return false; | ||
// dummy implementation as the WebWorker branch is not stable yet | ||
static BackendManagerInterface select( | ||
[HiveStorageBackendPreference? backendPreference]) { | ||
switch (backendPreference) { | ||
default: | ||
return native.BackendManager(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
import 'dart:html'; | ||
import 'dart:indexed_db'; | ||
|
||
import 'package:hive/hive.dart'; | ||
|
||
/// represents a [BoxCollection] for raw use with indexed DB | ||
class BoxCollection { | ||
final Database _db; | ||
final Set<String> boxNames; | ||
|
||
BoxCollection(this._db, this.boxNames); | ||
|
||
static Future<BoxCollection> open( | ||
String name, | ||
Set<String> boxNames, { | ||
dynamic path, | ||
HiveCipher? key, | ||
}) async { | ||
final factory = window.indexedDB; | ||
if (factory == null) { | ||
throw Exception( | ||
'Unable to open FluffyBox collection - IndexedDB not supported in this browser!'); | ||
} | ||
final _db = await factory.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); | ||
} | ||
|
||
Future<Box<V>> openBox<V>(String name, | ||
{bool preload = false, | ||
Box<V> Function(String, BoxCollection)? boxCreator}) async { | ||
if (!boxNames.contains(name)) { | ||
throw Exception( | ||
'Box with name $name is not in the known box names of this collection.'); | ||
} | ||
final i = _openBoxes.indexWhere((box) => box.name == name); | ||
if (i != -1) { | ||
return _openBoxes[i] as Box<V>; | ||
} | ||
final box = boxCreator?.call(name, this) ?? Box<V>(name, this); | ||
if (preload) { | ||
box._cache.addAll(await box.getAllValues()); | ||
} | ||
_openBoxes.add(box); | ||
return box; | ||
} | ||
|
||
final List<Box> _openBoxes = []; | ||
|
||
List<Future<void> Function(Transaction txn)>? _txnCache; | ||
|
||
Future<void> transaction( | ||
Future<void> Function() action, { | ||
List<String>? boxNames, | ||
bool readOnly = false, | ||
}) async { | ||
boxNames ??= this.boxNames.toList(); | ||
if (_txnCache != null) { | ||
await action(); | ||
return; | ||
} | ||
_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) { | ||
fun(txn); | ||
} | ||
await txn.completed; | ||
return; | ||
} | ||
|
||
void close() => _db.close(); | ||
|
||
Future<void> deleteFromDisk() async { | ||
final factory = window.indexedDB; | ||
for (final box in _openBoxes) { | ||
box._cache.clear(); | ||
box._cachedKeys = null; | ||
} | ||
_openBoxes.clear(); | ||
_db.close(); | ||
if (factory == null || _db.name == null) { | ||
throw Exception('Unable to delete fluffybox collection'); | ||
} | ||
factory.deleteDatabase(_db.name!); | ||
} | ||
} | ||
|
||
class Box<V> { | ||
final String name; | ||
final BoxCollection boxCollection; | ||
final Map<String, V?> _cache = {}; | ||
Set<String>? _cachedKeys; | ||
|
||
Box(this.name, this.boxCollection) { | ||
if (!(V is String || | ||
V is int || | ||
V is Object || | ||
V is List<Object?> || | ||
V is Map<String, Object?> || | ||
V is double)) { | ||
throw Exception( | ||
'Value type ${V.runtimeType} is not one of the allowed value types {String, int, double, List<Object?>, Map<String, Object?>}.'); | ||
} | ||
} | ||
|
||
Future<List<String>> getAllKeys([Transaction? txn]) async { | ||
final cachedKey = _cachedKeys; | ||
if (cachedKey != null) return cachedKey.toList(); | ||
txn ??= boxCollection._db.transaction(name, 'readonly'); | ||
final store = txn.objectStore(name); | ||
final request = store.getAllKeys(null); | ||
await request.onSuccess.first; | ||
final List<String> keys = | ||
List.from(request.result.cast<String>() as Iterable); | ||
_cachedKeys = 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] = 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(store.getObject)); | ||
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 (val == null) { | ||
return delete(key, txn); | ||
} | ||
final txnCache = boxCollection._txnCache; | ||
if (txnCache != null) { | ||
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, key); | ||
_cache[key] = val; | ||
_cachedKeys?.add(key); | ||
return; | ||
} | ||
|
||
Future<void> delete(String key, [Transaction? txn]) async { | ||
final txnCache = boxCollection._txnCache; | ||
if (txnCache != null) { | ||
txnCache.add((txn) => delete(key, txn)); | ||
_cache[key] = null; | ||
_cachedKeys?.remove(key); | ||
return; | ||
} | ||
|
||
txn ??= boxCollection._db.transaction(name, 'readwrite'); | ||
final store = txn.objectStore(name); | ||
await store.delete(key); | ||
_cache[key] = null; | ||
_cachedKeys?.remove(key); | ||
return; | ||
} | ||
|
||
Future<void> deleteAll(List<String> keys, [Transaction? txn]) async { | ||
final txnCache = boxCollection._txnCache; | ||
if (txnCache != null) { | ||
txnCache.add((txn) => deleteAll(keys, txn)); | ||
for (var key in keys) { | ||
_cache[key] = null; | ||
} | ||
_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[key] = null; | ||
_cachedKeys?.removeAll(keys); | ||
} | ||
return; | ||
} | ||
|
||
Future<void> clear([Transaction? txn]) async { | ||
final txnCache = boxCollection._txnCache; | ||
if (txnCache != null) { | ||
txnCache.add(clear); | ||
_cache.clear(); | ||
_cachedKeys = null; | ||
return; | ||
} | ||
|
||
txn ??= boxCollection._db.transaction(name, 'readwrite'); | ||
final store = txn.objectStore(name); | ||
await store.clear(); | ||
_cache.clear(); | ||
_cachedKeys = null; | ||
return; | ||
} | ||
|
||
Future<void> flush() => Future.value(); | ||
} |
Oops, something went wrong.