@voidhash/mimic-effect 1.0.0-beta.1 → 1.0.0-beta.11

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 (142) hide show
  1. package/.turbo/turbo-build.log +116 -74
  2. package/dist/ColdStorage.cjs +9 -5
  3. package/dist/ColdStorage.d.cts.map +1 -1
  4. package/dist/ColdStorage.d.mts.map +1 -1
  5. package/dist/ColdStorage.mjs +9 -5
  6. package/dist/ColdStorage.mjs.map +1 -1
  7. package/dist/DocumentInstance.cjs +264 -0
  8. package/dist/DocumentInstance.d.cts +78 -0
  9. package/dist/DocumentInstance.d.cts.map +1 -0
  10. package/dist/DocumentInstance.d.mts +78 -0
  11. package/dist/DocumentInstance.d.mts.map +1 -0
  12. package/dist/DocumentInstance.mjs +265 -0
  13. package/dist/DocumentInstance.mjs.map +1 -0
  14. package/dist/Errors.cjs +10 -1
  15. package/dist/Errors.d.cts +18 -3
  16. package/dist/Errors.d.cts.map +1 -1
  17. package/dist/Errors.d.mts +18 -3
  18. package/dist/Errors.d.mts.map +1 -1
  19. package/dist/Errors.mjs +9 -1
  20. package/dist/Errors.mjs.map +1 -1
  21. package/dist/HotStorage.cjs +40 -12
  22. package/dist/HotStorage.d.cts +21 -1
  23. package/dist/HotStorage.d.cts.map +1 -1
  24. package/dist/HotStorage.d.mts +21 -1
  25. package/dist/HotStorage.d.mts.map +1 -1
  26. package/dist/HotStorage.mjs +40 -12
  27. package/dist/HotStorage.mjs.map +1 -1
  28. package/dist/Metrics.cjs +29 -1
  29. package/dist/Metrics.d.cts +5 -0
  30. package/dist/Metrics.d.cts.map +1 -1
  31. package/dist/Metrics.d.mts +5 -0
  32. package/dist/Metrics.d.mts.map +1 -1
  33. package/dist/Metrics.mjs +26 -1
  34. package/dist/Metrics.mjs.map +1 -1
  35. package/dist/MimicClusterServerEngine.cjs +44 -139
  36. package/dist/MimicClusterServerEngine.d.cts.map +1 -1
  37. package/dist/MimicClusterServerEngine.d.mts +1 -1
  38. package/dist/MimicClusterServerEngine.d.mts.map +1 -1
  39. package/dist/MimicClusterServerEngine.mjs +46 -141
  40. package/dist/MimicClusterServerEngine.mjs.map +1 -1
  41. package/dist/MimicServer.cjs +20 -20
  42. package/dist/MimicServer.d.cts.map +1 -1
  43. package/dist/MimicServer.d.mts.map +1 -1
  44. package/dist/MimicServer.mjs +20 -20
  45. package/dist/MimicServer.mjs.map +1 -1
  46. package/dist/MimicServerEngine.cjs +92 -11
  47. package/dist/MimicServerEngine.d.cts +12 -4
  48. package/dist/MimicServerEngine.d.cts.map +1 -1
  49. package/dist/MimicServerEngine.d.mts +12 -4
  50. package/dist/MimicServerEngine.d.mts.map +1 -1
  51. package/dist/MimicServerEngine.mjs +94 -13
  52. package/dist/MimicServerEngine.mjs.map +1 -1
  53. package/dist/PresenceManager.cjs +5 -5
  54. package/dist/PresenceManager.d.cts.map +1 -1
  55. package/dist/PresenceManager.d.mts.map +1 -1
  56. package/dist/PresenceManager.mjs +5 -5
  57. package/dist/PresenceManager.mjs.map +1 -1
  58. package/dist/Protocol.d.cts +1 -1
  59. package/dist/Protocol.d.mts +1 -1
  60. package/dist/Types.d.cts +9 -2
  61. package/dist/Types.d.cts.map +1 -1
  62. package/dist/Types.d.mts +9 -2
  63. package/dist/Types.d.mts.map +1 -1
  64. package/dist/index.cjs +5 -6
  65. package/dist/index.d.cts +3 -3
  66. package/dist/index.d.mts +3 -3
  67. package/dist/index.mjs +3 -3
  68. package/dist/testing/ColdStorageTestSuite.cjs +508 -0
  69. package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
  70. package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
  71. package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
  72. package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
  73. package/dist/testing/ColdStorageTestSuite.mjs +508 -0
  74. package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
  75. package/dist/testing/FailingStorage.cjs +162 -0
  76. package/dist/testing/FailingStorage.d.cts +43 -0
  77. package/dist/testing/FailingStorage.d.cts.map +1 -0
  78. package/dist/testing/FailingStorage.d.mts +43 -0
  79. package/dist/testing/FailingStorage.d.mts.map +1 -0
  80. package/dist/testing/FailingStorage.mjs +163 -0
  81. package/dist/testing/FailingStorage.mjs.map +1 -0
  82. package/dist/testing/HotStorageTestSuite.cjs +858 -0
  83. package/dist/testing/HotStorageTestSuite.d.cts +42 -0
  84. package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
  85. package/dist/testing/HotStorageTestSuite.d.mts +42 -0
  86. package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
  87. package/dist/testing/HotStorageTestSuite.mjs +858 -0
  88. package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
  89. package/dist/testing/StorageIntegrationTestSuite.cjs +487 -0
  90. package/dist/testing/StorageIntegrationTestSuite.d.cts +37 -0
  91. package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
  92. package/dist/testing/StorageIntegrationTestSuite.d.mts +37 -0
  93. package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
  94. package/dist/testing/StorageIntegrationTestSuite.mjs +487 -0
  95. package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
  96. package/dist/testing/assertions.cjs +117 -0
  97. package/dist/testing/assertions.mjs +112 -0
  98. package/dist/testing/assertions.mjs.map +1 -0
  99. package/dist/testing/index.cjs +14 -0
  100. package/dist/testing/index.d.cts +6 -0
  101. package/dist/testing/index.d.mts +6 -0
  102. package/dist/testing/index.mjs +7 -0
  103. package/dist/testing/types.cjs +15 -0
  104. package/dist/testing/types.d.cts +90 -0
  105. package/dist/testing/types.d.cts.map +1 -0
  106. package/dist/testing/types.d.mts +90 -0
  107. package/dist/testing/types.d.mts.map +1 -0
  108. package/dist/testing/types.mjs +16 -0
  109. package/dist/testing/types.mjs.map +1 -0
  110. package/package.json +8 -3
  111. package/src/ColdStorage.ts +21 -12
  112. package/src/DocumentInstance.ts +531 -0
  113. package/src/Errors.ts +15 -1
  114. package/src/HotStorage.ts +130 -24
  115. package/src/Metrics.ts +30 -0
  116. package/src/MimicClusterServerEngine.ts +120 -275
  117. package/src/MimicServer.ts +83 -75
  118. package/src/MimicServerEngine.ts +230 -30
  119. package/src/PresenceManager.ts +44 -34
  120. package/src/Types.ts +9 -2
  121. package/src/index.ts +5 -35
  122. package/src/testing/ColdStorageTestSuite.ts +589 -0
  123. package/src/testing/FailingStorage.ts +338 -0
  124. package/src/testing/HotStorageTestSuite.ts +1161 -0
  125. package/src/testing/StorageIntegrationTestSuite.ts +736 -0
  126. package/src/testing/assertions.ts +188 -0
  127. package/src/testing/index.ts +83 -0
  128. package/src/testing/types.ts +100 -0
  129. package/tests/ColdStorage.test.ts +8 -120
  130. package/tests/DocumentInstance.test.ts +669 -0
  131. package/tests/HotStorage.test.ts +7 -126
  132. package/tests/StorageIntegration.test.ts +259 -0
  133. package/tsdown.config.ts +1 -1
  134. package/dist/DocumentManager.cjs +0 -229
  135. package/dist/DocumentManager.d.cts +0 -59
  136. package/dist/DocumentManager.d.cts.map +0 -1
  137. package/dist/DocumentManager.d.mts +0 -59
  138. package/dist/DocumentManager.d.mts.map +0 -1
  139. package/dist/DocumentManager.mjs +0 -227
  140. package/dist/DocumentManager.mjs.map +0 -1
  141. package/src/DocumentManager.ts +0 -506
  142. package/tests/DocumentManager.test.ts +0 -335
