From 5f2e21c79ee2a83e4c1a7edf8913897b2db2a5f1 Mon Sep 17 00:00:00 2001 From: Martin Kamleithner <martin.kamleithner@gmail.com> Date: Fri, 7 Feb 2025 20:18:50 +0000 Subject: [PATCH 1/3] feat(ferry_hive_ce_store): inital commit --- packages/ferry_hive_ce_store/.gitignore | 11 + packages/ferry_hive_ce_store/CHANGELOG.md | 3 + packages/ferry_hive_ce_store/LICENSE | 21 ++ packages/ferry_hive_ce_store/README.md | 32 ++ .../ferry_hive_ce_store/analysis_options.yaml | 7 + .../lib/ferry_hive_store.dart | 3 + .../lib/src/hive_store.dart | 56 +++ packages/ferry_hive_ce_store/pubspec.yaml | 21 ++ .../test/operations_test.dart | 357 ++++++++++++++++++ 9 files changed, 511 insertions(+) create mode 100644 packages/ferry_hive_ce_store/.gitignore create mode 100644 packages/ferry_hive_ce_store/CHANGELOG.md create mode 100644 packages/ferry_hive_ce_store/LICENSE create mode 100644 packages/ferry_hive_ce_store/README.md create mode 100644 packages/ferry_hive_ce_store/analysis_options.yaml create mode 100644 packages/ferry_hive_ce_store/lib/ferry_hive_store.dart create mode 100644 packages/ferry_hive_ce_store/lib/src/hive_store.dart create mode 100644 packages/ferry_hive_ce_store/pubspec.yaml create mode 100644 packages/ferry_hive_ce_store/test/operations_test.dart diff --git a/packages/ferry_hive_ce_store/.gitignore b/packages/ferry_hive_ce_store/.gitignore new file mode 100644 index 00000000..6e334af5 --- /dev/null +++ b/packages/ferry_hive_ce_store/.gitignore @@ -0,0 +1,11 @@ +# Dart +.dart_tool +.packages +pubspec.lock + +# Documentation +doc/api + +build/ + +test/__hive_data__ \ No newline at end of file diff --git a/packages/ferry_hive_ce_store/CHANGELOG.md b/packages/ferry_hive_ce_store/CHANGELOG.md new file mode 100644 index 00000000..b30d5066 --- /dev/null +++ b/packages/ferry_hive_ce_store/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +- Initial release. Copy of ferry_hive_store but with dependency to hive_ce \ No newline at end of file diff --git a/packages/ferry_hive_ce_store/LICENSE b/packages/ferry_hive_ce_store/LICENSE new file mode 100644 index 00000000..f0f701a1 --- /dev/null +++ b/packages/ferry_hive_ce_store/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Sat Mandir Khalsa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/ferry_hive_ce_store/README.md b/packages/ferry_hive_ce_store/README.md new file mode 100644 index 00000000..1503f2da --- /dev/null +++ b/packages/ferry_hive_ce_store/README.md @@ -0,0 +1,32 @@ +[![MIT License][license-badge]][license-link] +[![PRs Welcome][prs-badge]][prs-link] +[![Watch on GitHub][github-watch-badge]][github-watch-link] +[![Star on GitHub][github-star-badge]][github-star-link] +[![Watch on GitHub][github-forks-badge]][github-forks-link] +[![Discord][discord-badge]][discord-link] + +[license-badge]: https://img.shields.io/github/license/gql-dart/ferry.svg?style=for-the-badge + +[license-link]: https://github.com/gql-dart/ferry/blob/master/LICENSE + +[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge + +[prs-link]: https://github.com/gql-dart/ferry/issues + +[github-watch-badge]: https://img.shields.io/github/watchers/gql-dart/ferry.svg?style=for-the-badge&logo=github&logoColor=ffffff + +[github-watch-link]: https://github.com/gql-dart/ferry/watchers + +[github-star-badge]: https://img.shields.io/github/stars/gql-dart/ferry.svg?style=for-the-badge&logo=github&logoColor=ffffff + +[github-star-link]: https://github.com/gql-dart/ferry/stargazers + +[github-forks-badge]: https://img.shields.io/github/forks/gql-dart/ferry.svg?style=for-the-badge&logo=github&logoColor=ffffff + +[github-forks-link]: https://github.com/gql-dart/ferry/network/members + +[discord-badge]: https://img.shields.io/discord/559455668810153989.svg?style=for-the-badge&logo=discord&logoColor=ffffff + +[discord-link]: https://discord.gg/YBFCTXNbwY + +A Store implementation that uses `hive_ce` to persist data. \ No newline at end of file diff --git a/packages/ferry_hive_ce_store/analysis_options.yaml b/packages/ferry_hive_ce_store/analysis_options.yaml new file mode 100644 index 00000000..565d40da --- /dev/null +++ b/packages/ferry_hive_ce_store/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:lints/recommended.yaml + +analyzer: + language: + strict-raw-types: true + strict-inference: true + strict-casts: true \ No newline at end of file diff --git a/packages/ferry_hive_ce_store/lib/ferry_hive_store.dart b/packages/ferry_hive_ce_store/lib/ferry_hive_store.dart new file mode 100644 index 00000000..76cc81c3 --- /dev/null +++ b/packages/ferry_hive_ce_store/lib/ferry_hive_store.dart @@ -0,0 +1,3 @@ +export 'package:ferry_store/ferry_store.dart'; + +export './src/hive_store.dart'; diff --git a/packages/ferry_hive_ce_store/lib/src/hive_store.dart b/packages/ferry_hive_ce_store/lib/src/hive_store.dart new file mode 100644 index 00000000..cd063afd --- /dev/null +++ b/packages/ferry_hive_ce_store/lib/src/hive_store.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; +import 'package:hive_ce/hive.dart'; +import 'package:ferry_store/ferry_store.dart'; + +class HiveStore extends Store { + final Box<dynamic> box; + final JsonEquals _jsonEquals; + + HiveStore(this.box, [JsonEquals? jsonEquals]) + : _jsonEquals = jsonEquals ?? jsonMapEquals; + + @override + Iterable<String> get keys => List.from(box.keys); + + @override + Stream<Map<String, dynamic>?> watch(String dataId, {bool distinct = true}) { + final stream = box + .watch(key: dataId) + .map<Map<String, dynamic>?>((event) => + event.value == null ? null : Map.from(event.value as Map)) + .startWith(get(dataId)); + + if (distinct) { + return stream.distinct(_jsonEquals); + } + return stream; + } + + @override + Map<String, dynamic>? get(String dataId) { + final value = box.get(dataId); + return value == null ? null : Map.from(value as Map); + } + + @override + void put(String dataId, Map<String, dynamic>? value) => + box.put(dataId, value); + + @override + void putAll(Map<String, Map<String, dynamic>?> data) => box.putAll(data); + + @override + void delete(String dataId) => box.delete(dataId); + + @override + void deleteAll(Iterable<String> dataIds) => box.deleteAll(dataIds); + + // NOTE: we can't currently use box.clear since it isn't synchronous + // https://github.com/hivedb/hive/issues/219 + @override + void clear() => box.deleteAll(keys); + + @override + Future<void> dispose() => box.close(); +} diff --git a/packages/ferry_hive_ce_store/pubspec.yaml b/packages/ferry_hive_ce_store/pubspec.yaml new file mode 100644 index 00000000..6fddd55e --- /dev/null +++ b/packages/ferry_hive_ce_store/pubspec.yaml @@ -0,0 +1,21 @@ +name: ferry_hive_ce_store +version: 0.0.1 +homepage: https://ferry.gql-dart.dev +description: Hive-based Store implementation for Ferry GraphQL client +repository: https://github.com/gql-dart/ferry +environment: + sdk: '>=3.0.0 <4.0.0' +topics: + - graphql + - gql + - ferry + - hive_ce + - hive +dependencies: + hive_ce: ^2.0.0 + ferry_store: ^0.6.0 + rxdart: ^0.28.0 + collection: ^1.15.0 +dev_dependencies: + test: ^1.16.8 + lints: ^5.0.0 \ No newline at end of file diff --git a/packages/ferry_hive_ce_store/test/operations_test.dart b/packages/ferry_hive_ce_store/test/operations_test.dart new file mode 100644 index 00000000..9eba794e --- /dev/null +++ b/packages/ferry_hive_ce_store/test/operations_test.dart @@ -0,0 +1,357 @@ +import 'package:ferry_hive_ce_store/ferry_hive_store.dart'; +import 'package:test/test.dart'; +import 'package:hive_ce/hive.dart'; + +void main() { + Hive.init('./test/__hive_data__'); + + group('CRUD operations', () { + test('starts empty', () async { + final box = await Hive.openBox<dynamic>('graphql'); + await box.clear(); + final store = HiveStore(box); + + expect(store.keys.length, equals(0)); + }); + test('can include seeded data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + for (var entry in data.entries) { + expect(store.get(entry.key), equals(entry.value)); + } + }); + + test('can get data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + for (var entry in data.entries) { + expect(store.get(entry.key), equals(entry.value)); + } + }); + + test('can put data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + final store = HiveStore(box); + + for (var entry in data.entries) { + store.put(entry.key, entry.value); + } + + for (var entry in data.entries) { + expect(store.get(entry.key), equals(entry.value)); + } + }); + + test('can put all data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + final store = HiveStore(box); + + store.putAll(data); + + for (var entry in data.entries) { + expect(store.get(entry.key), equals(entry.value)); + } + }); + + test('can delete data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + final key = store.keys.first; + + store.delete(key); + expect( + store.get(key), + equals(null), + ); + }); + + test('can clear data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + store.clear(); + expect(store.keys.length, equals(0)); + }); + }); + + group('watch operation', () { + test('gets initial data when watching', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + expect(store.watch(data.keys.first), emits(data.values.first)); + }); + + test('put method triggers new data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + final newData = { + 'posts': [ + {'\$ref': 'Post:456'} + ] + }; + + expect( + store.watch(data.keys.first), + emitsInOrder([ + data.values.first, + newData, + ]), + ); + + await Future.delayed(Duration.zero); + store.put(data.keys.first, newData); + }); + + test('changes to underlying box triggers new data', () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + final newData = { + 'posts': [ + {'\$ref': 'Post:456'} + ] + }; + + expect( + store.watch(data.keys.first), + emitsInOrder([ + data.values.first, + newData, + ]), + ); + + await Future.delayed(Duration.zero); + await box.put(data.keys.first, newData); + }); + + test("put method doesn't trigger with correct key and same data", () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + expect( + store.watch(data.keys.first), + emitsInOrder([ + data.values.first, + emitsDone, + ]), + ); + + await Future.delayed(Duration.zero); + store.put(data.keys.first, data.values.first); + await store.dispose(); + }); + + test("put method doesn't trigger with different key", () async { + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + final box = await Hive.openBox('graphql'); + await box.clear(); + await box.putAll(data); + final store = HiveStore(box); + + final newPostKey = 'Post:456'; + final newPostValue = { + 'id': '456', + '__typename': 'Post', + }; + + expect( + store.watch(data.keys.first), + emitsInOrder([ + data.values.first, + emitsDone, + ]), + ); + + await Future.delayed(Duration.zero); + store.put(newPostKey, newPostValue); + await store.dispose(); + }); + }); + + test('mutating map does not break hive', () async { + final box = await Hive.openBox<dynamic>('graphql'); + + addTearDown(() async { + await box.clear(); + return box.close(); + }); + + final store = HiveStore(box); + + final data = { + 'Query': { + 'posts': [ + {'\$ref': 'Post:123'} + ] + }, + 'Post:123': { + 'id': '123', + '__typename': 'Post', + }, + }; + + store.putAll(data); + + final post = store.get('Post:123') as Map<String, dynamic>; + + post['id'] = '456'; + + final updatedPost = store.get('Post:123') as Map<String, dynamic>; + + expect(updatedPost['id'], equals('123')); + }); +} From 5325d4abbbff1380217da889d1ce21cd93dae7a6 Mon Sep 17 00:00:00 2001 From: Martin Kamleithner <martin.kamleithner@gmail.com> Date: Fri, 7 Feb 2025 20:24:19 +0000 Subject: [PATCH 2/3] feat(ferry_hive_ce_store): lints --- .../test/operations_test.dart | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/ferry_hive_ce_store/test/operations_test.dart b/packages/ferry_hive_ce_store/test/operations_test.dart index 9eba794e..7f14c324 100644 --- a/packages/ferry_hive_ce_store/test/operations_test.dart +++ b/packages/ferry_hive_ce_store/test/operations_test.dart @@ -26,7 +26,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -49,7 +49,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -72,7 +72,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); final store = HiveStore(box); @@ -98,7 +98,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); final store = HiveStore(box); @@ -122,7 +122,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -149,7 +149,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -173,7 +173,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -194,7 +194,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -213,7 +213,7 @@ void main() { ]), ); - await Future.delayed(Duration.zero); + await Future<void>.delayed(Duration.zero); store.put(data.keys.first, newData); }); @@ -230,7 +230,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -249,7 +249,7 @@ void main() { ]), ); - await Future.delayed(Duration.zero); + await Future<void>.delayed(Duration.zero); await box.put(data.keys.first, newData); }); @@ -266,7 +266,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -279,7 +279,7 @@ void main() { ]), ); - await Future.delayed(Duration.zero); + await Future<void>.delayed(Duration.zero); store.put(data.keys.first, data.values.first); await store.dispose(); }); @@ -297,7 +297,7 @@ void main() { }, }; - final box = await Hive.openBox('graphql'); + final box = await Hive.openBox<dynamic>('graphql'); await box.clear(); await box.putAll(data); final store = HiveStore(box); @@ -316,7 +316,7 @@ void main() { ]), ); - await Future.delayed(Duration.zero); + await Future<void>.delayed(Duration.zero); store.put(newPostKey, newPostValue); await store.dispose(); }); From 648e7c3a8618d6c99fbc1165c8879eb738eb60d7 Mon Sep 17 00:00:00 2001 From: Martin Kamleithner <martin.kamleithner@gmail.com> Date: Fri, 7 Feb 2025 22:38:31 +0000 Subject: [PATCH 3/3] feat(ferry_hive_ce_store): flush --- packages/ferry_hive_ce_store/lib/src/hive_store.dart | 3 +++ packages/ferry_hive_ce_store/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ferry_hive_ce_store/lib/src/hive_store.dart b/packages/ferry_hive_ce_store/lib/src/hive_store.dart index cd063afd..4d92cad8 100644 --- a/packages/ferry_hive_ce_store/lib/src/hive_store.dart +++ b/packages/ferry_hive_ce_store/lib/src/hive_store.dart @@ -53,4 +53,7 @@ class HiveStore extends Store { @override Future<void> dispose() => box.close(); + + @override + Future<void> flush() => box.flush(); } diff --git a/packages/ferry_hive_ce_store/pubspec.yaml b/packages/ferry_hive_ce_store/pubspec.yaml index 6fddd55e..5d6e7847 100644 --- a/packages/ferry_hive_ce_store/pubspec.yaml +++ b/packages/ferry_hive_ce_store/pubspec.yaml @@ -13,7 +13,7 @@ topics: - hive dependencies: hive_ce: ^2.0.0 - ferry_store: ^0.6.0 + ferry_store: ^0.6.1 rxdart: ^0.28.0 collection: ^1.15.0 dev_dependencies: