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: