@voidhash/mimic-effect 0.0.9 → 1.0.0-beta.2

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 (227) hide show
  1. package/.turbo/turbo-build.log +136 -90
  2. package/README.md +385 -0
  3. package/dist/ColdStorage.cjs +60 -0
  4. package/dist/ColdStorage.d.cts +53 -0
  5. package/dist/ColdStorage.d.cts.map +1 -0
  6. package/dist/ColdStorage.d.mts +53 -0
  7. package/dist/ColdStorage.d.mts.map +1 -0
  8. package/dist/ColdStorage.mjs +60 -0
  9. package/dist/ColdStorage.mjs.map +1 -0
  10. package/dist/DocumentManager.cjs +263 -82
  11. package/dist/DocumentManager.d.cts +44 -22
  12. package/dist/DocumentManager.d.cts.map +1 -1
  13. package/dist/DocumentManager.d.mts +44 -22
  14. package/dist/DocumentManager.d.mts.map +1 -1
  15. package/dist/DocumentManager.mjs +259 -67
  16. package/dist/DocumentManager.mjs.map +1 -1
  17. package/dist/Errors.cjs +54 -0
  18. package/dist/Errors.d.cts +96 -0
  19. package/dist/Errors.d.cts.map +1 -0
  20. package/dist/Errors.d.mts +96 -0
  21. package/dist/Errors.d.mts.map +1 -0
  22. package/dist/Errors.mjs +48 -0
  23. package/dist/Errors.mjs.map +1 -0
  24. package/dist/HotStorage.cjs +100 -0
  25. package/dist/HotStorage.d.cts +70 -0
  26. package/dist/HotStorage.d.cts.map +1 -0
  27. package/dist/HotStorage.d.mts +70 -0
  28. package/dist/HotStorage.d.mts.map +1 -0
  29. package/dist/HotStorage.mjs +100 -0
  30. package/dist/HotStorage.mjs.map +1 -0
  31. package/dist/Metrics.cjs +143 -0
  32. package/dist/Metrics.d.cts +31 -0
  33. package/dist/Metrics.d.cts.map +1 -0
  34. package/dist/Metrics.d.mts +31 -0
  35. package/dist/Metrics.d.mts.map +1 -0
  36. package/dist/Metrics.mjs +126 -0
  37. package/dist/Metrics.mjs.map +1 -0
  38. package/dist/MimicAuthService.cjs +61 -45
  39. package/dist/MimicAuthService.d.cts +61 -48
  40. package/dist/MimicAuthService.d.cts.map +1 -1
  41. package/dist/MimicAuthService.d.mts +61 -48
  42. package/dist/MimicAuthService.d.mts.map +1 -1
  43. package/dist/MimicAuthService.mjs +60 -36
  44. package/dist/MimicAuthService.mjs.map +1 -1
  45. package/dist/MimicClusterServerEngine.cjs +521 -0
  46. package/dist/MimicClusterServerEngine.d.cts +17 -0
  47. package/dist/MimicClusterServerEngine.d.cts.map +1 -0
  48. package/dist/MimicClusterServerEngine.d.mts +17 -0
  49. package/dist/MimicClusterServerEngine.d.mts.map +1 -0
  50. package/dist/MimicClusterServerEngine.mjs +523 -0
  51. package/dist/MimicClusterServerEngine.mjs.map +1 -0
  52. package/dist/MimicServer.cjs +205 -96
  53. package/dist/MimicServer.d.cts +9 -110
  54. package/dist/MimicServer.d.cts.map +1 -1
  55. package/dist/MimicServer.d.mts +9 -110
  56. package/dist/MimicServer.d.mts.map +1 -1
  57. package/dist/MimicServer.mjs +206 -90
  58. package/dist/MimicServer.mjs.map +1 -1
  59. package/dist/MimicServerEngine.cjs +97 -0
  60. package/dist/MimicServerEngine.d.cts +78 -0
  61. package/dist/MimicServerEngine.d.cts.map +1 -0
  62. package/dist/MimicServerEngine.d.mts +78 -0
  63. package/dist/MimicServerEngine.d.mts.map +1 -0
  64. package/dist/MimicServerEngine.mjs +97 -0
  65. package/dist/MimicServerEngine.mjs.map +1 -0
  66. package/dist/PresenceManager.cjs +75 -91
  67. package/dist/PresenceManager.d.cts +17 -66
  68. package/dist/PresenceManager.d.cts.map +1 -1
  69. package/dist/PresenceManager.d.mts +17 -66
  70. package/dist/PresenceManager.d.mts.map +1 -1
  71. package/dist/PresenceManager.mjs +74 -78
  72. package/dist/PresenceManager.mjs.map +1 -1
  73. package/dist/Protocol.cjs +146 -0
  74. package/dist/Protocol.d.cts +203 -0
  75. package/dist/Protocol.d.cts.map +1 -0
  76. package/dist/Protocol.d.mts +203 -0
  77. package/dist/Protocol.d.mts.map +1 -0
  78. package/dist/Protocol.mjs +132 -0
  79. package/dist/Protocol.mjs.map +1 -0
  80. package/dist/Types.d.cts +172 -0
  81. package/dist/Types.d.cts.map +1 -0
  82. package/dist/Types.d.mts +172 -0
  83. package/dist/Types.d.mts.map +1 -0
  84. package/dist/_virtual/rolldown_runtime.cjs +1 -25
  85. package/dist/_virtual/rolldown_runtime.mjs +4 -1
  86. package/dist/index.cjs +37 -75
  87. package/dist/index.d.cts +13 -12
  88. package/dist/index.d.mts +13 -12
  89. package/dist/index.mjs +12 -12
  90. package/dist/testing/ColdStorageTestSuite.cjs +508 -0
  91. package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
  92. package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
  93. package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
  94. package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
  95. package/dist/testing/ColdStorageTestSuite.mjs +508 -0
  96. package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
  97. package/dist/testing/FailingStorage.cjs +135 -0
  98. package/dist/testing/FailingStorage.d.cts +43 -0
  99. package/dist/testing/FailingStorage.d.cts.map +1 -0
  100. package/dist/testing/FailingStorage.d.mts +43 -0
  101. package/dist/testing/FailingStorage.d.mts.map +1 -0
  102. package/dist/testing/FailingStorage.mjs +136 -0
  103. package/dist/testing/FailingStorage.mjs.map +1 -0
  104. package/dist/testing/HotStorageTestSuite.cjs +585 -0
  105. package/dist/testing/HotStorageTestSuite.d.cts +40 -0
  106. package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
  107. package/dist/testing/HotStorageTestSuite.d.mts +40 -0
  108. package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
  109. package/dist/testing/HotStorageTestSuite.mjs +585 -0
  110. package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
  111. package/dist/testing/StorageIntegrationTestSuite.cjs +349 -0
  112. package/dist/testing/StorageIntegrationTestSuite.d.cts +35 -0
  113. package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
  114. package/dist/testing/StorageIntegrationTestSuite.d.mts +35 -0
  115. package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
  116. package/dist/testing/StorageIntegrationTestSuite.mjs +349 -0
  117. package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
  118. package/dist/testing/assertions.cjs +114 -0
  119. package/dist/testing/assertions.mjs +109 -0
  120. package/dist/testing/assertions.mjs.map +1 -0
  121. package/dist/testing/index.cjs +14 -0
  122. package/dist/testing/index.d.cts +6 -0
  123. package/dist/testing/index.d.mts +6 -0
  124. package/dist/testing/index.mjs +7 -0
  125. package/dist/testing/types.cjs +15 -0
  126. package/dist/testing/types.d.cts +90 -0
  127. package/dist/testing/types.d.cts.map +1 -0
  128. package/dist/testing/types.d.mts +90 -0
  129. package/dist/testing/types.d.mts.map +1 -0
  130. package/dist/testing/types.mjs +16 -0
  131. package/dist/testing/types.mjs.map +1 -0
  132. package/package.json +18 -3
  133. package/src/ColdStorage.ts +136 -0
  134. package/src/DocumentManager.ts +550 -190
  135. package/src/Errors.ts +114 -0
  136. package/src/HotStorage.ts +239 -0
  137. package/src/Metrics.ts +187 -0
  138. package/src/MimicAuthService.ts +126 -64
  139. package/src/MimicClusterServerEngine.ts +946 -0
  140. package/src/MimicServer.ts +448 -195
  141. package/src/MimicServerEngine.ts +276 -0
  142. package/src/PresenceManager.ts +169 -240
  143. package/src/Protocol.ts +350 -0
  144. package/src/Types.ts +231 -0
  145. package/src/index.ts +57 -23
  146. package/src/testing/ColdStorageTestSuite.ts +589 -0
  147. package/src/testing/FailingStorage.ts +286 -0
  148. package/src/testing/HotStorageTestSuite.ts +762 -0
  149. package/src/testing/StorageIntegrationTestSuite.ts +504 -0
  150. package/src/testing/assertions.ts +181 -0
  151. package/src/testing/index.ts +83 -0
  152. package/src/testing/types.ts +100 -0
  153. package/tests/ColdStorage.test.ts +24 -0
  154. package/tests/DocumentManager.test.ts +158 -287
  155. package/tests/HotStorage.test.ts +24 -0
  156. package/tests/MimicAuthService.test.ts +102 -134
  157. package/tests/MimicClusterServerEngine.test.ts +587 -0
  158. package/tests/MimicServer.test.ts +90 -226
  159. package/tests/MimicServerEngine.test.ts +521 -0
  160. package/tests/PresenceManager.test.ts +22 -63
  161. package/tests/Protocol.test.ts +190 -0
  162. package/tests/StorageIntegration.test.ts +259 -0
  163. package/tsconfig.json +1 -1
  164. package/tsdown.config.ts +1 -1
  165. package/dist/DocumentProtocol.cjs +0 -94
  166. package/dist/DocumentProtocol.d.cts +0 -113
  167. package/dist/DocumentProtocol.d.cts.map +0 -1
  168. package/dist/DocumentProtocol.d.mts +0 -113
  169. package/dist/DocumentProtocol.d.mts.map +0 -1
  170. package/dist/DocumentProtocol.mjs +0 -89
  171. package/dist/DocumentProtocol.mjs.map +0 -1
  172. package/dist/MimicConfig.cjs +0 -60
  173. package/dist/MimicConfig.d.cts +0 -141
  174. package/dist/MimicConfig.d.cts.map +0 -1
  175. package/dist/MimicConfig.d.mts +0 -141
  176. package/dist/MimicConfig.d.mts.map +0 -1
  177. package/dist/MimicConfig.mjs +0 -50
  178. package/dist/MimicConfig.mjs.map +0 -1
  179. package/dist/MimicDataStorage.cjs +0 -83
  180. package/dist/MimicDataStorage.d.cts +0 -113
  181. package/dist/MimicDataStorage.d.cts.map +0 -1
  182. package/dist/MimicDataStorage.d.mts +0 -113
  183. package/dist/MimicDataStorage.d.mts.map +0 -1
  184. package/dist/MimicDataStorage.mjs +0 -74
  185. package/dist/MimicDataStorage.mjs.map +0 -1
  186. package/dist/WebSocketHandler.cjs +0 -365
  187. package/dist/WebSocketHandler.d.cts +0 -34
  188. package/dist/WebSocketHandler.d.cts.map +0 -1
  189. package/dist/WebSocketHandler.d.mts +0 -34
  190. package/dist/WebSocketHandler.d.mts.map +0 -1
  191. package/dist/WebSocketHandler.mjs +0 -355
  192. package/dist/WebSocketHandler.mjs.map +0 -1
  193. package/dist/auth/NoAuth.cjs +0 -43
  194. package/dist/auth/NoAuth.d.cts +0 -22
  195. package/dist/auth/NoAuth.d.cts.map +0 -1
  196. package/dist/auth/NoAuth.d.mts +0 -22
  197. package/dist/auth/NoAuth.d.mts.map +0 -1
  198. package/dist/auth/NoAuth.mjs +0 -36
  199. package/dist/auth/NoAuth.mjs.map +0 -1
  200. package/dist/errors.cjs +0 -74
  201. package/dist/errors.d.cts +0 -89
  202. package/dist/errors.d.cts.map +0 -1
  203. package/dist/errors.d.mts +0 -89
  204. package/dist/errors.d.mts.map +0 -1
  205. package/dist/errors.mjs +0 -67
  206. package/dist/errors.mjs.map +0 -1
  207. package/dist/storage/InMemoryDataStorage.cjs +0 -57
  208. package/dist/storage/InMemoryDataStorage.d.cts +0 -19
  209. package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
  210. package/dist/storage/InMemoryDataStorage.d.mts +0 -19
  211. package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
  212. package/dist/storage/InMemoryDataStorage.mjs +0 -48
  213. package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
  214. package/src/DocumentProtocol.ts +0 -112
  215. package/src/MimicConfig.ts +0 -211
  216. package/src/MimicDataStorage.ts +0 -157
  217. package/src/WebSocketHandler.ts +0 -735
  218. package/src/auth/NoAuth.ts +0 -46
  219. package/src/errors.ts +0 -113
  220. package/src/storage/InMemoryDataStorage.ts +0 -66
  221. package/tests/DocumentProtocol.test.ts +0 -113
  222. package/tests/InMemoryDataStorage.test.ts +0 -190
  223. package/tests/MimicConfig.test.ts +0 -290
  224. package/tests/MimicDataStorage.test.ts +0 -190
  225. package/tests/NoAuth.test.ts +0 -94
  226. package/tests/WebSocketHandler.test.ts +0 -321
  227. package/tests/errors.test.ts +0 -77