package/src/HotStorage.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { Context, Effect, HashMap, Layer, Ref } from "effect";
7
7
  import type { WalEntry } from "./Types";
8
- import { HotStorageError } from "./Errors";
8
+ import { HotStorageError, WalVersionGapError } from "./Errors";
9
9
 
10
10
  // =============================================================================
11
11
  // HotStorage Interface
@@ -27,6 +27,32 @@ export interface HotStorage {
27
27
  entry: WalEntry
28
28
  ) => Effect.Effect<void, HotStorageError>;
29
29
 
30
+ /**
31
+ * Append a WAL entry with version gap checking.
32
+ *
33
+ * This is an atomic operation that:
34
+ * 1. Verifies the previous entry has version = expectedVersion - 1
35
+ * (or this is the first entry if expectedVersion === 1, accounting for baseVersion)
36
+ * 2. Appends the entry if check passes
37
+ *
38
+ * Use this for two-phase commit to guarantee WAL ordering at write time.
39
+ *
40
+ * @param documentId - Document ID
41
+ * @param entry - WAL entry to append
42
+ * @param expectedVersion - The version this entry should have (entry.version)
43
+ * @param baseVersion - Optional known snapshot version. When provided, an empty WAL
44
+ * is treated as "at this version" rather than "new document at version 0".
45
+ * This is necessary after truncation or restart to correctly validate
46
+ * that the next entry is baseVersion + 1.
47
+ * @returns Effect that fails with WalVersionGapError if gap detected
48
+ */
49
+ readonly appendWithCheck: (
50
+ documentId: string,
51
+ entry: WalEntry,
52
+ expectedVersion: number,
53
+ baseVersion?: number
54
+ ) => Effect.Effect<void, HotStorageError | WalVersionGapError>;
55
+
30
56
  /**
31
57
  * Get all WAL entries for a document since a given version.
32
58
  * Returns entries with version > sinceVersion, ordered by version.
@@ -115,20 +141,96 @@ export namespace InMemory {
115
141
  export const make = (): Layer.Layer<HotStorageTag> =>
116
142
  Layer.effect(
117
143
  HotStorageTag,
118
- Effect.gen(function* () {
144
+ Effect.fn("hot-storage.in-memory.create")(function* () {
119
145
  const store = yield* Ref.make(HashMap.empty<string, WalEntry[]>());
120
146
 
121
147
  return {
122
- append: (documentId, entry) =>
123
- Ref.update(store, (map) => {
124
- const existing = HashMap.get(map, documentId);
125
- const entries =
126
- existing._tag === "Some" ? existing.value : [];
127
- return HashMap.set(map, documentId, [...entries, entry]);
128
- }),
148
+ append: Effect.fn("hot-storage.append")(
149
+ function* (documentId: string, entry: WalEntry) {
150
+ yield* Ref.update(store, (map) => {
151
+ const existing = HashMap.get(map, documentId);
152
+ const entries =
153
+ existing._tag === "Some" ? existing.value : [];
154
+ return HashMap.set(map, documentId, [...entries, entry]);
155
+ });
156
+ }
157
+ ),
158
+
159
+ appendWithCheck: Effect.fn("hot-storage.append-with-check")(
160
+ function* (
161
+ documentId: string,
162
+ entry: WalEntry,
163
+ expectedVersion: number,
164
+ baseVersion?: number
165
+ ) {
166
+ type CheckResult =
167
+ | { type: "ok" }
168
+ | { type: "gap"; lastVersion: number | undefined };
169
+
170
+ // Use Ref.modify for atomic check + update
171
+ const result: CheckResult = yield* Ref.modify(
172
+ store,
173
+ (map): [CheckResult, HashMap.HashMap<string, WalEntry[]>] => {
174
+ const existing = HashMap.get(map, documentId);
175
+ const entries =
176
+ existing._tag === "Some" ? existing.value : [];
177
+
178
+ // Find the highest version in existing entries
179
+ const lastEntryVersion =
180
+ entries.length > 0
181
+ ? Math.max(...entries.map((e) => e.version))
182
+ : 0;
129
183
 
130
- getEntries: (documentId, sinceVersion) =>
131
- Effect.gen(function* () {
184
+ // Effective "last version" is max of entries and baseVersion
185
+ // This handles the case after truncation or restart where
186
+ // WAL is empty but we know the snapshot version
187
+ const effectiveLastVersion =
188
+ baseVersion !== undefined
189
+ ? Math.max(lastEntryVersion, baseVersion)
190
+ : lastEntryVersion;
191
+
192
+ // Gap check
193
+ if (expectedVersion === 1) {
194
+ // First entry: should have no entries with version >= 1
195
+ // and baseVersion should be 0 or undefined
196
+ if (effectiveLastVersion >= 1) {
197
+ return [{ type: "gap", lastVersion: effectiveLastVersion }, map];
198
+ }
199
+ } else {
200
+ // Not first: effective last version should be expectedVersion - 1
201
+ if (effectiveLastVersion !== expectedVersion - 1) {
202
+ return [
203
+ {
204
+ type: "gap",
205
+ lastVersion: effectiveLastVersion > 0 ? effectiveLastVersion : undefined,
206
+ },
207
+ map,
208
+ ];
209
+ }
210
+ }
211
+
212
+ // No gap: append and return success
213
+ return [
214
+ { type: "ok" },
215
+ HashMap.set(map, documentId, [...entries, entry]),
216
+ ];
217
+ }
218
+ );
219
+
220
+ if (result.type === "gap") {
221
+ return yield* Effect.fail(
222
+ new WalVersionGapError({
223
+ documentId,
224
+ expectedVersion,
225
+ actualPreviousVersion: result.lastVersion,
226
+ })
227
+ );
228
+ }
229
+ }
230
+ ),
231
+
232
+ getEntries: Effect.fn("hot-storage.get-entries")(
233
+ function* (documentId: string, sinceVersion: number) {
132
234
  const current = yield* Ref.get(store);
133
235
  const existing = HashMap.get(current, documentId);
134
236
  const entries =
@@ -136,21 +238,25 @@ export namespace InMemory {
136
238
  return entries
137
239
  .filter((e) => e.version > sinceVersion)
138
240
  .sort((a, b) => a.version - b.version);
139
- }),
241
+ }
242
+ ),
140
243
 
141
- truncate: (documentId, upToVersion) =>
142
- Ref.update(store, (map) => {
143
- const existing = HashMap.get(map, documentId);
144
- if (existing._tag === "None") {
145
- return map;
146
- }
147
- const filtered = existing.value.filter(
148
- (e) => e.version > upToVersion
149
- );
150
- return HashMap.set(map, documentId, filtered);
151
- }),
244
+ truncate: Effect.fn("hot-storage.truncate")(
245
+ function* (documentId: string, upToVersion: number) {
246
+ yield* Ref.update(store, (map) => {
247
+ const existing = HashMap.get(map, documentId);
248
+ if (existing._tag === "None") {
249
+ return map;
250
+ }
251
+ const filtered = existing.value.filter(
252
+ (e) => e.version > upToVersion
253
+ );
254
+ return HashMap.set(map, documentId, filtered);
255
+ });
256
+ }
257
+ ),
152
258
  };
153
- })
259
+ })()
154
260
  );
155
261
  }
156
262
 
package/src/Metrics.ts CHANGED
@@ -99,6 +99,11 @@ export const transactionsLatency = Metric.histogram(
99
99
  */
