@voidhash/mimic-effect 1.0.0-beta.3 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +21 -21
  2. package/dist/MimicServer.d.cts +1 -1
  3. package/dist/MimicServer.d.cts.map +1 -1
  4. package/dist/MimicServer.d.mts +1 -1
  5. package/dist/MimicServer.d.mts.map +1 -1
  6. package/dist/testing/FailingStorage.cjs +27 -0
  7. package/dist/testing/FailingStorage.d.cts.map +1 -1
  8. package/dist/testing/FailingStorage.d.mts.map +1 -1
  9. package/dist/testing/FailingStorage.mjs +28 -1
  10. package/dist/testing/FailingStorage.mjs.map +1 -1
  11. package/dist/testing/HotStorageTestSuite.cjs +253 -6
  12. package/dist/testing/HotStorageTestSuite.d.cts +2 -0
  13. package/dist/testing/HotStorageTestSuite.d.cts.map +1 -1
  14. package/dist/testing/HotStorageTestSuite.d.mts +2 -0
  15. package/dist/testing/HotStorageTestSuite.d.mts.map +1 -1
  16. package/dist/testing/HotStorageTestSuite.mjs +255 -8
  17. package/dist/testing/HotStorageTestSuite.mjs.map +1 -1
  18. package/dist/testing/StorageIntegrationTestSuite.cjs +150 -12
  19. package/dist/testing/StorageIntegrationTestSuite.d.cts +2 -0
  20. package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -1
  21. package/dist/testing/StorageIntegrationTestSuite.d.mts +2 -0
  22. package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -1
  23. package/dist/testing/StorageIntegrationTestSuite.mjs +151 -13
  24. package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -1
  25. package/dist/testing/types.d.cts +3 -3
  26. package/package.json +3 -3
  27. package/src/testing/FailingStorage.ts +53 -1
  28. package/src/testing/HotStorageTestSuite.ts +346 -3
  29. package/src/testing/StorageIntegrationTestSuite.ts +239 -7
@@ -18,7 +18,7 @@
18
18
  import { Effect, Layer, Ref, HashMap } from "effect";
19
19
  import { ColdStorageTag, type ColdStorage } from "../ColdStorage";
20
20
  import { HotStorageTag, type HotStorage } from "../HotStorage";
21
- import { ColdStorageError, HotStorageError } from "../Errors";
21
+ import { ColdStorageError, HotStorageError, WalVersionGapError } from "../Errors";
22
22
  import type { StoredDocument, WalEntry } from "../Types";
23
23
 
24
24
  // =============================================================================
@@ -224,6 +224,58 @@ export const makeHotStorage = (
224
224
  });
225
225
  }),
226
226
 
227
+ appendWithCheck: (documentId, entry, expectedVersion) =>
228
+ Effect.gen(function* () {
229
+ const fail = yield* shouldFail("append");
230
+ if (fail) {
231
+ return yield* Effect.fail(
232
+ new HotStorageError({
233
+ documentId,
234
+ operation: "appendWithCheck",
235
+ cause: new Error(errorMessage),
236
+ })
237
+ );
238
+ }
239
+
240
+ type CheckResult =
241
+ | { type: "ok" }
242
+ | { type: "gap"; lastVersion: number | undefined };
243
+
244
+ const result: CheckResult = yield* Ref.modify(store, (map): [CheckResult, HashMap.HashMap<string, WalEntry[]>] => {
245
+ const existing = HashMap.get(map, documentId);
246
+ const entries = existing._tag === "Some" ? existing.value : [];
247
+
248
+ const lastVersion = entries.length > 0
249
+ ? Math.max(...entries.map((e) => e.version))
250
+ : 0;
251
+
252
+ if (expectedVersion === 1) {
253
+ if (lastVersion >= 1) {
254
+ return [{ type: "gap", lastVersion }, map];
255
+ }
256
+ } else {
257
+ if (lastVersion !== expectedVersion - 1) {
258
+ return [{ type: "gap", lastVersion: lastVersion > 0 ? lastVersion : undefined }, map];
259
+ }
260
+ }
261
+
262
+ return [
263
+ { type: "ok" },
264
+ HashMap.set(map, documentId, [...entries, entry]),
265
+ ];
266
+ });
267
+
268
+ if (result.type === "gap") {
269
+ return yield* Effect.fail(
270
+ new WalVersionGapError({
271
+ documentId,
272
+ expectedVersion,
273
+ actualPreviousVersion: result.lastVersion,
274
+ })
275
+ );
276
+ }
277
+ }),
278
+
227
279
  getEntries: (documentId, sinceVersion) =>