package/src/Errors.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @voidhash/mimic-effect - Error Types
3
+ *
4
+ * All error types used throughout the mimic-effect package.
5
+ */
6
+ import { Data } from "effect";
7
+
8
+ // =============================================================================
9
+ // Storage Errors
10
+ // =============================================================================
11
+
12
+ /**
13
+ * Error when ColdStorage (snapshot storage) operations fail
14
+ */
15
+ export class ColdStorageError extends Data.TaggedError("ColdStorageError")<{
16
+ readonly documentId: string;
17
+ readonly operation: "load" | "save" | "delete";
18
+ readonly cause: unknown;
19
+ }> {}
20
+
21
+ /**
22
+ * Error when HotStorage (WAL storage) operations fail
23
+ */
24
+ export class HotStorageError extends Data.TaggedError("HotStorageError")<{
25
+ readonly documentId: string;
26
+ readonly operation: "append" | "getEntries" | "truncate" | "appendWithCheck";
27
+ readonly cause: unknown;
28
+ }> {}
29
+
30
+ /**
31
+ * Error when WAL append detects a version gap.
32
+ * This indicates either:
33
+ * - A bug in the application (skipped a version)
34
+ * - Concurrent writes to the same document
35
+ * - Data corruption in WAL storage
36
+ */
37
+ export class WalVersionGapError extends Data.TaggedError("WalVersionGapError")<{
38
+ readonly documentId: string;
39
+ readonly expectedVersion: number;
40
+ readonly actualPreviousVersion: number | undefined;
41
+ }> {}
42
+
43
+ // =============================================================================
44
+ // Auth Errors
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Error when authentication fails (invalid token, expired, etc.)
49
+ */
50
+ export class AuthenticationError extends Data.TaggedError(
51
+ "AuthenticationError"
52
+ )<{
53
+ readonly reason: string;
54
+ }> {}
55
+
56
+ /**
57
+ * Error when authorization fails (user doesn't have required permission)
58
+ */
59
+ export class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
60
+ readonly reason: string;
61
+ readonly required: "read" | "write";
62
+ readonly actual: "read" | "write";
63
+ }> {}
64
+
65
+ // =============================================================================
66
+ // Connection Errors
67
+ // =============================================================================
68
+
69
+ /**
70
+ * Error when document ID is missing from WebSocket request path
71
+ */
72
+ export class MissingDocumentIdError extends Data.TaggedError(
73
+ "MissingDocumentIdError"
74
+ )<{
75
+ readonly path: string;
76
+ }> {}
77
+
78
+ /**
79
+ * Error when WebSocket message cannot be parsed
80
+ */
81
+ export class MessageParseError extends Data.TaggedError("MessageParseError")<{
82
+ readonly cause: unknown;
83
+ }> {}
84
+
85
+ // =============================================================================
86
+ // Transaction Errors
87
+ // =============================================================================
88
+
89
+ /**
90
+ * Error when a transaction is rejected by the document
91
+ */
92
+ export class TransactionRejectedError extends Data.TaggedError(
93
+ "TransactionRejectedError"
94
+ )<{
95
+ readonly transactionId: string;
96
+ readonly reason: string;
97
+ }> {}
98
+
99
+ // =============================================================================
100
+ // Union Type
101
+ // =============================================================================
102
+
103
+ /**
104
+ * Union of all mimic-effect errors
105
+ */
106
+ export type MimicError =
107
+ | ColdStorageError
108
+ | HotStorageError
109
+ | WalVersionGapError
110
+ | AuthenticationError
111
+ | AuthorizationError
112
+ | MissingDocumentIdError
113
+ | MessageParseError
114
+ | TransactionRejectedError;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @voidhash/mimic-effect - HotStorage
3
+ *
4
+ * Interface and implementations for Write-Ahead Log (WAL) storage.
5
+ */
6
+ import { Context, Effect, HashMap, Layer, Ref } from "effect";
7
+ import type { WalEntry } from "./Types";
8
+ import { HotStorageError, WalVersionGapError } from "./Errors";
9
+
10
+ // =============================================================================
11
+ // HotStorage Interface
12
+ // =============================================================================
13
+
14
+ /**
15
+ * HotStorage interface for storing Write-Ahead Log entries.
16
+ *
17
+ * This is the "hot" tier of the two-tier storage system.
18
+ * It stores every transaction as a WAL entry for durability between snapshots.
19
+ * WAL entries are small (just the transaction) and writes are append-only.
20
+ */
21
+ export interface HotStorage {
22
+ /**
23
+ * Append a WAL entry for a document.
24
+ */
25
+ readonly append: (
26
+ documentId: string,
27
+ entry: WalEntry
28
+ ) => Effect.Effect<void, HotStorageError>;
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)
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
+ * @returns Effect that fails with WalVersionGapError if gap detected
44
+ */
45
+ readonly appendWithCheck: (
46
+ documentId: string,
47
+ entry: WalEntry,
48
+ expectedVersion: number
49
+ ) => Effect.Effect<void, HotStorageError | WalVersionGapError>;
50
+
51
+ /**
52
+ * Get all WAL entries for a document since a given version.
53
+ * Returns entries with version > sinceVersion, ordered by version.
54
+ */
55
+ readonly getEntries: (
56
+ documentId: string,
57
+ sinceVersion: number
58
+ ) => Effect.Effect<WalEntry[], HotStorageError>;
59
+
60
+ /**
61
+ * Truncate WAL entries up to (and including) a given version.
62
+ * Called after a snapshot is saved to remove entries that are now in the snapshot.
63
+ */
64
+ readonly truncate: (
65
+ documentId: string,
66
+ upToVersion: number
67
+ ) => Effect.Effect<void, HotStorageError>;
68
+ }
69
+
70
+ // =============================================================================
71
+ // Context Tag
72
+ // =============================================================================
73
+
74
+ /**
75
+ * Context tag for HotStorage service
76
+ */
77
+ export class HotStorageTag extends Context.Tag("@voidhash/mimic-effect/HotStorage")<
78
+ HotStorageTag,
79
+ HotStorage
80
+ >() {}
81
+
82
+ // =============================================================================
83
+ // Factory
84
+ // =============================================================================
85
+
86
+ /**
87
+ * Create a HotStorage layer from an Effect that produces a HotStorage service.
88
+ *
89
+ * This allows you to access other Effect services when implementing custom storage.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const Hot = HotStorage.make(
94
+ * Effect.gen(function*() {
95
+ * const redis = yield* RedisService
96
+ *
97
+ * return {
98
+ * append: (documentId, entry) =>
99
+ * redis.rpush(`wal:${documentId}`, JSON.stringify(entry)),
100
+ * getEntries: (documentId, sinceVersion) =>
101
+ * redis.lrange(`wal:${documentId}`, 0, -1).pipe(
102
+ * Effect.map(entries =>
103
+ * entries
104
+ * .map(e => JSON.parse(e))
105
+ * .filter(e => e.version > sinceVersion)
106
+ * .sort((a, b) => a.version - b.version)
107
+ * )
108
+ * ),
109
+ * truncate: (documentId, upToVersion) =>
110
+ * // Implementation depends on Redis data structure
111
+ * Effect.void,
112
+ * }
113
+ * })
114
+ * )
115
+ * ```
116
+ */
117
+ export const make = <E, R>(
118
+ effect: Effect.Effect<HotStorage, E, R>
119
+ ): Layer.Layer<HotStorageTag, E, R> =>
120
+ Layer.effect(HotStorageTag, effect);
121
+
122
+ // =============================================================================
123
+ // InMemory Implementation
124
+ // =============================================================================
125
+
126
+ /**
127
+ * In-memory HotStorage implementation.
128
+ *
129
+ * Useful for testing and development. Not suitable for production
130
+ * as data is lost when the process restarts.
131
+ */
132
+ export namespace InMemory {
133
+ /**
134
+ * Create an in-memory HotStorage layer.
135
+ */
136
+ export const make = (): Layer.Layer<HotStorageTag> =>
137
+ Layer.effect(
138
+ HotStorageTag,
139
+ Effect.gen(function* () {
140
+ const store = yield* Ref.make(HashMap.empty<string, WalEntry[]>());
141
+
142
+ return {
143
+ append: (documentId, entry) =>
144
+ Ref.update(store, (map) => {
145
+ const existing = HashMap.get(map, documentId);
146
+ const entries =
147
+ existing._tag === "Some" ? existing.value : [];
148
+ return HashMap.set(map, documentId, [...entries, entry]);
149
+ }),
150
+
151
+ appendWithCheck: (documentId, entry, expectedVersion) =>
152
+ Effect.gen(function* () {
153
+ type CheckResult =
154
+ | { type: "ok" }
155
+ | { type: "gap"; lastVersion: number | undefined };
156
+
157
+ // Use Ref.modify for atomic check + update
158
+ const result: CheckResult = yield* Ref.modify(store, (map): [CheckResult, HashMap.HashMap<string, WalEntry[]>] => {
159
+ const existing = HashMap.get(map, documentId);
160
+ const entries = existing._tag === "Some" ? existing.value : [];
161
+
162
+ // Find the highest version in existing entries
163
+ const lastVersion = entries.length > 0
164
+ ? Math.max(...entries.map((e) => e.version))
165
+ : 0;
166
+
167
+ // Gap check
168
+ if (expectedVersion === 1) {
169
+ // First entry: should have no entries with version >= 1
170
+ if (lastVersion >= 1) {
171
+ return [
172
+ { type: "gap", lastVersion },
173
+ map,
174
+ ];
175
+ }
176
+ } else {
177
+ // Not first: last entry should have version = expectedVersion - 1
178
+ if (lastVersion !== expectedVersion - 1) {
179
+ return [
180
+ { type: "gap", lastVersion: lastVersion > 0 ? lastVersion : undefined },
181
+ map,
182
+ ];
183
+ }
184
+ }
185
+
186
+ // No gap: append and return success
187
+ return [
188
+ { type: "ok" },
189
+ HashMap.set(map, documentId, [...entries, entry]),
190
+ ];
191
+ });
192
+
193
+ if (result.type === "gap") {
194
+ return yield* Effect.fail(
195
+ new WalVersionGapError({
196
+ documentId,
197
+ expectedVersion,
198
+ actualPreviousVersion: result.lastVersion,
199
+ })
200
+ );
201
+ }
202
+ }),
203
+
204
+ getEntries: (documentId, sinceVersion) =>
205
+ Effect.gen(function* () {
206
+ const current = yield* Ref.get(store);
207
+ const existing = HashMap.get(current, documentId);
208
+ const entries =
209
+ existing._tag === "Some" ? existing.value : [];
210
+ return entries
211
+ .filter((e) => e.version > sinceVersion)
212
+ .sort((a, b) => a.version - b.version);
213
+ }),
214
+
215
+ truncate: (documentId, upToVersion) =>
216
+ Ref.update(store, (map) => {
217
+ const existing = HashMap.get(map, documentId);
218
+ if (existing._tag === "None") {
219
+ return map;
220
+ }
221
+ const filtered = existing.value.filter(
222
+ (e) => e.version > upToVersion
223
+ );
224
+ return HashMap.set(map, documentId, filtered);
225
+ }),
226
+ };
227
+ })
228
+ );
229
+ }
230
+
231
+ // =============================================================================
232
+ // Re-export namespace
233
+ // =============================================================================
234
+
235
+ export const HotStorage = {
236
+ Tag: HotStorageTag,
237
+ make,
238
+ InMemory,
239
+ };
package/src/Metrics.ts ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @voidhash/mimic-effect - Metrics
3
+ *
4
+ * Observability metrics using Effect's Metric API.
5
+ */
6
+ import { Metric, MetricBoundaries } from "effect";
7
+
8
+ // =============================================================================
9
+ // Connection Metrics
10
+ // =============================================================================
11
+
12
+ /**
13
+ * Current active WebSocket connections
14
+ */
15
+ export const connectionsActive = Metric.gauge("mimic.connections.active");
16
+
17
+ /**
18
+ * Total connections over lifetime
19
+ */
20
+ export const connectionsTotal = Metric.counter("mimic.connections.total");
21
+
22
+ /**
23
+ * Connection duration histogram (milliseconds)
24
+ */
25
+ export const connectionsDuration = Metric.histogram(
26
+ "mimic.connections.duration_ms",
27
+ MetricBoundaries.exponential({
28
+ start: 100,
29
+ factor: 2,
30
+ count: 15, // Up to ~3.2 million ms (~53 minutes)
31
+ })
32
+ );
33
+
34
+ /**
35
+ * Connection errors (auth failures, etc.)
36
+ */
37
+ export const connectionsErrors = Metric.counter("mimic.connections.errors");
38
+
39
+ // =============================================================================
40
+ // Document Metrics
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Documents currently in memory
45
+ */
46
+ export const documentsActive = Metric.gauge("mimic.documents.active");
47
+
48
+ /**
49
+ * New documents created
50
+ */
51
+ export const documentsCreated = Metric.counter("mimic.documents.created");
52
+
53
+ /**
54
+ * Documents restored from storage
55
+ */
56
+ export const documentsRestored = Metric.counter("mimic.documents.restored");
57
+
58
+ /**
59
+ * Documents garbage collected (evicted)
60
+ */
61
+ export const documentsEvicted = Metric.counter("mimic.documents.evicted");
62
+
63
+ // =============================================================================
64
+ // Transaction Metrics
65
+ // =============================================================================
66
+
67
+ /**
68
+ * Successfully processed transactions
69
+ */
70
+ export const transactionsProcessed = Metric.counter(
71
+ "mimic.transactions.processed"
72
+ );
73
+
74
+ /**
75
+ * Rejected transactions
76
+ */
77
+ export const transactionsRejected = Metric.counter(
78
+ "mimic.transactions.rejected"
79
+ );
80
+
81
+ /**
82
+ * Transaction processing latency histogram (milliseconds)
83
+ */
84
+ export const transactionsLatency = Metric.histogram(
85
+ "mimic.transactions.latency_ms",
86
+ MetricBoundaries.exponential({
87
+ start: 0.1,
88
+ factor: 2,
89
+ count: 15, // Up to ~1638 ms
90
+ })
91
+ );
92
+
93
+ // =============================================================================
94
+ // Storage Metrics
95
+ // =============================================================================
96
+
97
+ /**
98
+ * Snapshots saved to ColdStorage
99
+ */
100
+ export const storageSnapshots = Metric.counter("mimic.storage.snapshots");
101
+
102
+ /**
103
+ * Snapshot save duration histogram (milliseconds)
104
+ */
105
+ export const storageSnapshotLatency = Metric.histogram(
106
+ "mimic.storage.snapshot_latency_ms",
107
+ MetricBoundaries.exponential({
108
+ start: 1,
109
+ factor: 2,
110
+ count: 12, // Up to ~4 seconds
111
+ })
112
+ );
113
+
114
+ /**
115
+ * WAL entries written to HotStorage
116
+ */
117
+ export const storageWalAppends = Metric.counter("mimic.storage.wal_appends");
118
+
119
+ /**
120
+ * Version gaps detected during WAL replay
121
+ */
122
+ export const storageVersionGaps = Metric.counter("mimic.storage.version_gaps");
123
+
124
+ /**
125
+ * Failed WAL appends causing transaction rollback
126
+ */
127
+ export const walAppendFailures = Metric.counter("mimic.storage.wal_append_failures");
128
+
129
+ /**
130
+ * ColdStorage load failures during document restore
131
+ */
132
+ export const coldStorageLoadFailures = Metric.counter("mimic.storage.cold_load_failures");
133
+
134
+ /**
135
+ * HotStorage getEntries failures during document restore
136
+ */
137
+ export const hotStorageLoadFailures = Metric.counter("mimic.storage.hot_load_failures");
138
+
139
+ // =============================================================================
140
+ // Presence Metrics
141
+ // =============================================================================
142
+
143
+ /**
144
+ * Presence set operations
145
+ */
146
+ export const presenceUpdates = Metric.counter("mimic.presence.updates");
147
+
148
+ /**
149
+ * Active presence entries
150
+ */
151
+ export const presenceActive = Metric.gauge("mimic.presence.active");
152
+
153
+ // =============================================================================
154
+ // Export namespace
155
+ // =============================================================================
156
+
157
+ export const MimicMetrics = {
158
+ // Connection
159
+ connectionsActive,
160
+ connectionsTotal,
161
+ connectionsDuration,
162
+ connectionsErrors,
163
+
164
+ // Document
165
+ documentsActive,
166
+ documentsCreated,
167
+ documentsRestored,
168
+ documentsEvicted,
169
+
170
+ // Transaction
171
+ transactionsProcessed,
172
+ transactionsRejected,
173
+ transactionsLatency,
174
+
175
+ // Storage
176
+ storageSnapshots,
177
+ storageSnapshotLatency,
178
+ storageWalAppends,
179
+ storageVersionGaps,
180
+ walAppendFailures,
181
+ coldStorageLoadFailures,
182
+ hotStorageLoadFailures,
183
+
184
+ // Presence
185
+ presenceUpdates,
186
+ presenceActive,
187
+ };