Skip to content

Commit

Permalink
feat: upstream FluffyBox patches
Browse files Browse the repository at this point in the history
- 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
Show file tree
Hide file tree
Showing 17 changed files with 962 additions and 82 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ unlinked_spec.ds
# Coverage
coverage/

# JavaScipt
web_worker.dart.js*

# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
1 change: 1 addition & 0 deletions hive/.pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!lib/src/backend/js/web_worker/web_worker.dart.js*
10 changes: 6 additions & 4 deletions hive/lib/hive.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,25 @@ import 'package:hive/src/object/hive_object.dart';
import 'package:hive/src/util/extensions.dart';
import 'package:meta/meta.dart';

export 'src/box_collection.dart';
export 'src/object/hive_object.dart' show HiveObject, HiveObjectMixin;

part 'src/annotations/hive_field.dart';
part 'src/annotations/hive_type.dart';
part 'src/binary/binary_reader.dart';
part 'src/binary/binary_writer.dart';
part 'src/box/box_base.dart';
part 'src/box/box.dart';
part 'src/box/box_base.dart';
part 'src/box/lazy_box.dart';
part 'src/crypto/hive_aes_cipher.dart';
part 'src/crypto/hive_cipher.dart';
part 'src/hive.dart';
part 'src/hive_error.dart';
part 'src/object/hive_collection.dart';
part 'src/object/hive_list.dart';
part 'src/registry/type_registry.dart';
part 'src/object/hive_storage_backend_preference.dart';
part 'src/registry/type_adapter.dart';
part 'src/hive_error.dart';
part 'src/hive.dart';
part 'src/registry/type_registry.dart';

/// Global constant to access Hive.
// ignore: non_constant_identifier_names
Expand Down
48 changes: 10 additions & 38 deletions hive/lib/src/backend/js/backend_manager.dart
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();
}
}
}
237 changes: 237 additions & 0 deletions hive/lib/src/backend/js/box_collection_indexed_db.dart
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();
}
Loading

0 comments on commit d34462c

Please sign in to comment.