100
100
  export const storageSnapshots = Metric.counter("mimic.storage.snapshots");
101
101
 
102
+ /**
103
+ * Snapshots triggered by idle document detection
104
+ */
105
+ export const storageIdleSnapshots = Metric.counter("mimic.storage.idle_snapshots");
106
+
102
107
  /**
103
108
  * Snapshot save duration histogram (milliseconds)
104
109
  */
@@ -116,6 +121,26 @@ export const storageSnapshotLatency = Metric.histogram(
116
121
  */
117
122
  export const storageWalAppends = Metric.counter("mimic.storage.wal_appends");
118
123
 
124
+ /**
125
+ * Version gaps detected during WAL replay
126
+ */
127
+ export const storageVersionGaps = Metric.counter("mimic.storage.version_gaps");
128
+
129
+ /**
130
+ * Failed WAL appends causing transaction rollback
131
+ */
132
+ export const walAppendFailures = Metric.counter("mimic.storage.wal_append_failures");
133
+
134
+ /**
135
+ * ColdStorage load failures during document restore
136
+ */
137
+ export const coldStorageLoadFailures = Metric.counter("mimic.storage.cold_load_failures");
138
+
139
+ /**
140
+ * HotStorage getEntries failures during document restore
141
+ */
142
+ export const hotStorageLoadFailures = Metric.counter("mimic.storage.hot_load_failures");
143
+
119
144
  // =============================================================================
120
145
  // Presence Metrics
121
146
  // =============================================================================
@@ -154,8 +179,13 @@ export const MimicMetrics = {
154
179
 
155
180
  // Storage
156
181
  storageSnapshots,
182
+ storageIdleSnapshots,
157
183
  storageSnapshotLatency,
158
184
  storageWalAppends,
185
+ storageVersionGaps,
186
+ walAppendFailures,
187
+ coldStorageLoadFailures,
188
+ hotStorageLoadFailures,
159
189
 
160
190
  // Presence
161
191
  presenceUpdates,