diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b26b4661b..6f6632fbf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,7 +24,6 @@ jobs: lint: name: Check lints runs-on: ubuntu-latest - if: ${{ false }} steps: - uses: actions/checkout@v3 - name: Setup Flutter @@ -45,7 +44,76 @@ jobs: dart tool/generate_all_tests.dart working-directory: packages/isar_test - name: Lint - run: flutter analyze + run: flutter analyze --no-fatal-infos + + dart_backends: + name: Dart memory and web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ secrets.FLUTTER_VERSION }} + - name: Resolve Isar + run: dart pub get + working-directory: packages/isar + - name: Test memory without native core + run: dart test test/memory_engine_test.dart + working-directory: packages/isar + - name: Resolve backend conformance tests + run: flutter pub get + working-directory: packages/isar_test + - name: Test full memory backend conformance + run: flutter test -j 1 --dart-define=ISAR_TEST_BACKEND=memory + working-directory: packages/isar_test + - name: Test IndexedDB in Chrome + run: dart test -p chrome test/web_indexeddb_test.dart + working-directory: packages/isar + - name: Test IndexedDB with WebAssembly + run: dart test -p chrome --compiler dart2wasm test/web_indexeddb_test.dart + working-directory: packages/isar + - name: Compile JavaScript boundary + run: dart compile js tool/compile_smoke.dart -o /tmp/isar-smoke.js + working-directory: packages/isar + - name: Compile WebAssembly boundary + run: dart compile wasm tool/compile_smoke.dart -o /tmp/isar-smoke.wasm + working-directory: packages/isar + - name: Build inspector JavaScript + run: flutter build web + working-directory: packages/isar_inspector + - name: Build inspector WebAssembly + run: flutter build web --wasm + working-directory: packages/isar_inspector + - name: Reject retired web bridge + run: | + ! rg "dart:(html|indexed_db|js'|js_util)|package:js/|unpkg.com/isar" \ + packages + + web_conformance: + name: Web conformance (${{ matrix.compiler }}, shard ${{ matrix.shard }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [dart2js, dart2wasm] + shard: [0, 1, 2, 3] + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ secrets.FLUTTER_VERSION }} + - name: Resolve test dependencies + run: flutter pub get + working-directory: packages/isar_test + - name: Generate test models + run: dart run build_runner build --delete-conflicting-outputs + working-directory: packages/isar_test + - name: Generate conformance aggregator + run: dart tool/generate_all_tests.dart + working-directory: packages/isar_test + - name: Run shared web conformance + run: dart test -p chrome --compiler ${{ matrix.compiler }} test/web_conformance_${{ matrix.shard }}_test.dart --reporter=failures-only + working-directory: packages/isar_test test: name: Dart Test diff --git a/packages/isar/README.md b/packages/isar/README.md index 2466b448e..648359af3 100644 --- a/packages/isar/README.md +++ b/packages/isar/README.md @@ -116,6 +116,26 @@ final isar = await Isar.open( ); ``` +On web, omit `directory`. `Isar.open()` automatically uses IndexedDB and is +asynchronous. The web backend is currently beta: it hydrates the database into +memory and executes scan-based queries, so avoid making large-database +performance claims. Browser quota and private-mode restrictions apply. +Synchronous APIs, inspector, isolates, compact/copy, and multi-tab writers are +not supported on web. Both JavaScript and WebAssembly builds are supported. +Records are stored individually in per-collection IndexedDB object stores. +Commits update records, links, counters, schema metadata, and a monotonic +revision in one atomic transaction. Adding or removing collections, properties, +indexes, or links is migrated automatically; changing an existing property's +type is rejected. A BroadcastChannel lease permits one owner per database name, +and revision checks reject stale writers. + +For disposable databases in tests or tools, import `package:isar/isar_memory.dart` +and use `IsarMemory.open()` or `IsarMemory.openSync()`. Memory instances load no +native library, create no files, and discard their state when closed. Disk +persistence and schema migration, isolates, configured file-size limits, and +filesystem compact/copy operations are intentionally unavailable for memory +instances; CRUD, transactions, queries, links, JSON, and watchers are supported. + ### 4. Query the database ```dart diff --git a/packages/isar/lib/isar.dart b/packages/isar/lib/isar.dart index 429086e5f..d32d5e2a4 100644 --- a/packages/isar/lib/isar.dart +++ b/packages/isar/lib/isar.dart @@ -5,15 +5,17 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:typed_data'; +import 'package:isar/src/common/platform.dart' + if (dart.library.js_interop) 'package:isar/src/common/platform_web.dart' + as platform; import 'package:isar/src/isar_connect_api.dart'; import 'package:isar/src/native/isar_core.dart' - if (dart.library.html) 'package:isar/src/web/isar_web.dart'; + if (dart.library.js_interop) 'package:isar/src/web/isar_web.dart'; import 'package:isar/src/native/isar_link_impl.dart' - if (dart.library.html) 'package:isar/src/web/isar_link_impl.dart'; + if (dart.library.js_interop) 'package:isar/src/dart_engine/link_impl.dart'; import 'package:isar/src/native/open.dart' - if (dart.library.html) 'package:isar/src/web/open.dart'; -import 'package:isar/src/native/split_words.dart' - if (dart.library.html) 'package:isar/src/web/split_words.dart'; + if (dart.library.js_interop) 'package:isar/src/web/indexeddb_open.dart'; +import 'package:isar/src/web/split_words.dart'; import 'package:meta/meta.dart'; import 'package:meta/meta_meta.dart'; @@ -46,4 +48,4 @@ part 'src/schema/schema.dart'; @protected typedef IsarUint8List = Uint8List; -const bool _kIsWeb = identical(0, 0.0); +const bool _kIsWeb = platform.isWeb; diff --git a/packages/isar/lib/isar_memory.dart b/packages/isar/lib/isar_memory.dart new file mode 100644 index 000000000..ba7d9f57f --- /dev/null +++ b/packages/isar/lib/isar_memory.dart @@ -0,0 +1,51 @@ +library isar_memory; + +import 'package:isar/isar.dart'; +import 'package:isar/src/dart_engine/engine.dart'; + +/// Opens disposable Isar databases backed entirely by Dart memory. +abstract final class IsarMemory { + /// Opens an in-memory database asynchronously. + static Future open( + List> schemas, { + String name = Isar.defaultName, + }) async => + openSync(schemas, name: name); + + /// Opens an in-memory database synchronously. + static Isar openSync( + List> schemas, { + String name = Isar.defaultName, + }) { + _validate(name, schemas); + return DartEngineIsar(name, schemas); + } + + static void _validate( + String name, + List> schemas, + ) { + if (name.isEmpty || name.startsWith('_')) { + throw IsarError('Instance names must not be empty or start with "_".'); + } + if (Isar.getInstance(name) != null) { + throw IsarError('Instance has already been opened.'); + } + if (schemas.isEmpty) { + throw IsarError('At least one collection needs to be opened.'); + } + final names = schemas.map((schema) => schema.name).toSet(); + if (names.length != schemas.length) { + throw IsarError('Duplicate collection schema.'); + } + for (final schema in schemas) { + for (final target in schema.links.values.map((link) => link.target)) { + if (!names.contains(target)) { + throw IsarError( + 'Collection ${schema.name} depends on missing collection $target.', + ); + } + } + } + } +} diff --git a/packages/isar/lib/src/common/isar_common.dart b/packages/isar/lib/src/common/isar_common.dart index ea0873e32..59fa5a178 100644 --- a/packages/isar/lib/src/common/isar_common.dart +++ b/packages/isar/lib/src/common/isar_common.dart @@ -181,11 +181,11 @@ abstract class IsarCommon extends Isar { await Future.wait(_activeAsyncTxns); await super.close(); - return performClose(deleteFromDisk); + return await performClose(deleteFromDisk); } /// @nodoc - bool performClose(bool deleteFromDisk); + FutureOr performClose(bool deleteFromDisk); } /// @nodoc diff --git a/packages/isar/lib/src/common/isar_link_backend.dart b/packages/isar/lib/src/common/isar_link_backend.dart new file mode 100644 index 000000000..ecf623c04 --- /dev/null +++ b/packages/isar/lib/src/common/isar_link_backend.dart @@ -0,0 +1,22 @@ +import 'package:isar/isar.dart'; + +/// Internal backend-neutral dispatch used by generated link objects. +abstract interface class IsarLinkBackend { + Future updateLinkBackend({ + required IsarCollection targetCollection, + required String linkName, + required Id sourceId, + required Iterable link, + required Iterable unlink, + required bool reset, + }); + + void updateLinkBackendSync({ + required IsarCollection targetCollection, + required String linkName, + required Id sourceId, + required Iterable link, + required Iterable unlink, + required bool reset, + }); +} diff --git a/packages/isar/lib/src/common/isar_link_common.dart b/packages/isar/lib/src/common/isar_link_common.dart index 410ccd3bf..8fd3cc847 100644 --- a/packages/isar/lib/src/common/isar_link_common.dart +++ b/packages/isar/lib/src/common/isar_link_common.dart @@ -1,11 +1,12 @@ import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; - -const bool _kIsWeb = identical(0, 0.0); +import 'package:isar/src/common/platform.dart' + if (dart.library.js_interop) 'package:isar/src/common/platform_web.dart' + as platform; /// @nodoc abstract class IsarLinkCommon extends IsarLinkBaseImpl - with IsarLink { + implements IsarLink { OBJ? _value; @override @@ -16,7 +17,7 @@ abstract class IsarLinkCommon extends IsarLinkBaseImpl @override OBJ? get value { - if (isAttached && !isLoaded && !isChanged && !_kIsWeb) { + if (isAttached && !isLoaded && !isChanged && !platform.isWeb) { loadSync(); } return _value; diff --git a/packages/isar/lib/src/common/isar_links_common.dart b/packages/isar/lib/src/common/isar_links_common.dart index 98975b765..2e8013d2b 100644 --- a/packages/isar/lib/src/common/isar_links_common.dart +++ b/packages/isar/lib/src/common/isar_links_common.dart @@ -2,12 +2,14 @@ import 'dart:collection'; import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; - -const bool _kIsWeb = identical(0, 0.0); +import 'package:isar/src/common/platform.dart' + if (dart.library.js_interop) 'package:isar/src/common/platform_web.dart' + as platform; /// @nodoc abstract class IsarLinksCommon extends IsarLinkBaseImpl - with IsarLinks, SetMixin { + with SetMixin + implements IsarLinks { final _objects = {}; /// @nodoc @@ -22,8 +24,14 @@ abstract class IsarLinksCommon extends IsarLinkBaseImpl @override bool get isChanged => addedObjects.isNotEmpty || removedObjects.isNotEmpty; + @override + Future count() => filter().count(); + + @override + int countSync() => filter().countSync(); + Map get _loadedObjects { - if (isAttached && !isLoaded && !_kIsWeb) { + if (isAttached && !isLoaded && !platform.isWeb) { loadSync(); } return _objects; diff --git a/packages/isar/lib/src/common/platform.dart b/packages/isar/lib/src/common/platform.dart new file mode 100644 index 000000000..2cdc5068f --- /dev/null +++ b/packages/isar/lib/src/common/platform.dart @@ -0,0 +1 @@ +const bool isWeb = false; diff --git a/packages/isar/lib/src/common/platform_web.dart b/packages/isar/lib/src/common/platform_web.dart new file mode 100644 index 000000000..69fda2b02 --- /dev/null +++ b/packages/isar/lib/src/common/platform_web.dart @@ -0,0 +1 @@ +const bool isWeb = true; diff --git a/packages/isar/lib/src/dart_engine/engine.dart b/packages/isar/lib/src/dart_engine/engine.dart new file mode 100644 index 000000000..9fae26334 --- /dev/null +++ b/packages/isar/lib/src/dart_engine/engine.dart @@ -0,0 +1,1649 @@ +// ignore_for_file: invalid_use_of_protected_member, public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:isar/isar.dart'; +import 'package:isar/src/common/isar_common.dart'; +import 'package:isar/src/common/isar_link_backend.dart'; +import 'package:isar/src/web/isar_reader_impl.dart'; +import 'package:isar/src/web/isar_writer_impl.dart'; + +typedef EngineState = Map>>; + +class DartEngineIsar extends IsarCommon { + DartEngineIsar( + super.name, + List> schemas, { + EngineState? initialState, + this.persist, + this.onClose, + }) { + for (final schema in schemas) { + offsets[schema.type] = _offsets(schema); + for (final embedded in schema.embeddedSchemas.values) { + offsets[embedded.type] = _offsets(embedded); + } + _state[schema.name] = initialState?[schema.name] ?? {}; + } + if (initialState != null) { + for (final entry in initialState.entries) { + if (entry.key.startsWith('@')) { + _state[entry.key] = entry.value; + } + } + } + + final collections = >{}; + for (final schema in schemas) { + schema.toCollection(() { + final typed = schema as CollectionSchema; + collections[OBJ] = DartEngineCollection(this, typed); + }); + } + attachCollections(collections); + } + + final offsets = >{}; + EngineState _state = {}; + final Future Function(EngineState state)? persist; + final FutureOr Function(bool deleteFromDisk)? onClose; + Future _writeTail = Future.value(); + final _watchers = >{}; + final _objectWatchers = >{}; + + @override + String? get directory => null; + + @override + Future beginTxn(bool write, bool silent) async { + if (!write) { + return EngineTransaction(this, false, silent, _copyState(_state)); + } + final previous = _writeTail; + final release = Completer(); + _writeTail = previous.then((_) => release.future); + await previous; + return EngineTransaction( + this, + true, + silent, + _copyState(_state), + release: release, + ); + } + + @override + Transaction beginTxnSync(bool write, bool silent) => + EngineTransaction(this, write, silent, _copyState(_state)); + + void publish(EngineTransaction transaction) { + if (!transaction.write) return; + _state = transaction.state; + if (!transaction.silent) { + for (final name in transaction.changed) { + _watchers[name]?.add(null); + } + for (final entry in transaction.changedObjects.entries) { + for (final id in entry.value) { + _objectWatchers['${entry.key}:$id']?.add(null); + } + } + } + } + + Future commit(EngineTransaction transaction) async { + if (!transaction.write) return; + await persist?.call(_copyState(transaction.state)); + publish(transaction); + } + + Stream watchCollection(String name, bool fireImmediately) { + final controller = + _watchers.putIfAbsent(name, StreamController.broadcast); + if (!fireImmediately) return controller.stream; + return (() async* { + yield null; + yield* controller.stream; + })(); + } + + Stream watchObject(String name, Id id, bool fireImmediately) { + final key = '$name:$id'; + final controller = + _objectWatchers.putIfAbsent(key, StreamController.broadcast); + if (!fireImmediately) return controller.stream; + return (() async* { + yield null; + yield* controller.stream; + })(); + } + + String _linkStore(String collection, String link) => + '@link:$collection:$link'; + + DartEngineCollection _collection(String name) => + getCollectionByNameInternal(name) as DartEngineCollection; + + Set linkedIds( + EngineTransaction transaction, + String collection, + String link, + Id sourceId, + ) { + final source = _collection(collection); + final schema = source.schema.link(link); + if (!schema.isBacklink) { + final record = transaction.state[_linkStore(collection, link)] + ?[sourceId] ?? + const {}; + return record.keys.whereType().toSet(); + } + final result = {}; + final store = + transaction.state[_linkStore(schema.target, schema.linkName!)] ?? + const {}; + for (final entry in store.entries) { + if (entry.value.containsKey(sourceId)) result.add(entry.key); + } + return result; + } + + void updateLink( + EngineTransaction transaction, + String collection, + String link, + Id sourceId, { + required Iterable add, + required Iterable remove, + required bool reset, + }) { + final source = _collection(collection); + final schema = source.schema.link(link); + if (schema.isBacklink) { + final store = transaction.state.putIfAbsent( + _linkStore(schema.target, schema.linkName!), + () => {}, + ); + if (reset) { + for (final targets in store.values) { + targets.remove(sourceId); + } + } + for (final id in remove) { + store[id]?.remove(sourceId); + } + for (final id in add) { + store.putIfAbsent(id, () => {})[sourceId] = true; + } + transaction.changed + ..add(collection) + ..add(schema.target); + transaction.changedObjects + .putIfAbsent(collection, () => {}) + .add(sourceId); + return; + } + final store = transaction.state.putIfAbsent( + _linkStore(collection, link), + () => {}, + ); + final targets = store.putIfAbsent(sourceId, () => {}); + if (reset) targets.clear(); + for (final id in remove) { + targets.remove(id); + } + for (final id in add) { + if (schema.single) targets.clear(); + targets[id] = true; + } + transaction.changed.add(collection); + transaction.changedObjects.putIfAbsent(collection, () => {}).add(sourceId); + } + + void cleanupLinks( + EngineTransaction transaction, + String collection, + Id id, + ) { + for (final entry in transaction.state.entries) { + if (!entry.key.startsWith('@link:')) continue; + final parts = entry.key.split(':'); + if (parts.length < 3) continue; + final sourceCollection = parts[1]; + final linkName = parts.sublist(2).join(':'); + if (sourceCollection == collection) { + entry.value.remove(id); + } + final link = _collection(sourceCollection).schema.link(linkName); + if (link.target == collection) { + for (final targets in entry.value.values) { + targets.remove(id); + } + } + } + } + + @override + Future getSize({ + bool includeIndexes = false, + bool includeLinks = false, + }) async => + getSizeSync( + includeIndexes: includeIndexes, + includeLinks: includeLinks, + ); + + @override + int getSizeSync({ + bool includeIndexes = false, + bool includeLinks = false, + }) => + _state.values.fold( + 0, + (total, collection) => + total + + collection.values.fold( + 0, + (size, value) => size + _estimateValue(value), + ), + ); + + @override + Future copyToFile(String targetPath) => + Future.error(UnsupportedError('The Dart engine has no filesystem.')); + + @override + Future performClose(bool deleteFromDisk) async { + _state.clear(); + await onClose?.call(deleteFromDisk); + for (final watcher in _watchers.values) { + await watcher.close(); + } + for (final watcher in _objectWatchers.values) { + await watcher.close(); + } + return true; + } + + @override + Future verify() async { + for (final collection in _state.values) { + for (final entry in collection.entries) { + if (entry.key == Isar.autoIncrement) { + throw IsarError('Invalid auto increment id in committed state.'); + } + } + } + } +} + +class EngineTransaction extends Transaction { + EngineTransaction( + DartEngineIsar isar, + bool write, + this.silent, + this.state, { + this.release, + }) : super(isar, true, write); + + final bool silent; + final EngineState state; + final Completer? release; + final changed = {}; + final changedObjects = >{}; + bool _active = true; + + @override + bool get active => _active; + + @override + Future abort() async => abortSync(); + + @override + void abortSync() { + _active = false; + if (release?.isCompleted == false) release!.complete(); + } + + @override + Future commit() async { + if (!_active) throw IsarError('Transaction is no longer active.'); + await (isar as DartEngineIsar).commit(this); + _active = false; + if (release?.isCompleted == false) release!.complete(); + } + + @override + void commitSync() { + if (!_active) throw IsarError('Transaction is no longer active.'); + if (write && (isar as DartEngineIsar).persist != null) { + throw UnsupportedError('Persistent transactions must be asynchronous.'); + } + (isar as DartEngineIsar).publish(this); + _active = false; + if (release?.isCompleted == false) release!.complete(); + } +} + +class DartEngineCollection extends IsarCollection + implements IsarLinkBackend { + DartEngineCollection(this.isar, this.schema); + + @override + final DartEngineIsar isar; + @override + final CollectionSchema schema; + + List get _offsets => isar.offsets[OBJ]!; + + Map _serialize(OBJ object) { + final data = {}; + schema.serialize(object, IsarWriterImpl(data), _offsets, isar.offsets); + return _deepCopy(data) as Map; + } + + OBJ _deserialize(Id id, Map data) { + final object = schema.deserialize( + id, + IsarReaderImpl(_deepCopy(data) as Map), + _offsets, + isar.offsets, + ); + schema.attach(this, id, object); + return object; + } + + Map> _records(EngineTransaction transaction) => + transaction.state[name]!; + + int _counter(EngineTransaction transaction) => + transaction.state['@counters']?[schema.id]?[0] as int? ?? 0; + + void _setCounter(EngineTransaction transaction, int value) { + final counters = transaction.state.putIfAbsent('@counters', () => {}); + counters[schema.id] = {0: value}; + } + + @override + Future> getAll(List ids) => + isar.getTxn(false, (EngineTransaction transaction) async { + final records = _records(transaction); + return ids + .map((id) => + records[id] == null ? null : _deserialize(id, records[id]!)) + .toList(); + }); + + @override + List getAllSync(List ids) => + isar.getTxnSync(false, (EngineTransaction transaction) { + final records = _records(transaction); + return ids + .map((id) => + records[id] == null ? null : _deserialize(id, records[id]!)) + .toList(); + }); + + Id _put(EngineTransaction transaction, OBJ object, String? indexName) { + final records = _records(transaction); + var id = schema.getId(object); + if (indexName != null) { + final existing = + _findByIndex(records, indexName, _indexKey(object, indexName)); + if (existing != null) id = existing; + } + if (id == Isar.autoIncrement) { + id = _counter(transaction) + 1; + } + if (id > _counter(transaction)) { + _setCounter(transaction, id); + } + final data = _serialize(object); + for (final index in schema.indexes.values.where((index) => index.unique)) { + final key = _indexKeyData(data, index.name); + final duplicate = _findByIndex(records, index.name, key); + if (duplicate != null && duplicate != id) { + if (index.replace) { + records.remove(duplicate); + } else { + throw IsarUniqueViolationError(); + } + } + } + records[id] = data; + schema.attach(this, id, object); + transaction.changed.add(name); + transaction.changedObjects.putIfAbsent(name, () => {}).add(id); + return id; + } + + @override + Future> putAll(List objects) => putAllByIndex(null, objects); + + @override + Future> putAllByIndex(String? indexName, List objects) => + isar.getTxn( + true, + (EngineTransaction transaction) async => objects + .map((object) => _put(transaction, object, indexName)) + .toList()); + + @override + List putAllSync(List objects, {bool saveLinks = true}) => + putAllByIndexSync(null, objects, saveLinks: saveLinks); + + @override + List putAllByIndexSync( + String? indexName, + List objects, { + bool saveLinks = true, + }) => + isar.getTxnSync(true, (EngineTransaction transaction) { + final ids = objects + .map((object) => _put(transaction, object, indexName)) + .toList(); + if (saveLinks) { + for (final object in objects) { + for (final link in schema.getLinks(object)) { + link.saveSync(); + } + } + } + return ids; + }); + + IndexKey _indexKey(OBJ object, String indexName) => + _indexKeyData(_serialize(object), indexName); + + IndexKey _indexKeyData(Map data, String indexName) { + final keys = [..._indexKeysData(data, indexName)]; + if (keys.isEmpty) return const []; + keys.sort(_compareKeys); + return keys.first; + } + + List _indexKeysData( + Map data, + String indexName, + ) { + final index = schema.index(indexName); + if (index.properties.length == 1) { + final indexProperty = index.properties.first; + final property = schema.property(indexProperty.name); + final value = _propertyValue(data, indexProperty.name); + if (property.type.isList && indexProperty.type != IndexType.hash) { + if (value == null) return const []; + if (value is! List || value.isEmpty) return const []; + return value.map((element) { + dynamic normalized = element; + if (normalized is String && !indexProperty.caseSensitive) { + normalized = normalized.toLowerCase(); + } + return [normalized]; + }).toList(); + } + } + return [ + index.properties.map((property) { + var value = _propertyValue(data, property.name); + if (value is String && !property.caseSensitive) { + value = value.toLowerCase(); + } + return value; + }).toList(), + ]; + } + + dynamic _propertyValue(Map data, String propertyName) { + final property = schema.property(propertyName); + return schema.deserializeProp( + IsarReaderImpl(data), + property.id, + _offsets[property.id], + isar.offsets, + ); + } + + IndexKey? _normalizeIndexKey(String indexName, IndexKey? key) { + if (key == null) return null; + final properties = schema.index(indexName).properties; + return [ + for (var i = 0; i < key.length; i++) + key[i] is String && + i < properties.length && + !properties[i].caseSensitive + ? (key[i] as String).toLowerCase() + : key[i], + ]; + } + + Id? _findByIndex( + Map> records, + String indexName, + IndexKey key, + ) { + final normalizedKey = _normalizeIndexKey(indexName, key)!; + for (final entry in records.entries) { + if (_indexKeysData(entry.value, indexName).any( + (candidate) => _compareKeyToBound(candidate, normalizedKey) == 0, + )) { + return entry.key; + } + } + return null; + } + + @override + Future> getAllByIndex(String indexName, List keys) => + isar.getTxn(false, (EngineTransaction transaction) async { + final records = _records(transaction); + return keys.map((key) { + final id = _findByIndex(records, indexName, key); + return id == null ? null : _deserialize(id, records[id]!); + }).toList(); + }); + + @override + List getAllByIndexSync(String indexName, List keys) => + isar.getTxnSync(false, (EngineTransaction transaction) { + final records = _records(transaction); + return keys.map((key) { + final id = _findByIndex(records, indexName, key); + return id == null ? null : _deserialize(id, records[id]!); + }).toList(); + }); + + @override + Future deleteAll(List ids) => isar.getTxn(true, + (EngineTransaction transaction) async => _deleteIds(transaction, ids)); + + @override + int deleteAllSync(List ids) => isar.getTxnSync( + true, (EngineTransaction transaction) => _deleteIds(transaction, ids)); + + int _deleteIds(EngineTransaction transaction, Iterable ids) { + final records = _records(transaction); + var count = 0; + for (final id in ids.toSet()) { + if (records.remove(id) != null) { + count++; + transaction.changedObjects.putIfAbsent(name, () => {}).add(id); + isar.cleanupLinks(transaction, name, id); + } + } + if (count != 0) transaction.changed.add(name); + return count; + } + + @override + Future deleteAllByIndex(String indexName, List keys) => + isar.getTxn(true, (EngineTransaction transaction) async { + final records = _records(transaction); + return _deleteIds( + transaction, + keys + .map((key) => _findByIndex(records, indexName, key)) + .whereType(), + ); + }); + + @override + int deleteAllByIndexSync(String indexName, List keys) => + isar.getTxnSync(true, (EngineTransaction transaction) { + final records = _records(transaction); + return _deleteIds( + transaction, + keys + .map((key) => _findByIndex(records, indexName, key)) + .whereType(), + ); + }); + + @override + Future clear() => + isar.getTxn(true, (EngineTransaction transaction) async { + final ids = _records(transaction).keys.toList(); + transaction.changedObjects.putIfAbsent(name, () => {}).addAll(ids); + _records(transaction).clear(); + for (final id in ids) { + isar.cleanupLinks(transaction, name, id); + } + _setCounter(transaction, 0); + transaction.changed.add(name); + }); + + @override + void clearSync() => isar.getTxnSync(true, (EngineTransaction transaction) { + final ids = _records(transaction).keys.toList(); + transaction.changedObjects.putIfAbsent(name, () => {}).addAll(ids); + _records(transaction).clear(); + for (final id in ids) { + isar.cleanupLinks(transaction, name, id); + } + _setCounter(transaction, 0); + transaction.changed.add(name); + }); + + @override + Future count() async => (await getAllIds()).length; + + Future> getAllIds() => isar.getTxn( + false, + (EngineTransaction transaction) async => + _records(transaction).keys.toList()); + + @override + int countSync() => isar.getTxnSync( + false, + (EngineTransaction transaction) => _records(transaction).length, + ); + + @override + Query buildQuery({ + List whereClauses = const [], + bool whereDistinct = false, + Sort whereSort = Sort.asc, + FilterOperation? filter, + List sortBy = const [], + List distinctBy = const [], + int? offset, + int? limit, + String? property, + }) => + DartEngineQuery( + this, + whereClauses, + whereDistinct, + whereSort, + filter, + sortBy, + distinctBy, + offset, + limit, + property, + ); + + @override + Future importJson(List> json) => + isar.getTxn(true, (EngineTransaction transaction) async { + _importJson(transaction, json); + }); + + @override + void importJsonSync(List> json) => + isar.getTxnSync(true, (EngineTransaction transaction) { + _importJson(transaction, json); + }); + + void _importJson( + EngineTransaction transaction, + List> objects, + ) { + final records = _records(transaction); + for (final object in objects) { + var id = object[schema.idName] as int? ?? Isar.autoIncrement; + if (id == Isar.autoIncrement) id = _counter(transaction) + 1; + if (id > _counter(transaction)) _setCounter(transaction, id); + final data = _encodeJsonObject(schema, object); + data['@json'] = _deepCopy(object); + for (final index + in schema.indexes.values.where((index) => index.unique)) { + final duplicate = _findByIndex( + records, + index.name, + _indexKeyData(data, index.name), + ); + if (duplicate != null && duplicate != id) { + if (index.replace) { + records.remove(duplicate); + } else { + throw IsarUniqueViolationError(); + } + } + } + records[id] = data; + transaction.changed.add(name); + transaction.changedObjects.putIfAbsent(name, () => {}).add(id); + } + } + + Map _encodeJsonObject( + Schema objectSchema, + Map json, + ) { + final offsets = isar.offsets[objectSchema.type]!; + return { + for (final property in objectSchema.properties.values) + offsets[property.id]: + _encodeJsonProperty(property, json[property.name]), + }; + } + + dynamic _encodeJsonProperty(PropertySchema property, dynamic value) { + if (value == null) return null; + if (property.type == IsarType.object) { + final embedded = schema.embeddedSchemas[property.target]!; + return _encodeJsonObject(embedded, (value as Map).cast()); + } + if (property.type == IsarType.objectList) { + final embedded = schema.embeddedSchemas[property.target]!; + return (value as List) + .map((item) => item == null + ? null + : _encodeJsonObject(embedded, (item as Map).cast())) + .toList(); + } + if (property.type == IsarType.bool) return value == true ? 1 : 0; + if (property.type == IsarType.boolList) { + return (value as List) + .map((item) => item == null ? null : (item == true ? 1 : 0)) + .toList(); + } + if (property.type.scalarType == IsarType.dateTime) { + dynamic encode(dynamic item) => item == null + ? null + : item is int + ? item + : (item is DateTime ? item : DateTime.parse(item as String)) + .toUtc() + .microsecondsSinceEpoch; + return property.type.isList + ? (value as List).map(encode).toList() + : encode(value); + } + if (property.type.scalarType == IsarType.float) { + dynamic encode(dynamic item) => item == null + ? null + : (Float32List(1)..[0] = (item as num).toDouble())[0]; + return property.type.isList + ? (value as List).map(encode).toList() + : encode(value); + } + return _deepCopy(value); + } + + Map _decodeJsonObject( + Schema objectSchema, + Map data, + ) { + final offsets = isar.offsets[objectSchema.type]!; + return { + for (final property in objectSchema.properties.values) + property.name: _decodeJsonProperty( + property, + data[offsets[property.id]], + ), + }; + } + + dynamic _decodeJsonProperty(PropertySchema property, dynamic value) { + if (value == null) return null; + if (property.type == IsarType.object) { + return _decodeJsonObject( + schema.embeddedSchemas[property.target]!, + value as Map, + ); + } + if (property.type == IsarType.objectList) { + final embedded = schema.embeddedSchemas[property.target]!; + return (value as List) + .map((item) => item == null + ? null + : _decodeJsonObject( + embedded, + item as Map, + )) + .toList(); + } + if (property.type == IsarType.bool) return value == 1; + if (property.type == IsarType.boolList) { + return (value as List) + .map((item) => item == null ? null : item == 1) + .toList(); + } + return _deepCopy(value); + } + + @override + Future importJsonRaw(Uint8List jsonBytes) async { + try { + final decoded = jsonDecode(utf8.decode(jsonBytes)); + if (decoded is! List) throw const FormatException('Expected a list.'); + await importJson(decoded.cast()); + } catch (error) { + throw IsarError('Invalid JSON: $error'); + } + } + + @override + void importJsonRawSync(Uint8List jsonBytes) { + try { + final decoded = jsonDecode(utf8.decode(jsonBytes)); + if (decoded is! List) throw const FormatException('Expected a list.'); + importJsonSync(decoded.cast()); + } catch (error) { + throw IsarError('Invalid JSON: $error'); + } + } + + @override + Future getSize({ + bool includeIndexes = false, + bool includeLinks = false, + }) async => + getSizeSync(includeIndexes: includeIndexes, includeLinks: includeLinks); + + @override + int getSizeSync({ + bool includeIndexes = false, + bool includeLinks = false, + }) => + isar.getTxnSync(false, (EngineTransaction transaction) { + final records = _records(transaction); + var size = records.values.fold( + 0, + (total, value) => total + _estimateValue(value), + ); + if (includeIndexes) { + for (final record in records.values) { + for (final index in schema.indexes.values) { + size += _indexKeysData(record, index.name).length * 16; + } + } + } + if (includeLinks) { + for (final link in schema.links.values.where( + (link) => !link.isBacklink, + )) { + for (final id in records.keys) { + size += + isar.linkedIds(transaction, name, link.name, id).length * 16; + } + } + } + return size; + }); + + @override + Stream watchLazy({bool fireImmediately = false}) => + isar.watchCollection(name, fireImmediately); + + @override + Stream watchObject(Id id, {bool fireImmediately = false}) { + final changes = isar.watchObject(name, id, false).asyncMap((_) => get(id)); + if (!fireImmediately) return changes; + return (() async* { + yield await get(id); + yield* changes; + })(); + } + + @override + Stream watchObjectLazy(Id id, {bool fireImmediately = false}) => + isar.watchObject(name, id, fireImmediately); + + @override + Future verify(List objects) async { + final actual = await getAll(objects.map(schema.getId).toList()); + if (actual.any((object) => object == null)) { + throw IsarError('Collection contents do not match expected objects.'); + } + } + + @override + Future updateLinkBackend({ + required IsarCollection targetCollection, + required String linkName, + required Id sourceId, + required Iterable link, + required Iterable unlink, + required bool reset, + }) async { + final target = targetCollection as DartEngineCollection; + await isar.getTxn(true, (EngineTransaction transaction) async { + final add = []; + for (final object in link) { + var id = target.schema.getId(object); + if (id == Isar.autoIncrement) id = await target.put(object); + add.add(id); + } + isar.updateLink( + transaction, + name, + linkName, + sourceId, + add: add, + remove: unlink.map(target.schema.getId), + reset: reset, + ); + }); + } + + @override + void updateLinkBackendSync({ + required IsarCollection targetCollection, + required String linkName, + required Id sourceId, + required Iterable link, + required Iterable unlink, + required bool reset, + }) { + final target = targetCollection as DartEngineCollection; + isar.getTxnSync(true, (EngineTransaction transaction) { + final add = []; + for (final object in link) { + var id = target.schema.getId(object); + if (id == Isar.autoIncrement) id = target.putSync(object); + add.add(id); + } + isar.updateLink( + transaction, + name, + linkName, + sourceId, + add: add, + remove: unlink.map(target.schema.getId), + reset: reset, + ); + }); + } + + @override + Future verifyLink( + String linkName, + List sourceIds, + List targetIds, + ) => + isar.getTxn(false, (EngineTransaction transaction) async { + if (sourceIds.length != targetIds.length) { + throw IsarError('Source and target link ids must have equal length.'); + } + final expected = { + for (var i = 0; i < sourceIds.length; i++) + '${sourceIds[i]}:${targetIds[i]}', + }; + final actual = {}; + for (final sourceId in _records(transaction).keys) { + for (final targetId + in isar.linkedIds(transaction, name, linkName, sourceId)) { + actual.add('$sourceId:$targetId'); + } + } + if (!_setEquals(actual, expected)) { + throw IsarError('Link contents do not match expected ids.'); + } + }); +} + +class DartEngineQuery extends Query { + DartEngineQuery( + this.collection, + this.whereClauses, + this.whereDistinct, + this.whereSort, + this.filter, + this.sortBy, + this.distinctBy, + this.offset, + this.limit, + this.property, + ); + + final DartEngineCollection collection; + final List whereClauses; + final bool whereDistinct; + final Sort whereSort; + final FilterOperation? filter; + final List sortBy; + final List distinctBy; + final int? offset; + final int? limit; + final String? property; + + @override + Isar get isar => collection.isar; + + List<_Result> _results(EngineTransaction transaction) { + final records = collection._records(transaction); + var entries = records.entries + .where((entry) => _matchesWhere(entry, transaction)) + .toList(); + final indexWhere = whereClauses.whereType().firstOrNull; + entries.sort((a, b) { + var result = indexWhere == null + ? a.key.compareTo(b.key) + : _compareKeys( + collection._indexKeyData(a.value, indexWhere.indexName), + collection._indexKeyData(b.value, indexWhere.indexName), + ); + if (result == 0) result = a.key.compareTo(b.key); + return whereSort == Sort.asc ? result : -result; + }); + if (filter != null) { + entries = entries + .where((entry) => _matchesFilter(entry, filter!, transaction)) + .toList(); + } + if (sortBy.isNotEmpty) { + entries.sort((a, b) { + for (final sort in sortBy) { + final result = _compare( + _value(a, sort.property), + _value(b, sort.property), + ); + if (result != 0) return sort.sort == Sort.asc ? result : -result; + } + return a.key.compareTo(b.key); + }); + } + final seen = {}; + var results = entries.map((entry) { + final value = property == null + ? collection._deserialize(entry.key, entry.value) + : _value(entry, property!); + return _Result(entry.key, value as T); + }); + if (whereDistinct || distinctBy.isNotEmpty) { + results = results.where((result) { + final entry = MapEntry(result.id, records[result.id]!); + final key = distinctBy.isEmpty + ? indexWhere == null + ? result.id.toString() + : collection + ._indexKeyData(entry.value, indexWhere.indexName) + .toString() + : distinctBy.map((d) { + final value = _value(entry, d.property); + return d.caseSensitive != false + ? value + : value.toString().toLowerCase(); + }).join('\u0000'); + return seen.add(key.toString()); + }); + } + if (offset != null) results = results.skip(offset!); + if (limit != null) results = results.take(limit!); + return results.toList(); + } + + bool _matchesWhere( + MapEntry> entry, + EngineTransaction transaction, + ) { + if (whereClauses.isEmpty) return true; + return whereClauses.any((clause) { + if (clause is IdWhereClause) { + return _inRange( + entry.key, + clause.lower, + clause.upper, + clause.includeLower, + clause.includeUpper, + ); + } + if (clause is IndexWhereClause) { + final index = collection.schema.index(clause.indexName); + if (index.properties.length == 1) { + final indexProperty = index.properties.first; + final property = collection.schema.property(indexProperty.name); + final value = collection._propertyValue( + entry.value, + indexProperty.name, + ); + if (property.type.isList && + indexProperty.type != IndexType.hash && + value == null) { + return false; + } + } + return collection + ._indexKeysData(entry.value, clause.indexName) + .any((key) => _keyInRange( + key, + collection._normalizeIndexKey( + clause.indexName, + clause.lower, + ), + collection._normalizeIndexKey( + clause.indexName, + clause.upper, + ), + clause.includeLower, + clause.includeUpper, + )); + } + if (clause is LinkWhereClause) { + return collection.isar + .linkedIds( + transaction, + clause.linkCollection, + clause.linkName, + clause.id, + ) + .contains(entry.key); + } + return false; + }); + } + + dynamic _value(MapEntry> entry, String name) { + if (name == collection.schema.idName) return entry.key; + return collection._propertyValue(entry.value, name); + } + + bool _matchesFilter( + MapEntry> entry, + FilterOperation operation, + EngineTransaction transaction, + ) { + if (operation is FilterGroup) { + final values = operation.filters + .map((child) => _matchesFilter(entry, child, transaction)) + .toList(); + switch (operation.type) { + case FilterGroupType.and: + return values.every((value) => value); + case FilterGroupType.or: + return values.isEmpty || values.any((value) => value); + case FilterGroupType.xor: + return values.where((value) => value).length == 1; + case FilterGroupType.not: + return values.isEmpty || + operation.filters.first is FilterGroup && + (operation.filters.first as FilterGroup).filters.isEmpty || + !values.first; + } + } + if (operation is LinkFilter) { + final ids = collection.isar.linkedIds( + transaction, + collection.name, + operation.linkName, + entry.key, + ); + if (operation.filter == null) { + return ids.length >= operation.lower! && ids.length <= operation.upper!; + } + final targetName = collection.schema.link(operation.linkName).target; + final target = collection.isar.getCollectionByNameInternal(targetName) + as DartEngineCollection; + final targetQuery = DartEngineQuery( + target, + const [], + false, + Sort.asc, + null, + const [], + const [], + null, + null, + null, + ); + final records = target._records(transaction); + return ids.any((id) { + final data = records[id]; + return data != null && + targetQuery._matchesFilter( + MapEntry(id, data), + operation.filter!, + transaction, + ); + }); + } + if (operation is ObjectFilter) { + final property = collection.schema.property(operation.property); + final raw = entry.value[collection._offsets[property.id]]; + final target = collection.schema.embeddedSchemas[property.target]!; + bool matches(dynamic value) => + value is Map && + _matchesEmbedded(target, value, operation.filter); + return raw is List ? raw.any(matches) : matches(raw); + } + if (operation is! FilterCondition) return false; + final propertyType = operation.property == collection.schema.idName + ? IsarType.long + : collection.schema.property(operation.property).type; + final raw = _value(entry, operation.property); + return _matchesCondition(raw, operation, propertyType); + } + + bool _matchesEmbedded( + Schema schema, + Map data, + FilterOperation operation, + ) { + if (operation is FilterGroup) { + final values = operation.filters + .map((child) => _matchesEmbedded(schema, data, child)) + .toList(); + switch (operation.type) { + case FilterGroupType.and: + return values.every((value) => value); + case FilterGroupType.or: + return values.isEmpty || values.any((value) => value); + case FilterGroupType.xor: + return values.where((value) => value).length == 1; + case FilterGroupType.not: + return values.isEmpty || + operation.filters.first is FilterGroup && + (operation.filters.first as FilterGroup).filters.isEmpty || + !values.first; + } + } + if (operation is ObjectFilter) { + final property = schema.property(operation.property); + final raw = data[collection.isar.offsets[schema.type]![property.id]]; + final target = collection.schema.embeddedSchemas[property.target]!; + bool matches(dynamic value) => + value is Map && + _matchesEmbedded(target, value, operation.filter); + return raw is List ? raw.any(matches) : matches(raw); + } + if (operation is! FilterCondition) return false; + final property = schema.property(operation.property); + final raw = schema.deserializeProp( + IsarReaderImpl(data), + property.id, + collection.isar.offsets[schema.type]![property.id], + collection.isar.offsets, + ); + return _matchesCondition(raw, operation, property.type); + } + + bool _matchesCondition( + dynamic raw, + FilterCondition operation, + IsarType propertyType, + ) { + if (operation.type == FilterConditionType.listLength) { + if (raw is! List) return false; + final length = raw.length; + return length >= (operation.value1! as int) && + length <= (operation.value2! as int); + } + if (operation.type == FilterConditionType.isNull) return raw == null; + if (operation.type == FilterConditionType.isNotNull) return raw != null; + if (operation.type == FilterConditionType.elementIsNull) { + return raw is List && raw.contains(null); + } + if (operation.type == FilterConditionType.elementIsNotNull) { + return raw is List && raw.any((value) => value != null); + } + if (propertyType.isList && raw == null) return false; + final values = raw is List ? raw : [raw]; + return values.any( + (value) => _matchesValue( + value, + operation, + propertyType.scalarType == IsarType.float, + ), + ); + } + + bool _matchesValue( + dynamic value, + FilterCondition condition, + bool float32, + ) { + dynamic normalize(dynamic input) { + if (input is DateTime) return input.toUtc().microsecondsSinceEpoch; + if (input is String && !condition.caseSensitive) + return input.toLowerCase(); + if (float32 && input is double) { + return (Float32List(1)..[0] = input)[0]; + } + return input; + } + + final actual = normalize(value); + final first = normalize(condition.value1); + final second = normalize(condition.value2); + final comparison = _compare(actual, first); + switch (condition.type) { + case FilterConditionType.equalTo: + if (actual is double && first is double) { + return actual == first || + actual.isNaN && first.isNaN || + (actual - first).abs() <= condition.epsilon; + } + return comparison == 0; + case FilterConditionType.greaterThan: + if (actual is double && first is double && condition.epsilon != 0) { + final threshold = condition.include1 + ? first - condition.epsilon + : first + condition.epsilon; + return condition.include1 ? actual >= threshold : actual > threshold; + } + return condition.include1 ? comparison >= 0 : comparison > 0; + case FilterConditionType.lessThan: + if (actual is double && first is double && condition.epsilon != 0) { + final threshold = condition.include1 + ? first + condition.epsilon + : first - condition.epsilon; + return condition.include1 ? actual <= threshold : actual < threshold; + } + return condition.include1 ? comparison <= 0 : comparison < 0; + case FilterConditionType.between: + if (actual is double && + first is double && + second is double && + condition.epsilon != 0) { + final lower = condition.include1 + ? actual >= first - condition.epsilon + : actual > first + condition.epsilon; + final upper = condition.include2 + ? actual <= second + condition.epsilon + : actual < second - condition.epsilon; + return lower && upper; + } else { + final lower = condition.include1 ? comparison >= 0 : comparison > 0; + final upperCompare = _compare(actual, second); + return lower && + (condition.include2 ? upperCompare <= 0 : upperCompare < 0); + } + case FilterConditionType.startsWith: + return actual is String && actual.startsWith(first as String); + case FilterConditionType.endsWith: + return actual is String && actual.endsWith(first as String); + case FilterConditionType.contains: + return actual is String && actual.contains(first as String); + case FilterConditionType.matches: + if (actual is! String || first is! String) return false; + final pattern = RegExp( + '^${RegExp.escape(first).replaceAll(r'\*', '.*').replaceAll(r'\?', '.')}\$', + ); + return pattern.hasMatch(actual); + default: + return false; + } + } + + @override + Future> findAll() => collection.isar.getTxn( + false, + (EngineTransaction transaction) async => + _results(transaction).map((result) => result.value).toList()); + + @override + List findAllSync() => collection.isar.getTxnSync( + false, + (EngineTransaction transaction) => + _results(transaction).map((result) => result.value).toList()); + + @override + Future findFirst() async => (await findAll()).firstOrNull; + + @override + T? findFirstSync() => findAllSync().firstOrNull; + + @override + Future aggregate(AggregationOp op) => collection.isar.getTxn( + false, + (EngineTransaction transaction) async => _aggregate( + _results(transaction).map((result) => result.value), op), + ); + + @override + R? aggregateSync(AggregationOp op) => collection.isar.getTxnSync( + false, + (EngineTransaction transaction) => _aggregate( + _results(transaction).map((result) => result.value), op), + ); + + R? _aggregate(Iterable iterable, AggregationOp op) { + final values = iterable.toList(); + if (op == AggregationOp.count) return values.length as R; + if (op == AggregationOp.isEmpty) return (values.isEmpty ? 1 : 0) as R; + final nonNull = values.whereType().toList(); + if (nonNull.isEmpty) { + if (op == AggregationOp.sum) { + return (R == int ? 0 : 0.0) as R; + } + if (op == AggregationOp.average) return double.nan as R; + return null; + } + if (op == AggregationOp.min || op == AggregationOp.max) { + nonNull.sort(_compare); + return (op == AggregationOp.min ? nonNull.first : nonNull.last) as R; + } + final numbers = nonNull.cast(); + final sum = numbers.fold(0, (total, value) => total + value); + if (op == AggregationOp.average) return (sum / numbers.length) as R; + return (R == int ? sum.toInt() : sum.toDouble()) as R; + } + + @override + Future deleteFirst() => collection.isar.getTxn( + true, + (EngineTransaction transaction) async { + final first = _results(transaction).firstOrNull?.id; + return first != null && + collection._deleteIds(transaction, [first]) == 1; + }, + ); + + @override + bool deleteFirstSync() => collection.isar.getTxnSync( + true, + (EngineTransaction transaction) { + final first = _results(transaction).firstOrNull?.id; + return first != null && + collection._deleteIds(transaction, [first]) == 1; + }, + ); + + @override + Future deleteAll() => + collection.isar.getTxn(true, (EngineTransaction transaction) async { + final ids = _results(transaction).map((result) => result.id).toList(); + return collection._deleteIds(transaction, ids); + }); + + @override + int deleteAllSync() => + collection.isar.getTxnSync(true, (EngineTransaction transaction) { + final ids = _results(transaction).map((result) => result.id).toList(); + return collection._deleteIds(transaction, ids); + }); + + @override + Stream> watch({bool fireImmediately = false}) { + var signature = _watchSignature(); + final changes = collection + .watchLazy() + .asyncMap?>((_) async { + final nextSignature = _watchSignature(); + if (nextSignature == signature) return null; + signature = nextSignature; + return findAll(); + }) + .where((value) => value != null) + .cast>(); + if (!fireImmediately) return changes; + return (() async* { + yield await findAll(); + yield* changes; + })(); + } + + @override + Stream watchLazy({bool fireImmediately = false}) => + watch(fireImmediately: fireImmediately).map((_) {}); + + String _watchSignature() => collection.isar.getTxnSync( + false, + (EngineTransaction transaction) { + final records = collection._records(transaction); + return _results(transaction) + .map((result) => '${result.id}:${records[result.id]}') + .join('|'); + }, + ); + + Map _json(_Result result) { + final data = collection.isar.getTxnSync( + false, + (EngineTransaction transaction) => + collection._records(transaction)[result.id]!, + ); + final original = data['@json']; + if (original is Map) { + final json = Map.from(original); + json[collection.schema.idName] = result.id; + return json; + } + return { + collection.schema.idName: result.id, + ...collection._decodeJsonObject(collection.schema, data), + }; + } + + @override + Future exportJsonRaw(R Function(Uint8List) callback) async => + exportJsonRawSync(callback); + + @override + R exportJsonRawSync(R Function(Uint8List) callback) { + final results = collection.isar.getTxnSync( + false, + (EngineTransaction transaction) => _results(transaction), + ); + return callback( + Uint8List.fromList(utf8.encode(jsonEncode(results.map(_json).toList()))), + ); + } +} + +class _Result { + const _Result(this.id, this.value); + final Id id; + final T value; +} + +List _offsets(Schema schema) { + if (schema.properties.isEmpty) return const [0]; + final max = schema.properties.values + .map((property) => property.id) + .reduce((a, b) => a > b ? a : b); + return List.generate(max + 2, (index) => index); +} + +EngineState _copyState(EngineState state) => { + for (final collection in state.entries) + collection.key: { + for (final object in collection.value.entries) + object.key: _deepCopy(object.value) as Map, + }, + }; + +dynamic _deepCopy(dynamic value) { + if (value is Map) { + return { + for (final entry in value.entries) + entry.key as Object: _deepCopy(entry.value), + }; + } + if (value is List) return value.map(_deepCopy).toList(); + if (value is DateTime) + return DateTime.fromMicrosecondsSinceEpoch(value.microsecondsSinceEpoch, + isUtc: value.isUtc); + if (value is Uint8List) return Uint8List.fromList(value); + return value; +} + +int _estimateValue(dynamic value) { + if (value == null) return 0; + if (value is bool) return 1; + if (value is num || value is DateTime) return 8; + if (value is String) return utf8.encode(value).length; + if (value is Uint8List) return value.length; + if (value is Iterable) { + return value.fold(0, (size, item) => size + _estimateValue(item)); + } + if (value is Map) { + return value.values.fold(0, (size, item) => size + _estimateValue(item)); + } + return utf8.encode(value.toString()).length; +} + +bool _setEquals(Set first, Set second) => + first.length == second.length && first.containsAll(second); + +int _compare(dynamic a, dynamic b) { + if (identical(a, b)) return 0; + if (a == null) return -1; + if (b == null) return 1; + if (a is num && b is num) { + if (a.isNaN) return b.isNaN ? 0 : -1; + if (b.isNaN) return 1; + return a.compareTo(b); + } + if (a is DateTime && b is DateTime) return a.compareTo(b); + if (a is bool && b is bool) return a == b ? 0 : (a ? 1 : -1); + if (a is String && b is String) return a.compareTo(b); + if (a is List && b is List) { + for (var i = 0; i < a.length && i < b.length; i++) { + final result = _compare(a[i], b[i]); + if (result != 0) return result; + } + return a.length.compareTo(b.length); + } + return a.toString().compareTo(b.toString()); +} + +int _compareKeys(IndexKey a, IndexKey b) { + for (var i = 0; i < a.length && i < b.length; i++) { + final result = _compare(a[i], b[i]); + if (result != 0) return result; + } + return a.length.compareTo(b.length); +} + +bool _inRange( + dynamic value, + dynamic lower, + dynamic upper, + bool includeLower, + bool includeUpper, +) => + (lower == null || + (includeLower + ? _compare(value, lower) >= 0 + : _compare(value, lower) > 0)) && + (upper == null || + (includeUpper + ? _compare(value, upper) <= 0 + : _compare(value, upper) < 0)); + +bool _keyInRange( + IndexKey value, + IndexKey? lower, + IndexKey? upper, + bool includeLower, + bool includeUpper, +) => + (lower == null || + lower.isEmpty || + (includeLower + ? _compareKeyToBound(value, lower) >= 0 + : _compareKeyToBound(value, lower) > 0)) && + (upper == null || + upper.isEmpty || + (includeUpper + ? _compareKeyToBound(value, upper) <= 0 + : _compareKeyToBound(value, upper) < 0)); + +int _compareKeyToBound(IndexKey key, IndexKey bound) { + for (var i = 0; i < key.length && i < bound.length; i++) { + final result = _compare(key[i], bound[i]); + if (result != 0) return result; + } + if (bound.length <= key.length) return 0; + return -1; +} + +extension on List { + T? get firstOrNull => isEmpty ? null : first; +} diff --git a/packages/isar/lib/src/dart_engine/link_impl.dart b/packages/isar/lib/src/dart_engine/link_impl.dart new file mode 100644 index 000000000..256646b36 --- /dev/null +++ b/packages/isar/lib/src/dart_engine/link_impl.dart @@ -0,0 +1,79 @@ +import 'package:isar/isar.dart'; +import 'package:isar/src/common/isar_link_base_impl.dart'; +import 'package:isar/src/common/isar_link_common.dart'; +import 'package:isar/src/common/isar_links_common.dart'; +import 'package:isar/src/dart_engine/engine.dart'; + +mixin DartEngineLinkMixin on IsarLinkBaseImpl { + @override + Id Function(OBJ) get getId => targetCollection.schema.getId; + + @override + Future update({ + Iterable link = const [], + Iterable unlink = const [], + bool reset = false, + }) async { + final source = sourceCollection as DartEngineCollection; + final target = targetCollection as DartEngineCollection; + final isar = source.isar; + final sourceId = requireAttached(); + final add = []; + for (final object in link) { + var id = getId(object); + if (id == Isar.autoIncrement) { + id = await target.put(object); + } + add.add(id); + } + final remove = unlink.map(requireGetId).toList(); + await isar.getTxn(true, (EngineTransaction transaction) async { + isar.updateLink( + transaction, + source.name, + linkName, + sourceId, + add: add, + remove: remove, + reset: reset, + ); + }); + } + + @override + void updateSync({ + Iterable link = const [], + Iterable unlink = const [], + bool reset = false, + }) { + final source = sourceCollection as DartEngineCollection; + final target = targetCollection as DartEngineCollection; + final isar = source.isar; + final sourceId = requireAttached(); + isar.getTxnSync(true, (EngineTransaction transaction) { + final add = []; + for (final object in link) { + var id = getId(object); + if (id == Isar.autoIncrement) { + id = target.putSync(object); + } + add.add(id); + } + isar.updateLink( + transaction, + source.name, + linkName, + sourceId, + add: add, + remove: unlink.map(requireGetId), + reset: reset, + ); + }); + } +} + +class IsarLinkImpl extends IsarLinkCommon + with DartEngineLinkMixin {} + +class IsarLinksImpl extends IsarLinksCommon + with DartEngineLinkMixin {} diff --git a/packages/isar/lib/src/dart_engine/open.dart b/packages/isar/lib/src/dart_engine/open.dart new file mode 100644 index 000000000..2b1edc616 --- /dev/null +++ b/packages/isar/lib/src/dart_engine/open.dart @@ -0,0 +1,24 @@ +import 'package:isar/isar.dart'; +import 'package:isar/src/dart_engine/engine.dart'; + +Future openIsar({ + required List> schemas, + String? directory, + required String name, + required int maxSizeMiB, + required bool relaxedDurability, + CompactCondition? compactOnLaunch, +}) async { + return DartEngineIsar(name, schemas); +} + +Isar openIsarSync({ + required List> schemas, + String? directory, + required String name, + required int maxSizeMiB, + required bool relaxedDurability, + CompactCondition? compactOnLaunch, +}) { + throw UnsupportedError('Synchronous Isar.open is not supported on web.'); +} diff --git a/packages/isar/lib/src/isar.dart b/packages/isar/lib/src/isar.dart index a67ed7182..41c97c0d1 100644 --- a/packages/isar/lib/src/isar.dart +++ b/packages/isar/lib/src/isar.dart @@ -35,6 +35,12 @@ abstract class Isar { /// Placeholder for an auto-increment id. static const Id autoIncrement = isarAutoIncrementId; + /// @nodoc + @protected + static int schemaId(String nativeId, int webId) { + return _kIsWeb ? webId : int.parse(nativeId); + } + static final Map _instances = {}; static final Set _openCallbacks = {}; static final Set _closeCallbacks = {}; @@ -87,7 +93,7 @@ abstract class Isar { /// Open a new Isar instance. static Future open( List> schemas, { - required String directory, + String? directory, String name = defaultName, int maxSizeMiB = Isar.defaultMaxSizeMiB, bool relaxedDurability = true, @@ -117,7 +123,7 @@ abstract class Isar { /// Open a new Isar instance. static Isar openSync( List> schemas, { - required String directory, + String? directory, String name = defaultName, int maxSizeMiB = Isar.defaultMaxSizeMiB, bool relaxedDurability = true, diff --git a/packages/isar/lib/src/native/bindings.dart b/packages/isar/lib/src/native/bindings.dart index 734bc9d1f..3813b1569 100644 --- a/packages/isar/lib/src/native/bindings.dart +++ b/packages/isar/lib/src/native/bindings.dart @@ -2181,7 +2181,7 @@ class IsarCoreBindings { .asFunction)>(); } -class CObject extends ffi.Struct { +final class CObject extends ffi.Struct { @ffi.Int64() external int id; @@ -2191,36 +2191,36 @@ class CObject extends ffi.Struct { external int buffer_length; } -class CObjectSet extends ffi.Struct { +final class CObjectSet extends ffi.Struct { external ffi.Pointer objects; @ffi.Uint32() external int length; } -class CIsarCollection extends ffi.Opaque {} +final class CIsarCollection extends ffi.Opaque {} -class CIsarTxn extends ffi.Opaque {} +final class CIsarTxn extends ffi.Opaque {} -class CIndexKey extends ffi.Opaque {} +final class CIndexKey extends ffi.Opaque {} typedef DartPostCObjectFnType = ffi.Pointer< ffi.NativeFunction)>>; typedef DartPort = ffi.Int64; -class CDartCObject extends ffi.Opaque {} +final class CDartCObject extends ffi.Opaque {} -class CFilter extends ffi.Opaque {} +final class CFilter extends ffi.Opaque {} -class CIsarInstance extends ffi.Opaque {} +final class CIsarInstance extends ffi.Opaque {} -class CQueryBuilder extends ffi.Opaque {} +final class CQueryBuilder extends ffi.Opaque {} -class CQuery extends ffi.Opaque {} +final class CQuery extends ffi.Opaque {} -class CAggregationResult extends ffi.Opaque {} +final class CAggregationResult extends ffi.Opaque {} -class CWatchHandle extends ffi.Opaque {} +final class CWatchHandle extends ffi.Opaque {} const int IsarIndex_MAX_STRING_INDEX_SIZE = 1024; diff --git a/packages/isar/lib/src/native/isar_collection_impl.dart b/packages/isar/lib/src/native/isar_collection_impl.dart index 0551ee37d..8b90a7860 100644 --- a/packages/isar/lib/src/native/isar_collection_impl.dart +++ b/packages/isar/lib/src/native/isar_collection_impl.dart @@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart'; import 'package:isar/isar.dart'; +import 'package:isar/src/common/isar_link_backend.dart'; import 'package:isar/src/native/bindings.dart'; import 'package:isar/src/native/encode_string.dart'; import 'package:isar/src/native/index_key.dart'; @@ -18,7 +19,8 @@ import 'package:isar/src/native/isar_writer_impl.dart'; import 'package:isar/src/native/query_build.dart'; import 'package:isar/src/native/txn.dart'; -class IsarCollectionImpl extends IsarCollection { +class IsarCollectionImpl extends IsarCollection + implements IsarLinkBackend { IsarCollectionImpl({ required this.isar, required this.ptr, @@ -646,4 +648,82 @@ class IsarCollectionImpl extends IsarCollection { await txn.wait(); }); } + + @override + Future updateLinkBackend({ + required IsarCollection targetCollection, + required String linkName, + required Id sourceId, + required Iterable link, + required Iterable unlink, + required bool reset, + }) { + final target = targetCollection as IsarCollectionImpl; + final linkList = link.toList(); + final unlinkList = unlink.toList(); + final getId = target.schema.getId; + return isar.getTxn(true, (Txn txn) { + final count = linkList.length + unlinkList.length; + final idsPtr = txn.alloc(count); + final ids = idsPtr.asTypedList(count); + for (var i = 0; i < linkList.length; i++) { + ids[i] = _requireLinkId(getId, linkList[i]); + } + for (var i = 0; i < unlinkList.length; i++) { + ids[linkList.length + i] = _requireLinkId(getId, unlinkList[i]); + } + IC.isar_link_update_all( + ptr, + txn.ptr, + schema.link(linkName).id, + sourceId, + idsPtr, + linkList.length, + unlinkList.length, + reset, + ); + return txn.wait(); + }); + } + + Id _requireLinkId(Id Function(T) getId, T object) { + final id = getId(object); + if (id == Isar.autoIncrement) { + throw IsarError( + 'Object "$object" has no id and can therefore not be linked. ' + 'Make sure to .put() objects before you use them in links.', + ); + } + return id; + } + + @override + void updateLinkBackendSync({ + required IsarCollection targetCollection, + required String linkName, + required Id sourceId, + required Iterable link, + required Iterable unlink, + required bool reset, + }) { + final target = targetCollection as IsarCollectionImpl; + final linkId = schema.link(linkName).id; + final getId = target.schema.getId; + isar.getTxnSync(true, (Txn txn) { + if (reset) { + nCall(IC.isar_link_unlink_all(ptr, txn.ptr, linkId, sourceId)); + } + for (final object in link) { + var id = getId(object); + if (id == Isar.autoIncrement) { + id = target.putByIndexSyncInternal(txn: txn, object: object); + } + nCall(IC.isar_link(ptr, txn.ptr, linkId, sourceId, id)); + } + for (final object in unlink) { + nCall( + IC.isar_link_unlink(ptr, txn.ptr, linkId, sourceId, getId(object))); + } + }); + } } diff --git a/packages/isar/lib/src/native/isar_link_impl.dart b/packages/isar/lib/src/native/isar_link_impl.dart index 3c954c7dd..48d80f597 100644 --- a/packages/isar/lib/src/native/isar_link_impl.dart +++ b/packages/isar/lib/src/native/isar_link_impl.dart @@ -1,26 +1,11 @@ // ignore_for_file: public_member_api_docs -import 'dart:ffi'; - -import 'package:isar/isar.dart'; import 'package:isar/src/common/isar_link_base_impl.dart'; +import 'package:isar/src/common/isar_link_backend.dart'; import 'package:isar/src/common/isar_link_common.dart'; import 'package:isar/src/common/isar_links_common.dart'; -import 'package:isar/src/native/isar_collection_impl.dart'; -import 'package:isar/src/native/isar_core.dart'; -import 'package:isar/src/native/txn.dart'; mixin IsarLinkBaseMixin on IsarLinkBaseImpl { - @override - IsarCollectionImpl get sourceCollection => - super.sourceCollection as IsarCollectionImpl; - - @override - IsarCollectionImpl get targetCollection => - super.targetCollection as IsarCollectionImpl; - - late final int linkId = sourceCollection.schema.link(linkName).id; - @override late final getId = targetCollection.schema.getId; @@ -29,89 +14,30 @@ mixin IsarLinkBaseMixin on IsarLinkBaseImpl { Iterable link = const [], Iterable unlink = const [], bool reset = false, - }) { - final linkList = link.toList(); - final unlinkList = unlink.toList(); - - final containingId = requireAttached(); - return targetCollection.isar.getTxn(true, (Txn txn) { - final count = linkList.length + unlinkList.length; - final idsPtr = txn.alloc(count); - final ids = idsPtr.asTypedList(count); - - for (var i = 0; i < linkList.length; i++) { - ids[i] = requireGetId(linkList[i]); - } - for (var i = 0; i < unlinkList.length; i++) { - ids[linkList.length + i] = requireGetId(unlinkList[i]); - } - - IC.isar_link_update_all( - sourceCollection.ptr, - txn.ptr, - linkId, - containingId, - idsPtr, - linkList.length, - unlinkList.length, - reset, + }) => + (sourceCollection as IsarLinkBackend).updateLinkBackend( + targetCollection: targetCollection, + linkName: linkName, + sourceId: requireAttached(), + link: link, + unlink: unlink, + reset: reset, ); - return txn.wait(); - }); - } @override void updateSync({ Iterable link = const [], Iterable unlink = const [], bool reset = false, - }) { - final containingId = requireAttached(); - targetCollection.isar.getTxnSync(true, (Txn txn) { - if (reset) { - nCall( - IC.isar_link_unlink_all( - sourceCollection.ptr, - txn.ptr, - linkId, - containingId, - ), - ); - } - - for (final object in link) { - var id = getId(object); - if (id == Isar.autoIncrement) { - id = targetCollection.putByIndexSyncInternal( - txn: txn, - object: object, - ); - } - - nCall( - IC.isar_link( - sourceCollection.ptr, - txn.ptr, - linkId, - containingId, - id, - ), - ); - } - for (final object in unlink) { - final unlinkId = requireGetId(object); - nCall( - IC.isar_link_unlink( - sourceCollection.ptr, - txn.ptr, - linkId, - containingId, - unlinkId, - ), - ); - } - }); - } + }) => + (sourceCollection as IsarLinkBackend).updateLinkBackendSync( + targetCollection: targetCollection, + linkName: linkName, + sourceId: requireAttached(), + link: link, + unlink: unlink, + reset: reset, + ); } class IsarLinkImpl extends IsarLinkCommon diff --git a/packages/isar/lib/src/native/open.dart b/packages/isar/lib/src/native/open.dart index 1b169e0c7..5ed891145 100644 --- a/packages/isar/lib/src/native/open.dart +++ b/packages/isar/lib/src/native/open.dart @@ -72,12 +72,17 @@ void _initializeInstance( Future openIsar({ required List> schemas, - required String directory, + String? directory, required String name, required int maxSizeMiB, required bool relaxedDurability, CompactCondition? compactOnLaunch, }) async { + if (directory == null) { + throw IsarError( + 'A directory is required when opening Isar on native platforms.', + ); + } initializeCoreBinary(); IC.isar_connect_dart_api(NativeApi.postCObject.cast()); @@ -118,12 +123,17 @@ Future openIsar({ Isar openIsarSync({ required List> schemas, - required String directory, + String? directory, required String name, required int maxSizeMiB, required bool relaxedDurability, CompactCondition? compactOnLaunch, }) { + if (directory == null) { + throw IsarError( + 'A directory is required when opening Isar on native platforms.', + ); + } initializeCoreBinary(); IC.isar_connect_dart_api(NativeApi.postCObject.cast()); return using((Arena alloc) { diff --git a/packages/isar/lib/src/native/query_build.dart b/packages/isar/lib/src/native/query_build.dart index 1e7871746..f85065e41 100644 --- a/packages/isar/lib/src/native/query_build.dart +++ b/packages/isar/lib/src/native/query_build.dart @@ -1009,6 +1009,8 @@ void _buildConditionStringOp({ ), ); break; + default: + throw IsarError('Unsupported string condition $conditionType'); } } else { throw IsarError('Unsupported type for condition'); diff --git a/packages/isar/lib/src/web/bindings.dart b/packages/isar/lib/src/web/bindings.dart deleted file mode 100644 index d4a1b76be..000000000 --- a/packages/isar/lib/src/web/bindings.dart +++ /dev/null @@ -1,188 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'dart:indexed_db'; -import 'dart:js'; - -import 'package:isar/isar.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart'; - -@JS('JSON.stringify') -external String stringify(dynamic value); - -@JS('indexedDB.cmp') -external int idbCmp(dynamic value1, dynamic value2); - -@JS('Object.keys') -external List objectKeys(dynamic obj); - -Map jsMapToDart(Object obj) { - final keys = objectKeys(obj); - final map = {}; - for (final key in keys) { - map[key] = getProperty(obj, key); - } - return map; -} - -@JS('Promise') -class Promise {} - -extension PromiseX on Promise { - Future wait() => promiseToFuture(this); -} - -@JS('openIsar') -external Promise openIsarJs( - String name, - List schemas, - bool relaxedDurability, -); - -@JS('IsarTxn') -class IsarTxnJs { - external Promise commit(); - - external void abort(); - - external bool get write; -} - -@JS('IsarInstance') -class IsarInstanceJs { - external IsarTxnJs beginTxn(bool write); - - external IsarCollectionJs getCollection(String name); - - external Promise close(bool deleteFromDisk); -} - -typedef ChangeCallbackJs = void Function(); - -typedef ObjectChangeCallbackJs = void Function(Object? object); - -typedef QueryChangeCallbackJs = void Function(List results); - -typedef StopWatchingJs = JsFunction; - -@JS('IsarCollection') -class IsarCollectionJs { - external IsarLinkJs getLink(String name); - - external Promise getAll(IsarTxnJs txn, List ids); - - external Promise getAllByIndex( - IsarTxnJs txn, - String indexName, - List> values, - ); - - external Promise putAll(IsarTxnJs txn, List objects); - - external Promise deleteAll(IsarTxnJs txn, List ids); - - external Promise deleteAllByIndex( - IsarTxnJs txn, - String indexName, - List keys, - ); - - external Promise clear(IsarTxnJs txn); - - external StopWatchingJs watchLazy(ChangeCallbackJs callback); - - external StopWatchingJs watchObject(Id id, ObjectChangeCallbackJs callback); - - external StopWatchingJs watchQuery( - QueryJs query, - QueryChangeCallbackJs callback, - ); - - external StopWatchingJs watchQueryLazy( - QueryJs query, - ChangeCallbackJs callback, - ); -} - -@JS('IsarLink') -class IsarLinkJs { - external Promise update( - IsarTxnJs txn, - bool backlink, - Id id, - List addedTargets, - List deletedTargets, - ); - - external Promise clear(IsarTxnJs txn, Id id, bool backlink); -} - -@JS('IdWhereClause') -@anonymous -class IdWhereClauseJs { - external KeyRange? range; -} - -@JS('IndexWhereClause') -@anonymous -class IndexWhereClauseJs { - external String indexName; - external KeyRange? range; -} - -@JS('LinkWhereClause') -@anonymous -class LinkWhereClauseJs { - external String linkCollection; - external String linkName; - external bool backlink; - external Id id; -} - -@JS('Function') -class FilterJs { - external FilterJs(String id, String obj, String method); -} - -@JS('Function') -class SortCmpJs { - external SortCmpJs(String a, String b, String method); -} - -@JS('Function') -class DistinctValueJs { - external DistinctValueJs(String obj, String method); -} - -@JS('IsarQuery') -class QueryJs { - external QueryJs( - IsarCollectionJs collection, - List whereClauses, - bool whereDistinct, - bool whereAscending, - FilterJs? filter, - SortCmpJs? sortCmp, - DistinctValueJs? distinctValue, - int? offset, - int? limit, - ); - - external Promise findFirst(IsarTxnJs txn); - - external Promise findAll(IsarTxnJs txn); - - external Promise deleteFirst(IsarTxnJs txn); - - external Promise deleteAll(IsarTxnJs txn); - - external Promise min(IsarTxnJs txn, String propertyName); - - external Promise max(IsarTxnJs txn, String propertyName); - - external Promise sum(IsarTxnJs txn, String propertyName); - - external Promise average(IsarTxnJs txn, String propertyName); - - external Promise count(IsarTxnJs txn); -} diff --git a/packages/isar/lib/src/web/indexeddb_open.dart b/packages/isar/lib/src/web/indexeddb_open.dart new file mode 100644 index 000000000..01f846cd3 --- /dev/null +++ b/packages/isar/lib/src/web/indexeddb_open.dart @@ -0,0 +1,538 @@ +// ignore_for_file: invalid_use_of_protected_member + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_interop'; + +import 'package:isar/isar.dart'; +import 'package:isar/src/dart_engine/engine.dart'; +import 'package:web/web.dart'; + +const _metaStore = '@meta'; +const _linksStore = '@links'; +const _countersStore = '@counters'; +const _legacyStore = 'state'; +const _schemaKey = 'schema'; +const _revisionKey = 'revision'; + +String _collectionStore(String name) => 'collection:$name'; + +Future openIsar({ + required List> schemas, + String? directory, + required String name, + required int maxSizeMiB, + required bool relaxedDurability, + CompactCondition? compactOnLaunch, +}) async { + final lease = await _acquireLease(name); + IDBDatabase? database; + var handedOff = false; + try { + database = await _openDatabase(name, schemas); + final expectedSchema = + jsonEncode(schemas.map((schema) => schema.toJson()).toList()); + final storedSchema = await _readString(database, _metaStore, _schemaKey); + if (storedSchema != null && + !_schemasCompatible(storedSchema, expectedSchema)) { + throw IsarError( + 'The existing web schema contains an incompatible property change.', + ); + } + + var revision = int.tryParse( + await _readString(database, _metaStore, _revisionKey) ?? '', + ) ?? + 0; + var state = await _readState(database, schemas); + if (state == null && database.objectStoreNames.contains(_legacyStore)) { + final snapshot = await _readString(database, _legacyStore, 'snapshot'); + if (snapshot != null) state = _decodeState(snapshot); + } + state ??= {}; + if (storedSchema != null && storedSchema != expectedSchema) { + state = _migrateState(state, storedSchema, schemas); + } + + if (storedSchema == null || storedSchema != expectedSchema) { + await _writeState( + database, + schemas, + state, + revision, + expectedSchema, + checkRevision: storedSchema != null, + ); + revision++; + } + + final ownedDatabase = database; + handedOff = true; + return DartEngineIsar( + name, + schemas, + initialState: state, + persist: (nextState) async { + await _writeState( + ownedDatabase, + schemas, + nextState, + revision, + expectedSchema, + checkRevision: true, + ); + revision++; + }, + onClose: (deleteFromDisk) async { + ownedDatabase.close(); + lease.close(); + if (deleteFromDisk) await _deleteDatabase(name); + }, + ); + } finally { + if (!handedOff) { + database?.close(); + lease.close(); + } + } +} + +Isar openIsarSync({ + required List> schemas, + String? directory, + required String name, + required int maxSizeMiB, + required bool relaxedDurability, + CompactCondition? compactOnLaunch, +}) { + throw UnsupportedError('Synchronous Isar.open is not supported on web.'); +} + +List _stores(List> schemas) => [ + _metaStore, + _linksStore, + _countersStore, + for (final schema in schemas) _collectionStore(schema.name), + ]; + +Future _openDatabase( + String name, + List> schemas, +) async { + var database = await _openRequest(name, null, schemas); + final missing = _stores(schemas) + .where((store) => !database.objectStoreNames.contains(store)); + if (missing.isEmpty) return database; + final version = database.version + 1; + database.close(); + database = await _openRequest(name, version, schemas); + return database; +} + +Future _openRequest( + String name, + int? version, + List> schemas, +) { + final completer = Completer(); + final request = version == null + ? window.indexedDB.open('isar-$name') + : window.indexedDB.open('isar-$name', version); + request.onupgradeneeded = ((Event event) { + final database = request.result as IDBDatabase; + for (final store in _stores(schemas)) { + if (!database.objectStoreNames.contains(store)) { + database.createObjectStore(store); + } + } + }).toJS; + request.onsuccess = ((Event event) { + final database = request.result as IDBDatabase; + database.onversionchange = ((Event event) => database.close()).toJS; + if (!completer.isCompleted) completer.complete(database); + }).toJS; + request.onerror = ((Event event) { + if (!completer.isCompleted) { + completer.completeError( + IsarError(request.error?.message ?? 'Failed to open IndexedDB.'), + ); + } + }).toJS; + request.onblocked = ((Event event) { + if (!completer.isCompleted) { + completer.completeError( + IsarError('IndexedDB open was blocked by another tab.'), + ); + } + }).toJS; + return completer.future; +} + +Future _readState( + IDBDatabase database, + List> schemas, +) async { + final state = >>{}; + var found = false; + for (final schema in schemas) { + final values = + await _readAllStrings(database, _collectionStore(schema.name)); + final records = >{}; + for (final value in values) { + final record = jsonDecode(value) as Map; + records[record['id'] as int] = + _integerKeys(record['data']) as Map; + } + state[schema.name] = records; + found |= records.isNotEmpty; + } + for (final value in await _readAllStrings(database, _linksStore)) { + final record = jsonDecode(value) as Map; + state.putIfAbsent( + record['store'] as String, () => {})[record['id'] as int] = + _integerKeys(record['data']) as Map; + found = true; + } + final counters = >{}; + for (final value in await _readAllStrings(database, _countersStore)) { + final record = jsonDecode(value) as Map; + counters[record['id'] as int] = + _integerKeys(record['data']) as Map; + found = true; + } + if (counters.isNotEmpty) state['@counters'] = counters; + return found ? state : null; +} + +Future _writeState( + IDBDatabase database, + List> schemas, + EngineState state, + int expectedRevision, + String schemaJson, { + required bool checkRevision, +}) async { + final names = _stores(schemas); + final transaction = database.transaction( + names.map((name) => name.toJS).toList().toJS, + 'readwrite', + ); + final meta = transaction.objectStore(_metaStore); + if (checkRevision) { + final result = await _request(meta.get(_revisionKey.toJS)); + final actual = result == null ? 0 : int.parse((result as JSString).toDart); + if (actual != expectedRevision) { + transaction.abort(); + throw IsarError( + 'IndexedDB revision changed from $expectedRevision to $actual.', + ); + } + } + + for (final schema in schemas) { + final store = transaction.objectStore(_collectionStore(schema.name)); + store.clear(); + for (final record in (state[schema.name] ?? const {}).entries) { + store.put( + jsonEncode({ + 'id': record.key, + 'data': _stringifyKeys(record.value), + }).toJS, + record.key.toString().toJS, + ); + } + } + + final links = transaction.objectStore(_linksStore); + links.clear(); + for (final collection in state.entries.where( + (entry) => entry.key.startsWith('@link:'), + )) { + for (final record in collection.value.entries) { + links.put( + jsonEncode({ + 'store': collection.key, + 'id': record.key, + 'data': _stringifyKeys(record.value), + }).toJS, + '${collection.key}:${record.key}'.toJS, + ); + } + } + + final counters = transaction.objectStore(_countersStore); + counters.clear(); + for (final record in (state['@counters'] ?? const {}).entries) { + counters.put( + jsonEncode({ + 'id': record.key, + 'data': _stringifyKeys(record.value), + }).toJS, + record.key.toString().toJS, + ); + } + meta.put(schemaJson.toJS, _schemaKey.toJS); + meta.put((expectedRevision + 1).toString().toJS, _revisionKey.toJS); + await _transaction(transaction); +} + +Future _readString( + IDBDatabase database, + String storeName, + String key, +) async { + final transaction = database.transaction(storeName.toJS, 'readonly'); + final result = + await _request(transaction.objectStore(storeName).get(key.toJS)); + return result == null ? null : (result as JSString).toDart; +} + +Future> _readAllStrings( + IDBDatabase database, + String storeName, +) async { + final transaction = database.transaction(storeName.toJS, 'readonly'); + final result = await _request(transaction.objectStore(storeName).getAll()); + if (result == null) return const []; + return (result as JSArray) + .toDart + .map((value) => (value as JSString).toDart) + .toList(); +} + +Future _deleteDatabase(String name) { + final completer = Completer(); + final request = window.indexedDB.deleteDatabase('isar-$name'); + request.onsuccess = ((Event event) => completer.complete()).toJS; + request.onerror = ((Event event) => completer.completeError( + IsarError(request.error?.message ?? 'Failed to delete IndexedDB.'), + )).toJS; + request.onblocked = ((Event event) => completer.completeError( + IsarError('IndexedDB deletion was blocked by another tab.'), + )).toJS; + return completer.future; +} + +Future _acquireLease(String name) async { + final channel = BroadcastChannel('isar-owner-$name'); + final token = + '${DateTime.now().microsecondsSinceEpoch}-${window.crypto.randomUUID()}'; + var claimed = false; + var conflict = false; + channel.onmessage = ((Event event) { + final message = ((event as MessageEvent).data as JSString?)?.toDart; + if (message == null) return; + if (claimed && message.startsWith('probe:')) { + channel.postMessage('owned:${message.substring(6)}'.toJS); + } else if (message == 'owned:$token') { + conflict = true; + } else if (message.startsWith('claim:') && message != 'claim:$token') { + conflict = true; + } + }).toJS; + channel.postMessage('probe:$token'.toJS); + await Future.delayed(const Duration(milliseconds: 75)); + if (conflict) { + channel.close(); + throw IsarError('Another tab owns Isar instance "$name".'); + } + claimed = true; + channel.postMessage('claim:$token'.toJS); + await Future.delayed(const Duration(milliseconds: 75)); + if (conflict) { + channel.close(); + throw IsarError('Another tab is opening Isar instance "$name".'); + } + return channel; +} + +Future _request(IDBRequest request) { + final completer = Completer(); + request.onsuccess = + ((Event event) => completer.complete(request.result)).toJS; + request.onerror = ((Event event) => completer.completeError( + IsarError(request.error?.message ?? 'IndexedDB request failed.'), + )).toJS; + return completer.future; +} + +Future _transaction(IDBTransaction transaction) { + final completer = Completer(); + transaction.oncomplete = ((Event event) => completer.complete()).toJS; + transaction.onerror = ((Event event) => completer.completeError( + IsarError( + transaction.error?.message ?? 'IndexedDB transaction failed.'), + )).toJS; + transaction.onabort = ((Event event) => completer.completeError( + IsarError( + transaction.error?.message ?? 'IndexedDB transaction aborted.'), + )).toJS; + return completer.future; +} + +bool _schemasCompatible(String storedSource, String expectedSource) { + final stored = + (jsonDecode(storedSource) as List).cast>(); + final expected = + (jsonDecode(expectedSource) as List).cast>(); + final expectedCollections = { + for (final collection in expected) collection['name']: collection, + }; + for (final oldCollection in stored) { + final collection = expectedCollections[oldCollection['name']]; + if (collection == null) continue; + final properties = { + for (final property in collection['properties'] as List) + (property as Map)['name']: property, + }; + for (final oldProperty in oldCollection['properties'] as List) { + final old = oldProperty as Map; + final property = properties[old['name']]; + if (property != null && property['type'] != old['type']) return false; + } + } + return true; +} + +EngineState _migrateState( + EngineState state, + String storedSource, + List> schemas, +) { + final oldCollections = { + for (final collection in (jsonDecode(storedSource) as List)) + (collection as Map)['name'] as String: collection, + }; + final collectionNames = schemas.map((schema) => schema.name).toSet(); + final validLinkStores = { + for (final schema in schemas) + for (final link in schema.links.values) + if (collectionNames.contains(link.target)) + '@link:${schema.name}:${link.name}', + }; + return { + for (final schema in schemas) + schema.name: { + for (final record in (state[schema.name] ?? const {}).entries) + record.key: _migrateRecord( + record.value, + oldCollections[schema.name], + schema, + ), + }, + for (final entry in state.entries) + if (entry.key == '@counters' || validLinkStores.contains(entry.key)) + entry.key: entry.value, + }; +} + +Map _migrateRecord( + Map data, + Map? oldSchema, + Schema schema, { + Map>? oldEmbedded, + Map>? newEmbedded, +}) { + if (oldSchema == null) return data; + final oldProperties = + (oldSchema['properties'] as List).cast>(); + oldEmbedded ??= { + for (final embedded in (oldSchema['embeddedSchemas'] as List? ?? const [])) + (embedded as Map)['name'] as String: embedded, + }; + newEmbedded ??= + schema is CollectionSchema ? schema.embeddedSchemas : const {}; + final migrated = {}; + for (final property in schema.properties.values) { + final oldId = oldProperties.indexWhere( + (oldProperty) => oldProperty['name'] == property.name, + ); + if (oldId < 0 || !data.containsKey(oldId)) continue; + if (oldProperties[oldId]['target'] != property.target) continue; + var value = data[oldId]; + if (property.target != null && value != null) { + final target = property.target!; + final targetSchema = newEmbedded[target]; + final oldTarget = oldEmbedded[target]; + if (targetSchema != null && oldTarget != null) { + if (value is List) { + value = [ + for (final element in value) + if (element is Map) + _migrateRecord( + element, + oldTarget, + targetSchema, + oldEmbedded: oldEmbedded, + newEmbedded: newEmbedded, + ) + else + element, + ]; + } else if (value is Map) { + value = _migrateRecord( + value, + oldTarget, + targetSchema, + oldEmbedded: oldEmbedded, + newEmbedded: newEmbedded, + ); + } + } + } + migrated[property.id] = value; + } + if (data.containsKey('@json')) migrated['@json'] = data['@json']; + return migrated; +} + +EngineState _decodeState(String source) { + final decoded = jsonDecode(source) as Map; + return { + for (final collection in decoded.entries) + collection.key: { + for (final object in (collection.value as Map).entries) + int.parse(object.key): + _integerKeys(object.value) as Map, + }, + }; +} + +dynamic _stringifyKeys(dynamic value) { + if (value is double && !value.isFinite) { + return { + '@double': value.isNaN + ? 'nan' + : value.isNegative + ? '-infinity' + : 'infinity', + }; + } + if (value is Map) { + return { + for (final entry in value.entries) + entry.key.toString(): _stringifyKeys(entry.value), + }; + } + if (value is List) return value.map(_stringifyKeys).toList(); + return value; +} + +dynamic _integerKeys(dynamic value) { + if (value is Map) { + if (value.length == 1 && value['@double'] is String) { + return switch (value['@double']) { + 'nan' => double.nan, + '-infinity' => double.negativeInfinity, + 'infinity' => double.infinity, + _ => value, + }; + } + return { + for (final entry in value.entries) + int.tryParse(entry.key) ?? entry.key: _integerKeys(entry.value), + }; + } + if (value is List) return value.map(_integerKeys).toList(); + return value; +} diff --git a/packages/isar/lib/src/web/isar_collection_impl.dart b/packages/isar/lib/src/web/isar_collection_impl.dart deleted file mode 100644 index d89ad6931..000000000 --- a/packages/isar/lib/src/web/isar_collection_impl.dart +++ /dev/null @@ -1,266 +0,0 @@ -// ignore_for_file: public_member_api_docs, invalid_use_of_protected_member - -import 'dart:async'; -import 'dart:convert'; -import 'dart:js'; -import 'dart:js_util'; -import 'dart:typed_data'; - -import 'package:isar/isar.dart'; -import 'package:isar/src/web/bindings.dart'; -import 'package:isar/src/web/isar_impl.dart'; -import 'package:isar/src/web/isar_reader_impl.dart'; -import 'package:isar/src/web/isar_web.dart'; -import 'package:isar/src/web/isar_writer_impl.dart'; -import 'package:isar/src/web/query_build.dart'; -import 'package:meta/dart2js.dart'; - -class IsarCollectionImpl extends IsarCollection { - IsarCollectionImpl({ - required this.isar, - required this.native, - required this.schema, - }); - - @override - final IsarImpl isar; - final IsarCollectionJs native; - - @override - final CollectionSchema schema; - - @override - String get name => schema.name; - - late final _offsets = isar.offsets[OBJ]!; - - @tryInline - OBJ deserializeObject(Object object) { - final id = getProperty(object, idName); - final reader = IsarReaderImpl(object); - return schema.deserialize(id, reader, _offsets, isar.offsets); - } - - @tryInline - List deserializeObjects(dynamic objects) { - final list = objects as List; - final results = []; - for (final object in list) { - results.add(object is Object ? deserializeObject(object) : null); - } - return results; - } - - @override - Future> getAll(List ids) { - return isar.getTxn(false, (IsarTxnJs txn) async { - final objects = await native.getAll(txn, ids).wait>(); - return deserializeObjects(objects); - }); - } - - @override - Future> getAllByIndex(String indexName, List keys) { - return isar.getTxn(false, (IsarTxnJs txn) async { - final objects = await native - .getAllByIndex(txn, indexName, keys) - .wait>(); - return deserializeObjects(objects); - }); - } - - @override - List getAllSync(List ids) => unsupportedOnWeb(); - - @override - List getAllByIndexSync(String indexName, List keys) => - unsupportedOnWeb(); - - @override - Future> putAll(List objects) { - return putAllByIndex(null, objects); - } - - @override - List putAllSync(List objects, {bool saveLinks = true}) => - unsupportedOnWeb(); - - @override - Future> putAllByIndex(String? indexName, List objects) { - return isar.getTxn(true, (IsarTxnJs txn) async { - final serialized = []; - for (final object in objects) { - final jsObj = newObject(); - final writer = IsarWriterImpl(jsObj); - schema.serialize(object, writer, _offsets, isar.offsets); - setProperty(jsObj, idName, schema.getId(object)); - serialized.add(jsObj); - } - final ids = await native.putAll(txn, serialized).wait>(); - for (var i = 0; i < objects.length; i++) { - final object = objects[i]; - final id = ids[i] as Id; - schema.attach(this, id, object); - } - - return ids.cast().toList(); - }); - } - - @override - List putAllByIndexSync( - String indexName, - List objects, { - bool saveLinks = true, - }) => - unsupportedOnWeb(); - - @override - Future deleteAll(List ids) async { - await isar.getTxn(true, (IsarTxnJs txn) { - return native.deleteAll(txn, ids).wait(); - }); - return ids.length; - } - - @override - Future deleteAllByIndex(String indexName, List keys) { - return isar.getTxn(true, (IsarTxnJs txn) { - return native.deleteAllByIndex(txn, indexName, keys).wait(); - }); - } - - @override - int deleteAllSync(List ids) => unsupportedOnWeb(); - - @override - int deleteAllByIndexSync(String indexName, List keys) => - unsupportedOnWeb(); - - @override - Future clear() { - return isar.getTxn(true, (IsarTxnJs txn) { - return native.clear(txn).wait(); - }); - } - - @override - void clearSync() => unsupportedOnWeb(); - - @override - Future importJson(List> json) { - return isar.getTxn(true, (IsarTxnJs txn) async { - await native.putAll(txn, json.map(jsify).toList()).wait(); - }); - } - - @override - Future importJsonRaw(Uint8List jsonBytes) { - final json = jsonDecode(const Utf8Decoder().convert(jsonBytes)) as List; - return importJson(json.cast()); - } - - @override - void importJsonSync(List> json) => unsupportedOnWeb(); - - @override - void importJsonRawSync(Uint8List jsonBytes) => unsupportedOnWeb(); - - @override - Future count() => where().count(); - - @override - int countSync() => unsupportedOnWeb(); - - @override - Future getSize({ - bool includeIndexes = false, - bool includeLinks = false, - }) => - unsupportedOnWeb(); - - @override - int getSizeSync({ - bool includeIndexes = false, - bool includeLinks = false, - }) => - unsupportedOnWeb(); - - @override - Stream watchLazy({bool fireImmediately = false}) { - JsFunction? stop; - final controller = StreamController( - onCancel: () { - stop?.apply([]); - }, - ); - - final void Function() callback = allowInterop(() => controller.add(null)); - stop = native.watchLazy(callback); - - return controller.stream; - } - - @override - Stream watchObject( - Id id, { - bool fireImmediately = false, - bool deserialize = true, - }) { - JsFunction? stop; - final controller = StreamController( - onCancel: () { - stop?.apply([]); - }, - ); - - final Null Function(Object? obj) callback = allowInterop((Object? obj) { - final object = deserialize && obj != null ? deserializeObject(obj) : null; - controller.add(object); - }); - stop = native.watchObject(id, callback); - - return controller.stream; - } - - @override - Stream watchObjectLazy(Id id, {bool fireImmediately = false}) => - watchObject(id, deserialize: false); - - @override - Query buildQuery({ - List whereClauses = const [], - bool whereDistinct = false, - Sort whereSort = Sort.asc, - FilterOperation? filter, - List sortBy = const [], - List distinctBy = const [], - int? offset, - int? limit, - String? property, - }) { - return buildWebQuery( - this, - whereClauses, - whereDistinct, - whereSort, - filter, - sortBy, - distinctBy, - offset, - limit, - property, - ); - } - - @override - Future verify(List objects) => unsupportedOnWeb(); - - @override - Future verifyLink( - String linkName, - List sourceIds, - List targetIds, - ) => - unsupportedOnWeb(); -} diff --git a/packages/isar/lib/src/web/isar_impl.dart b/packages/isar/lib/src/web/isar_impl.dart deleted file mode 100644 index 5c0efb41f..000000000 --- a/packages/isar/lib/src/web/isar_impl.dart +++ /dev/null @@ -1,135 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:html'; - -import 'package:isar/isar.dart'; - -import 'package:isar/src/web/bindings.dart'; -import 'package:isar/src/web/isar_web.dart'; - -const Symbol _zoneTxn = #zoneTxn; - -class IsarImpl extends Isar { - IsarImpl(super.name, this.instance); - - final IsarInstanceJs instance; - final offsets = >{}; - final List> _activeAsyncTxns = []; - - @override - final String? directory = null; - - void requireNotInTxn() { - if (Zone.current[_zoneTxn] != null) { - throw IsarError( - 'Cannot perform this operation from within an active transaction.', - ); - } - } - - Future _txn( - bool write, - bool silent, - Future Function() callback, - ) async { - requireOpen(); - requireNotInTxn(); - - final completer = Completer(); - _activeAsyncTxns.add(completer.future); - - final txn = instance.beginTxn(write); - - final zone = Zone.current.fork( - zoneValues: {_zoneTxn: txn}, - ); - - T result; - try { - result = await zone.run(callback); - await txn.commit().wait(); - } catch (e) { - txn.abort(); - if (e is DomException) { - if (e.name == DomException.CONSTRAINT) { - throw IsarUniqueViolationError(); - } else { - throw IsarError('${e.name}: ${e.message}'); - } - } else { - rethrow; - } - } finally { - completer.complete(); - _activeAsyncTxns.remove(completer.future); - } - - return result; - } - - @override - Future txn(Future Function() callback) { - return _txn(false, false, callback); - } - - @override - Future writeTxn(Future Function() callback, {bool silent = false}) { - return _txn(true, silent, callback); - } - - @override - T txnSync(T Function() callback) => unsupportedOnWeb(); - - @override - T writeTxnSync(T Function() callback, {bool silent = false}) => - unsupportedOnWeb(); - - Future getTxn(bool write, Future Function(IsarTxnJs txn) callback) { - final currentTxn = Zone.current[_zoneTxn] as IsarTxnJs?; - if (currentTxn != null) { - if (write && !currentTxn.write) { - throw IsarError( - 'Operation cannot be performed within a read transaction.', - ); - } - return callback(currentTxn); - } else if (!write) { - return _txn(false, false, () { - return callback(Zone.current[_zoneTxn] as IsarTxnJs); - }); - } else { - throw IsarError('Write operations require an explicit transaction.'); - } - } - - @override - Future getSize({ - bool includeIndexes = false, - bool includeLinks = false, - }) => - unsupportedOnWeb(); - - @override - int getSizeSync({ - bool includeIndexes = false, - bool includeLinks = false, - }) => - unsupportedOnWeb(); - - @override - Future copyToFile(String targetPath) => unsupportedOnWeb(); - - @override - Future close({bool deleteFromDisk = false}) async { - requireOpen(); - requireNotInTxn(); - await Future.wait(_activeAsyncTxns); - await super.close(); - await instance.close(deleteFromDisk).wait(); - return true; - } - - @override - Future verify() => unsupportedOnWeb(); -} diff --git a/packages/isar/lib/src/web/isar_link_impl.dart b/packages/isar/lib/src/web/isar_link_impl.dart deleted file mode 100644 index 6efa9c154..000000000 --- a/packages/isar/lib/src/web/isar_link_impl.dart +++ /dev/null @@ -1,75 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:isar/isar.dart'; -import 'package:isar/src/common/isar_link_base_impl.dart'; -import 'package:isar/src/common/isar_link_common.dart'; -import 'package:isar/src/common/isar_links_common.dart'; -import 'package:isar/src/web/bindings.dart'; -import 'package:isar/src/web/isar_collection_impl.dart'; -import 'package:isar/src/web/isar_web.dart'; - -mixin IsarLinkBaseMixin on IsarLinkBaseImpl { - @override - IsarCollectionImpl get sourceCollection => - super.sourceCollection as IsarCollectionImpl; - - @override - IsarCollectionImpl get targetCollection => - super.targetCollection as IsarCollectionImpl; - - @override - late final Id Function(OBJ) getId = targetCollection.schema.getId; - - late final String? backlinkLinkName = - sourceCollection.schema.link(linkName).linkName; - - late final IsarLinkJs jsLink = backlinkLinkName != null - ? targetCollection.native.getLink(backlinkLinkName!) - : sourceCollection.native.getLink(linkName); - - @override - Future update({ - Iterable link = const [], - Iterable unlink = const [], - bool reset = false, - }) { - final linkList = link.toList(); - final unlinkList = unlink.toList(); - - final containingId = requireAttached(); - final backlink = backlinkLinkName != null; - - final linkIds = List.filled(linkList.length, 0); - for (var i = 0; i < linkList.length; i++) { - linkIds[i] = requireGetId(linkList[i]); - } - - final unlinkIds = List.filled(unlinkList.length, 0); - for (var i = 0; i < unlinkList.length; i++) { - unlinkIds[i] = requireGetId(unlinkList[i]); - } - - return targetCollection.isar.getTxn(true, (IsarTxnJs txn) async { - if (reset) { - await jsLink.clear(txn, containingId, backlink).wait(); - } - return jsLink - .update(txn, backlink, containingId, linkIds, unlinkIds) - .wait(); - }); - } - - @override - void updateSync({ - Iterable link = const [], - Iterable unlink = const [], - bool reset = false, - }) => - unsupportedOnWeb(); -} - -class IsarLinkImpl extends IsarLinkCommon - with IsarLinkBaseMixin {} - -class IsarLinksImpl extends IsarLinksCommon - with IsarLinkBaseMixin {} diff --git a/packages/isar/lib/src/web/isar_reader_impl.dart b/packages/isar/lib/src/web/isar_reader_impl.dart index fac7e8b42..7d371d456 100644 --- a/packages/isar/lib/src/web/isar_reader_impl.dart +++ b/packages/isar/lib/src/web/isar_reader_impl.dart @@ -1,12 +1,14 @@ // ignore_for_file: public_member_api_docs import 'package:isar/isar.dart'; -import 'package:js/js_util.dart'; import 'package:meta/dart2js.dart'; -const nullNumber = double.negativeInfinity; +T getProperty(Object object, Object key) { + return (object as Map)[key] as T; +} + const idName = '_id'; -final nullDate = DateTime.fromMillisecondsSinceEpoch(0); +final nullDate = DateTime.fromMicrosecondsSinceEpoch(0); class IsarReaderImpl implements IsarReader { IsarReaderImpl(this.object); @@ -35,78 +37,78 @@ class IsarReaderImpl implements IsarReader { @override int readByte(int offset) { final value = getProperty(object, offset); - return value is int ? value : nullNumber as int; + return value is int ? value : 0; } @tryInline @override int? readByteOrNull(int offset) { final value = getProperty(object, offset); - return value is int && value != nullNumber ? value : null; + return value is int ? value : null; } @tryInline @override int readInt(int offset) { final value = getProperty(object, offset); - return value is int ? value : nullNumber as int; + return value is int ? value : -2147483648; } @tryInline @override int? readIntOrNull(int offset) { final value = getProperty(object, offset); - return value is int && value != nullNumber ? value : null; + return value is int ? value : null; } @tryInline @override double readFloat(int offset) { final value = getProperty(object, offset); - return value is double ? value : nullNumber; + return value is num ? value.toDouble() : double.nan; } @tryInline @override double? readFloatOrNull(int offset) { final value = getProperty(object, offset); - return value is double && value != nullNumber ? value : null; + return value is double ? value : null; } @tryInline @override int readLong(int offset) { final value = getProperty(object, offset); - return value is int ? value : nullNumber as int; + return value is int ? value : Isar.minId; } @tryInline @override int? readLongOrNull(int offset) { final value = getProperty(object, offset); - return value is int && value != nullNumber ? value : null; + return value is int ? value : null; } @tryInline @override double readDouble(int offset) { final value = getProperty(object, offset); - return value is double && value != nullNumber ? value : nullNumber; + return value is num ? value.toDouble() : double.nan; } @tryInline @override double? readDoubleOrNull(int offset) { final value = getProperty(object, offset); - return value is double && value != nullNumber ? value : null; + return value is double ? value : null; } @tryInline @override DateTime readDateTime(int offset) { final value = getProperty(object, offset); - return value is int && value != nullNumber - ? DateTime.fromMillisecondsSinceEpoch(value, isUtc: true).toLocal() + return value is int + ? DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true).toLocal() : nullDate; } @@ -114,8 +116,8 @@ class IsarReaderImpl implements IsarReader { @override DateTime? readDateTimeOrNull(int offset) { final value = getProperty(object, offset); - return value is int && value != nullNumber - ? DateTime.fromMillisecondsSinceEpoch(value, isUtc: true).toLocal() + return value is int + ? DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true).toLocal() : null; } @@ -177,18 +179,14 @@ class IsarReaderImpl implements IsarReader { @override List? readByteList(int offset) { final value = getProperty(object, offset); - return value is List - ? value.map((e) => e is int ? e : nullNumber as int).toList() - : null; + return value is List ? value.map((e) => e is int ? e : 0).toList() : null; } @tryInline @override List? readIntList(int offset) { final value = getProperty(object, offset); - return value is List - ? value.map((e) => e is int ? e : nullNumber as int).toList() - : null; + return value is List ? value.map((e) => e is int ? e : 0).toList() : null; } @tryInline @@ -196,7 +194,7 @@ class IsarReaderImpl implements IsarReader { List? readIntOrNullList(int offset) { final value = getProperty(object, offset); return value is List - ? value.map((e) => e is int && e != nullNumber ? e : null).toList() + ? value.map((e) => e is int ? e : null).toList() : null; } @@ -205,7 +203,7 @@ class IsarReaderImpl implements IsarReader { List? readFloatList(int offset) { final value = getProperty(object, offset); return value is List - ? value.map((e) => e is double ? e : nullNumber).toList() + ? value.map((e) => e is double ? e : 0.0).toList() : null; } @@ -214,7 +212,7 @@ class IsarReaderImpl implements IsarReader { List? readFloatOrNullList(int offset) { final value = getProperty(object, offset); return value is List - ? value.map((e) => e is double && e != nullNumber ? e : null).toList() + ? value.map((e) => e is double ? e : null).toList() : null; } @@ -222,9 +220,7 @@ class IsarReaderImpl implements IsarReader { @override List? readLongList(int offset) { final value = getProperty(object, offset); - return value is List - ? value.map((e) => e is int ? e : nullNumber as int).toList() - : null; + return value is List ? value.map((e) => e is int ? e : 0).toList() : null; } @tryInline @@ -232,7 +228,7 @@ class IsarReaderImpl implements IsarReader { List? readLongOrNullList(int offset) { final value = getProperty(object, offset); return value is List - ? value.map((e) => e is int && e != nullNumber ? e : null).toList() + ? value.map((e) => e is int ? e : null).toList() : null; } @@ -241,7 +237,7 @@ class IsarReaderImpl implements IsarReader { List? readDoubleList(int offset) { final value = getProperty(object, offset); return value is List - ? value.map((e) => e is double ? e : nullNumber).toList() + ? value.map((e) => e is double ? e : 0.0).toList() : null; } @@ -250,7 +246,7 @@ class IsarReaderImpl implements IsarReader { List? readDoubleOrNullList(int offset) { final value = getProperty(object, offset); return value is List - ? value.map((e) => e is double && e != nullNumber ? e : null).toList() + ? value.map((e) => e is double ? e : null).toList() : null; } @@ -261,8 +257,8 @@ class IsarReaderImpl implements IsarReader { return value is List ? value .map( - (e) => e is int && e != nullNumber - ? DateTime.fromMillisecondsSinceEpoch(e, isUtc: true) + (e) => e is int + ? DateTime.fromMicrosecondsSinceEpoch(e, isUtc: true) .toLocal() : nullDate, ) @@ -277,8 +273,8 @@ class IsarReaderImpl implements IsarReader { return value is List ? value .map( - (e) => e is int && e != nullNumber - ? DateTime.fromMillisecondsSinceEpoch(e, isUtc: true) + (e) => e is int + ? DateTime.fromMicrosecondsSinceEpoch(e, isUtc: true) .toLocal() : null, ) diff --git a/packages/isar/lib/src/web/isar_writer_impl.dart b/packages/isar/lib/src/web/isar_writer_impl.dart index a4a65ca04..4d63abe47 100644 --- a/packages/isar/lib/src/web/isar_writer_impl.dart +++ b/packages/isar/lib/src/web/isar_writer_impl.dart @@ -1,10 +1,14 @@ // ignore_for_file: public_member_api_docs import 'package:isar/isar.dart'; -import 'package:isar/src/web/isar_reader_impl.dart'; -import 'package:js/js_util.dart'; import 'package:meta/dart2js.dart'; +T newObject() => {} as T; + +void setProperty(Object object, Object key, Object? value) { + (object as Map)[key] = value; +} + class IsarWriterImpl implements IsarWriter { IsarWriterImpl(this.object); @@ -17,7 +21,7 @@ class IsarWriterImpl implements IsarWriter { ? 1 : value == false ? 0 - : nullNumber; + : null; setProperty(object, offset, number); } @@ -30,25 +34,25 @@ class IsarWriterImpl implements IsarWriter { @tryInline @override void writeInt(int offset, int? value) { - setProperty(object, offset, value ?? nullNumber); + setProperty(object, offset, value); } @tryInline @override void writeFloat(int offset, double? value) { - setProperty(object, offset, value ?? nullNumber); + setProperty(object, offset, value); } @tryInline @override void writeLong(int offset, int? value) { - setProperty(object, offset, value ?? nullNumber); + setProperty(object, offset, value); } @tryInline @override void writeDouble(int offset, double? value) { - setProperty(object, offset, value ?? nullNumber); + setProperty(object, offset, value); } @tryInline @@ -57,14 +61,14 @@ class IsarWriterImpl implements IsarWriter { setProperty( object, offset, - value?.toUtc().millisecondsSinceEpoch ?? nullNumber, + value?.toUtc().microsecondsSinceEpoch, ); } @tryInline @override void writeString(int offset, String? value) { - setProperty(object, offset, value ?? nullNumber); + setProperty(object, offset, value); } @tryInline @@ -86,7 +90,7 @@ class IsarWriterImpl implements IsarWriter { @tryInline @override void writeByteList(int offset, List? values) { - setProperty(object, offset, values ?? nullNumber); + setProperty(object, offset, values); } @tryInline @@ -98,54 +102,52 @@ class IsarWriterImpl implements IsarWriter { ? 0 : e == true ? 1 - : nullNumber, + : null, ) .toList(); - setProperty(object, offset, list ?? nullNumber); + setProperty(object, offset, list); } @tryInline @override void writeIntList(int offset, List? values) { - final list = values?.map((e) => e ?? nullNumber).toList(); - setProperty(object, offset, list ?? nullNumber); + final list = values?.toList(); + setProperty(object, offset, list); } @tryInline @override void writeFloatList(int offset, List? values) { - final list = values?.map((e) => e ?? nullNumber).toList(); - setProperty(object, offset, list ?? nullNumber); + final list = values?.toList(); + setProperty(object, offset, list); } @tryInline @override void writeLongList(int offset, List? values) { - final list = values?.map((e) => e ?? nullNumber).toList(); - setProperty(object, offset, list ?? nullNumber); + final list = values?.toList(); + setProperty(object, offset, list); } @tryInline @override void writeDoubleList(int offset, List? values) { - final list = values?.map((e) => e ?? nullNumber).toList(); - setProperty(object, offset, list ?? nullNumber); + final list = values?.toList(); + setProperty(object, offset, list); } @tryInline @override void writeDateTimeList(int offset, List? values) { - final list = values - ?.map((e) => e?.toUtc().millisecondsSinceEpoch ?? nullNumber) - .toList(); - setProperty(object, offset, list ?? nullNumber); + final list = values?.map((e) => e?.toUtc().microsecondsSinceEpoch).toList(); + setProperty(object, offset, list); } @tryInline @override void writeStringList(int offset, List? values) { - final list = values?.map((e) => e ?? nullNumber).toList(); - setProperty(object, offset, list ?? nullNumber); + final list = values?.toList(); + setProperty(object, offset, list); } @tryInline diff --git a/packages/isar/lib/src/web/open.dart b/packages/isar/lib/src/web/open.dart deleted file mode 100644 index 5e5079192..000000000 --- a/packages/isar/lib/src/web/open.dart +++ /dev/null @@ -1,82 +0,0 @@ -// ignore_for_file: public_member_api_docs, invalid_use_of_protected_member - -import 'dart:html'; -//import 'dart:js_util'; - -import 'package:isar/isar.dart'; -/*import 'package:isar/src/common/schemas.dart'; - -import 'package:isar/src/web/bindings.dart'; -import 'package:isar/src/web/isar_collection_impl.dart'; -import 'package:isar/src/web/isar_impl.dart';*/ -import 'package:isar/src/web/isar_web.dart'; -import 'package:meta/meta.dart'; - -bool _loaded = false; -Future initializeIsarWeb([String? jsUrl]) async { - if (_loaded) { - return; - } - _loaded = true; - - final script = ScriptElement(); - script.type = 'text/javascript'; - // ignore: unsafe_html - script.src = 'https://unpkg.com/isar@${Isar.version}/dist/index.js'; - script.async = true; - document.head!.append(script); - await script.onLoad.first.timeout( - const Duration(seconds: 30), - onTimeout: () { - throw IsarError('Failed to load Isar'); - }, - ); -} - -@visibleForTesting -void doNotInitializeIsarWeb() { - _loaded = true; -} - -Future openIsar({ - required List> schemas, - String? directory, - required String name, - required int maxSizeMiB, - required bool relaxedDurability, - CompactCondition? compactOnLaunch, -}) async { - throw IsarError('Please use Isar 2.5.0 if you need web support. ' - 'A 3.x version with web support will be released soon.'); - /*await initializeIsarWeb(); - final schemasJson = getSchemas(schemas).map((e) => e.toJson()); - final schemasJs = jsify(schemasJson.toList()) as List; - final instance = await openIsarJs(name, schemasJs, relaxedDurability) - .wait(); - final isar = IsarImpl(name, instance); - final cols = >{}; - for (final schema in schemas) { - final col = instance.getCollection(schema.name); - schema.toCollection(() { - schema as CollectionSchema; - cols[OBJ] = IsarCollectionImpl( - isar: isar, - native: col, - schema: schema, - ); - }); - } - - isar.attachCollections(cols); - return isar;*/ -} - -Isar openIsarSync({ - required List> schemas, - String? directory, - required String name, - required int maxSizeMiB, - required bool relaxedDurability, - CompactCondition? compactOnLaunch, -}) => - unsupportedOnWeb(); diff --git a/packages/isar/lib/src/web/query_build.dart b/packages/isar/lib/src/web/query_build.dart deleted file mode 100644 index 89ff6dc8b..000000000 --- a/packages/isar/lib/src/web/query_build.dart +++ /dev/null @@ -1,375 +0,0 @@ -// ignore_for_file: public_member_api_docs, invalid_use_of_protected_member - -import 'dart:indexed_db'; - -import 'package:isar/isar.dart'; - -import 'package:isar/src/web/bindings.dart'; -import 'package:isar/src/web/isar_collection_impl.dart'; -import 'package:isar/src/web/isar_web.dart'; -import 'package:isar/src/web/query_impl.dart'; - -Query buildWebQuery( - IsarCollectionImpl col, - List whereClauses, - bool whereDistinct, - Sort whereSort, - FilterOperation? filter, - List sortBy, - List distinctBy, - int? offset, - int? limit, - String? property, -) { - final whereClausesJs = whereClauses.map((wc) { - if (wc is IdWhereClause) { - return _buildIdWhereClause(wc); - } else if (wc is IndexWhereClause) { - return _buildIndexWhereClause(col.schema, wc); - } else { - return _buildLinkWhereClause(col, wc as LinkWhereClause); - } - }).toList(); - - final filterJs = filter != null ? _buildFilter(col.schema, filter) : null; - final sortJs = sortBy.isNotEmpty ? _buildSort(sortBy) : null; - final distinctJs = distinctBy.isNotEmpty ? _buildDistinct(distinctBy) : null; - - final queryJs = QueryJs( - col.native, - whereClausesJs, - whereDistinct, - whereSort == Sort.asc, - filterJs, - sortJs, - distinctJs, - offset, - limit, - ); - - QueryDeserialize deserialize; - //if (property == null) { - deserialize = col.deserializeObject as T Function(Object); - /*} else { - deserialize = (jsObj) => col.schema.deserializeProp(jsObj, property) as T; - }*/ - - return QueryImpl(col, queryJs, deserialize, property); -} - -dynamic _valueToJs(dynamic value) { - if (value == null) { - return double.negativeInfinity; - } else if (value == true) { - return 1; - } else if (value == false) { - return 0; - } else if (value is DateTime) { - return value.toUtc().millisecondsSinceEpoch; - } else if (value is List) { - return value.map(_valueToJs).toList(); - } else { - return value; - } -} - -IdWhereClauseJs _buildIdWhereClause(IdWhereClause wc) { - return IdWhereClauseJs() - ..range = _buildKeyRange( - wc.lower, - wc.upper, - wc.includeLower, - wc.includeUpper, - ); -} - -IndexWhereClauseJs _buildIndexWhereClause( - CollectionSchema schema, - IndexWhereClause wc, -) { - final index = schema.index(wc.indexName); - - final lower = wc.lower?.toList(); - final upper = wc.upper?.toList(); - if (upper != null) { - while (index.properties.length > upper.length) { - upper.add([]); - } - } - - dynamic lowerUnwrapped = wc.lower; - if (index.properties.length == 1 && lower != null) { - lowerUnwrapped = lower.isNotEmpty ? lower[0] : null; - } - - dynamic upperUnwrapped = upper; - if (index.properties.length == 1 && upper != null) { - upperUnwrapped = upper.isNotEmpty ? upper[0] : double.infinity; - } - - return IndexWhereClauseJs() - ..indexName = wc.indexName - ..range = _buildKeyRange( - wc.lower != null ? _valueToJs(lowerUnwrapped) : null, - wc.upper != null ? _valueToJs(upperUnwrapped) : null, - wc.includeLower, - wc.includeUpper, - ); -} - -LinkWhereClauseJs _buildLinkWhereClause( - IsarCollectionImpl col, - LinkWhereClause wc, -) { - // ignore: unused_local_variable - final linkCol = col.isar.getCollectionByNameInternal(wc.linkCollection)! - as IsarCollectionImpl; - //final backlinkLinkName = linkCol.schema.backlinkLinkNames[wc.linkName]; - return LinkWhereClauseJs() - ..linkCollection = wc.linkCollection - //..linkName = backlinkLinkName ?? wc.linkName - //..backlink = backlinkLinkName != null - ..id = wc.id; -} - -KeyRange? _buildKeyRange( - dynamic lower, - dynamic upper, - bool includeLower, - bool includeUpper, -) { - if (lower != null) { - if (upper != null) { - final boundsEqual = idbCmp(lower, upper) == 0; - if (boundsEqual) { - if (includeLower && includeUpper) { - return KeyRange.only(lower); - } else { - // empty range - return KeyRange.upperBound(double.negativeInfinity, true); - } - } - - return KeyRange.bound( - lower, - upper, - !includeLower, - !includeUpper, - ); - } else { - return KeyRange.lowerBound(lower, !includeLower); - } - } else if (upper != null) { - return KeyRange.upperBound(upper, !includeUpper); - } - return null; -} - -FilterJs? _buildFilter( - CollectionSchema schema, - FilterOperation filter, -) { - final filterStr = _buildFilterOperation(schema, filter); - if (filterStr != null) { - return FilterJs('id', 'obj', 'return $filterStr'); - } else { - return null; - } -} - -String? _buildFilterOperation( - CollectionSchema schema, - FilterOperation filter, -) { - if (filter is FilterGroup) { - return _buildFilterGroup(schema, filter); - } else if (filter is LinkFilter) { - unsupportedOnWeb(); - } else if (filter is FilterCondition) { - return _buildCondition(schema, filter); - } else { - return null; - } -} - -String? _buildFilterGroup(CollectionSchema schema, FilterGroup group) { - final builtConditions = group.filters - .map((op) => _buildFilterOperation(schema, op)) - .where((e) => e != null) - .toList(); - - if (builtConditions.isEmpty) { - return null; - } - - if (group.type == FilterGroupType.not) { - return '!(${builtConditions[0]})'; - } else if (builtConditions.length == 1) { - return builtConditions[0]; - } else if (group.type == FilterGroupType.xor) { - final conditions = builtConditions.join(','); - return 'IsarQuery.xor($conditions)'; - } else { - final op = group.type == FilterGroupType.or ? '||' : '&&'; - final condition = builtConditions.join(op); - return '($condition)'; - } -} - -String _buildCondition( - CollectionSchema schema, - FilterCondition condition, -) { - dynamic _prepareFilterValue(dynamic value) { - if (value == null) { - return null; - } else if (value is String) { - return stringify(value); - } else { - return _valueToJs(value); - } - } - - final isListOp = condition.type != FilterConditionType.isNull && - condition.type != FilterConditionType.listLength && - schema.property(condition.property).type.isList; - final accessor = - condition.property == schema.idName ? 'id' : 'obj.${condition.property}'; - final variable = isListOp ? 'e' : accessor; - - final cond = _buildConditionInternal( - conditionType: condition.type, - variable: variable, - val1: _prepareFilterValue(condition.value1), - include1: condition.include1, - val2: _prepareFilterValue(condition.value2), - include2: condition.include2, - caseSensitive: condition.caseSensitive, - ); - - if (isListOp) { - return '(Array.isArray($accessor) && $accessor.some(e => $cond))'; - } else { - return cond; - } -} - -String _buildConditionInternal({ - required FilterConditionType conditionType, - required String variable, - required Object? val1, - required bool include1, - required Object? val2, - required bool include2, - required bool caseSensitive, -}) { - final isNull = '($variable == null || $variable === -Infinity)'; - switch (conditionType) { - case FilterConditionType.equalTo: - if (val1 == null) { - return isNull; - } else if (val1 is String && !caseSensitive) { - return '$variable?.toLowerCase() === ${val1.toLowerCase()}'; - } else { - return '$variable === $val1'; - } - case FilterConditionType.between: - final val = val1 ?? val2; - final lowerOp = include1 ? '>=' : '>'; - final upperOp = include2 ? '<=' : '<'; - if (val == null) { - return isNull; - } else if ((val1 is String?) && (val2 is String?) && !caseSensitive) { - final lower = val1?.toLowerCase() ?? '-Infinity'; - final upper = val2?.toLowerCase() ?? '-Infinity'; - final variableLc = '$variable?.toLowerCase() ?? -Infinity'; - final lowerCond = 'indexedDB.cmp($variableLc, $lower) $lowerOp 0'; - final upperCond = 'indexedDB.cmp($variableLc, $upper) $upperOp 0'; - return '($lowerCond && $upperCond)'; - } else { - final lowerCond = - 'indexedDB.cmp($variable, ${val1 ?? '-Infinity'}) $lowerOp 0'; - final upperCond = - 'indexedDB.cmp($variable, ${val2 ?? '-Infinity'}) $upperOp 0'; - return '($lowerCond && $upperCond)'; - } - case FilterConditionType.lessThan: - if (val1 == null) { - if (include1) { - return isNull; - } else { - return 'false'; - } - } else { - final op = include1 ? '<=' : '<'; - if (val1 is String && !caseSensitive) { - return 'indexedDB.cmp($variable?.toLowerCase() ?? ' - '-Infinity, ${val1.toLowerCase()}) $op 0'; - } else { - return 'indexedDB.cmp($variable, $val1) $op 0'; - } - } - case FilterConditionType.greaterThan: - if (val1 == null) { - if (include1) { - return 'true'; - } else { - return '!$isNull'; - } - } else { - final op = include1 ? '>=' : '>'; - if (val1 is String && !caseSensitive) { - return 'indexedDB.cmp($variable?.toLowerCase() ?? ' - '-Infinity, ${val1.toLowerCase()}) $op 0'; - } else { - return 'indexedDB.cmp($variable, $val1) $op 0'; - } - } - case FilterConditionType.startsWith: - case FilterConditionType.endsWith: - case FilterConditionType.contains: - final op = conditionType == FilterConditionType.startsWith - ? 'startsWith' - : conditionType == FilterConditionType.endsWith - ? 'endsWith' - : 'includes'; - if (val1 is String) { - final isString = 'typeof $variable == "string"'; - if (!caseSensitive) { - return '($isString && $variable.toLowerCase() ' - '.$op(${val1.toLowerCase()}))'; - } else { - return '($isString && $variable.$op($val1))'; - } - } else { - throw IsarError('Unsupported type for condition'); - } - case FilterConditionType.matches: - throw UnimplementedError(); - case FilterConditionType.isNull: - return isNull; - // ignore: no_default_cases - default: - throw UnimplementedError(); - } -} - -SortCmpJs _buildSort(List properties) { - final sort = properties.map((e) { - final op = e.sort == Sort.asc ? '' : '-'; - return '${op}indexedDB.cmp(a.${e.property} ?? "-Infinity", b.${e.property} ' - '?? "-Infinity")'; - }).join('||'); - return SortCmpJs('a', 'b', 'return $sort'); -} - -DistinctValueJs _buildDistinct(List properties) { - final distinct = properties.map((e) { - if (e.caseSensitive == false) { - return 'obj.${e.property}?.toLowerCase() ?? "-Infinity"'; - } else { - return 'obj.${e.property}?.toString() ?? "-Infinity"'; - } - }).join('+'); - return DistinctValueJs('obj', 'return $distinct'); -} diff --git a/packages/isar/lib/src/web/query_impl.dart b/packages/isar/lib/src/web/query_impl.dart deleted file mode 100644 index e2bfc1537..000000000 --- a/packages/isar/lib/src/web/query_impl.dart +++ /dev/null @@ -1,180 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:convert'; -import 'dart:js'; -import 'dart:typed_data'; - -import 'package:isar/isar.dart'; -import 'package:isar/src/web/bindings.dart'; - -import 'package:isar/src/web/isar_collection_impl.dart'; -import 'package:isar/src/web/isar_web.dart'; - -typedef QueryDeserialize = T Function(Object); - -class QueryImpl extends Query { - QueryImpl(this.col, this.queryJs, this.deserialize, this.propertyName); - final IsarCollectionImpl col; - final QueryJs queryJs; - final QueryDeserialize deserialize; - final String? propertyName; - - @override - Isar get isar => col.isar; - - @override - Future findFirst() { - return col.isar.getTxn(false, (IsarTxnJs txn) async { - final result = await queryJs.findFirst(txn).wait(); - if (result == null) { - return null; - } - return deserialize(result); - }); - } - - @override - T? findFirstSync() => unsupportedOnWeb(); - - @override - Future> findAll() { - return col.isar.getTxn(false, (IsarTxnJs txn) async { - final result = await queryJs.findAll(txn).wait>(); - return result.map((e) => deserialize(e as Object)).toList(); - }); - } - - @override - List findAllSync() => unsupportedOnWeb(); - - @override - Future aggregate(AggregationOp op) { - return col.isar.getTxn(false, (IsarTxnJs txn) async { - final property = propertyName ?? col.schema.idName; - - num? result; - switch (op) { - case AggregationOp.min: - result = await queryJs.min(txn, property).wait(); - break; - case AggregationOp.max: - result = await queryJs.max(txn, property).wait(); - break; - case AggregationOp.sum: - result = await queryJs.sum(txn, property).wait(); - break; - case AggregationOp.average: - result = await queryJs.average(txn, property).wait(); - break; - case AggregationOp.count: - result = await queryJs.count(txn).wait(); - break; - // ignore: no_default_cases - default: - throw UnimplementedError(); - } - - if (result == null) { - return null; - } - - if (R == DateTime) { - return DateTime.fromMillisecondsSinceEpoch(result.toInt()).toLocal() - as R; - } else if (R == int) { - return result.toInt() as R; - } else if (R == double) { - return result.toDouble() as R; - } else { - return null; - } - }); - } - - @override - R? aggregateSync(AggregationOp op) => unsupportedOnWeb(); - - @override - Future deleteFirst() { - return col.isar.getTxn(true, (IsarTxnJs txn) { - return queryJs.deleteFirst(txn).wait(); - }); - } - - @override - bool deleteFirstSync() => unsupportedOnWeb(); - - @override - Future deleteAll() { - return col.isar.getTxn(true, (IsarTxnJs txn) { - return queryJs.deleteAll(txn).wait(); - }); - } - - @override - int deleteAllSync() => unsupportedOnWeb(); - - @override - Stream> watch({bool fireImmediately = false}) { - JsFunction? stop; - final controller = StreamController>( - onCancel: () { - stop?.apply([]); - }, - ); - - if (fireImmediately) { - findAll().then(controller.add); - } - - final Null Function(List results) callback = - allowInterop((List results) { - controller.add(results.map((e) => deserialize(e as Object)).toList()); - }); - stop = col.native.watchQuery(queryJs, callback); - - return controller.stream; - } - - @override - Stream watchLazy({bool fireImmediately = false}) { - JsFunction? stop; - final controller = StreamController( - onCancel: () { - stop?.apply([]); - }, - ); - - final Null Function() callback = allowInterop(() { - controller.add(null); - }); - stop = col.native.watchQueryLazy(queryJs, callback); - - return controller.stream; - } - - @override - Future exportJsonRaw(R Function(Uint8List) callback) async { - return col.isar.getTxn(false, (IsarTxnJs txn) async { - final result = await queryJs.findAll(txn).wait(); - final jsonStr = stringify(result); - return callback(const Utf8Encoder().convert(jsonStr)); - }); - } - - @override - Future>> exportJson() { - return col.isar.getTxn(false, (IsarTxnJs txn) async { - final result = await queryJs.findAll(txn).wait>(); - return result.map((e) => jsMapToDart(e as Object)).toList(); - }); - } - - @override - R exportJsonRawSync(R Function(Uint8List) callback) => unsupportedOnWeb(); - - @override - List> exportJsonSync({bool primitiveNull = true}) => - unsupportedOnWeb(); -} diff --git a/packages/isar/lib/src/web/split_words.dart b/packages/isar/lib/src/web/split_words.dart index fa29ddd7e..83bc5c903 100644 --- a/packages/isar/lib/src/web/split_words.dart +++ b/packages/isar/lib/src/web/split_words.dart @@ -1,5 +1,9 @@ // ignore_for_file: public_member_api_docs -import 'package:isar/src/web/isar_web.dart'; +final _word = RegExp( + r"[\p{L}\p{N}\p{M}]+(?:[’'][\p{L}\p{N}\p{M}]+|\.(?=\p{N})[\p{N}\p{M}]*)*", + unicode: true, +); -List isarSplitWords(String input) => unsupportedOnWeb(); +List isarSplitWords(String input) => + _word.allMatches(input).map((match) => match.group(0)!).toList(); diff --git a/packages/isar/pubspec.yaml b/packages/isar/pubspec.yaml index e3c0ba550..afd43f3cd 100644 --- a/packages/isar/pubspec.yaml +++ b/packages/isar/pubspec.yaml @@ -9,12 +9,12 @@ funding: - https://github.com/sponsors/leisim/ environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.5.0 <4.0.0" dependencies: ffi: ">=2.0.0 <3.0.0" - js: ^0.6.4 meta: ^1.7.0 + web: ^1.1.0 dev_dependencies: ffigen: ">=6.1.2 <8.0.0" diff --git a/packages/isar/test/isar_reader_writer_test.dart b/packages/isar/test/isar_reader_writer_test.dart index 5fc83de07..2cebf6022 100644 --- a/packages/isar/test/isar_reader_writer_test.dart +++ b/packages/isar/test/isar_reader_writer_test.dart @@ -201,7 +201,7 @@ String? _readString(IsarReader reader, int offset, bool nullable) { } void _writeString(IsarWriter writer, int offset, dynamic value) { - final bytes = value is String ? utf8.encode(value) as Uint8List : null; + final bytes = value is String ? utf8.encode(value) : null; writer.writeByteList(offset, bytes); } diff --git a/packages/isar/test/memory_engine_test.dart b/packages/isar/test/memory_engine_test.dart new file mode 100644 index 000000000..afc4ca952 --- /dev/null +++ b/packages/isar/test/memory_engine_test.dart @@ -0,0 +1,164 @@ +import 'package:isar/isar.dart'; +import 'package:isar/isar_memory.dart'; +import 'package:test/test.dart'; + +class MemoryObject { + MemoryObject(this.name, {this.id = Isar.autoIncrement}); + + int id; + String name; +} + +const memoryObjectSchema = CollectionSchema( + id: 930001, + name: 'MemoryObject', + idName: 'id', + properties: { + 'name': PropertySchema(id: 0, name: 'name', type: IsarType.string), + }, + indexes: { + 'name': IndexSchema( + id: 0, + name: 'name', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: 'name', + type: IndexType.value, + caseSensitive: true, + ), + ], + ), + }, + links: {}, + embeddedSchemas: {}, + estimateSize: _estimateSize, + serialize: _serialize, + deserialize: _deserialize, + deserializeProp: _deserializeProp, + getId: _getId, + getLinks: _getLinks, + attach: _attach, + version: Isar.version, +); + +int _estimateSize( + MemoryObject object, + List offsets, + Map> allOffsets, +) => + object.name.length; + +void _serialize( + MemoryObject object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.name); +} + +MemoryObject _deserialize( + int id, + IsarReader reader, + List offsets, + Map> allOffsets, +) => + MemoryObject(reader.readString(offsets[0]), id: id); + +dynamic _deserializeProp( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) => + reader.readString(offset); + +int _getId(MemoryObject object) => object.id; +List> _getLinks(MemoryObject object) => const []; +void _attach(IsarCollection col, int id, MemoryObject object) { + object.id = id; +} + +void main() { + tearDown(() async { + await Isar.getInstance()?.close(deleteFromDisk: true); + }); + + test('supports sync CRUD, indexes, and rollback', () { + final isar = IsarMemory.openSync([memoryObjectSchema]); + final objects = isar.collection(); + + final first = MemoryObject('first'); + isar.writeTxnSync(() => objects.putSync(first)); + expect(first.id, 1); + expect(objects.getSync(first.id)?.name, 'first'); + expect(objects.getByIndexSync('name', ['first'])?.id, first.id); + + expect( + () => isar.writeTxnSync(() { + objects.putSync(MemoryObject('rolled back')); + throw StateError('rollback'); + }), + throwsStateError, + ); + expect(objects.where().countSync(), 1); + }); + + test('supports async CRUD and exact delete counts', () async { + final isar = await IsarMemory.open( + [memoryObjectSchema], + name: 'async-memory', + ); + final objects = isar.collection(); + await isar.writeTxn(() => objects.putAll([ + MemoryObject('one'), + MemoryObject('two'), + ])); + expect(await objects.where().count(), 2); + expect(await isar.writeTxn(() => objects.deleteAll([1, 1, 99])), 1); + expect(await objects.where().count(), 1); + }); + + test('serializes concurrent async writes', () async { + final isar = await IsarMemory.open( + [memoryObjectSchema], + name: 'write-queue', + ); + final objects = isar.collection(); + await Future.wait([ + isar.writeTxn(() async { + await Future.delayed(const Duration(milliseconds: 10)); + await objects.put(MemoryObject('first queued')); + }), + isar.writeTxn(() => objects.put(MemoryObject('second queued'))), + ]); + expect(await objects.where().count(), 2); + }); + + test('rejects nested transactions and honors silent watchers', () async { + final isar = await IsarMemory.open( + [memoryObjectSchema], + name: 'transaction-semantics', + ); + final objects = isar.collection(); + expect( + () => isar.writeTxnSync(() => isar.txnSync(() {})), + throwsA(isA()), + ); + + var events = 0; + final subscription = objects.watchLazy().listen((_) => events++); + await isar.writeTxn( + () => objects.put(MemoryObject('silent')), + silent: true, + ); + await Future.delayed(Duration.zero); + expect(events, 0); + await isar.writeTxn(() => objects.put(MemoryObject('watched'))); + await Future.delayed(Duration.zero); + expect(events, 1); + await subscription.cancel(); + }); +} diff --git a/packages/isar/test/web_indexeddb_test.dart b/packages/isar/test/web_indexeddb_test.dart new file mode 100644 index 000000000..dc9c916fc --- /dev/null +++ b/packages/isar/test/web_indexeddb_test.dart @@ -0,0 +1,224 @@ +@TestOn('browser') +library; + +import 'package:isar/isar.dart'; +import 'package:isar/src/web/indexeddb_open.dart' as indexeddb; +import 'package:test/test.dart'; + +class WebObject { + WebObject( + this.value, { + this.id = Isar.autoIncrement, + this.count = 0, + }); + + int id; + String value; + int count; +} + +const webObjectSchema = CollectionSchema( + id: 930002, + name: 'WebObject', + idName: 'id', + properties: { + 'value': PropertySchema(id: 0, name: 'value', type: IsarType.string), + }, + indexes: {}, + links: {}, + embeddedSchemas: {}, + estimateSize: _estimateSize, + serialize: _serialize, + deserialize: _deserialize, + deserializeProp: _deserializeProp, + getId: _getId, + getLinks: _getLinks, + attach: _attach, + version: Isar.version, +); + +const webObjectSchemaV2 = CollectionSchema( + id: 930002, + name: 'WebObject', + idName: 'id', + properties: { + 'value': PropertySchema(id: 0, name: 'value', type: IsarType.string), + 'count': PropertySchema(id: 1, name: 'count', type: IsarType.long), + }, + indexes: {}, + links: {}, + embeddedSchemas: {}, + estimateSize: _estimateSize, + serialize: _serializeV2, + deserialize: _deserializeV2, + deserializeProp: _deserializePropV2, + getId: _getId, + getLinks: _getLinks, + attach: _attach, + version: Isar.version, +); + +int _estimateSize( + WebObject object, + List offsets, + Map> allOffsets, +) => + object.value.length; + +void _serialize( + WebObject object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.value); +} + +void _serializeV2( + WebObject object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer + ..writeString(offsets[0], object.value) + ..writeLong(offsets[1], object.count); +} + +WebObject _deserialize( + int id, + IsarReader reader, + List offsets, + Map> allOffsets, +) => + WebObject(reader.readString(offsets[0]), id: id); + +WebObject _deserializeV2( + int id, + IsarReader reader, + List offsets, + Map> allOffsets, +) => + WebObject( + reader.readString(offsets[0]), + id: id, + count: reader.readLong(offsets[1]), + ); + +dynamic _deserializeProp( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) => + reader.readString(offset); + +dynamic _deserializePropV2( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) => + propertyId == 0 ? reader.readString(offset) : reader.readLong(offset); + +int _getId(WebObject object) => object.id; +List> _getLinks(WebObject object) => const []; +void _attach(IsarCollection col, int id, WebObject object) { + object.id = id; +} + +void main() { + test('splits Unicode words without native code', () { + expect(Isar.splitWords('Hello, 世界 123'), ['Hello', '世界', '123']); + }); + + test('persists committed state and rolls back failed transactions', () async { + const name = 'indexeddb-reopen-test'; + var isar = await Isar.open([webObjectSchema], name: name); + var objects = isar.collection(); + await isar.writeTxn(() => objects.clear()); + await isar.writeTxn(() => objects.put(WebObject('persisted'))); + await isar.close(); + + isar = await Isar.open([webObjectSchema], name: name); + objects = isar.collection(); + expect((await objects.where().findFirst())?.value, 'persisted'); + + await expectLater( + isar.writeTxn(() async { + await objects.put(WebObject('rolled-back')); + throw StateError('rollback'); + }), + throwsStateError, + ); + expect(await objects.where().count(), 1); + await isar.close(); + }); + + test('deletes IndexedDB when closing with deleteFromDisk', () async { + const name = 'indexeddb-delete-test'; + var isar = await Isar.open([webObjectSchema], name: name); + await isar.writeTxn( + () => isar.collection().put(WebObject('temporary')), + ); + await isar.close(deleteFromDisk: true); + + isar = await Isar.open([webObjectSchema], name: name); + expect(await isar.collection().count(), 0); + await isar.close(deleteFromDisk: true); + }); + + test('migrates compatible added properties with defaults', () async { + const name = 'indexeddb-migration-test'; + var isar = await Isar.open([webObjectSchema], name: name); + await isar.writeTxn( + () => isar.collection().put(WebObject('old')), + ); + await isar.close(); + + isar = await Isar.open([webObjectSchemaV2], name: name); + final object = await isar.collection().where().findFirst(); + expect(object?.value, 'old'); + expect(object?.count, 0); + await isar.close(deleteFromDisk: true); + }); + + test('rejects a concurrent direct owner', () async { + const name = 'indexeddb-owner-test'; + final first = await indexeddb.openIsar( + schemas: [webObjectSchema], + name: name, + maxSizeMiB: 256, + relaxedDurability: true, + ); + await expectLater( + indexeddb.openIsar( + schemas: [webObjectSchema], + name: name, + maxSizeMiB: 256, + relaxedDurability: true, + ), + throwsA(isA()), + ); + await first.close(deleteFromDisk: true); + }); + + test('notifies query watchers after commits', () async { + final isar = await Isar.open( + [webObjectSchema], + name: 'indexeddb-query-watcher-test', + ); + final objects = isar.collection(); + await isar.writeTxn(objects.clear); + + final changed = objects + .where() + .watchLazy() + .first + .timeout(const Duration(seconds: 2)); + await isar.writeTxn(() => objects.put(WebObject('watched'))); + await changed; + + await isar.close(deleteFromDisk: true); + }); +} diff --git a/packages/isar/tool/compile_smoke.dart b/packages/isar/tool/compile_smoke.dart new file mode 100644 index 000000000..16c0a5407 --- /dev/null +++ b/packages/isar/tool/compile_smoke.dart @@ -0,0 +1,8 @@ +import 'package:isar/isar.dart'; + +void main() { + if (Isar.version.isEmpty) { + throw StateError('Missing Isar version'); + } + Isar.open(const [], name: 'compile-smoke'); +} diff --git a/packages/isar_generator/lib/src/code_gen/collection_schema_generator.dart b/packages/isar_generator/lib/src/code_gen/collection_schema_generator.dart index 61a23109b..ebe92e29e 100644 --- a/packages/isar_generator/lib/src/code_gen/collection_schema_generator.dart +++ b/packages/isar_generator/lib/src/code_gen/collection_schema_generator.dart @@ -1,11 +1,10 @@ import 'package:dartx/dartx.dart'; import 'package:isar/isar.dart'; -import 'package:isar_generator/src/isar_type.dart'; import 'package:isar_generator/src/object_info.dart'; String generateSchema(ObjectInfo object) { - var code = 'const ${object.dartName.capitalize()}Schema = '; + var code = 'final ${object.dartName.capitalize()}Schema = '; if (!object.isEmbedded) { code += 'CollectionSchema('; } else { @@ -20,7 +19,7 @@ String generateSchema(ObjectInfo object) { code += ''' name: r'${object.isarName}', - id: ${object.id}, + id: ${_generateId(object.id)}, properties: {$properties}, estimateSize: ${object.estimateSizeName}, @@ -88,7 +87,7 @@ String _generateIndexSchema(ObjectIndex index) { return ''' IndexSchema( - id: ${index.id}, + id: ${_generateId(index.id)}, name: r'${index.name}', unique: ${index.unique}, replace: ${index.replace}, @@ -103,10 +102,16 @@ String _generateLinkSchema(ObjectInfo object, ObjectLink link) { } return ''' LinkSchema( - id: ${link.id(object.isarName)}, + id: ${_generateId(link.id(object.isarName))}, name: r'${link.isarName}', target: r'${link.targetCollectionIsarName}', single: ${link.isSingle}, $linkName )'''; } + +String _generateId(int id) { + const maxWebInteger = 9007199254740991; + final webId = id.remainder(maxWebInteger); + return "Isar.schemaId('$id', $webId)"; +} diff --git a/packages/isar_generator/pubspec.yaml b/packages/isar_generator/pubspec.yaml index 25e610d4f..10b84b7ad 100644 --- a/packages/isar_generator/pubspec.yaml +++ b/packages/isar_generator/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/isar/isar homepage: https://isar.dev environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.5.0 <4.0.0" dependencies: analyzer: '>=7.0.0 <8.0.0' diff --git a/packages/isar_inspector/lib/collection/collection_area.dart b/packages/isar_inspector/lib/collection/collection_area.dart index 97dc48bae..db3afcc1b 100644 --- a/packages/isar_inspector/lib/collection/collection_area.dart +++ b/packages/isar_inspector/lib/collection/collection_area.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:html'; import 'dart:math'; import 'package:clickup_fading_scroll/clickup_fading_scroll.dart'; @@ -16,6 +15,7 @@ import 'package:isar_inspector/connect_client.dart'; import 'package:isar_inspector/object/isar_object.dart'; import 'package:isar_inspector/query_builder/query_group.dart'; import 'package:isar_inspector/util.dart'; +import 'package:web/web.dart' as web; const objectsPerPage = 20; @@ -292,12 +292,12 @@ class _CollectionAreaState extends State { final data = await widget.client.exportJson(query); try { final base64 = base64Encode(utf8.encode(jsonEncode(data))); - final anchor = - AnchorElement(href: 'data:application/octet-stream;base64,$base64') - ..target = 'blank' - ..download = '${widget.collection}.json'; + final anchor = web.HTMLAnchorElement() + ..href = 'data:application/octet-stream;base64,$base64' + ..target = 'blank' + ..download = '${widget.collection}.json'; - document.body!.append(anchor); + web.document.body!.append(anchor); anchor.click(); anchor.remove(); } catch (_) {} diff --git a/packages/isar_inspector/lib/error_screen.dart b/packages/isar_inspector/lib/error_screen.dart index 38641793f..b0539d08b 100644 --- a/packages/isar_inspector/lib/error_screen.dart +++ b/packages/isar_inspector/lib/error_screen.dart @@ -1,7 +1,5 @@ -// ignore: avoid_web_libraries_in_flutter -import 'dart:html'; - import 'package:flutter/material.dart'; +import 'package:web/web.dart' as web; class ErrorScreen extends StatelessWidget { const ErrorScreen({super.key}); @@ -20,7 +18,7 @@ class ErrorScreen extends StatelessWidget { const Text('Please make sure your Isar instance is running.'), const SizedBox(height: 40), ElevatedButton( - onPressed: window.location.reload, + onPressed: () => web.window.location.reload(), child: const Text('Retry Connection'), ), ], diff --git a/packages/isar_inspector/lib/main.dart b/packages/isar_inspector/lib/main.dart index 3e38e296f..917723169 100644 --- a/packages/isar_inspector/lib/main.dart +++ b/packages/isar_inspector/lib/main.dart @@ -1,13 +1,10 @@ -// ignore: avoid_web_libraries_in_flutter -import 'dart:html'; - import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; - import 'package:isar_inspector/connection_screen.dart'; +import 'package:web/web.dart' as web; void main() async { - if (window.navigator.userAgent.toLowerCase().contains('chrome')) { + if (web.window.navigator.userAgent.toLowerCase().contains('chrome')) { runApp( DarkMode( notifier: DarkModeNotifier(), diff --git a/packages/isar_inspector/pubspec.yaml b/packages/isar_inspector/pubspec.yaml index cd2b6eed9..a17ed9b4a 100644 --- a/packages/isar_inspector/pubspec.yaml +++ b/packages/isar_inspector/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: "none" version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.5.0 <4.0.0" dependencies: clickup_fading_scroll: @@ -12,12 +12,13 @@ dependencies: flutter: sdk: flutter flutter_svg: any - font_awesome_flutter: any + font_awesome_flutter: ^10.1.0 go_router: ^5.0.0 - google_fonts: ^3.0.1 + google_fonts: ^8.2.0 isar: path: ../isar vm_service: ^9.0.0 + web: ^1.1.0 web_socket_channel: ^2.2.0 dev_dependencies: diff --git a/packages/isar_test/lib/src/common.dart b/packages/isar_test/lib/src/common.dart index d4fc83cc7..2fdf35666 100644 --- a/packages/isar_test/lib/src/common.dart +++ b/packages/isar_test/lib/src/common.dart @@ -5,15 +5,62 @@ import 'dart:io'; import 'dart:math'; import 'package:isar/isar.dart'; +import 'package:isar/isar_memory.dart'; import 'package:isar_test/src/init_native.dart' - if (dart.library.html) 'package:isar_test/src/init_web.dart'; + if (dart.library.js_interop) 'package:isar_test/src/init_web.dart'; +import 'package:isar_test/src/platform.dart' + if (dart.library.js_interop) 'package:isar_test/src/platform_web.dart' + as platform; import 'package:isar_test/src/sync_async_helper.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; import 'package:test_api/src/backend/invoker.dart'; -const kIsWeb = identical(0, 0.0); +const kIsWeb = platform.isWeb; +const isMemoryBackend = String.fromEnvironment('ISAR_TEST_BACKEND') == 'memory'; + +enum TestBackend { native, memory, web } + +const testBackend = kIsWeb + ? TestBackend.web + : isMemoryBackend + ? TestBackend.memory + : TestBackend.native; + +enum BackendCapability { + diskPersistence, + schemaMigration, + isolates, + filesystem, + configuredMaxSize, +} + +bool supportsCapability(BackendCapability capability) { + return switch (testBackend) { + TestBackend.native => true, + TestBackend.memory => false, + TestBackend.web => switch (capability) { + BackendCapability.diskPersistence || + BackendCapability.schemaMigration => + true, + BackendCapability.isolates || + BackendCapability.filesystem || + BackendCapability.configuredMaxSize => + false, + }, + }; +} + +bool skipIfUnsupported(BackendCapability capability) { + if (supportsCapability(capability)) return false; + test( + 'requires ${capability.name}', + () {}, + skip: 'The ${testBackend.name} backend does not support ${capability.name}.', + ); + return true; +} final testErrors = []; int testCount = 0; @@ -21,7 +68,9 @@ int testCount = 0; var _setUp = false; Future _prepareTest() async { if (!_setUp) { - await init(); + if (!isMemoryBackend) { + await init(); + } _setUp = true; } } @@ -44,7 +93,7 @@ void isarTestSync( Timeout? timeout, bool skip = false, }) { - if (!kIsWeb) { + if (!kIsWeb || isMemoryBackend) { _isarTest(name, true, body, timeout: timeout, skip: skip); } } @@ -116,19 +165,27 @@ Future openTempIsar( bool closeAutomatically = true, }) async { await _prepareTest(); - if (!kIsWeb && directory == null && testTempPath == null) { + if (!kIsWeb && + !isMemoryBackend && + directory == null && + testTempPath == null) { final dartToolDir = path.join(Directory.current.path, '.dart_tool'); testTempPath = path.join(dartToolDir, 'test', 'tmp'); await Directory(testTempPath!).create(recursive: true); } - final isar = await tOpen( - schemas: schemas, - name: name ?? getRandomName(), - maxSizeMiB: maxSizeMiB, - directory: testTempPath ?? '', - compactOnLaunch: compactOnLaunch, - ); + final instanceName = name ?? getRandomName(); + final isar = isMemoryBackend + ? syncTest + ? IsarMemory.openSync(schemas, name: instanceName) + : await IsarMemory.open(schemas, name: instanceName) + : await tOpen( + schemas: schemas, + name: instanceName, + maxSizeMiB: maxSizeMiB, + directory: directory ?? testTempPath, + compactOnLaunch: compactOnLaunch, + ); if (Invoker.current != null && closeAutomatically) { addTearDown(() async { @@ -139,6 +196,6 @@ Future openTempIsar( } // ignore: invalid_use_of_visible_for_testing_member - if (!kIsWeb) await isar.verify(); + if (!kIsWeb || isMemoryBackend) await isar.verify(); return isar; } diff --git a/packages/isar_test/lib/src/init_web.dart b/packages/isar_test/lib/src/init_web.dart index e7589c675..cbfcf1684 100644 --- a/packages/isar_test/lib/src/init_web.dart +++ b/packages/isar_test/lib/src/init_web.dart @@ -1,12 +1 @@ -// ignore_for_file: avoid_web_libraries_in_flutter, implementation_imports - -import 'dart:js' as js; - -import 'package:isar/src/web/open.dart' as isar_web; -import 'package:isar_test/src/isar_web_src.dart'; - -Future init() async { - js.context.callMethod('eval', [isarWebSrc]); - // ignore: invalid_use_of_visible_for_testing_member - isar_web.doNotInitializeIsarWeb(); -} +Future init() async {} diff --git a/packages/isar_test/lib/src/isar_web_src.dart b/packages/isar_test/lib/src/isar_web_src.dart deleted file mode 100644 index 827f9e102..000000000 --- a/packages/isar_test/lib/src/isar_web_src.dart +++ /dev/null @@ -1 +0,0 @@ -const isarWebSrc = ''; diff --git a/packages/isar_test/lib/src/platform.dart b/packages/isar_test/lib/src/platform.dart new file mode 100644 index 000000000..2cdc5068f --- /dev/null +++ b/packages/isar_test/lib/src/platform.dart @@ -0,0 +1 @@ +const bool isWeb = false; diff --git a/packages/isar_test/lib/src/platform_web.dart b/packages/isar_test/lib/src/platform_web.dart new file mode 100644 index 000000000..69fda2b02 --- /dev/null +++ b/packages/isar_test/lib/src/platform_web.dart @@ -0,0 +1 @@ +const bool isWeb = true; diff --git a/packages/isar_test/lib/src/sync_async_helper.dart b/packages/isar_test/lib/src/sync_async_helper.dart index 0836b0ce3..9b043e5ac 100644 --- a/packages/isar_test/lib/src/sync_async_helper.dart +++ b/packages/isar_test/lib/src/sync_async_helper.dart @@ -8,7 +8,7 @@ bool get syncTest => Zone.current[#syncTest] as bool? ?? false; Future tOpen({ required List> schemas, - required String directory, + String? directory, String name = Isar.defaultName, int maxSizeMiB = Isar.defaultMaxSizeMiB, bool relaxedDurability = true, diff --git a/packages/isar_test/pubspec.yaml b/packages/isar_test/pubspec.yaml index 25e7e1827..0eab90678 100644 --- a/packages/isar_test/pubspec.yaml +++ b/packages/isar_test/pubspec.yaml @@ -5,14 +5,14 @@ publish_to: "none" version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.5.0 <4.0.0" dependencies: flutter: sdk: flutter integration_test: sdk: flutter - intl: 0.18.0 + intl: ^0.20.2 isar: any isar_flutter_libs: any json_annotation: ^4.7.0 @@ -20,7 +20,7 @@ dependencies: path: ^1.8.2 path_provider: ^2.0.10 test: ^1.21.4 - test_api: ^0.4.12 + test_api: ^0.7.7 dev_dependencies: build_runner: any diff --git a/packages/isar_test/test/compact_on_launch_test.dart b/packages/isar_test/test/compact_on_launch_test.dart index 28255c0d1..faec3ac9c 100644 --- a/packages/isar_test/test/compact_on_launch_test.dart +++ b/packages/isar_test/test/compact_on_launch_test.dart @@ -28,6 +28,8 @@ class Model { } void main() { + if (skipIfUnsupported(BackendCapability.filesystem)) return; + group('Compact on launch', () { late Isar isar; late File file; diff --git a/packages/isar_test/test/copy_to_file_test.dart b/packages/isar_test/test/copy_to_file_test.dart index 4fef1a35c..1eb3e763d 100644 --- a/packages/isar_test/test/copy_to_file_test.dart +++ b/packages/isar_test/test/copy_to_file_test.dart @@ -29,6 +29,8 @@ class Model { } void main() { + if (skipIfUnsupported(BackendCapability.filesystem)) return; + group('Copy to file', () { late Isar isar; diff --git a/packages/isar_test/test/default_value/default_test.dart b/packages/isar_test/test/default_value/default_test.dart index ac2bca445..2b52cc0f5 100644 --- a/packages/isar_test/test/default_value/default_test.dart +++ b/packages/isar_test/test/default_value/default_test.dart @@ -75,6 +75,8 @@ class DefaultListModel { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + group('Default value', () { isarTest('scalar', () async { final emptyObj = EmptyModel(0); diff --git a/packages/isar_test/test/default_value/no_default_test.dart b/packages/isar_test/test/default_value/no_default_test.dart index 480d60bf6..2ac574760 100644 --- a/packages/isar_test/test/default_value/no_default_test.dart +++ b/packages/isar_test/test/default_value/no_default_test.dart @@ -89,6 +89,8 @@ class NoDefaultListModel { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + group('No default value', () { isarTest('scalar', () async { final emptyObj = EmptyModel(0); @@ -102,7 +104,7 @@ void main() { expect(obj.boolValue, false); expect(obj.byteValue, 0); expect(obj.shortValue, -2147483648); - expect(obj.intValue, -9223372036854775808); + expect(obj.intValue, Isar.minId); expect(obj.floatValue, isNaN); expect(obj.doubleValue, isNaN); expect( @@ -136,7 +138,7 @@ void main() { ); expect( await isar2.noDefaultModels.where().intValueProperty().tFindFirst(), - -9223372036854775808, + Isar.minId, ); expect( await isar2.noDefaultModels.where().floatValueProperty().tFindFirst(), diff --git a/packages/isar_test/test/default_value/nullable_test.dart b/packages/isar_test/test/default_value/nullable_test.dart index ec980cd91..551f00c9f 100644 --- a/packages/isar_test/test/default_value/nullable_test.dart +++ b/packages/isar_test/test/default_value/nullable_test.dart @@ -86,6 +86,8 @@ class NullableListModel { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + group('Nullable value', () { isarTest('scalar', () async { final emptyObj = EmptyModel(0); diff --git a/packages/isar_test/test/filter/filter_list_length_test.dart b/packages/isar_test/test/filter/filter_list_length_test.dart index cf1f9a302..e9a84a697 100644 --- a/packages/isar_test/test/filter/filter_list_length_test.dart +++ b/packages/isar_test/test/filter/filter_list_length_test.dart @@ -96,7 +96,7 @@ void main() { await qEqual(isar.models.filter().doublesLengthEqualTo(6), [obj1]); await qEqual(isar.models.filter().doublesLengthEqualTo(42), []); await qEqual( - isar.models.filter().doublesLengthEqualTo(9223372036854775807), + isar.models.filter().doublesLengthEqualTo(Isar.maxId), [], ); }); diff --git a/packages/isar_test/test/filter/filter_string_list_test.dart b/packages/isar_test/test/filter/filter_string_list_test.dart index 0e6ffece2..58c82838b 100644 --- a/packages/isar_test/test/filter/filter_string_list_test.dart +++ b/packages/isar_test/test/filter/filter_string_list_test.dart @@ -458,7 +458,7 @@ void main() { ); }); - isarTestVm('.elementMatches() VM', () async { + isarTest('.elementMatches()', () async { await qEqualSet( isar.stringModels.filter().stringsElementMatches('?????????'), [obj1, obj2, obj4, obj5], @@ -508,42 +508,6 @@ void main() { ); }); - isarTestWeb('.elementMatches() WEB', () async { - expect( - await isar.stringModels - .filter() - .stringsElementMatches('?????????') - .tFindAll(), - anyOf(throwsUnimplementedError, throwsUnsupportedError), - ); - - expect( - await isar.stringModels - .filter() - .nullableStringsElementMatches('??????????????????') - .tFindAll(), - anyOf(throwsUnimplementedError, throwsUnsupportedError), - ); - - expect( - await isar.stringModels - .filter() - .stringsNullableElementMatches('??????????????????') - .tFindAll(), - anyOf(throwsUnimplementedError, throwsUnsupportedError), - ); - - expect( - await isar.stringModels - .filter() - .nullableStringsNullableElementContains( - '???????????????????????????', - ) - .tFindAll(), - anyOf(throwsUnimplementedError, throwsUnsupportedError), - ); - }); - isarTest('.elementIsNull()', () async { await qEqualSet( isar.stringModels.filter().nullableStringsElementIsNull(), diff --git a/packages/isar_test/test/filter/filter_string_test.dart b/packages/isar_test/test/filter/filter_string_test.dart index 2bdc2a7d4..63181ac41 100644 --- a/packages/isar_test/test/filter/filter_string_test.dart +++ b/packages/isar_test/test/filter/filter_string_test.dart @@ -223,7 +223,7 @@ void main() { */ }); - isarTestVm('.matches() VM', () async { + isarTest('.matches()', () async { await qEqualSet( isar.stringModels.filter().fieldMatches('*ng 5'), [obj5, obj6], @@ -237,13 +237,6 @@ void main() { await qEqualSet(isar.stringModels.filter().fieldMatches('*4?'), []); }); - isarTestWeb('.matches() WEB', () async { - expect( - await isar.stringModels.filter().fieldMatches('*ng 5').tFindAll(), - anyOf(throwsUnimplementedError, throwsUnsupportedError), - ); - }); - isarTest('.isEmpty()', () async { await qEqualSet( isar.stringModels.filter().fieldIsEmpty(), diff --git a/packages/isar_test/test/filter/link/filter_links_test.dart b/packages/isar_test/test/filter/link/filter_links_test.dart index 48aa9c9cc..ac38d4b1c 100644 --- a/packages/isar_test/test/filter/link/filter_links_test.dart +++ b/packages/isar_test/test/filter/link/filter_links_test.dart @@ -294,7 +294,7 @@ void main() { ); await qEqualSet( - isar.sourceModels.filter().linksLengthLessThan(9223372036854775807), + isar.sourceModels.filter().linksLengthLessThan(Isar.maxId), [source1, source2, source3, source4, source5, source6], ); }); diff --git a/packages/isar_test/test/index/multi_entry_test.dart b/packages/isar_test/test/index/multi_entry_test.dart index d31a5a535..3c3128ae3 100644 --- a/packages/isar_test/test/index/multi_entry_test.dart +++ b/packages/isar_test/test/index/multi_entry_test.dart @@ -164,7 +164,7 @@ void main() { bools: [false, false, false], ints: [-154123, 3], doubles: [234.34, -123e3], - dateTimes: [DateTime(1204512, 1, 3), DateTime(1970)], + dateTimes: [DateTime(120451, 1, 3), DateTime(1970)], stringsSensitive: ['FORK', 'Joe'], stringsInsensitive: ['foo', 'bar', 'tomato', 'fries'], ); diff --git a/packages/isar_test/test/instance_test.dart b/packages/isar_test/test/instance_test.dart index f24ddb9d6..055b2636e 100644 --- a/packages/isar_test/test/instance_test.dart +++ b/packages/isar_test/test/instance_test.dart @@ -63,7 +63,7 @@ void main() { }); expect(obj4.id, 21); await qEqual(isar.models.where(), [obj1, obj2, obj3, obj4]); - }); + }, skip: !supportsCapability(BackendCapability.diskPersistence)); isarTest('Prevents usage of closed collection', () async { final isar = await openTempIsar([ModelSchema]); diff --git a/packages/isar_test/test/isolate_test.dart b/packages/isar_test/test/isolate_test.dart index 19ef56873..8c2b19633 100644 --- a/packages/isar_test/test/isolate_test.dart +++ b/packages/isar_test/test/isolate_test.dart @@ -52,6 +52,8 @@ Future _isolateFunc(SendPort port) async { } void main() { + if (skipIfUnsupported(BackendCapability.isolates)) return; + isarTest('Isolate test', () async { final isar = await openTempIsar([TestModelSchema], name: 'test'); diff --git a/packages/isar_test/test/max_size_test.dart b/packages/isar_test/test/max_size_test.dart index 264bdeca5..3e087edd7 100644 --- a/packages/isar_test/test/max_size_test.dart +++ b/packages/isar_test/test/max_size_test.dart @@ -12,6 +12,8 @@ class Model { } void main() { + if (skipIfUnsupported(BackendCapability.configuredMaxSize)) return; + group('Max Size', () { test('default', () async { final isar = await openTempIsar([ModelSchema]); diff --git a/packages/isar_test/test/migration/add_remove_collection_test.dart b/packages/isar_test/test/migration/add_remove_collection_test.dart index 1b22ecbc9..17842ebf1 100644 --- a/packages/isar_test/test/migration/add_remove_collection_test.dart +++ b/packages/isar_test/test/migration/add_remove_collection_test.dart @@ -39,6 +39,8 @@ class Model2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Add collection', () async { final isar1 = await openTempIsar([Model1Schema]); final obj1A = Model1(5, 'col1_a'); diff --git a/packages/isar_test/test/migration/add_remove_embedded_field_test.dart b/packages/isar_test/test/migration/add_remove_embedded_field_test.dart index eaea6fdd0..aa1b17517 100644 --- a/packages/isar_test/test/migration/add_remove_embedded_field_test.dart +++ b/packages/isar_test/test/migration/add_remove_embedded_field_test.dart @@ -84,6 +84,8 @@ class Embedded2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Add field', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() { diff --git a/packages/isar_test/test/migration/add_remove_field_test.dart b/packages/isar_test/test/migration/add_remove_field_test.dart index 0fa548d77..16d1a376f 100644 --- a/packages/isar_test/test/migration/add_remove_field_test.dart +++ b/packages/isar_test/test/migration/add_remove_field_test.dart @@ -38,6 +38,8 @@ class Col2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Add field', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() { diff --git a/packages/isar_test/test/migration/add_remove_index_test.dart b/packages/isar_test/test/migration/add_remove_index_test.dart index c08cc5d36..e4492f476 100644 --- a/packages/isar_test/test/migration/add_remove_index_test.dart +++ b/packages/isar_test/test/migration/add_remove_index_test.dart @@ -34,6 +34,8 @@ class Col2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Add remove index', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() { diff --git a/packages/isar_test/test/migration/add_remove_link_test.dart b/packages/isar_test/test/migration/add_remove_link_test.dart index bacec20f8..6fa5878a4 100644 --- a/packages/isar_test/test/migration/add_remove_link_test.dart +++ b/packages/isar_test/test/migration/add_remove_link_test.dart @@ -34,6 +34,8 @@ class Col2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Add remove link', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() async { diff --git a/packages/isar_test/test/migration/change_field_embedded_test.dart b/packages/isar_test/test/migration/change_field_embedded_test.dart index 582601f00..9bc0b264f 100644 --- a/packages/isar_test/test/migration/change_field_embedded_test.dart +++ b/packages/isar_test/test/migration/change_field_embedded_test.dart @@ -53,6 +53,8 @@ class Embedded2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Change field embedded', () async { final isar1 = await openTempIsar([Model1Schema]); await isar1.tWriteTxn(() { diff --git a/packages/isar_test/test/migration/change_field_nullability_test.dart b/packages/isar_test/test/migration/change_field_nullability_test.dart index a0692e17d..9aadc8a63 100644 --- a/packages/isar_test/test/migration/change_field_nullability_test.dart +++ b/packages/isar_test/test/migration/change_field_nullability_test.dart @@ -33,6 +33,8 @@ class Col2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Change field nullability', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() { diff --git a/packages/isar_test/test/migration/change_field_type_test.dart b/packages/isar_test/test/migration/change_field_type_test.dart index 11724be3a..ffdb060d5 100644 --- a/packages/isar_test/test/migration/change_field_type_test.dart +++ b/packages/isar_test/test/migration/change_field_type_test.dart @@ -37,6 +37,8 @@ class Model2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Change field type', () async { final isar1 = await openTempIsar([Model1Schema]); final obj1A = Model1(1, 'a', 'OBJ1'); diff --git a/packages/isar_test/test/migration/change_link_links_test.dart b/packages/isar_test/test/migration/change_link_links_test.dart index 73b22f392..0d2fb47bd 100644 --- a/packages/isar_test/test/migration/change_link_links_test.dart +++ b/packages/isar_test/test/migration/change_link_links_test.dart @@ -31,6 +31,8 @@ class Col2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Add remove link', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() async { diff --git a/packages/isar_test/test/regression/issue_235_rename_field_test.dart b/packages/isar_test/test/regression/issue_235_rename_field_test.dart index bd5d2977a..3dd8abc2c 100644 --- a/packages/isar_test/test/regression/issue_235_rename_field_test.dart +++ b/packages/isar_test/test/regression/issue_235_rename_field_test.dart @@ -54,6 +54,8 @@ class Col2 { } void main() { + if (skipIfUnsupported(BackendCapability.schemaMigration)) return; + isarTest('Regression 235 Rename field', () async { final isar1 = await openTempIsar([Col1Schema]); await isar1.tWriteTxn(() { diff --git a/packages/isar_test/test/stress/twitter_test.dart b/packages/isar_test/test/stress/twitter_test.dart index 809352095..10cd8bc7d 100644 --- a/packages/isar_test/test/stress/twitter_test.dart +++ b/packages/isar_test/test/stress/twitter_test.dart @@ -27,6 +27,8 @@ Future> downloadTweets(String dir, int index) async { } void main() async { + if (skipIfUnsupported(BackendCapability.filesystem)) return; + group( 'Twitter Stress', () { diff --git a/packages/isar_test/test/transaction_test.dart b/packages/isar_test/test/transaction_test.dart index 896f7eafd..a0c309be3 100644 --- a/packages/isar_test/test/transaction_test.dart +++ b/packages/isar_test/test/transaction_test.dart @@ -27,7 +27,7 @@ void main() { isar = await openTempIsar([ModelSchema]); }); - isarTest('Sync txn cannot be opened in sync txn', () { + isarTestVm('Sync txn cannot be opened in sync txn', () { isar.txnSync(() { expect( () => isar.txnSync(() {}), @@ -79,7 +79,7 @@ void main() { }); }); - isarTest('Async txn cannot be opened in sync txn', () { + isarTestVm('Async txn cannot be opened in sync txn', () { isar.txnSync(() { expect( () => isar.txn(() async {}), @@ -131,7 +131,7 @@ void main() { }); }); - isarTest('Sync txn can be opened during async write txn', () async { + isarTestVm('Sync txn can be opened during async write txn', () async { final c = Completer(); final txnFuture = isar.writeTxn(() async { await c.future; @@ -144,7 +144,7 @@ void main() { await txnFuture; }); - isarTest('Sync write txn cannot be opened during async write txn', + isarTestVm('Sync write txn cannot be opened during async write txn', () async { final c = Completer(); final txnFuture = isar.writeTxn(() async { @@ -172,7 +172,7 @@ void main() { await txnFuture; }); - isarTest('Sync writing requires sync write txn', () async { + isarTestVm('Sync writing requires sync write txn', () async { expect( () => isar.models.putSync(Model()), throwsIsarError('require an explicit transaction'), @@ -186,7 +186,7 @@ void main() { }); }); - isarTest('Async writing requires async write txn', () async { + isarTestVm('Async writing requires async write txn', () async { await expectLater( () => isar.models.put(Model()), throwsIsarError('require an explicit transaction'), diff --git a/packages/isar_test/test/watcher_test.dart b/packages/isar_test/test/watcher_test.dart index c709e05bf..0dc50b5d5 100644 --- a/packages/isar_test/test/watcher_test.dart +++ b/packages/isar_test/test/watcher_test.dart @@ -257,14 +257,8 @@ void main() { await isar.tWriteTxn(() => col.tDelete(1)); await listenerLazy.next; - if (kIsWeb) { - expect(await listener.next, [obj2]); - } await isar.tWriteTxn(() => col.tDelete(2)); - if (kIsWeb) { - await listenerLazy.next; - } expect(await listener.next, []); await listenerLazy.done(); @@ -283,10 +277,6 @@ void main() { expect(await listener.next, []); await isar.tWriteTxn(() => col.tDeleteAll([3])); - if (kIsWeb) { - await listenerLazy.next; - expect(await listener.next, []); - } await listenerLazy.done(); await listener.done(); @@ -301,14 +291,8 @@ void main() { await isar.writeTxn(() => col.deleteByValue(obj1.value)); await listenerLazy.next; - if (kIsWeb) { - expect(await listener.next, [obj2]); - } await isar.writeTxn(() => col.deleteByValue(obj2.value)); - if (kIsWeb) { - await listenerLazy.next; - } expect(await listener.next, []); await listenerLazy.done(); @@ -328,10 +312,6 @@ void main() { expect(await listener.next, []); await isar.writeTxn(() => col.deleteAllByValue([obj3.value])); - if (kIsWeb) { - await listenerLazy.next; - expect(await listener.next, []); - } await listenerLazy.done(); await listener.done(); diff --git a/packages/isar_test/test/web_conformance_0_test.dart b/packages/isar_test/test/web_conformance_0_test.dart new file mode 100644 index 000000000..553a0d69a --- /dev/null +++ b/packages/isar_test/test/web_conformance_0_test.dart @@ -0,0 +1,3 @@ +import 'all_tests.dart' as conformance; + +void main() => conformance.main(shardIndex: 0, shardCount: 4); diff --git a/packages/isar_test/test/web_conformance_1_test.dart b/packages/isar_test/test/web_conformance_1_test.dart new file mode 100644 index 000000000..787cd5e32 --- /dev/null +++ b/packages/isar_test/test/web_conformance_1_test.dart @@ -0,0 +1,3 @@ +import 'all_tests.dart' as conformance; + +void main() => conformance.main(shardIndex: 1, shardCount: 4); diff --git a/packages/isar_test/test/web_conformance_2_test.dart b/packages/isar_test/test/web_conformance_2_test.dart new file mode 100644 index 000000000..9bde89e9a --- /dev/null +++ b/packages/isar_test/test/web_conformance_2_test.dart @@ -0,0 +1,3 @@ +import 'all_tests.dart' as conformance; + +void main() => conformance.main(shardIndex: 2, shardCount: 4); diff --git a/packages/isar_test/test/web_conformance_3_test.dart b/packages/isar_test/test/web_conformance_3_test.dart new file mode 100644 index 000000000..32b928c71 --- /dev/null +++ b/packages/isar_test/test/web_conformance_3_test.dart @@ -0,0 +1,3 @@ +import 'all_tests.dart' as conformance; + +void main() => conformance.main(shardIndex: 3, shardCount: 4); diff --git a/packages/isar_test/tool/generate_all_tests.dart b/packages/isar_test/tool/generate_all_tests.dart index 568666aa1..751b323c3 100644 --- a/packages/isar_test/tool/generate_all_tests.dart +++ b/packages/isar_test/tool/generate_all_tests.dart @@ -6,25 +6,33 @@ void main() { .listSync(recursive: true) .where((FileSystemEntity e) => e is File && e.path.endsWith('_test.dart')) .map((FileSystemEntity e) => e.path) - .toList(); + .where((String path) => !path.contains('web_conformance_')) + .toList() + ..sort(); final imports = files.map((String e) { final dartPath = e.replaceAll(p.separator, '/'); final name = e.split('.')[0].replaceAll(p.separator, '_'); return "import '../$dartPath' as $name;"; }).join('\n'); + final webImports = files.map((String e) { + final dartPath = e.replaceAll(p.separator, '/').substring('test/'.length); + final name = e.split('.')[0].replaceAll(p.separator, '_'); + return "import '$dartPath' as $name;"; + }).join('\n'); - final calls = files.map((String e) { + final calls = files.asMap().entries.map((entry) { + final index = entry.key; + final e = entry.value; final content = File(e).readAsStringSync(); var call = "${e.split('.')[0].replaceAll(p.separator, '_')}.main();"; if (e.contains('stress')) { call = 'if (stress) $call'; } if (content.startsWith("@TestOn('vm')")) { - return 'if (!kIsWeb) $call'; - } else { - return call; + call = 'if (!kIsWeb) $call'; } + return 'if ($index % shardCount == shardIndex) $call'; }).join('\n'); final code = """ @@ -33,7 +41,7 @@ void main() { import 'package:isar_test/isar_test.dart'; $imports - void main() { + void main({int shardIndex = 0, int shardCount = 1}) { const stress = bool.fromEnvironment('STRESS'); $calls } @@ -41,4 +49,7 @@ void main() { Directory('integration_test').createSync(); File('integration_test${p.separator}all_tests.dart').writeAsStringSync(code); + File('test${p.separator}all_tests.dart').writeAsStringSync( + code.replaceFirst(imports, webImports), + ); } diff --git a/packages/isar_web/.eslintrc.yml b/packages/isar_web/.eslintrc.yml deleted file mode 100644 index 0c1206396..000000000 --- a/packages/isar_web/.eslintrc.yml +++ /dev/null @@ -1,13 +0,0 @@ -env: - browser: true - commonjs: true - es2021: true -extends: - - 'eslint:recommended' - - 'plugin:@typescript-eslint/recommended' -parser: '@typescript-eslint/parser' -parserOptions: - ecmaVersion: 12 -plugins: - - '@typescript-eslint' -rules: {} diff --git a/packages/isar_web/.gitignore b/packages/isar_web/.gitignore deleted file mode 100644 index 5c9ed204b..000000000 --- a/packages/isar_web/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -.idea -.vscode -dist \ No newline at end of file diff --git a/packages/isar_web/.prettierrc b/packages/isar_web/.prettierrc deleted file mode 100644 index 97b711153..000000000 --- a/packages/isar_web/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "semi": false, - "singleQuote": true, - "trailingComma": "all", - "arrowParens": "avoid" -} diff --git a/packages/isar_web/README.md b/packages/isar_web/README.md deleted file mode 100644 index 360025034..000000000 --- a/packages/isar_web/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# isar-web - -The web runtime for the Isar database. diff --git a/packages/isar_web/package-lock.json b/packages/isar_web/package-lock.json deleted file mode 100644 index 36aae7c15..000000000 --- a/packages/isar_web/package-lock.json +++ /dev/null @@ -1,2889 +0,0 @@ -{ - "name": "isar", - "version": "2.5.1", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "isar", - "version": "2.5.1", - "license": "Apache-2.0", - "devDependencies": { - "broadcast-channel": "^4.11.0", - "fast-deep-equal": "^3.1.1", - "ts-loader": "^9.2.6", - "typescript": "^4.5.5", - "webpack": "^5.67.0", - "webpack-cli": "^5.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", - "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.13.tgz", - "integrity": "sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw==", - "dev": true - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.0.tgz", - "integrity": "sha512-war4OU8NGjBqU3DP3bx6ciODXIh7dSXcpQq+P4K2Tqyd8L5OjZ7COx9QXx/QdCIwL2qoX09Wr4Cwf7uS4qdEng==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.0.tgz", - "integrity": "sha512-NNxDgbo4VOkNhOlTgY0Elhz3vKpOJq4/PKeKg7r8cmYM+GQA9vDofLYyup8jS6EpUvhNmR30cHTCEIyvXpskwA==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.0.tgz", - "integrity": "sha512-Rumq5mHvGXamnOh3O8yLk1sjx8dB30qF1OeR6VC00DIR6SLJ4bwwUGKC4pE7qBFoQyyh0H9sAg3fikYgAqVR0w==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/broadcast-channel": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-4.18.1.tgz", - "integrity": "sha512-eV1srWgt6H4hbtGqD7THn60me66WA5l0LogpssuX9jK6NK26HzIZr+VsrlD7Obe0BtYnwoo/a4v4z5gfty04DA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.16.0", - "oblivious-set": "1.1.1", - "p-queue": "6.6.2", - "rimraf": "3.0.2", - "unload": "2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/pubkey" - } - }, - "node_modules/browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "dependencies": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001304", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001304.tgz", - "integrity": "sha512-bdsfZd6K6ap87AGqSHJP/s1V+U6Z5lyrcbBu3ovbCCf8cSYpwTtGrCBObMpJqwxfTbLW6YTIdbb1jEeTelcpYQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.4.57", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz", - "integrity": "sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw==", - "dev": true - }, - "node_modules/enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", - "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "dependencies": { - "mime-db": "1.51.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "node_modules/oblivious-set": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.1.1.tgz", - "integrity": "sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==", - "dev": true - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", - "dev": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "acorn": "^8.5.0" - }, - "peerDependenciesMeta": { - "acorn": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz", - "integrity": "sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ==", - "dev": true, - "dependencies": { - "jest-worker": "^27.4.1", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-loader": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", - "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/unload": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unload/-/unload-2.3.1.tgz", - "integrity": "sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.6.2", - "detect-node": "2.1.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dev": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", - "dev": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.0.tgz", - "integrity": "sha512-AACDTo20yG+xn6HPW5xjbn2Be4KUzQPebWXsDMHwPPyKh9OnTOJgZN2Nc+g/FZKV3ObRTYsGvibAvc+5jAUrVA==", - "dev": true, - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.0.0", - "@webpack-cli/info": "^2.0.0", - "@webpack-cli/serve": "^2.0.0", - "colorette": "^2.0.14", - "commander": "^9.4.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", - "dev": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - }, - "dependencies": { - "@babel/runtime": { - "version": "7.17.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", - "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "@discoveryjs/json-ext": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", - "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", - "dev": true - }, - "@types/eslint": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "@types/node": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.13.tgz", - "integrity": "sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw==", - "dev": true - }, - "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", - "dev": true, - "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true - }, - "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", - "dev": true, - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.11.1", - "@xtuc/long": "4.2.2" - } - }, - "@webpack-cli/configtest": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.0.tgz", - "integrity": "sha512-war4OU8NGjBqU3DP3bx6ciODXIh7dSXcpQq+P4K2Tqyd8L5OjZ7COx9QXx/QdCIwL2qoX09Wr4Cwf7uS4qdEng==", - "dev": true, - "requires": {} - }, - "@webpack-cli/info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.0.tgz", - "integrity": "sha512-NNxDgbo4VOkNhOlTgY0Elhz3vKpOJq4/PKeKg7r8cmYM+GQA9vDofLYyup8jS6EpUvhNmR30cHTCEIyvXpskwA==", - "dev": true, - "requires": {} - }, - "@webpack-cli/serve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.0.tgz", - "integrity": "sha512-Rumq5mHvGXamnOh3O8yLk1sjx8dB30qF1OeR6VC00DIR6SLJ4bwwUGKC4pE7qBFoQyyh0H9sAg3fikYgAqVR0w==", - "dev": true, - "requires": {} - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", - "dev": true - }, - "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "broadcast-channel": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-4.18.1.tgz", - "integrity": "sha512-eV1srWgt6H4hbtGqD7THn60me66WA5l0LogpssuX9jK6NK26HzIZr+VsrlD7Obe0BtYnwoo/a4v4z5gfty04DA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.16.0", - "oblivious-set": "1.1.1", - "p-queue": "6.6.2", - "rimraf": "3.0.2", - "unload": "2.3.1" - } - }, - "browserslist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", - "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001286", - "electron-to-chromium": "^1.4.17", - "escalade": "^3.1.1", - "node-releases": "^2.0.1", - "picocolors": "^1.0.0" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "caniuse-lite": { - "version": "1.0.30001304", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001304.tgz", - "integrity": "sha512-bdsfZd6K6ap87AGqSHJP/s1V+U6Z5lyrcbBu3ovbCCf8cSYpwTtGrCBObMpJqwxfTbLW6YTIdbb1jEeTelcpYQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true - }, - "clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.57", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.57.tgz", - "integrity": "sha512-FNC+P5K1n6pF+M0zIK+gFCoXcJhhzDViL3DRIGy2Fv5PohuSES1JHR7T+GlwxSxlzx4yYbsuzCZvHxcBSRCIOw==", - "dev": true - }, - "enhanced-resolve": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz", - "integrity": "sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true - }, - "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "interpret": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", - "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", - "dev": true - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "jest-worker": { - "version": "27.4.6", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz", - "integrity": "sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", - "dev": true - }, - "mime-types": { - "version": "2.1.34", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", - "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", - "dev": true, - "requires": { - "mime-db": "1.51.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node-releases": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", - "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", - "dev": true - }, - "oblivious-set": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.1.1.tgz", - "integrity": "sha512-Oh+8fK09mgGmAshFdH6hSVco6KZmd1tTwNFWj35OvzdmJTMZtAkbn05zar2iG3v6sDs1JLEtOiBGNb6BHwkb2w==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - } - }, - "p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, - "requires": { - "resolve": "^1.20.0" - } - }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "requires": { - "resolve-from": "^5.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, - "terser": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", - "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz", - "integrity": "sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ==", - "dev": true, - "requires": { - "jest-worker": "^27.4.1", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "ts-loader": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.2.tgz", - "integrity": "sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4" - } - }, - "typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", - "dev": true - }, - "unload": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unload/-/unload-2.3.1.tgz", - "integrity": "sha512-MUZEiDqvAN9AIDRbbBnVYVvfcR6DrjCqeU2YQMmliFZl9uaBUjTkhuDQkBiyAy8ad5bx1TXVbqZ3gg7namsWjA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.6.2", - "detect-node": "2.1.0" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dev": true, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "webpack": { - "version": "5.75.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", - "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", - "dev": true, - "requires": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - } - }, - "webpack-cli": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.0.tgz", - "integrity": "sha512-AACDTo20yG+xn6HPW5xjbn2Be4KUzQPebWXsDMHwPPyKh9OnTOJgZN2Nc+g/FZKV3ObRTYsGvibAvc+5jAUrVA==", - "dev": true, - "requires": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.0.0", - "@webpack-cli/info": "^2.0.0", - "@webpack-cli/serve": "^2.0.0", - "colorette": "^2.0.14", - "commander": "^9.4.1", - "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^3.1.1", - "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" - }, - "dependencies": { - "commander": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", - "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", - "dev": true - } - } - }, - "webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", - "dev": true, - "requires": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - } - }, - "webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } -} diff --git a/packages/isar_web/package.json b/packages/isar_web/package.json deleted file mode 100644 index 95e04fa78..000000000 --- a/packages/isar_web/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "isar", - "version": "2.5.1", - "description": "The web bindings for Isar, a multi-platform database for Dart.", - "author": "Simon Leier", - "license": "Apache-2.0", - "repository": "https://github.com/isar/isar-web", - "homepage": "https://isar.dev/", - "main": "dist/index.js", - "directories": { - "test": "test" - }, - "scripts": { - "build": "rm -rf ./out && npx tsc" - }, - "devDependencies": { - "ts-loader": "^9.2.6", - "typescript": "^4.5.5", - "webpack": "^5.67.0", - "webpack-cli": "^5.0.0", - "fast-deep-equal": "^3.1.1", - "broadcast-channel": "^4.11.0" - } -} \ No newline at end of file diff --git a/packages/isar_web/src/bulk-delete.ts b/packages/isar_web/src/bulk-delete.ts deleted file mode 100644 index e73d9711c..000000000 --- a/packages/isar_web/src/bulk-delete.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IsarTxn } from './txn' - -export function bulkDelete( - txn: IsarTxn, - storeName: string, - keys: (IDBValidKey | IDBKeyRange)[], -): Promise { - return new Promise((resolve, reject) => { - const len = keys.length - const lastItem = len - 1 - if (len === 0) return resolve() - const store = txn.txn.objectStore(storeName) - for (let i = 0; i < keys.length; i++) { - const req = store.delete(keys[i]) - req.onerror = () => { - txn.abort() - reject(req.error) - } - if (i === lastItem) { - req.onsuccess = () => { - resolve() - } - } - } - }) -} - -export function bulkDeleteByIndex( - txn: IsarTxn, - storeName: string, - indexName: string, - keys: IDBValidKey[], -): Promise { - if (keys.length === 0) return Promise.resolve([]) - return new Promise((resolve, reject) => { - const store = txn.txn.objectStore(storeName) - const index = store.index(indexName) - - const primaryKeys: IDBValidKey[] = [] - for (var i = 0; i < keys.length; i++) { - const indexReq = index.getAllKeys(keys[i]) - const isLast = i === keys.length - 1 - indexReq.onsuccess = () => { - primaryKeys.push(...indexReq.result) - if (isLast) { - bulkDelete(txn, storeName, primaryKeys).then( - () => resolve(primaryKeys), - reject, - ) - } - } - indexReq.onerror = () => { - txn.abort() - reject(indexReq.error) - } - } - }) -} diff --git a/packages/isar_web/src/collection.ts b/packages/isar_web/src/collection.ts deleted file mode 100644 index 332cad1d7..000000000 --- a/packages/isar_web/src/collection.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { bulkDelete, bulkDeleteByIndex } from './bulk-delete' -import { idName, IsarInstance } from './instance' -import { IsarLink } from './link' -import { IndexSchema, IsarType, Schema } from './schema' -import { IsarTxn } from './txn' -import { IsarWatchable } from './watcher' - -interface UniqueIndex { - readonly name: string - readonly accessors: string[] -} - -export type IndexKey = string | number | IndexKey[] - -export class IsarCollection extends IsarWatchable { - readonly isar: IsarInstance - readonly name: string - private readonly uniqueIndexes: ReadonlyArray - private readonly links: ReadonlyArray - // only backlinks that don't target this collection - private readonly backlinkStoreNames: ReadonlyArray - private readonly multiEntryIndexes: string[] - private readonly indexKeyPaths = new Map() - - constructor( - isar: IsarInstance, - schema: Schema, - backlinkStoreNames: string[], - ) { - super() - this.isar = isar - this.name = schema.name - this.uniqueIndexes = schema.indexes - .filter(i => i.unique) - .map(i => ({ - name: i.name, - accessors: i.properties.map(p => p.name), - })) - this.links = schema.links.map( - l => new IsarLink(isar, l.name, schema.name, l.target), - ) - this.backlinkStoreNames = backlinkStoreNames - this.multiEntryIndexes = schema.indexes - .filter(i => IndexSchema.isIndexMultiEntry(schema, i)) - .map(i => i.name) - this.indexKeyPaths = new Map( - schema.indexes.map(i => [i.name, i.properties.map(p => p.name)]), - ) - } - - getLink(name: string): IsarLink | undefined { - return this.links.find(l => l.name === name) - } - - getIndexKeyPath(indexName: string): string[] { - return this.indexKeyPaths.get(indexName)! - } - - isMultiEntryIndex(indexName: string): boolean { - return this.multiEntryIndexes.includes(indexName) - } - - get(txn: IsarTxn, id: number): Promise { - let store = txn.txn.objectStore(this.name) - return new Promise((resolve, reject) => { - let req = store.get(id) - req.onsuccess = () => { - const object = req.result - if (object) { - object[idName] = id - } - resolve(object) - } - req.onerror = () => { - reject(req.error) - } - }) - } - - getAll(txn: IsarTxn, ids: number[]): Promise<(OBJ | undefined)[]> { - return new Promise((resolve, reject) => { - const store = txn.txn.objectStore(this.name) - const results: (OBJ | undefined)[] = [] - for (let i = 0; i < ids.length; i++) { - const id = ids[i] - const req = store.get(id) - req.onsuccess = () => { - const object = req.result - if (object) { - object[idName] = id - } - results.push(object) - if (results.length == ids.length) { - resolve(results) - } - } - req.onerror = () => { - reject(req.error) - } - } - }) - } - - getAllByIndex( - txn: IsarTxn, - indexName: string, - keys: IndexKey[], - ): Promise<(OBJ | undefined)[]> { - if (keys.length === 0) { - return Promise.resolve([]) - } - - keys.sort(indexedDB.cmp) - return new Promise((resolve, reject) => { - const store = txn.txn.objectStore(this.name) - const results: (OBJ | undefined)[] = [] - const cursorReq = store.index(indexName).openCursor() - cursorReq.onsuccess = () => { - const cursor = cursorReq.result - if (cursor) { - const object = cursor.value - if (results.length > 0 || cursor.key === keys[0]) { - if (object) { - object[idName] = cursor.primaryKey - results.push(object) - } else { - results.push(undefined) - } - } - if (results.length == keys.length) { - resolve(results) - } else { - cursor.continue(keys[results.length]) - } - } else { - resolve([]) - } - } - cursorReq.onerror = e => { - reject(e) - } - }) - } - - putAll(txn: IsarTxn, objects: OBJ[]): Promise { - let store = txn.txn.objectStore(this.name) - return new Promise((resolve, reject) => { - const ids: (number | undefined)[] = [] - const changeSet = txn.getChangeSet(this.name) - for (let i = 0; i < objects.length; i++) { - const object = objects[i] as any - const id = object[idName] - - const req = store.put(object) - delete object[idName] - - ids.push(id) - if (!id) { - req.onsuccess = () => { - const id = req.result as number - ids[i] = id - changeSet.registerChange(id, object) - if (i === objects.length - 1) { - resolve(ids as number[]) - } - } - } else { - changeSet.registerChange(id, object) - if (i === objects.length - 1) { - req.onsuccess = () => { - resolve(ids as number[]) - } - } - } - req.onerror = () => { - txn.abort() - reject(req.error) - } - } - }) - } - - private deleteLinks(txn: IsarTxn, keys: IDBValidKey[]): Promise { - if (this.links.length === 0 && this.backlinkStoreNames.length === 0) { - return Promise.resolve() - } - const linkPromises = this.links.map(l => { - return bulkDelete(txn, l.storeName, keys.map(IsarLink.getLinkKeyRange)) - }) - const backlinkPromises = this.backlinkStoreNames.map(storeName => { - return bulkDeleteByIndex(txn, storeName, IsarLink.BacklinkIndex, keys) - }) - return Promise.all([...linkPromises, ...backlinkPromises]).then(() => { }) - } - - deleteAll(txn: IsarTxn, ids: number[]): Promise { - return bulkDelete(txn, this.name, ids).then(() => { - const changeSet = txn.getChangeSet(this.name) - for (let id of ids) { - changeSet.registerChange(id) - } - return this.deleteLinks(txn, ids) - }) - } - - deleteAllByIndex( - txn: IsarTxn, - indexName: string, - keys: IndexKey[], - ): Promise { - return bulkDeleteByIndex(txn, this.name, indexName, keys).then(ids => { - const changeSet = txn.getChangeSet(this.name) - for (let id of ids as number[]) { - changeSet.registerChange(id) - } - return this.deleteLinks(txn, ids).then(() => ids.length) - }) - } - - clear(txn: IsarTxn): Promise { - return new Promise((resolve, reject) => { - const storeNames = [ - this.name, - ...this.backlinkStoreNames, - ...this.links.map(l => l.storeName), - ] - for (let i = 0; i < storeNames.length; i++) { - const store = txn.txn.objectStore(this.name) - const req = store.clear() - req.onerror = () => { - reject(req.error) - } - if (i === storeNames.length - 1) { - req.onsuccess = () => { - txn.getChangeSet(this.name).registerCleared() - resolve() - } - } - } - }) - } -} diff --git a/packages/isar_web/src/cursor.ts b/packages/isar_web/src/cursor.ts deleted file mode 100644 index 121344708..000000000 --- a/packages/isar_web/src/cursor.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { IsarTxn } from './txn' - -type CursorParams = { - txn: IsarTxn - callback: CursorCallback - storeName: string - indexName?: string - range?: IDBKeyRange - offset?: number - direction?: IDBCursorDirection -} - -type CursorCallback = ( - id: any, - value: any, - next: Function, - resolve: Function, - reject: Function, -) => void - -export function useCursor(params: CursorParams): Promise { - return new Promise((resolve, reject) => { - const store = params.txn.txn.objectStore(params.storeName) - const source = - params.indexName != null ? store.index(params.indexName) : store - const multiEntry = params.indexName && (source as IDBIndex).multiEntry - - const cursorReq = source.openCursor(params.range, params.direction) - cursorReq.onsuccess = () => { - const cursor = cursorReq.result - if (cursor) { - if (params.offset) { - cursor.advance(params.offset) - params.offset = undefined - } else { - if (multiEntry) { - const isArray = Array.isArray( - cursor.value[source.keyPath as string], - ) - if (!isArray) { - cursor.continue() - return - } - } - params.callback( - cursor.primaryKey, - cursor.value, - function () { - cursor.continue() - }, - resolve, - reject, - ) - } - } else { - resolve() - } - } - cursorReq.onerror = e => { - reject(e) - } - }) -} diff --git a/packages/isar_web/src/index.ts b/packages/isar_web/src/index.ts deleted file mode 100644 index 4855edf75..000000000 --- a/packages/isar_web/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IsarCollection } from './collection' -import { IsarInstance } from './instance' -import { IsarLink } from './link' -import { openIsar } from './open' -import { IsarQuery } from './query' -import { IsarTxn } from './txn' - -;(window as any).openIsar = openIsar -;(window as any).IsarInstance = IsarInstance -;(window as any).IsarTxn = IsarTxn -;(window as any).IsarCollection = IsarCollection -;(window as any).IsarQuery = IsarQuery -;(window as any).IsarLink = IsarLink diff --git a/packages/isar_web/src/instance.ts b/packages/isar_web/src/instance.ts deleted file mode 100644 index f47cb4494..000000000 --- a/packages/isar_web/src/instance.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { IsarCollection } from './collection' -import { LinkSchema, Schema } from './schema' -import { IsarTxn } from './txn' -import { ChangeSet } from './watcher' -import { BroadcastChannel } from 'broadcast-channel' - -export const idName = '_id'; - -export class IsarInstance { - private static readonly bc = new BroadcastChannel('ISAR_CHANNEL') - - private readonly db: IDBDatabase - private readonly relaxedDurability: boolean - private collections: Map> = new Map() - private eventHandler: EventListener - - constructor(db: IDBDatabase, relaxedDurability: boolean, schemas: Schema[]) { - this.db = db - this.relaxedDurability = relaxedDurability - this.initializeCollections(schemas) - - this.eventHandler = (event: MessageEvent) => { - if ( - event.data && - event.data.type === 'change' && - event.data.instance == this.db.name - ) { - this.notifyWatchers(event.data.changes, true) - } - } - IsarInstance.bc.addEventListener('message', this.eventHandler) - } - - private initializeCollections(schemas: Schema[]) { - for (let schema of schemas) { - const backlinkStoreNames = schemas.flatMap(s => { - if (s.name === schema.name) { - return [] - } - return s.links - .filter(l => l.target === schema.name) - .map(l => { - return LinkSchema.getStoreName(s.name, l.target, l.name) - }) - }) - const col = new IsarCollection(this, schema, backlinkStoreNames) - this.collections.set(schema.name, col) - } - } - - notifyWatchers( - changes: Map>, - external: boolean = false, - ) { - let txn: IsarTxn | undefined - - const getTxn = () => { - if (txn == null) { - txn = this.beginTxn(false) - } - return txn! - } - for (let [colName, changeSet] of changes.entries()) { - const collection = this.getCollection(colName) - collection.notify(changeSet, getTxn) - } - - if (!external) { - const event: ChangeEvent = { - type: 'change', - instance: this.db.name, - changes, - } - IsarInstance.bc.postMessage(event) - } - } - - beginTxn(write: boolean): IsarTxn { - const names = this.db.objectStoreNames - const mode = write ? 'readwrite' : 'readonly' - const options = this.relaxedDurability ? { durability: 'relaxed' } : {} - const txn = (this.db as any).transaction(names, mode, options) - return new IsarTxn(this, txn, write) - } - - getCollection(name: string): IsarCollection { - return this.collections.get(name)! - } - - close(deleteFromDisk: boolean = false): Promise { - IsarInstance.bc.removeEventListener('message', this.eventHandler) - this.db.close() - if (deleteFromDisk) { - const req = indexedDB.deleteDatabase(this.db.name) - return new Promise((resolve, reject) => { - req.onsuccess = () => { - resolve() - } - req.onerror = () => { - reject(req.error) - } - }) - } else { - return Promise.resolve() - } - } -} - -type ChangeEvent = { - type: 'change' - instance: string - changes: Map> -} diff --git a/packages/isar_web/src/link.ts b/packages/isar_web/src/link.ts deleted file mode 100644 index ac91d42e4..000000000 --- a/packages/isar_web/src/link.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { bulkDelete } from './bulk-delete' -import { IsarInstance } from './instance' -import { LinkSchema } from './schema' -import { IsarTxn } from './txn' - -export class IsarLink { - static readonly BacklinkIndex = 'backlink' - - readonly isar: IsarInstance - readonly name: string - readonly sourceName: string - readonly targetName: string - readonly storeName: string - - constructor( - isar: IsarInstance, - name: string, - sourceName: string, - targetName: string, - ) { - this.isar = isar - this.name = name - this.sourceName = sourceName - this.targetName = targetName - this.storeName = LinkSchema.getStoreName(sourceName, targetName, name) - } - - private getLinkEntry(source: number, target: number, backlink: boolean): any { - if (backlink) { - ;[source, target] = [target, source] - } - return { - a: source, - b: target, - } - } - - static getLinkKeyRange(id: number): IDBKeyRange { - return IDBKeyRange.bound([id, -Infinity], [id, Infinity]) - } - - update( - txn: IsarTxn, - backlink: boolean, - id: number, - addedTargets: number[], - deletedTargets: number[], - ): Promise { - if (addedTargets.length === 0 && deletedTargets.length === 0) { - return Promise.resolve() - } - - return new Promise((resolve, reject) => { - const store = txn.txn.objectStore(this.storeName) - - const deletedEmpty = deletedTargets.length === 0 - for (let i = 0; i < addedTargets.length; i++) { - let target = addedTargets[i] - const req = store.add(this.getLinkEntry(id, target, backlink)) - if (deletedEmpty && i === addedTargets.length - 1) { - req.onsuccess = () => { - resolve() - } - } - req.onerror = () => { - txn.abort() - reject(req.error) - } - } - - for (let i = 0; i < deletedTargets.length; i++) { - let target = deletedTargets[i] - const key = backlink ? [target, id] : [id, target] - const req = store.delete(key) - if (i === deletedTargets.length - 1) { - req.onsuccess = () => { - resolve() - } - } - req.onerror = () => { - txn.abort() - reject(req.error) - } - } - }) - } - - clear(txn: IsarTxn, id: number, backlink: boolean): Promise { - return new Promise((resolve, reject) => { - const store = txn.txn.objectStore(this.storeName) - if (backlink) { - const keysRes = store.index(IsarLink.BacklinkIndex).getAllKeys(id) - keysRes.onsuccess = () => { - const keys = keysRes.result - if (keys.length > 0) { - const ids = keys.map(key => (key as number[])[1]) - bulkDelete(txn, this.storeName, ids).then(resolve, reject) - } else { - resolve() - } - } - keysRes.onerror = () => { - txn.abort() - reject(keysRes.error) - } - } else { - const deleteReq = store.delete(IsarLink.getLinkKeyRange(id)) - deleteReq.onsuccess = () => { - resolve() - } - deleteReq.onerror = () => { - txn.abort() - reject(deleteReq.error) - } - } - }) - } -} diff --git a/packages/isar_web/src/open.ts b/packages/isar_web/src/open.ts deleted file mode 100644 index a8772c8f8..000000000 --- a/packages/isar_web/src/open.ts +++ /dev/null @@ -1,161 +0,0 @@ -import equal from 'fast-deep-equal' -import { IsarInstance } from './instance' -import { IsarLink } from './link' -import { IndexSchema, IsarType, LinkSchema, Schema } from './schema' - -// Polyfill for older browsers -if (typeof IDBTransaction.prototype.commit !== "function") { - IDBTransaction.prototype.commit = function () { } -} - -export function openIsar( - name: string, - schemas: Schema[], - relaxedDurability: boolean, -): Promise { - return openInternal(name, schemas, relaxedDurability) -} - -function openInternal( - name: string, - schemas: Schema[], - relaxedDurability: boolean, - version?: number, -): Promise { - return new Promise((resolve, reject) => { - const req = indexedDB.open(name, version) - req.onsuccess = () => { - const db = req.result - if (version == null) { - const txn = db.transaction(db.objectStoreNames, 'readonly') - if (!performUpgrade(txn, true, schemas)) { - const newVersion = txn.db.version + 1 - db.close() - resolve(openInternal(name, schemas, relaxedDurability, newVersion)) - return - } - } - - const instance = new IsarInstance(db, relaxedDurability, schemas) - resolve(instance) - } - req.onupgradeneeded = () => { - performUpgrade(req.transaction!, false, schemas) - } - req.onerror = () => { - reject(req.error) - } - }) -} - -function performUpgrade( - txn: IDBTransaction, - dryRun: boolean, - schemas: Schema[], -): boolean { - const schemaStoreNames: string[] = [] - for (const schema of schemas) { - schemaStoreNames.push(schema.name) - const schemaIndexNames: string[] = [] - - let store: IDBObjectStore - if (!txn.objectStoreNames.contains(schema.name)) { - if (dryRun) { - return false - } - store = txn.db.createObjectStore(schema.name, { - autoIncrement: true, - }) - } else { - store = txn.objectStore(schema.name) - } - - for (let indexSchema of schema.indexes) { - schemaIndexNames.push(indexSchema.name) - if (store.indexNames.contains(indexSchema.name)) { - const index = store.index(indexSchema.name) - if (IndexSchema.matchesIndex(schema, indexSchema, index)) { - continue - } else { - if (!dryRun) { - store.deleteIndex(indexSchema.name) - } - } - } - if (dryRun) { - return false - } - store.createIndex(indexSchema.name, IndexSchema.getKeyPath(indexSchema), { - unique: indexSchema.unique, - multiEntry: IndexSchema.isIndexMultiEntry(schema, indexSchema), - }) - } - - for (let linkSchema of schema.links) { - const name = LinkSchema.getStoreName( - schema.name, - linkSchema.target, - linkSchema.name, - ) - let linkStore: IDBObjectStore - if (!txn.objectStoreNames.contains(name)) { - if (dryRun) { - return false - } - linkStore = txn.db.createObjectStore(name, { - keyPath: ['a', 'b'], - autoIncrement: false, - }) - } else { - linkStore = txn.objectStore(name) - } - schemaStoreNames.push(name) - - const indexesOk = equal( - [...linkStore.indexNames], - [IsarLink.BacklinkIndex], - ) - if (!indexesOk) { - if (dryRun) { - return false - } - for (let indexName of linkStore.indexNames) { - linkStore.deleteIndex(indexName) - } - linkStore.createIndex(IsarLink.BacklinkIndex, 'b') - } - } - - for (let indexName of store.indexNames) { - if (schemaIndexNames.indexOf(indexName) === -1) { - if (dryRun) { - return false - } - store.deleteIndex(indexName) - } - } - } - - for (let storeName of txn.objectStoreNames) { - if (schemaStoreNames.indexOf(storeName) === -1) { - if (dryRun) { - return false - } - txn.db.deleteObjectStore(storeName) - } - } - - return true -} - -type CollectionInfo = { - properties: { - [key: string]: CollectionProperty - } - nextId: number -} - -type CollectionProperty = { - id: number - type: IsarType -} \ No newline at end of file diff --git a/packages/isar_web/src/query.ts b/packages/isar_web/src/query.ts deleted file mode 100644 index 1a2d8d626..000000000 --- a/packages/isar_web/src/query.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { IsarCollection } from './collection' -import { useCursor } from './cursor' -import { idName } from './instance' -import { IsarLink } from './link' -import { IsarTxn } from './txn' - -type IdWherClause = { - range?: IDBKeyRange -} - -type IndexWherClause = { - indexName: string - range?: IDBKeyRange -} - -type LinkWherClause = { - linkCollection: string - linkName: string - backlink: boolean - id: number -} - -type WherClause = IdWherClause | IndexWherClause | LinkWherClause - -type Filter = (id: number, obj: any) => boolean - -type Cmp = (a: any, b: any) => number - -type DistinctValue = (obj: any) => string - -export class IsarQuery { - collection: IsarCollection - whereClauses: WherClause[] - whereClauseDirection: IDBCursorDirection - filter?: Filter - sortCmp?: Cmp - distinctValue?: DistinctValue - offset: number - limit: number - - constructor( - collection: IsarCollection, - whereClauses: WherClause[], - whereDistinct: boolean, - whereAscending: boolean, - filter?: Filter, - sortCmp?: Cmp, - distinctValue?: DistinctValue, - offset?: number, - limit?: number, - ) { - this.collection = collection - this.whereClauses = whereClauses - this.filter = filter - this.sortCmp = sortCmp - this.distinctValue = distinctValue - this.offset = offset ?? 0 - this.limit = limit ?? Infinity - - if (whereDistinct) { - this.whereClauseDirection = whereAscending ? 'nextunique' : 'prevunique' - } else { - this.whereClauseDirection = whereAscending ? 'next' : 'prev' - } - - if (this.whereClauses.length === 0) { - this.whereClauses.push({}) - } - } - - private getWhereClauseRange( - whereClause: IdWherClause | IndexWherClause, - ): IDBKeyRange { - return whereClause.range ?? IDBKeyRange.lowerBound(-Infinity) - } - - private async findInternal(txn: IsarTxn, limit: number): Promise { - const offset = this.offset - const unsortedLimit = !this.sortCmp ? offset + limit : Infinity - const unsortedDistinct = !this.sortCmp ? this.distinctValue : undefined - let results: OBJ[] = [] - const idsSet = new Set() - const distinctSet = new Set() - - const cursorCallback = ( - id: any, - object: any, - next: Function, - resolve: Function, - ) => { - if (idsSet.has(id)) { - next() - return - } else { - idsSet.add(id) - } - - if (this.filter) { - if (!this.filter(id, object)) { - next() - return - } - } - if (unsortedDistinct) { - const value = unsortedDistinct(object) - if (distinctSet.has(value)) { - next() - return - } else { - distinctSet.add(value) - } - } - object[idName] = id - results.push(object) - if (results.length < unsortedLimit) { - next() - } else { - resolve() - } - } - - for (const whereClause of this.whereClauses) { - if (results.length >= unsortedLimit) { - break - } - if ('linkName' in whereClause) { - const link = this.collection.isar - .getCollection(whereClause.linkCollection) - .getLink(whereClause.linkName)! - await useCursor({ - txn, - storeName: link.storeName, - indexName: whereClause.backlink ? IsarLink.BacklinkIndex : undefined, - range: IsarLink.getLinkKeyRange(whereClause.id), - direction: this.whereClauseDirection, - callback: (key, _, next, resolve, reject) => { - const id = (key as number[])[whereClause.backlink ? 0 : 1] - this.collection - .get(txn, id) - .then(obj => { - if (obj) { - cursorCallback(id, obj, next, resolve) - } else { - next() - } - }) - .catch(() => reject()) - }, - }) - } else { - const range = this.getWhereClauseRange(whereClause) - await useCursor({ - txn, - storeName: this.collection.name, - indexName: - 'indexName' in whereClause ? whereClause.indexName : undefined, - range: range, - direction: this.whereClauseDirection, - callback: cursorCallback, - }) - } - } - - if (this.sortCmp) { - results.sort(this.sortCmp) - - const distinctValue = this.distinctValue - if (distinctValue) { - results = results.filter(obj => { - const value = distinctValue!(obj) - if (!distinctSet.has(value)) { - distinctSet.add(value) - return true - } else { - return false - } - }) - } - } - - return results.slice(offset, offset + limit) - } - - findFirst(txn: IsarTxn): Promise { - return this.findInternal(txn, 1).then(results => { - return results.length > 0 ? results[0] : undefined - }) - } - - findAll(txn: IsarTxn): Promise { - return this.findInternal(txn, this.limit ?? Infinity) - } - - deleteFirst(txn: IsarTxn): Promise { - return this.findInternal(txn, 1).then(result => { - if (result.length !== 0) { - return this.collection - .deleteAll(txn, [result[0][idName]]) - .then(() => true) - } else { - return false - } - }) - } - - deleteAll(txn: IsarTxn): Promise { - return this.findInternal(txn, this.limit).then(result => { - return this.collection - .deleteAll(txn, result.map(obj => obj[idName])) - .then(() => result.length) - }) - } - - min(txn: IsarTxn, key: string): Promise { - return this.findAll(txn).then(results => { - let min: number | undefined = undefined - for (const obj of results) { - const value = (obj as any)[key] - if (value != null && (min == null || value < min)) { - min = value - } - } - return min - }) - } - - max(txn: IsarTxn, key: string): Promise { - return this.findAll(txn).then(results => { - let max: number | undefined = undefined - for (const obj of results) { - const value = (obj as any)[key] - if (value != null && (max == null || value > max)) { - max = value - } - } - return max - }) - } - - sum(txn: IsarTxn, key: string): Promise { - return this.findAll(txn).then(results => { - let sum = 0 - for (const obj of results) { - const value = (obj as any)[key] - if (value != null) { - sum += value - } - } - return sum - }) - } - - average(txn: IsarTxn, key: string): Promise { - return this.findAll(txn).then(results => { - let sum = 0 - let count = 0 - for (const obj of results) { - const value = (obj as any)[key] - if (value != null) { - sum += value - count++ - } - } - return sum / count - }) - } - - count(txn: IsarTxn): Promise { - return this.findAll(txn).then(result => result.length) - } - - private whereClauseMatches(id: number, object: OBJ) { - for (const whereClause of this.whereClauses) { - if ('linkName' in whereClause) { - return true - } else if ('indexName' in whereClause) { - if (this.collection.isMultiEntryIndex(whereClause.indexName)) { - const values = (object as any)[ - this.collection.getIndexKeyPath(whereClause.indexName!)[0] - ] - for (let value of values) { - if (this.getWhereClauseRange(whereClause).includes(value)) { - return true - } - } - } else { - let value = this.collection - .getIndexKeyPath(whereClause.indexName!) - .map(p => - p === this.collection.idName ? id : (object as any)[p], - ) - if (value.length === 1) { - value = value[0] - } - if (this.getWhereClauseRange(whereClause).includes(value)) { - return true - } - } - } else if (this.getWhereClauseRange(whereClause).includes(id)) { - return true - } - } - - return false - } - - whereClauseAndFilterMatch(id: number, idbObject: OBJ): boolean { - if (!this.whereClauseMatches(id, idbObject)) { - return false - } - - if (this.filter) { - if (!this.filter(id, idbObject)) { - return false - } - } - - return true - } -} diff --git a/packages/isar_web/src/schema.ts b/packages/isar_web/src/schema.ts deleted file mode 100644 index 752da7eee..000000000 --- a/packages/isar_web/src/schema.ts +++ /dev/null @@ -1,104 +0,0 @@ -import equal from 'fast-deep-equal' - -export type Schema = { - name: string - properties: Array - indexes: Array - links: Array -} - -type PropertySchema = { - name: string - type: IsarType -} - -type IndexSchema = { - name: string - unique: boolean - properties: Array -} - -type IndexPropertySchema = { - name: string - type: IndexType - caseSensitive: boolean -} - -export namespace IndexSchema { - export function isIndexMultiEntry( - schema: Schema, - indexSchema: IndexSchema, - ): boolean { - return indexSchema.properties.some(ip => { - const property = schema.properties.find(p => p.name === ip.name)! - return ip.type === IndexType.Value && IsarType.isList(property.type) - }) - } - - export function getKeyPath(indexSchema: IndexSchema): string | string[] { - return indexSchema.properties.length === 1 - ? indexSchema.properties[0].name - : indexSchema.properties.map(p => p.name) - } - - export function matchesIndex( - schema: Schema, - indexSchema: IndexSchema, - index: IDBIndex, - ): boolean { - return ( - index.name === indexSchema.name && - index.multiEntry === isIndexMultiEntry(schema, indexSchema) && - index.unique === indexSchema.unique && - equal(index.keyPath, getKeyPath(indexSchema)) - ) - } -} - -type LinkSchema = { - name: string - target: string -} - -export namespace LinkSchema { - export function getStoreName( - sourceName: string, - targetName: string, - linkName: string, - ): string { - return `_${sourceName}_${targetName}_${linkName}` - } -} - -export enum IsarType { - Bool = 'Bool', - Byte = 'Byte', - Int = 'Int', - Float = 'Float', - Long = 'Long', - Double = 'Double', - DateTime = 'DateTime', - String = 'String', - Object = 'Object', - BoolList = 'BoolList', - ByteList = 'ByteList', - IntList = 'IntList', - FloatList = 'FloatList', - LongList = 'LongList', - DoubleList = 'DoubleList', - DateTimeList = 'DateTimeList', - StringList = 'StringList', - ObjectList = 'ObjectList', -} - -export namespace IsarType { - export function isList(type: IsarType): boolean { - return type.endsWith('List') - } -} - -enum IndexType { - Value = 'Value', - Hash = 'Hash', - HashElements = 'HashElements', -} diff --git a/packages/isar_web/src/txn.ts b/packages/isar_web/src/txn.ts deleted file mode 100644 index 3c714e2de..000000000 --- a/packages/isar_web/src/txn.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { IsarInstance } from './instance' -import { IsarChangeSet } from './watcher' - -export class IsarTxn { - readonly isar: IsarInstance - readonly txn: IDBTransaction - active: boolean - readonly write: boolean - private readonly changes: Map> | undefined - - constructor(isar: IsarInstance, txn: IDBTransaction, write: boolean) { - this.isar = isar - this.txn = txn - this.active = true - this.write = write - - if (write) { - this.changes = new Map() - } - } - - getChangeSet(collectionName: string): IsarChangeSet { - let changeSet = this.changes!.get(collectionName) - if (changeSet == null) { - changeSet = new IsarChangeSet() - this.changes!.set(collectionName, changeSet) - } - return changeSet - } - - commit(): Promise { - return new Promise((resolve, reject) => { - this.active = false - - this.txn.oncomplete = () => { - if (this.changes) { - this.isar.notifyWatchers(this.changes) - } - resolve() - } - this.txn.onerror = () => { - reject(this.txn.error) - } - this.txn.commit() - }) - } - - abort() { - if (this.active) { - this.active = false - this.txn.abort() - } - } -} diff --git a/packages/isar_web/src/watcher.ts b/packages/isar_web/src/watcher.ts deleted file mode 100644 index bc1f6a163..000000000 --- a/packages/isar_web/src/watcher.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { IsarQuery } from './query' -import { IsarTxn } from './txn' - -type ChangeCallback = () => void - -type ObjectChangeCallback = (object?: OBJ) => void - -type QueryChangeCallback = (results: OBJ[]) => void - -type StopWatching = () => void - -type QueryWatcher = { - callback: ChangeCallback | QueryChangeCallback - query: IsarQuery - lazy: boolean -} - -export type ChangeSet = { - cleared: boolean - addedObjects: Map - deletedObjectIds: Set -} - -export class IsarChangeSet implements IsarChangeSet { - cleared: boolean = false - addedObjects: Map = new Map() - deletedObjectIds: Set = new Set() - - registerChange(id: number, idbObject?: OBJ) { - if (idbObject) { - this.addedObjects.set(id, idbObject) - this.deletedObjectIds.delete(id) - } else { - this.deletedObjectIds.add(id) - this.addedObjects.delete(id) - } - } - - registerCleared() { - this.addedObjects.clear() - this.deletedObjectIds.clear() - this.cleared = true - } -} - -export class IsarWatchable { - readonly collectionWatchers = new Set() - readonly objectWatchers: Map>> = - new Map() - readonly queryWatchers = new Set>() - - watchLazy(callback: ChangeCallback): StopWatching { - this.collectionWatchers.add(callback) - return () => this.collectionWatchers.delete(callback) - } - - watchObject(id: number, callback: ObjectChangeCallback): StopWatching { - let ow = this.objectWatchers.get(id) - if (ow == null) { - ow = new Set() - this.objectWatchers.set(id, ow) - } - ow.add(callback) - return () => { - if (ow!.size <= 1) { - this.objectWatchers.delete(id) - } else { - ow!.delete(callback) - } - } - } - - private watchQueryInternal( - query: IsarQuery, - lazy: boolean, - callback: ChangeCallback | QueryChangeCallback, - ): StopWatching { - const watcher = { callback, query, lazy } - this.queryWatchers.add(watcher) - return () => this.queryWatchers.delete(watcher) - } - - watchQuery( - query: IsarQuery, - callback: QueryChangeCallback, - ): StopWatching { - return this.watchQueryInternal(query, false, callback) - } - - watchQueryLazy( - query: IsarQuery, - callback: ChangeCallback, - ): StopWatching { - return this.watchQueryInternal(query, true, callback) - } - - notify(changes: ChangeSet, getTxn: () => IsarTxn) { - if ( - !changes.cleared && - changes.addedObjects.size === 0 && - changes.deletedObjectIds.size === 0 - ) { - return - } - - function notifyQuery(watcher: QueryWatcher) { - if (watcher.lazy) { - ;(watcher.callback as ChangeCallback)() - } else { - const txn = getTxn() - watcher.query.findAll(txn).then(watcher.callback) - } - } - - for (const watcher of this.collectionWatchers) { - watcher() - } - - let queryWatchers: Set> | undefined - if (changes.cleared || changes.deletedObjectIds.size > 0) { - for (const watcher of this.queryWatchers) { - notifyQuery(watcher) - } - } else { - queryWatchers = new Set(this.queryWatchers) - } - - if (changes.cleared) { - for (const [id, callbacks] of this.objectWatchers) { - for (let callback of callbacks) { - callback(changes.addedObjects.get(id)) - } - } - } else { - for (const id of changes.deletedObjectIds) { - const callbacks = this.objectWatchers.get(id) - if (callbacks != null) { - for (let callback of callbacks) { - callback(undefined) - } - } - } - - for (const [id, added] of changes.addedObjects) { - const ow = this.objectWatchers.get(id) - if (ow != null) { - for (let callback of ow) { - callback(added) - } - } - - if (queryWatchers != null) { - for (const watcher of queryWatchers) { - if (watcher.query.whereClauseAndFilterMatch(id, added)) { - notifyQuery(watcher) - queryWatchers.delete(watcher) - } - } - } - } - } - } -} diff --git a/packages/isar_web/tsconfig.json b/packages/isar_web/tsconfig.json deleted file mode 100644 index c64da9b02..000000000 --- a/packages/isar_web/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist/", - "noImplicitAny": true, - "strictNullChecks": true, - "module": "es6", - "target": "es2017", - "jsx": "react", - "allowJs": true, - "moduleResolution": "node", - "esModuleInterop": true, - "sourceMap": true, - } -} \ No newline at end of file diff --git a/packages/isar_web/webpack.config.js b/packages/isar_web/webpack.config.js deleted file mode 100644 index e5528bb21..000000000 --- a/packages/isar_web/webpack.config.js +++ /dev/null @@ -1,22 +0,0 @@ -const path = require('path') - -module.exports = { - entry: './src/index.ts', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - ], - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'], - }, - output: { - filename: 'index.js', - path: path.resolve(__dirname, 'dist'), - }, - devtool: 'source-map', -}