228
280
  Effect.gen(function* () {
229
281
  const fail = yield* shouldFail("getEntries");
@@ -5,8 +5,8 @@
5
5
  * These tests verify that an adapter correctly implements the HotStorage interface
6
6
  * and can reliably store/retrieve WAL entries for document recovery.
7
7
  */
8
- import { Effect } from "effect";
9
- import { Transaction } from "@voidhash/mimic";
8
+ import { Effect, Schema } from "effect";
9
+ import { Transaction, OperationPath, Operation, OperationDefinition } from "@voidhash/mimic";
10
10
  import { HotStorageTag } from "../HotStorage";
11
11
  import { type HotStorageError, WalVersionGapError } from "../Errors";
12
12
  import type { WalEntry } from "../Types";
@@ -25,6 +25,31 @@ import {
25
25
  */
26
26
  export type HotStorageTestError = TestError | HotStorageError | WalVersionGapError;
27
27
 
28
+ // =============================================================================
29
+ // Test Operation Definitions
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Test operation definition for creating proper Operation objects in tests.
34
+ * Using Schema.Unknown allows any payload type for flexibility in testing.
35
+ */
36
+ const TestSetDefinition = OperationDefinition.make({
37
+ kind: "test.set" as const,
38
+ payload: Schema.Unknown,
39
+ target: Schema.Unknown,
40
+ apply: (payload: unknown) => payload,
41
+ });
42
+
43
+ /**
44
+ * Custom operation definition for testing operation kind preservation.
45
+ */
46
+ const CustomOpDefinition = OperationDefinition.make({
47
+ kind: "custom.operation" as const,
48
+ payload: Schema.Unknown,
49
+ target: Schema.Unknown,
50
+ apply: (payload: unknown) => payload,
51
+ });
52
+
28
53
  // =============================================================================
29
54
  // Categories
30
55
  // =============================================================================
@@ -39,6 +64,7 @@ export const Categories = {
39
64
  LargeScaleOperations: "Large-Scale Operations",
40
65
  DocumentIdEdgeCases: "Document ID Edge Cases",
41
66
  GapChecking: "Gap Checking",
67
+ TransactionEncoding: "Transaction Encoding",
42
68
  } as const;
43
69
 
44
70
  // =============================================================================
@@ -57,7 +83,20 @@ const makeEntryWithData = (
57
83
  timestamp?: number
58
84
  ): WalEntry => ({
59
85
  transaction: Transaction.make([
60
- { type: "set", path: ["data"], value: data },
86
+ Operation.fromDefinition(OperationPath.make("data"), TestSetDefinition, data),
87
+ ]),
88
+ version,
89
+ timestamp: timestamp ?? Date.now(),
90
+ });
91
+
92
+ const makeEntryWithPath = (
93
+ version: number,
94
+ pathString: string,
95
+ payload: unknown,
96
+ timestamp?: number
97
+ ): WalEntry => ({
98
+ transaction: Transaction.make([
99
+ Operation.fromDefinition(OperationPath.make(pathString), TestSetDefinition, payload),
61
100
  ]),
62
101
  version,
63
102
  timestamp: timestamp ?? Date.now(),
@@ -704,6 +743,310 @@ const tests: StorageTestCase<HotStorageTestError, HotStorageTag>[] = [
704
743
  yield* assertEqual(entries[1]!.version, 4, "Second should be version 4");
705
744
  }),
706
745
  },
746
+
747
+ // ---------------------------------------------------------------------------
748
+ // Transaction Encoding (Critical for OperationPath preservation)
749
+ // ---------------------------------------------------------------------------
750
+ {
751
+ name: "OperationPath has _tag after roundtrip",
752
+ category: Categories.TransactionEncoding,
753
+ run: Effect.gen(function* () {
754
+ const storage = yield* HotStorageTag;
755
+ const entry = makeEntryWithPath(1, "users/0/name", "Alice");
756
+ yield* storage.append("op-path-tag", entry);
757
+ const entries = yield* storage.getEntries("op-path-tag", 0);
758
+ yield* assertLength(entries, 1, "Should have one entry");
759
+ const op = entries[0]!.transaction.ops[0]!;
760
+ yield* assertTrue(
761
+ op.path._tag === "OperationPath",
762
+ "path should have _tag 'OperationPath'"
763
+ );
764
+ }),
765
+ },
766
+
767
+ {
768
+ name: "OperationPath.toTokens() works after roundtrip",
769
+ category: Categories.TransactionEncoding,
770
+ run: Effect.gen(function* () {
771
+ const storage = yield* HotStorageTag;
772
+ const entry = makeEntryWithPath(1, "users/0/name", "Alice");
773
+ yield* storage.append("op-path-tokens", entry);
774
+ const entries = yield* storage.getEntries("op-path-tokens", 0);
775
+ yield* assertLength(entries, 1, "Should have one entry");
776
+ const op = entries[0]!.transaction.ops[0]!;
777
+ yield* assertTrue(
778
+ typeof op.path.toTokens === "function",
779
+ "path.toTokens should be a function"
780
+ );
781
+ const tokens = op.path.toTokens();
782
+ yield* assertEqual(
783
+ tokens,
784
+ ["users", "0", "name"],
785
+ "toTokens() should return correct path tokens"
786
+ );
787
+ }),
788
+ },
789
+
790
+ {
791
+ name: "OperationPath.concat() works after roundtrip",
792
+ category: Categories.TransactionEncoding,
793
+ run: Effect.gen(function* () {
794
+ const storage = yield* HotStorageTag;
795
+ const entry = makeEntryWithPath(1, "users/0", { name: "Alice" });
796
+ yield* storage.append("op-path-concat", entry);
797
+ const entries = yield* storage.getEntries("op-path-concat", 0);
798
+ yield* assertLength(entries, 1, "Should have one entry");
799
+ const op = entries[0]!.transaction.ops[0]!;
800
+ yield* assertTrue(
801
+ typeof op.path.concat === "function",
802
+ "path.concat should be a function"
803
+ );
804
+ const extended = op.path.concat(OperationPath.make("name"));
805
+ yield* assertEqual(
806
+ extended.toTokens(),
807
+ ["users", "0", "name"],
808
+ "concat() should work correctly"
809
+ );
810
+ }),
811
+ },
812
+
813
+ {
814
+ name: "OperationPath.append() works after roundtrip",
815
+ category: Categories.TransactionEncoding,
816
+ run: Effect.gen(function* () {
817
+ const storage = yield* HotStorageTag;
818
+ const entry = makeEntryWithPath(1, "users", []);
819
+ yield* storage.append("op-path-append", entry);
820
+ const entries = yield* storage.getEntries("op-path-append", 0);
821
+ yield* assertLength(entries, 1, "Should have one entry");
822
+ const op = entries[0]!.transaction.ops[0]!;
823
+ yield* assertTrue(
824
+ typeof op.path.append === "function",
825
+ "path.append should be a function"
826
+ );
827
+ const extended = op.path.append("0");
828
+ yield* assertEqual(
829
+ extended.toTokens(),
830
+ ["users", "0"],
831
+ "append() should work correctly"
832
+ );
833
+ }),
834
+ },
835
+
836
+ {
837
+ name: "OperationPath.pop() works after roundtrip",
838
+ category: Categories.TransactionEncoding,
839
+ run: Effect.gen(function* () {
840
+ const storage = yield* HotStorageTag;
841
+ const entry = makeEntryWithPath(1, "users/0/name", "Alice");
842
+ yield* storage.append("op-path-pop", entry);
843
+ const entries = yield* storage.getEntries("op-path-pop", 0);
844
+ yield* assertLength(entries, 1, "Should have one entry");
845
+ const op = entries[0]!.transaction.ops[0]!;
846
+ yield* assertTrue(
847
+ typeof op.path.pop === "function",
848
+ "path.pop should be a function"
849
+ );
850
+ const popped = op.path.pop();
851
+ yield* assertEqual(
852
+ popped.toTokens(),
853
+ ["users", "0"],
854
+ "pop() should remove last token"
855
+ );
856
+ }),
857
+ },
858
+
859
+ {
860
+ name: "OperationPath.shift() works after roundtrip",
861
+ category: Categories.TransactionEncoding,
862
+ run: Effect.gen(function* () {
863
+ const storage = yield* HotStorageTag;
864
+ const entry = makeEntryWithPath(1, "users/0/name", "Alice");
865
+ yield* storage.append("op-path-shift", entry);
866
+ const entries = yield* storage.getEntries("op-path-shift", 0);
867
+ yield* assertLength(entries, 1, "Should have one entry");
868
+ const op = entries[0]!.transaction.ops[0]!;
869
+ yield* assertTrue(
870
+ typeof op.path.shift === "function",
871
+ "path.shift should be a function"
872
+ );
873
+ const shifted = op.path.shift();
874
+ yield* assertEqual(
875
+ shifted.toTokens(),
876
+ ["0", "name"],
877
+ "shift() should remove first token"
878
+ );
879
+ }),
880
+ },
881
+
882
+ {
883
+ name: "transaction with multiple operations preserves all OperationPaths",
884
+ category: Categories.TransactionEncoding,
885
+ run: Effect.gen(function* () {
886
+ const storage = yield* HotStorageTag;
887
+ const entry: WalEntry = {
888
+ transaction: Transaction.make([
889
+ Operation.fromDefinition(OperationPath.make("users/0/name"), TestSetDefinition, "Alice"),
890
+ Operation.fromDefinition(OperationPath.make("users/1/name"), TestSetDefinition, "Bob"),
891
+ Operation.fromDefinition(OperationPath.make("count"), TestSetDefinition, 2),
892
+ ]),
893
+ version: 1,
894
+ timestamp: Date.now(),
895
+ };
896
+ yield* storage.append("multi-op-paths", entry);
897
+ const entries = yield* storage.getEntries("multi-op-paths", 0);
898
+ yield* assertLength(entries, 1, "Should have one entry");
899
+ const ops = entries[0]!.transaction.ops;
900
+ yield* assertLength([...ops], 3, "Should have 3 operations");
901
+ // Verify all paths have working methods
902
+ for (const op of ops) {
903
+ yield* assertTrue(
904
+ op.path._tag === "OperationPath",
905
+ "Each operation path should have _tag"
906
+ );
907
+ yield* assertTrue(
908
+ typeof op.path.toTokens === "function",
909
+ "Each operation path should have toTokens method"
910
+ );
911
+ }
912
+ yield* assertEqual(
913
+ ops[0]!.path.toTokens(),
914
+ ["users", "0", "name"],
915
+ "First path should be correct"
916
+ );
917
+ yield* assertEqual(
918
+ ops[1]!.path.toTokens(),
919
+ ["users", "1", "name"],
920
+ "Second path should be correct"
921
+ );
922
+ yield* assertEqual(
923
+ ops[2]!.path.toTokens(),
924
+ ["count"],
925
+ "Third path should be correct"
926
+ );
927
+ }),
928
+ },
929
+
930
+ {
931
+ name: "nested path with many segments survives roundtrip",
932
+ category: Categories.TransactionEncoding,
933
+ run: Effect.gen(function* () {
934
+ const storage = yield* HotStorageTag;
935
+ const deepPath = "level1/level2/level3/level4/level5";
936
+ const entry = makeEntryWithPath(1, deepPath, "deep value");
937
+ yield* storage.append("deep-path", entry);
938
+ const entries = yield* storage.getEntries("deep-path", 0);
939
+ yield* assertLength(entries, 1, "Should have one entry");
940
+ const op = entries[0]!.transaction.ops[0]!;
941
+ yield* assertEqual(
942
+ op.path.toTokens(),
943
+ ["level1", "level2", "level3", "level4", "level5"],
944
+ "Deep nested path should survive roundtrip"
945
+ );
946
+ }),
947
+ },
948
+
949
+ {
950
+ name: "empty path survives roundtrip",
951
+ category: Categories.TransactionEncoding,
952
+ run: Effect.gen(function* () {
953
+ const storage = yield* HotStorageTag;
954
+ const entry = makeEntryWithPath(1, "", { root: true });
955
+ yield* storage.append("empty-path", entry);
956
+ const entries = yield* storage.getEntries("empty-path", 0);
957
+ yield* assertLength(entries, 1, "Should have one entry");
958
+ const op = entries[0]!.transaction.ops[0]!;
959
+ yield* assertTrue(
960
+ op.path._tag === "OperationPath",
961
+ "Empty path should still be OperationPath"
962
+ );
963
+ yield* assertTrue(
964
+ typeof op.path.toTokens === "function",
965
+ "Empty path should have toTokens method"
966
+ );
967
+ }),
968
+ },
969
+
970
+ {
971
+ name: "transaction id is preserved after roundtrip",
972
+ category: Categories.TransactionEncoding,
973
+ run: Effect.gen(function* () {
974
+ const storage = yield* HotStorageTag;
975
+ const entry = makeEntryWithPath(1, "test", "value");
976
+ const originalId = entry.transaction.id;
977
+ yield* storage.append("tx-id-preserve", entry);
978
+ const entries = yield* storage.getEntries("tx-id-preserve", 0);
979
+ yield* assertLength(entries, 1, "Should have one entry");
980
+ yield* assertEqual(
981
+ entries[0]!.transaction.id,
982
+ originalId,
983
+ "Transaction id should be preserved"
984
+ );
985
+ }),
986
+ },
987
+
988
+ {
989
+ name: "transaction timestamp is preserved after roundtrip",
990
+ category: Categories.TransactionEncoding,
991
+ run: Effect.gen(function* () {
992
+ const storage = yield* HotStorageTag;
993
+ const entry = makeEntryWithPath(1, "test", "value");
994
+ const originalTimestamp = entry.transaction.timestamp;
995
+ yield* storage.append("tx-timestamp-preserve", entry);
996
+ const entries = yield* storage.getEntries("tx-timestamp-preserve", 0);
997
+ yield* assertLength(entries, 1, "Should have one entry");
998
+ yield* assertEqual(
999
+ entries[0]!.transaction.timestamp,
1000
+ originalTimestamp,
1001
+ "Transaction timestamp should be preserved"
1002
+ );
1003
+ }),
1004
+ },
1005
+
1006
+ {
1007
+ name: "operation kind is preserved after roundtrip",
1008
+ category: Categories.TransactionEncoding,
1009
+ run: Effect.gen(function* () {
1010
+ const storage = yield* HotStorageTag;
1011
+ const entry: WalEntry = {
1012
+ transaction: Transaction.make([
1013
+ Operation.fromDefinition(OperationPath.make("data"), CustomOpDefinition, "test"),
1014
+ ]),
1015
+ version: 1,
1016
+ timestamp: Date.now(),
1017
+ };
1018
+ yield* storage.append("op-kind-preserve", entry);
1019
+ const entries = yield* storage.getEntries("op-kind-preserve", 0);
1020
+ yield* assertLength(entries, 1, "Should have one entry");
1021
+ yield* assertEqual(
1022
+ entries[0]!.transaction.ops[0]!.kind,
1023
+ "custom.operation",
1024
+ "Operation kind should be preserved"
1025
+ );
1026
+ }),
1027
+ },
1028
+
1029
+ {
1030
+ name: "operation payload with complex object survives roundtrip",
1031
+ category: Categories.TransactionEncoding,
1032
+ run: Effect.gen(function* () {
1033
+ const storage = yield* HotStorageTag;
1034
+ const complexPayload = {
1035
+ nested: { value: 42, array: [1, 2, 3] },
1036
+ nullValue: null,
1037
+ string: "test",
1038
+ };
1039
+ const entry = makeEntryWithPath(1, "data", complexPayload);
1040
+ yield* storage.append("complex-payload", entry);
1041
+ const entries = yield* storage.getEntries("complex-payload", 0);
1042
+ yield* assertLength(entries, 1, "Should have one entry");
1043
+ yield* assertEqual(
1044
+ entries[0]!.transaction.ops[0]!.payload,
1045
+ complexPayload,
1046
+ "Complex payload should survive roundtrip"
1047
+ );
1048
+ }),
1049
+ },
707
1050
  ];
708
1051
 
709
1052
  // =============================================================================