diff --git a/sembast_test/pubspec.yaml b/sembast_test/pubspec.yaml index cb6d4c32..8eb6d995 100644 --- a/sembast_test/pubspec.yaml +++ b/sembast_test/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.3.0 publish_to: none environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: path: '>=1.6.0' diff --git a/sembast_test/test/web/idb_web_interop_test.dart b/sembast_test/test/web/idb_web_interop_test.dart new file mode 100644 index 00000000..bcfda3d5 --- /dev/null +++ b/sembast_test/test/web/idb_web_interop_test.dart @@ -0,0 +1,23 @@ +@TestOn('browser') +library; + +import 'package:idb_shim/idb_client_native_web.dart'; +import 'package:sembast_test/all_jdb_test.dart' as all_jdb_test; +import 'package:sembast_test/all_test.dart'; +import 'package:sembast_test/jdb_test_common.dart'; +import 'package:sembast_test/src/import_jdb.dart'; +import 'package:sembast_test/test_common.dart'; +import 'package:sembast_web/src/jdb_factory_idb.dart'; +import 'package:test/test.dart'; + +Future main() async { + var jdbFactory = JdbFactoryIdb(idbFactoryNative); + var factory = DatabaseFactoryJdb(jdbFactory); + + var testContext = DatabaseTestContextJdb()..factory = factory; + + group('idb_native', () { + defineTests(testContext); + all_jdb_test.defineTests(testContext); + }); +} diff --git a/sembast_web/CHANGELOG.md b/sembast_web/CHANGELOG.md index 508f4dbf..27a0f62b 100644 --- a/sembast_web/CHANGELOG.md +++ b/sembast_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.0-0 + +* web and js_interop support + ## 2.2.0 * Dart 3 only diff --git a/sembast_web/lib/sembast_web_interop.dart b/sembast_web/lib/sembast_web_interop.dart new file mode 100644 index 00000000..3a5b8ee7 --- /dev/null +++ b/sembast_web/lib/sembast_web_interop.dart @@ -0,0 +1,7 @@ +import 'package:sembast/sembast.dart'; +import 'package:sembast_web/src/web_interop/sembast_web.dart' as src; + +/// Sembast factory for the Web. +/// +/// Build on top of IndexedDB and localStorage. +DatabaseFactory get databaseFactoryWeb => src.databaseFactoryWeb; diff --git a/sembast_web/lib/src/web_interop.dart b/sembast_web/lib/src/web_interop.dart new file mode 100644 index 00000000..6501b382 --- /dev/null +++ b/sembast_web/lib/src/web_interop.dart @@ -0,0 +1,90 @@ +import 'dart:async'; +import 'dart:js_interop'; + +import 'package:idb_shim/idb_client_native_web.dart'; +import 'package:sembast_web/src/jdb_factory_idb.dart'; +import 'package:sembast_web/src/jdb_import.dart'; +import 'package:sembast_web/src/web_defs.dart'; +import 'package:web/web.dart' as web; + +/// The native jdb factory +var jdbFactoryIdbNative = JdbFactoryWeb(); + +/// The sembast idb native factory with web. +var databaseFactoryWeb = DatabaseFactoryWeb(); + +/// Web jdb factory. +class JdbFactoryWeb extends JdbFactoryIdb { + /// Web jdb factory. + JdbFactoryWeb() : super(idbFactoryNative); + + StreamSubscription? _revisionSubscription; + + @override + void start() { + stop(); + _revisionSubscription = storageRevisionStream.listen((storageRevision) { + var list = databases[storageRevision.name]!; + for (var jdbDatabase in list) { + jdbDatabase.addRevision(storageRevision.revision); + } + }); + } + + @override + void stop() { + _revisionSubscription?.cancel(); + _revisionSubscription = null; + } + + /// Notify other app (web only)) + @override + void notifyRevision(StorageRevision storageRevision) { + addStorageRevision(storageRevision); + } +} + +/// Web factory. +class DatabaseFactoryWeb extends DatabaseFactoryJdb { + /// Web factory. + DatabaseFactoryWeb() : super(jdbFactoryIdbNative); +} + +String _sembastStorageKeyPrefix = 'sembast_web/revision:'; + +/// add a storage revision +void addStorageRevision(StorageRevision storageRevision) { + if (debugStorageNotification) { + print('adding storage revision $storageRevision'); + } + var key = '$_sembastStorageKeyPrefix${storageRevision.name}'; + if (storageRevision.revision != 0) { + web.window.localStorage.setItem(key, storageRevision.revision.toString()); + } else { + web.window.localStorage.removeItem(key); + } +} + +StreamController? _storageRevisionController; + +/// Storage revision notification from all tabs +Stream get storageRevisionStream { + _storageRevisionController ??= + StreamController.broadcast(onListen: () { + web.window.onstorage = (web.StorageEvent event) { + if (debugStorageNotification) { + print('getting ${event.key}: ${event.newValue}'); + } + if (event.key?.startsWith(_sembastStorageKeyPrefix) ?? false) { + var name = event.key!.substring(_sembastStorageKeyPrefix.length); + var revision = + event.newValue == null ? 0 : (int.tryParse(event.newValue!) ?? 0); + _storageRevisionController?.add(StorageRevision(name, revision)); + } + }.toJS; + }, onCancel: () { + web.window.onstorage = null; + _storageRevisionController = null; + }); + return _storageRevisionController!.stream; +} diff --git a/sembast_web/lib/src/web_interop/sembast_web.dart b/sembast_web/lib/src/web_interop/sembast_web.dart new file mode 100644 index 00000000..d7e0b716 --- /dev/null +++ b/sembast_web/lib/src/web_interop/sembast_web.dart @@ -0,0 +1,3 @@ +export 'sembast_web_stub.dart' + if (dart.library.html) 'sembast_web_impl.dart' + if (dart.library.io) 'sembast_web_io.dart'; diff --git a/sembast_web/lib/src/web_interop/sembast_web_impl.dart b/sembast_web/lib/src/web_interop/sembast_web_impl.dart new file mode 100644 index 00000000..75bcefa5 --- /dev/null +++ b/sembast_web/lib/src/web_interop/sembast_web_impl.dart @@ -0,0 +1,7 @@ +import 'package:sembast/sembast.dart'; +import 'package:sembast_web/src/web_interop.dart' as src; + +/// Sembast factory for the Web. +/// +/// Build on top of IndexedDB and localStorage. +DatabaseFactory get databaseFactoryWeb => src.databaseFactoryWeb; diff --git a/sembast_web/lib/src/web_interop/sembast_web_io.dart b/sembast_web/lib/src/web_interop/sembast_web_io.dart new file mode 100644 index 00000000..5a8ffc53 --- /dev/null +++ b/sembast_web/lib/src/web_interop/sembast_web_io.dart @@ -0,0 +1,11 @@ +import 'package:sembast/sembast.dart'; + +/// Sembast factory for the Web. +/// +/// Build on top of IndexedDB and localStorage. +DatabaseFactory get databaseFactoryWeb => _stub( + 'databaseFactoryWeb not support on Flutter/VM. Use `sembast_sqflite` or `sembast` io implementation'); + +T _stub(String message) { + throw UnimplementedError(message); +} diff --git a/sembast_web/lib/src/web_interop/sembast_web_stub.dart b/sembast_web/lib/src/web_interop/sembast_web_stub.dart new file mode 100644 index 00000000..6b6dd3df --- /dev/null +++ b/sembast_web/lib/src/web_interop/sembast_web_stub.dart @@ -0,0 +1,10 @@ +import 'package:sembast/sembast.dart'; + +/// Sembast factory for the Web. +/// +/// Build on top of IndexedDB and localStorage. +DatabaseFactory get databaseFactoryWeb => _stub('databaseFactoryWeb'); + +T _stub(String message) { + throw UnimplementedError(message); +} diff --git a/sembast_web/pubspec.yaml b/sembast_web/pubspec.yaml index 21e65fee..9e728e78 100644 --- a/sembast_web/pubspec.yaml +++ b/sembast_web/pubspec.yaml @@ -1,17 +1,18 @@ name: sembast_web description: NoSQL persistent embedded database for the Web on top of IndexedDB -version: 2.2.0 +version: 2.3.0-0 homepage: https://github.com/tekartik/sembast.dart/tree/master/sembast_web topics: - nosql - database environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: + web: '>=0.5.0 <2.0.0' sembast: '>=3.4.3 <5.0.0' - idb_shim: '>=2.3.0+2 <4.0.0' + idb_shim: '>=2.4.0-0 <4.0.0' synchronized: '>=3.0.1 <5.0.0' dev_dependencies: lints: '>=1.0.1' diff --git a/sembast_web/test/web/idb_web_interop_simple_test.dart b/sembast_web/test/web/idb_web_interop_simple_test.dart new file mode 100644 index 00000000..f1adc2cf --- /dev/null +++ b/sembast_web/test/web/idb_web_interop_simple_test.dart @@ -0,0 +1,75 @@ +@TestOn('browser') +library; + +import 'package:sembast/sembast.dart'; +import 'package:sembast_web/sembast_web_interop.dart'; +import 'package:sembast_web/src/sembast_import.dart'; +import 'package:test/test.dart'; +import 'package:web/web.dart'; + +import '../multiplatform/idb_jdb_test.dart' as idb_jdb_test; +import '../multiplatform/idb_jdb_test.dart'; + +var testPath = '.dart_tool/sembast_web_interop/databases'; + +Future main() async { + var factory = databaseFactoryWeb; + + group('idb_native', () { + test('doc', () async { + // Declare our store (records are mapd, ids are ints) + var store = intMapStoreFactory.store(); + var factory = databaseFactoryWeb; + + // Open the database + var db = await factory.openDatabase('test'); + + // Add a new record + var key = + await store.add(db, {'name': 'Table', 'price': 15}); + + // Read the record + var value = await store.record(key).get(db); + + // Print the value + print(value); + + // Close the database + await db.close(); + }); + + test('open', () async { + var store = StoreRef.main(); + var record = store.record('key'); + await factory.deleteDatabase('test'); + var db = await factory.openDatabase('test'); + expect(await record.get(db), isNull); + await record.put(db, 'value'); + expect(await record.get(db), 'value'); + await db.close(); + + db = await factory.openDatabase('test'); + await record.put(db, 'value'); + expect(await record.get(db), 'value'); + await db.close(); + }); + + test('storage_notification', () async { + var store = StoreRef.main(); + await factory.deleteDatabase('test'); + var db = await factory.openDatabase('test'); + expect(window.localStorage['sembast_web/revision:test'], isNull); + var record = store.record('my_key'); + await record.put(db, 'my_value'); + expect(window.localStorage['sembast_web/revision:test'], '1'); + await db.close(); + expect(window.localStorage['sembast_web/revision:test'], '1'); + // Make sure the storage gets clears on deletion + await factory.deleteDatabase('test'); + expect(window.localStorage['sembast_web/revision:test'], isNull); + }); + + idb_jdb_test.defineTests( + asJdbJactoryIdb(asDatabaseFactoryIdb(databaseFactoryWeb).jdbFactory)); + }); +} diff --git a/sembast_web/test/web/src_web_interop_test.dart b/sembast_web/test/web/src_web_interop_test.dart new file mode 100644 index 00000000..9c49d9c4 --- /dev/null +++ b/sembast_web/test/web/src_web_interop_test.dart @@ -0,0 +1,35 @@ +@TestOn('browser') +library; + +import 'package:sembast/sembast.dart'; +import 'package:sembast_web/src/web_interop.dart'; +import 'package:test/test.dart'; + +var testPath = '.dart_tool/sembast_web/databases'; + +Future main() async { + var factory = databaseFactoryWeb; + + group('web', () { + test('notification', () async { + var revisionFuture = storageRevisionStream.first; + var store = StoreRef.main(); + var record = store.record('key'); + await factory.deleteDatabase('test'); + var db = await factory.openDatabase('test'); + expect(await record.get(db), isNull); + await record.put(db, 'value'); + expect(await record.get(db), 'value'); + + try { + var storageRevision = + await revisionFuture.timeout(const Duration(seconds: 10)); + expect(storageRevision.name, 'test'); + expect(storageRevision.revision, greaterThanOrEqualTo(1)); + } catch (e) { + print(e); + } + await db.close(); + }); + }); +}