@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
@@ -0,0 +1,946 @@
1
+ /**
2
+ * @voidhash/mimic-effect - MimicClusterServerEngine
3
+ *
4
+ * Clustered document management service using Effect Cluster for horizontal scaling.
5
+ * Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.
6
+ *
7
+ * This is an alternative to MimicServerEngine for distributed deployments.
8
+ */
9
+ import {
10
+ Context,
11
+ Duration,
12
+ Effect,
13
+ HashMap,
14
+ Layer,
15
+ Metric,
16
+ Option,
17
+ PubSub,
18
+ Ref,
19
+ Schema,
20
+ Scope,
21
+ Stream,
22
+ } from "effect";
23
+ import { Entity, Sharding } from "@effect/cluster";
24
+ import { Rpc } from "@effect/rpc";
25
+ import { Document, type Presence, type Primitive, type Transaction } from "@voidhash/mimic";
26
+ import { ServerDocument } from "@voidhash/mimic/server";
27
+ import type {
28
+ MimicClusterServerEngineConfig,
29
+ PresenceEntry,
30
+ PresenceEvent,
31
+ PresenceSnapshot,
32
+ ResolvedClusterConfig,
33
+ StoredDocument,
34
+ WalEntry,
35
+ } from "./Types";
36
+ import type * as Protocol from "./Protocol";
37
+ import { ColdStorageTag, type ColdStorage } from "./ColdStorage";
38
+ import { HotStorageTag, type HotStorage } from "./HotStorage";
39
+ import { MimicAuthServiceTag } from "./MimicAuthService";
40
+ import { MimicServerEngineTag, type MimicServerEngine } from "./MimicServerEngine";
41
+ import type { SubmitResult } from "./DocumentManager";
42
+ import * as Metrics from "./Metrics";
43
+
44
+ // =============================================================================
45
+ // Default Configuration
46
+ // =============================================================================
47
+
48
+ const DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);
49
+ const DEFAULT_MAX_TRANSACTION_HISTORY = 1000;
50
+ const DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);
51
+ const DEFAULT_SNAPSHOT_THRESHOLD = 100;
52
+ const DEFAULT_SHARD_GROUP = "mimic-documents";
53
+
54
+ // =============================================================================
55
+ // RPC Schemas
56
+ // =============================================================================
57
+
58
+ /**
59
+ * Schema for encoded transaction (wire format)
60
+ */
61
+ const EncodedTransactionSchema = Schema.Struct({
62
+ id: Schema.String,
63
+ ops: Schema.Array(Schema.Unknown),
64
+ });
65
+
66
+ /**
67
+ * Schema for submit result
68
+ */
69
+ const SubmitResultSchema = Schema.Union(
70
+ Schema.Struct({
71
+ success: Schema.Literal(true),
72
+ version: Schema.Number,
73
+ }),
74
+ Schema.Struct({
75
+ success: Schema.Literal(false),
76
+ reason: Schema.String,
77
+ })
78
+ );
79
+
80
+ /**
81
+ * Schema for snapshot response
82
+ */
83
+ const SnapshotResponseSchema = Schema.Struct({
84
+ state: Schema.Unknown,
85
+ version: Schema.Number,
86
+ });
87
+
88
+ /**
89
+ * Schema for presence entry
90
+ */
91
+ const PresenceEntrySchema = Schema.Struct({
92
+ data: Schema.Unknown,
93
+ userId: Schema.optional(Schema.String),
94
+ });
95
+
96
+ /**
97
+ * Schema for presence snapshot response
98
+ */
99
+ const PresenceSnapshotResponseSchema = Schema.Struct({
100
+ presences: Schema.Record({ key: Schema.String, value: PresenceEntrySchema }),
101
+ });
102
+
103
+ /**
104
+ * Schema for presence event
105
+ */
106
+ const PresenceEventSchema = Schema.Union(
107
+ Schema.Struct({
108
+ type: Schema.Literal("presence_update"),
109
+ id: Schema.String,
110
+ data: Schema.Unknown,
111
+ userId: Schema.optional(Schema.String),
112
+ }),
113
+ Schema.Struct({
114
+ type: Schema.Literal("presence_remove"),
115
+ id: Schema.String,
116
+ })
117
+ );
118
+
119
+ /**
120
+ * Schema for server message (for broadcasts)
121
+ */
122
+ const ServerMessageSchema = Schema.Unknown;
123
+
124
+ // =============================================================================
125
+ // Mimic Document Entity Definition
126
+ // =============================================================================
127
+
128
+ /**
129
+ * Define the Mimic Document Entity with its RPC protocol.
130
+ * This entity handles document operations for a single documentId.
131
+ */
132
+ const MimicDocumentEntity = Entity.make("MimicDocument", [
133
+ // Submit a transaction
134
+ Rpc.make("Submit", {
135
+ payload: { transaction: EncodedTransactionSchema },
136
+ success: SubmitResultSchema,
137
+ }),
138
+
139
+ // Get document snapshot
140
+ Rpc.make("GetSnapshot", {
141
+ success: SnapshotResponseSchema,
142
+ }),
143
+
144
+ // Touch document to prevent idle GC
145
+ Rpc.make("Touch", {
146
+ success: Schema.Void,
147
+ }),
148
+
149
+ // Set presence for a connection
150
+ Rpc.make("SetPresence", {
151
+ payload: {
152
+ connectionId: Schema.String,
153
+ entry: PresenceEntrySchema,
154
+ },
155
+ success: Schema.Void,
156
+ }),
157
+
158
+ // Remove presence for a connection
159
+ Rpc.make("RemovePresence", {
160
+ payload: { connectionId: Schema.String },
161
+ success: Schema.Void,
162
+ }),
163
+
164
+ // Get presence snapshot
165
+ Rpc.make("GetPresenceSnapshot", {
166
+ success: PresenceSnapshotResponseSchema,
167
+ }),
168
+ ]);
169
+
170
+ // =============================================================================
171
+ // Entity State Types
172
+ // =============================================================================
173
+
174
+ /**
175
+ * Document state managed by the entity
176
+ */
177
+ interface EntityDocumentState<TSchema extends Primitive.AnyPrimitive> {
178
+ readonly document: ServerDocument.ServerDocument<TSchema>;
179
+ readonly broadcastPubSub: PubSub.PubSub<Protocol.ServerMessage>;
180
+ readonly presences: HashMap.HashMap<string, PresenceEntry>;
181
+ readonly presencePubSub: PubSub.PubSub<PresenceEvent>;
182
+ readonly lastSnapshotVersion: number;
183
+ readonly lastSnapshotTime: number;
184
+ readonly transactionsSinceSnapshot: number;
185
+ }
186
+
187
+ // =============================================================================
188
+ // Config Context Tag
189
+ // =============================================================================
190
+
191
+ /**
192
+ * Context tag for cluster engine configuration
193
+ */
194
+ class MimicClusterConfigTag extends Context.Tag(
195
+ "@voidhash/mimic-effect/MimicClusterConfig"
196
+ )<MimicClusterConfigTag, ResolvedClusterConfig<Primitive.AnyPrimitive>>() {}
197
+
198
+ // =============================================================================
199
+ // Resolve Configuration
200
+ // =============================================================================
201
+
202
+ const resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(
203
+ config: MimicClusterServerEngineConfig<TSchema>
204
+ ): ResolvedClusterConfig<TSchema> => ({
205
+ schema: config.schema,
206
+ initial: config.initial,
207
+ presence: config.presence,
208
+ maxIdleTime: config.maxIdleTime
209
+ ? Duration.decode(config.maxIdleTime)
210
+ : DEFAULT_MAX_IDLE_TIME,
211
+ maxTransactionHistory:
212
+ config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,
213
+ snapshot: {
214
+ interval: config.snapshot?.interval
215
+ ? Duration.decode(config.snapshot.interval)
216
+ : DEFAULT_SNAPSHOT_INTERVAL,
217
+ transactionThreshold:
218
+ config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,
219
+ },
220
+ shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,
221
+ });
222
+
223
+ // =============================================================================
224
+ // Helper to decode/encode transactions
225
+ // =============================================================================
226
+
227
+ /**
228
+ * Decode an encoded transaction to a Transaction object
229
+ */
230
+ const decodeTransaction = (
231
+ encoded: { id: string; ops: readonly unknown[] }
232
+ ): Transaction.Transaction => {
233
+ // Import Transaction dynamically to avoid circular deps
234
+ const { Transaction } = require("@voidhash/mimic");
235
+ return Transaction.decode(encoded as Transaction.EncodedTransaction);
236
+ };
237
+
238
+ /**
239
+ * Encode a Transaction to wire format
240
+ */
241
+ const encodeTransaction = (
242
+ tx: Transaction.Transaction
243
+ ): { id: string; ops: readonly unknown[] } => {
244
+ const { Transaction } = require("@voidhash/mimic");
245
+ return Transaction.encode(tx);
246
+ };
247
+
248
+ // =============================================================================
249
+ // Entity Handler Factory
250
+ // =============================================================================
251
+
252
+ /**
253
+ * Create the entity handler for MimicDocument
254
+ */
255
+ const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
256
+ config: ResolvedClusterConfig<TSchema>,
257
+ coldStorage: ColdStorage,
258
+ hotStorage: HotStorage
259
+ ) =>
260
+ Effect.gen(function* () {
261
+ // Get entity address to determine documentId
262
+ const address = yield* Entity.CurrentAddress;
263
+ const documentId = address.entityId;
264
+
265
+ // Current schema version (hard-coded to 1 for now)
266
+ const SCHEMA_VERSION = 1;
267
+
268
+ // Compute initial state
269
+ const computeInitialState = (): Effect.Effect<
270
+ Primitive.InferSetInput<TSchema> | undefined
271
+ > => {
272
+ if (config.initial === undefined) {
273
+ return Effect.succeed(undefined);
274
+ }
275
+
276
+ if (typeof config.initial === "function") {
277
+ return (
278
+ config.initial as (ctx: {
279
+ documentId: string;
280
+ }) => Effect.Effect<Primitive.InferSetInput<TSchema>>
281
+ )({ documentId });
282
+ }
283
+
284
+ return Effect.succeed(
285
+ config.initial as Primitive.InferSetInput<TSchema>
286
+ );
287
+ };
288
+
289
+ // Load snapshot from ColdStorage (fatal if unavailable - entity cannot start)
290
+ const storedDoc = yield* coldStorage.load(documentId).pipe(
291
+ Effect.orDie // Entity cannot initialize without storage
292
+ );
293
+
294
+ let initialState: Primitive.InferSetInput<TSchema> | undefined;
295
+ let initialVersion = 0;
296
+
297
+ if (storedDoc) {
298
+ initialState =
299
+ storedDoc.state as Primitive.InferSetInput<TSchema>;
300
+ initialVersion = storedDoc.version;
301
+ } else {
302
+ initialState = yield* computeInitialState();
303
+ }
304
+
305
+ // Create PubSubs for broadcasting
306
+ const broadcastPubSub = yield* PubSub.unbounded<Protocol.ServerMessage>();
307
+ const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();
308
+
309
+ // Create state ref
310
+ const stateRef = yield* Ref.make<EntityDocumentState<TSchema>>({
311
+ document: undefined as unknown as ServerDocument.ServerDocument<TSchema>,
312
+ broadcastPubSub,
313
+ presences: HashMap.empty(),
314
+ presencePubSub,
315
+ lastSnapshotVersion: initialVersion,
316
+ lastSnapshotTime: Date.now(),
317
+ transactionsSinceSnapshot: 0,
318
+ });
319
+
320
+ // Create ServerDocument with callbacks
321
+ const document = ServerDocument.make({
322
+ schema: config.schema,
323
+ initialState,
324
+ initialVersion,
325
+ maxTransactionHistory: config.maxTransactionHistory,
326
+ onBroadcast: (message: ServerDocument.TransactionMessage) => {
327
+ Effect.runSync(
328
+ PubSub.publish(broadcastPubSub, {
329
+ type: "transaction",
330
+ transaction: message.transaction,
331
+ version: message.version,
332
+ } as Protocol.ServerMessage)
333
+ );
334
+ },
335
+ onRejection: (transactionId: string, reason: string) => {
336
+ Effect.runSync(
337
+ PubSub.publish(broadcastPubSub, {
338
+ type: "error",
339
+ transactionId,
340
+ reason,
341
+ } as Protocol.ServerMessage)
342
+ );
343
+ },
344
+ });
345
+
346
+ // Update state with document
347
+ yield* Ref.update(stateRef, (s) => ({ ...s, document }));
348
+
349
+ // Load WAL entries (fatal if unavailable - entity cannot start)
350
+ const walEntries = yield* hotStorage.getEntries(documentId, initialVersion).pipe(
351
+ Effect.orDie // Entity cannot initialize without storage
352
+ );
353
+
354
+ // Verify WAL continuity (warning only, non-blocking)
355
+ if (walEntries.length > 0) {
356
+ const firstWalVersion = walEntries[0]!.version;
357
+ const expectedFirst = initialVersion + 1;
358
+
359
+ if (firstWalVersion !== expectedFirst) {
360
+ yield* Effect.logWarning("WAL version gap detected", {
361
+ documentId,
362
+ snapshotVersion: initialVersion,
363
+ firstWalVersion,
364
+ expectedFirst,
365
+ });
366
+ yield* Metric.increment(Metrics.storageVersionGaps);
367
+ }
368
+
369
+ // Check internal gaps
370
+ for (let i = 1; i < walEntries.length; i++) {
371
+ const prev = walEntries[i - 1]!.version;
372
+ const curr = walEntries[i]!.version;
373
+ if (curr !== prev + 1) {
374
+ yield* Effect.logWarning("WAL internal gap detected", {
375
+ documentId,
376
+ previousVersion: prev,
377
+ currentVersion: curr,
378
+ });
379
+ }
380
+ }
381
+ }
382
+
383
+ // Replay WAL entries
384
+ for (const entry of walEntries) {
385
+ const result = document.submit(entry.transaction);
386
+ if (!result.success) {
387
+ yield* Effect.logWarning("Skipping corrupted WAL entry", {
388
+ documentId,
389
+ version: entry.version,
390
+ reason: result.reason,
391
+ });
392
+ }
393
+ }
394
+
395
+ // Track metrics
396
+ if (storedDoc) {
397
+ yield* Metric.increment(Metrics.documentsRestored);
398
+ } else {
399
+ yield* Metric.increment(Metrics.documentsCreated);
400
+ }
401
+ yield* Metric.incrementBy(Metrics.documentsActive, 1);
402
+
403
+ /**
404
+ * Save snapshot to ColdStorage derived from WAL entries.
405
+ * This ensures snapshots are always based on durable WAL data.
406
+ * Idempotent: skips save if already snapshotted at target version.
407
+ * Truncate failures are non-fatal and will be retried on next snapshot.
408
+ */
409
+ const saveSnapshot = (targetVersion: number) => Effect.gen(function* () {
410
+ const state = yield* Ref.get(stateRef);
411
+
412
+ // Idempotency check: skip if already snapshotted at this version
413
+ if (targetVersion <= state.lastSnapshotVersion) {
414
+ return;
415
+ }
416
+
417
+ const snapshotStartTime = Date.now();
418
+
419
+ // Load base snapshot from cold storage (best effort - log error but don't crash entity)
420
+ const baseSnapshotResult = yield* Effect.either(coldStorage.load(documentId));
421
+ if (baseSnapshotResult._tag === "Left") {
422
+ yield* Effect.logError("Failed to load base snapshot for WAL replay", {
423
+ documentId,
424
+ error: baseSnapshotResult.left,
425
+ });
426
+ return;
427
+ }
428
+ const baseSnapshot = baseSnapshotResult.right;
429
+ const baseVersion = baseSnapshot?.version ?? 0;
430
+ const baseState = baseSnapshot?.state as Primitive.InferState<TSchema> | undefined;
431
+
432
+ // Load WAL entries from base to target
433
+ const walEntriesResult = yield* Effect.either(hotStorage.getEntries(documentId, baseVersion));
434
+ if (walEntriesResult._tag === "Left") {
435
+ yield* Effect.logError("Failed to load WAL entries for snapshot", {
436
+ documentId,
437
+ error: walEntriesResult.left,
438
+ });
439
+ return;
440
+ }
441
+ const walEntries = walEntriesResult.right;
442
+ const relevantEntries = walEntries.filter(e => e.version <= targetVersion);
443
+
444
+ if (relevantEntries.length === 0 && !baseSnapshot) {
445
+ // Nothing to snapshot
446
+ return;
447
+ }
448
+
449
+ // Rebuild state by replaying WAL on base
450
+ let snapshotState: Primitive.InferState<TSchema> | undefined = baseState;
451
+ for (const entry of relevantEntries) {
452
+ // Create a temporary document to apply the transaction
453
+ const tempDoc = Document.make(config.schema, { initialState: snapshotState });
454
+ tempDoc.apply(entry.transaction.ops);
455
+ snapshotState = tempDoc.get();
456
+ }
457
+
458
+ if (snapshotState === undefined) {
459
+ return;
460
+ }
461
+
462
+ const snapshotVersion = relevantEntries.length > 0
463
+ ? relevantEntries[relevantEntries.length - 1]!.version
464
+ : baseVersion;
465
+
466
+ // Re-check before saving (in case another snapshot completed while we were working)
467
+ // This prevents a slower snapshot from overwriting a more recent one
468
+ const currentState = yield* Ref.get(stateRef);
469
+ if (snapshotVersion <= currentState.lastSnapshotVersion) {
470
+ return;
471
+ }
472
+
473
+ const storedDocument: StoredDocument = {
474
+ state: snapshotState,
475
+ version: snapshotVersion,
476
+ schemaVersion: SCHEMA_VERSION,
477
+ savedAt: Date.now(),
478
+ };
479
+
480
+ // Save to ColdStorage (best effort - log error but don't crash entity)
481
+ yield* Effect.catchAll(
482
+ coldStorage.save(documentId, storedDocument),
483
+ (e) =>
484
+ Effect.logError("Failed to save snapshot", { documentId, error: e })
485
+ );
486
+
487
+ const snapshotDuration = Date.now() - snapshotStartTime;
488
+ yield* Metric.increment(Metrics.storageSnapshots);
489
+ yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);
490
+
491
+ // Update tracking BEFORE truncate (for idempotency on retry)
492
+ yield* Ref.update(stateRef, (s) => ({
493
+ ...s,
494
+ lastSnapshotVersion: snapshotVersion,
495
+ lastSnapshotTime: Date.now(),
496
+ transactionsSinceSnapshot: 0,
497
+ }));
498
+
499
+ // Truncate WAL - non-fatal, will be retried on next snapshot
500
+ yield* Effect.catchAll(hotStorage.truncate(documentId, snapshotVersion), (e) =>
501
+ Effect.logWarning("WAL truncate failed - will retry on next snapshot", {
502
+ documentId,
503
+ version: snapshotVersion,
504
+ error: e,
505
+ })
506
+ );
507
+ });
508
+
509
+ /**
510
+ * Check if snapshot should be triggered
511
+ */
512
+ const checkSnapshotTriggers = Effect.gen(function* () {
513
+ const state = yield* Ref.get(stateRef);
514
+ const now = Date.now();
515
+ const currentVersion = state.document.getVersion();
516
+
517
+ const intervalMs = Duration.toMillis(config.snapshot.interval);
518
+ const threshold = config.snapshot.transactionThreshold;
519
+
520
+ if (state.transactionsSinceSnapshot >= threshold) {
521
+ yield* saveSnapshot(currentVersion);
522
+ return;
523
+ }
524
+
525
+ if (now - state.lastSnapshotTime >= intervalMs) {
526
+ yield* saveSnapshot(currentVersion);
527
+ return;
528
+ }
529
+ });
530
+
531
+ // Cleanup on entity finalization
532
+ yield* Effect.addFinalizer(() =>
533
+ Effect.gen(function* () {
534
+ // Save final snapshot before entity is garbage collected
535
+ const state = yield* Ref.get(stateRef);
536
+ const currentVersion = state.document.getVersion();
537
+ yield* saveSnapshot(currentVersion);
538
+ yield* Metric.incrementBy(Metrics.documentsActive, -1);
539
+ yield* Metric.increment(Metrics.documentsEvicted);
540
+ yield* Effect.logDebug("Entity finalized", { documentId });
541
+ })
542
+ );
543
+
544
+ // Return RPC handlers
545
+ return {
546
+ Submit: Effect.fnUntraced(function* ({ payload }) {
547
+ const submitStartTime = Date.now();
548
+ const state = yield* Ref.get(stateRef);
549
+
550
+ // Decode transaction
551
+ const transaction = decodeTransaction(payload.transaction);
552
+
553
+ // Phase 1: Validate (no side effects)
554
+ const validation = state.document.validate(transaction);
555
+
556
+ if (!validation.valid) {
557
+ // Track rejection
558
+ yield* Metric.increment(Metrics.transactionsRejected);
559
+ const latency = Date.now() - submitStartTime;
560
+ yield* Metric.update(Metrics.transactionsLatency, latency);
561
+
562
+ return {
563
+ success: false as const,
564
+ reason: validation.reason,
565
+ };
566
+ }
567
+
568
+ // Phase 2: Append to WAL with gap check (BEFORE state mutation)
569
+ const walEntry: WalEntry = {
570
+ transaction,
571
+ version: validation.nextVersion,
572
+ timestamp: Date.now(),
573
+ };
574
+
575
+ const appendResult = yield* Effect.either(
576
+ hotStorage.appendWithCheck(documentId, walEntry, validation.nextVersion)
577
+ );
578
+
579
+ if (appendResult._tag === "Left") {
580
+ // WAL append failed - do NOT apply, state unchanged
581
+ yield* Effect.logError("WAL append failed", {
582
+ documentId,
583
+ version: validation.nextVersion,
584
+ error: appendResult.left,
585
+ });
586
+ yield* Metric.increment(Metrics.walAppendFailures);
587
+
588
+ const latency = Date.now() - submitStartTime;
589
+ yield* Metric.update(Metrics.transactionsLatency, latency);
590
+
591
+ // Return failure - client must retry
592
+ return {
593
+ success: false as const,
594
+ reason: "Storage unavailable. Please retry.",
595
+ };
596
+ }
597
+
598
+ // Phase 3: Apply (state mutation + broadcast)
599
+ state.document.apply(transaction);
600
+
601
+ // Track metrics
602
+ const latency = Date.now() - submitStartTime;
603
+ yield* Metric.update(Metrics.transactionsLatency, latency);
604
+ yield* Metric.increment(Metrics.transactionsProcessed);
605
+ yield* Metric.increment(Metrics.storageWalAppends);
606
+
607
+ // Increment transaction count
608
+ yield* Ref.update(stateRef, (s) => ({
609
+ ...s,
610
+ transactionsSinceSnapshot: s.transactionsSinceSnapshot + 1,
611
+ }));
612
+
613
+ // Check snapshot triggers
614
+ yield* checkSnapshotTriggers;
615
+
616
+ return {
617
+ success: true as const,
618
+ version: validation.nextVersion,
619
+ };
620
+ }),
621
+
622
+ GetSnapshot: Effect.fnUntraced(function* () {
623
+ const state = yield* Ref.get(stateRef);
624
+ return state.document.getSnapshot();
625
+ }),
626
+
627
+ Touch: Effect.fnUntraced(function* () {
628
+ // Entity touch is handled automatically by the cluster framework
629
+ // Just update last activity time conceptually
630
+ return void 0;
631
+ }),
632
+
633
+ SetPresence: Effect.fnUntraced(function* ({ payload }) {
634
+ const { connectionId, entry } = payload;
635
+
636
+ yield* Ref.update(stateRef, (s) => ({
637
+ ...s,
638
+ presences: HashMap.set(s.presences, connectionId, entry),
639
+ }));
640
+
641
+ yield* Metric.increment(Metrics.presenceUpdates);
642
+ yield* Metric.incrementBy(Metrics.presenceActive, 1);
643
+
644
+ const state = yield* Ref.get(stateRef);
645
+ const event: PresenceEvent = {
646
+ type: "presence_update",
647
+ id: connectionId,
648
+ data: entry.data,
649
+ userId: entry.userId,
650
+ };
651
+ yield* PubSub.publish(state.presencePubSub, event);
652
+ }),
653
+
654
+ RemovePresence: Effect.fnUntraced(function* ({ payload }) {
655
+ const { connectionId } = payload;
656
+ const state = yield* Ref.get(stateRef);
657
+
658
+ if (!HashMap.has(state.presences, connectionId)) {
659
+ return;
660
+ }
661
+
662
+ yield* Ref.update(stateRef, (s) => ({
663
+ ...s,
664
+ presences: HashMap.remove(s.presences, connectionId),
665
+ }));
666
+
667
+ yield* Metric.incrementBy(Metrics.presenceActive, -1);
668
+
669
+ const event: PresenceEvent = {
670
+ type: "presence_remove",
671
+ id: connectionId,
672
+ };
673
+ yield* PubSub.publish(state.presencePubSub, event);
674
+ }),
675
+
676
+ GetPresenceSnapshot: Effect.fnUntraced(function* () {
677
+ const state = yield* Ref.get(stateRef);
678
+ const presences: Record<string, PresenceEntry> = {};
679
+ for (const [id, entry] of state.presences) {
680
+ presences[id] = entry;
681
+ }
682
+ return { presences };
683
+ }),
684
+ };
685
+ });
686
+
687
+ // =============================================================================
688
+ // Subscription Store (for managing subscriptions at the gateway level)
689
+ // =============================================================================
690
+
691
+ /**
692
+ * Store for managing document subscriptions
693
+ * This is needed because cluster entities don't support streaming directly
694
+ */
695
+ interface SubscriptionStore {
696
+ readonly getOrCreatePubSub: (
697
+ documentId: string
698
+ ) => Effect.Effect<PubSub.PubSub<Protocol.ServerMessage>>;
699
+ readonly getOrCreatePresencePubSub: (
700
+ documentId: string
701
+ ) => Effect.Effect<PubSub.PubSub<PresenceEvent>>;
702
+ }
703
+
704
+ class SubscriptionStoreTag extends Context.Tag(
705
+ "@voidhash/mimic-effect/SubscriptionStore"
706
+ )<SubscriptionStoreTag, SubscriptionStore>() {}
707
+
708
+ const subscriptionStoreLayer = Layer.effect(
709
+ SubscriptionStoreTag,
710
+ Effect.gen(function* () {
711
+ const documentPubSubs = yield* Ref.make(
712
+ HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()
713
+ );
714
+ const presencePubSubs = yield* Ref.make(
715
+ HashMap.empty<string, PubSub.PubSub<PresenceEvent>>()
716
+ );
717
+
718
+ return {
719
+ getOrCreatePubSub: (documentId: string) =>
720
+ Effect.gen(function* () {
721
+ const current = yield* Ref.get(documentPubSubs);
722
+ const existing = HashMap.get(current, documentId);
723
+ if (existing._tag === "Some") {
724
+ return existing.value;
725
+ }
726
+
727
+ const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();
728
+ yield* Ref.update(documentPubSubs, (map) =>
729
+ HashMap.set(map, documentId, pubsub)
730
+ );
731
+ return pubsub;
732
+ }),
733
+
734
+ getOrCreatePresencePubSub: (documentId: string) =>
735
+ Effect.gen(function* () {
736
+ const current = yield* Ref.get(presencePubSubs);
737
+ const existing = HashMap.get(current, documentId);
738
+ if (existing._tag === "Some") {
739
+ return existing.value;
740
+ }
741
+
742
+ const pubsub = yield* PubSub.unbounded<PresenceEvent>();
743
+ yield* Ref.update(presencePubSubs, (map) =>
744
+ HashMap.set(map, documentId, pubsub)
745
+ );
746
+ return pubsub;
747
+ }),
748
+ };
749
+ })
750
+ );
751
+
752
+ // =============================================================================
753
+ // Factory
754
+ // =============================================================================
755
+
756
+ /**
757
+ * Create a MimicClusterServerEngine layer.
758
+ *
759
+ * This creates a clustered document management service using Effect Cluster.
760
+ * Each document becomes a cluster Entity with automatic sharding and failover.
761
+ *
762
+ * @example
763
+ * ```typescript
764
+ * // 1. Create the engine
765
+ * const Engine = MimicClusterServerEngine.make({
766
+ * schema: DocSchema,
767
+ * initial: { title: "Untitled" },
768
+ * presence: CursorPresence,
769
+ * maxIdleTime: "5 minutes",
770
+ * snapshot: { interval: "5 minutes", transactionThreshold: 100 },
771
+ * shardGroup: "documents",
772
+ * })
773
+ *
774
+ * // 2. Create the WebSocket route
775
+ * const MimicRoute = MimicServer.layerHttpLayerRouter({
776
+ * path: "/mimic",
777
+ * })
778
+ *
779
+ * // 3. Wire together with cluster infrastructure
780
+ * const MimicLive = MimicRoute.pipe(
781
+ * Layer.provide(Engine),
782
+ * Layer.provide(ColdStorage.S3.make(...)),
783
+ * Layer.provide(HotStorage.Redis.make(...)),
784
+ * Layer.provide(MimicAuthService.make(...)),
785
+ * Layer.provide(ClusterInfrastructure),
786
+ * )
787
+ * ```
788
+ */
789
+ export const make = <TSchema extends Primitive.AnyPrimitive>(
790
+ config: MimicClusterServerEngineConfig<TSchema>
791
+ ): Layer.Layer<
792
+ MimicServerEngineTag,
793
+ never,
794
+ ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding
795
+ > => {
796
+ const resolvedConfig = resolveClusterConfig(config);
797
+
798
+ // Create config layer
799
+ const configLayer = Layer.succeed(
800
+ MimicClusterConfigTag,
801
+ resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>
802
+ );
803
+
804
+ // Create entity layer that registers with sharding
805
+ const entityLayer = MimicDocumentEntity.toLayer(
806
+ Effect.gen(function* () {
807
+ const clusterConfig = yield* MimicClusterConfigTag;
808
+ const coldStorage = yield* ColdStorageTag;
809
+ const hotStorage = yield* HotStorageTag;
810
+
811
+ return yield* createEntityHandler(
812
+ clusterConfig as ResolvedClusterConfig<TSchema>,
813
+ coldStorage,
814
+ hotStorage
815
+ );
816
+ }),
817
+ {
818
+ maxIdleTime: resolvedConfig.maxIdleTime,
819
+ concurrency: 1, // Sequential message processing per document
820
+ mailboxCapacity: 4096,
821
+ }
822
+ );
823
+
824
+ // Create the engine service
825
+ const engineLayer = Layer.scoped(
826
+ MimicServerEngineTag,
827
+ Effect.gen(function* () {
828
+ // Get entity client maker
829
+ const makeClient = yield* MimicDocumentEntity.client;
830
+
831
+ // Get subscription store
832
+ const subscriptionStore = yield* SubscriptionStoreTag;
833
+
834
+ const engine: MimicServerEngine = {
835
+ submit: (documentId, transaction) =>
836
+ Effect.gen(function* () {
837
+ const client = makeClient(documentId);
838
+ const encodedTx = encodeTransaction(transaction);
839
+ const result = yield* client.Submit({
840
+ transaction: encodedTx as { id: string; ops: unknown[] },
841
+ }).pipe(
842
+ Effect.catchAll((error) =>
843
+ Effect.succeed({
844
+ success: false as const,
845
+ reason: `Cluster error: ${String(error)}`,
846
+ })
847
+ )
848
+ );
849
+
850
+ // Broadcast to local subscribers if success
851
+ if (result.success) {
852
+ const pubsub =
853
+ yield* subscriptionStore.getOrCreatePubSub(documentId);
854
+ yield* PubSub.publish(pubsub, {
855
+ type: "transaction",
856
+ transaction,
857
+ version: result.version,
858
+ } as Protocol.ServerMessage);
859
+ }
860
+
861
+ return result;
862
+ }),
863
+
864
+ getSnapshot: (documentId) =>
865
+ Effect.gen(function* () {
866
+ const client = makeClient(documentId);
867
+ return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);
868
+ }),
869
+
870
+ subscribe: (documentId) =>
871
+ Effect.gen(function* () {
872
+ const pubsub =
873
+ yield* subscriptionStore.getOrCreatePubSub(documentId);
874
+ return Stream.fromPubSub(pubsub);
875
+ }),
876
+
877
+ touch: (documentId) =>
878
+ Effect.gen(function* () {
879
+ const client = makeClient(documentId);
880
+ yield* client.Touch(undefined as void).pipe(Effect.orDie);
881
+ }),
882
+
883
+ getPresenceSnapshot: (documentId) =>
884
+ Effect.gen(function* () {
885
+ const client = makeClient(documentId);
886
+ return yield* client.GetPresenceSnapshot(undefined as void).pipe(Effect.orDie);
887
+ }),
888
+
889
+ setPresence: (documentId, connectionId, entry) =>
890
+ Effect.gen(function* () {
891
+ const client = makeClient(documentId);
892
+ yield* client.SetPresence({ connectionId, entry }).pipe(Effect.orDie);
893
+
894
+ // Broadcast to local presence subscribers
895
+ const pubsub =
896
+ yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
897
+ yield* PubSub.publish(pubsub, {
898
+ type: "presence_update",
899
+ id: connectionId,
900
+ data: entry.data,
901
+ userId: entry.userId,
902
+ });
903
+ }),
904
+
905
+ removePresence: (documentId, connectionId) =>
906
+ Effect.gen(function* () {
907
+ const client = makeClient(documentId);
908
+ yield* client.RemovePresence({ connectionId }).pipe(Effect.orDie);
909
+
910
+ // Broadcast to local presence subscribers
911
+ const pubsub =
912
+ yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
913
+ yield* PubSub.publish(pubsub, {
914
+ type: "presence_remove",
915
+ id: connectionId,
916
+ });
917
+ }),
918
+
919
+ subscribePresence: (documentId) =>
920
+ Effect.gen(function* () {
921
+ const pubsub =
922
+ yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
923
+ return Stream.fromPubSub(pubsub);
924
+ }),
925
+
926
+ config: resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>,
927
+ };
928
+
929
+ return engine;
930
+ })
931
+ );
932
+
933
+ // Compose all layers
934
+ return Layer.mergeAll(entityLayer, engineLayer).pipe(
935
+ Layer.provideMerge(subscriptionStoreLayer),
936
+ Layer.provideMerge(configLayer)
937
+ );
938
+ };
939
+
940
+ // =============================================================================
941
+ // Re-export namespace
942
+ // =============================================================================
943
+
944
+ export const MimicClusterServerEngine = {
945
+ make,
946
+ };