@voidhash/mimic-effect 1.0.0-beta.6 → 1.0.0-beta.8

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 (66) hide show
  1. package/.turbo/turbo-build.log +41 -41
  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 +255 -0
  8. package/dist/DocumentInstance.d.cts +74 -0
  9. package/dist/DocumentInstance.d.cts.map +1 -0
  10. package/dist/DocumentInstance.d.mts +74 -0
  11. package/dist/DocumentInstance.d.mts.map +1 -0
  12. package/dist/DocumentInstance.mjs +256 -0
  13. package/dist/DocumentInstance.mjs.map +1 -0
  14. package/dist/HotStorage.cjs +17 -13
  15. package/dist/HotStorage.d.cts.map +1 -1
  16. package/dist/HotStorage.d.mts.map +1 -1
  17. package/dist/HotStorage.mjs +17 -13
  18. package/dist/HotStorage.mjs.map +1 -1
  19. package/dist/MimicClusterServerEngine.cjs +31 -215
  20. package/dist/MimicClusterServerEngine.d.cts.map +1 -1
  21. package/dist/MimicClusterServerEngine.d.mts.map +1 -1
  22. package/dist/MimicClusterServerEngine.mjs +36 -220
  23. package/dist/MimicClusterServerEngine.mjs.map +1 -1
  24. package/dist/MimicServer.cjs +19 -19
  25. package/dist/MimicServer.d.cts.map +1 -1
  26. package/dist/MimicServer.d.mts.map +1 -1
  27. package/dist/MimicServer.mjs +19 -19
  28. package/dist/MimicServer.mjs.map +1 -1
  29. package/dist/MimicServerEngine.cjs +71 -9
  30. package/dist/MimicServerEngine.d.cts +12 -7
  31. package/dist/MimicServerEngine.d.cts.map +1 -1
  32. package/dist/MimicServerEngine.d.mts +12 -7
  33. package/dist/MimicServerEngine.d.mts.map +1 -1
  34. package/dist/MimicServerEngine.mjs +73 -11
  35. package/dist/MimicServerEngine.mjs.map +1 -1
  36. package/dist/PresenceManager.cjs +5 -5
  37. package/dist/PresenceManager.d.cts.map +1 -1
  38. package/dist/PresenceManager.d.mts.map +1 -1
  39. package/dist/PresenceManager.mjs +5 -5
  40. package/dist/PresenceManager.mjs.map +1 -1
  41. package/dist/Protocol.d.cts +1 -1
  42. package/dist/Protocol.d.mts +1 -1
  43. package/dist/index.cjs +2 -4
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.mts +2 -2
  46. package/dist/index.mjs +2 -2
  47. package/dist/testing/types.d.cts +3 -3
  48. package/package.json +3 -3
  49. package/src/ColdStorage.ts +21 -12
  50. package/src/DocumentInstance.ts +510 -0
  51. package/src/HotStorage.ts +75 -58
  52. package/src/MimicClusterServerEngine.ts +93 -398
  53. package/src/MimicServer.ts +83 -75
  54. package/src/MimicServerEngine.ts +170 -34
  55. package/src/PresenceManager.ts +44 -34
  56. package/src/index.ts +3 -4
  57. package/tests/DocumentInstance.test.ts +669 -0
  58. package/dist/DocumentManager.cjs +0 -299
  59. package/dist/DocumentManager.d.cts +0 -67
  60. package/dist/DocumentManager.d.cts.map +0 -1
  61. package/dist/DocumentManager.d.mts +0 -67
  62. package/dist/DocumentManager.d.mts.map +0 -1
  63. package/dist/DocumentManager.mjs +0 -297
  64. package/dist/DocumentManager.mjs.map +0 -1
  65. package/src/DocumentManager.ts +0 -614
  66. package/tests/DocumentManager.test.ts +0 -335
@@ -10,7 +10,6 @@ import {
10
10
  Fiber,
11
11
  Layer,
12
12
  Metric,
13
- Scope,
14
13
  Stream,
15
14
  } from "effect";
16
15
  import {
@@ -93,14 +92,14 @@ interface ConnectionState {
93
92
  /**
94
93
  * Handle a WebSocket connection for a document.
95
94
  */
96
- const handleWebSocketConnection = (
97
- socket: Socket.Socket,
98
- documentId: string,
99
- engine: MimicServerEngine,
100
- authService: MimicAuthService,
101
- _routeConfig: ResolvedRouteConfig
102
- ): Effect.Effect<void, Socket.SocketError, Scope.Scope> =>
103
- Effect.gen(function* () {
95
+ const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(
96
+ function* (
97
+ socket: Socket.Socket,
98
+ documentId: string,
99
+ engine: MimicServerEngine,
100
+ authService: MimicAuthService,
101
+ _routeConfig: ResolvedRouteConfig
102
+ ) {
104
103
  const connectionId = crypto.randomUUID();
105
104
  const connectionStartTime = Date.now();
106
105
 
@@ -124,59 +123,62 @@ const handleWebSocketConnection = (
124
123
  write(Protocol.encodeServerMessage(message));
125
124
 
126
125
  // Send presence snapshot after auth
127
- const sendPresenceSnapshot = Effect.gen(function* () {
128
- if (!engine.config.presence) return;
126
+ const sendPresenceSnapshot = Effect.fn("presence.snapshot.send")(
127
+ function* () {
128
+ if (!engine.config.presence) return;
129
129
 
130
- const snapshot = yield* engine.getPresenceSnapshot(documentId);
131
- yield* sendMessage(
132
- Protocol.presenceSnapshotMessage(connectionId, snapshot.presences)
133
- );
134
- });
130
+ const snapshot = yield* engine.getPresenceSnapshot(documentId);
131
+ yield* sendMessage(
132
+ Protocol.presenceSnapshotMessage(connectionId, snapshot.presences)
133
+ );
134
+ }
135
+ );
135
136
 
136
137
  // Send document snapshot after auth
137
- const sendDocumentSnapshot = Effect.gen(function* () {
138
- const snapshot = yield* engine.getSnapshot(documentId);
139
- yield* sendMessage(
140
- Protocol.snapshotMessage(snapshot.state, snapshot.version)
141
- );
142
- });
143
-
144
- // Handle authentication
145
- const handleAuth = (token: string) =>
146
- Effect.gen(function* () {
147
- const result = yield* Effect.either(
148
- authService.authenticate(token, documentId)
138
+ const sendDocumentSnapshot = Effect.fn("document.snapshot.send")(
139
+ function* () {
140
+ const snapshot = yield* engine.getSnapshot(documentId);
141
+ yield* sendMessage(
142
+ Protocol.snapshotMessage(snapshot.state, snapshot.version)
149
143
  );
144
+ }
145
+ );
150
146
 
151
- if (result._tag === "Right") {
152
- state.authenticated = true;
153
- state.authContext = result.right;
147
+ // Handle authentication
148
+ const handleAuth = Effect.fn("auth.handle")(function* (token: string) {
149
+ const result = yield* Effect.either(
150
+ authService.authenticate(token, documentId)
151
+ );
154
152
 
155
- yield* sendMessage(
156
- Protocol.authResultSuccess(
157
- result.right.userId,
158
- result.right.permission
159
- )
160
- );
153
+ if (result._tag === "Right") {
154
+ state.authenticated = true;
155
+ state.authContext = result.right;
161
156
 
162
- // Send document snapshot after successful auth
163
- yield* sendDocumentSnapshot;
157
+ yield* sendMessage(
158
+ Protocol.authResultSuccess(
159
+ result.right.userId,
160
+ result.right.permission
161
+ )
162
+ );
164
163
 
165
- // Send presence snapshot after successful auth
166
- yield* sendPresenceSnapshot;
167
- } else {
168
- yield* Metric.increment(Metrics.connectionsErrors);
169
- yield* sendMessage(
170
- Protocol.authResultFailure(
171
- result.left.reason ?? "Authentication failed"
172
- )
173
- );
174
- }
175
- });
164
+ // Send document snapshot after successful auth
165
+ yield* sendDocumentSnapshot();
166
+
167
+ // Send presence snapshot after successful auth
168
+ yield* sendPresenceSnapshot();
169
+ } else {
170
+ yield* Metric.increment(Metrics.connectionsErrors);
171
+ yield* sendMessage(
172
+ Protocol.authResultFailure(
173
+ result.left.reason ?? "Authentication failed"
174
+ )
175
+ );
176
+ }
177
+ });
176
178
 
177
179
  // Handle presence set
178
- const handlePresenceSet = (data: unknown) =>
179
- Effect.gen(function* () {
180
+ const handlePresenceSet = Effect.fn("presence.set.handle")(
181
+ function* (data: unknown) {
180
182
  if (!state.authenticated) return;
181
183
  if (!state.authContext) return;
182
184
  if (!engine.config.presence) return;
@@ -206,20 +208,23 @@ const handleWebSocketConnection = (
206
208
  });
207
209
 
208
210
  state.hasPresence = true;
209
- });
211
+ }
212
+ );
210
213
 
211
214
  // Handle presence clear
212
- const handlePresenceClear = Effect.gen(function* () {
213
- if (!state.authenticated) return;
214
- if (!engine.config.presence) return;
215
+ const handlePresenceClear = Effect.fn("presence.clear.handle")(
216
+ function* () {
217
+ if (!state.authenticated) return;
218
+ if (!engine.config.presence) return;
215
219
 
216
- yield* engine.removePresence(documentId, connectionId);
217
- state.hasPresence = false;
218
- });
220
+ yield* engine.removePresence(documentId, connectionId);
221
+ state.hasPresence = false;
222
+ }
223
+ );
219
224
 
220
225
  // Handle a client message
221
- const handleMessage = (message: Protocol.ClientMessage) =>
222
- Effect.gen(function* () {
226
+ const handleMessage = Effect.fn("message.handle")(
227
+ function* (message: Protocol.ClientMessage) {
223
228
  // Touch document on any activity (prevents idle GC)
224
229
  yield* engine.touch(documentId);
225
230
 
@@ -283,14 +288,15 @@ const handleWebSocketConnection = (
283
288
  break;
284
289
 
285
290
  case "presence_clear":
286
- yield* handlePresenceClear;
291
+ yield* handlePresenceClear();
287
292
  break;
288
293
  }
289
- });
294
+ }
295
+ );
290
296
 
291
297
  // Subscribe to document broadcasts
292
298
  const subscribeFiber = yield* Effect.fork(
293
- Effect.gen(function* () {
299
+ Effect.fn("subscriptions.document.start")(function* () {
294
300
  // Wait until authenticated before subscribing
295
301
  while (!state.authenticated) {
296
302
  yield* Effect.sleep(Duration.millis(100));
@@ -303,12 +309,12 @@ const handleWebSocketConnection = (
303
309
  yield* Stream.runForEach(broadcastStream, (broadcast) =>
304
310
  sendMessage(broadcast as Protocol.ServerMessage)
305
311
  );
306
- }).pipe(Effect.scoped)
312
+ })().pipe(Effect.scoped)
307
313
  );
308
314
 
309
315
  // Subscribe to presence events (if presence is enabled)
310
316
  const presenceFiber = yield* Effect.fork(
311
- Effect.gen(function* () {
317
+ Effect.fn("subscriptions.presence.start")(function* () {
312
318
  if (!engine.config.presence) return;
313
319
 
314
320
  // Wait until authenticated before subscribing
@@ -334,12 +340,12 @@ const handleWebSocketConnection = (
334
340
  }
335
341
  })
336
342
  );
337
- }).pipe(Effect.scoped)
343
+ })().pipe(Effect.scoped)
338
344
  );
339
345
 
340
346
  // Ensure cleanup on disconnect
341
347
  yield* Effect.addFinalizer(() =>
342
- Effect.gen(function* () {
348
+ Effect.fn("connection.cleanup")(function* () {
343
349
  // Calculate connection duration
344
350
  const duration = Date.now() - connectionStartTime;
345
351
 
@@ -361,21 +367,22 @@ const handleWebSocketConnection = (
361
367
  documentId,
362
368
  durationMs: duration,
363
369
  });
364
- })
370
+ })()
365
371
  );
366
372
 
367
373
  // Process incoming messages
368
374
  yield* socket.runRaw((data) =>
369
- Effect.gen(function* () {
375
+ Effect.fn("message.process")(function* () {
370
376
  const message = yield* Protocol.parseClientMessage(data);
371
377
  yield* handleMessage(message);
372
- }).pipe(
378
+ })().pipe(
373
379
  Effect.catchAll((error) =>
374
380
  Effect.logError("Message handling error", error)
375
381
  )
376
382
  )
377
383
  );
378
- });
384
+ }
385
+ );
379
386
 
380
387
  // =============================================================================
381
388
  // Factory
@@ -437,8 +444,8 @@ export const layerHttpLayerRouter = (
437
444
 
438
445
  // Create the handler that receives the request
439
446
  // Engine and authService are captured in closure, not yielded per-request
440
- const handler = (request: HttpServerRequest.HttpServerRequest) =>
441
- Effect.gen(function* () {
447
+ const handler = Effect.fn("websocket.route.handler")(
448
+ function* (request: HttpServerRequest.HttpServerRequest) {
442
449
  // Extract document ID from path
443
450
  const documentIdResult = yield* Effect.either(
444
451
  extractDocumentId(request.url)
@@ -470,7 +477,8 @@ export const layerHttpLayerRouter = (
470
477
 
471
478
  // Return empty response - the WebSocket upgrade handles the connection
472
479
  return HttpServerResponse.empty();
473
- });
480
+ }
481
+ );
474
482
 
475
483
  yield* router.add("GET", routePath, handler);
476
484
  })
@@ -10,11 +10,15 @@ import {
10
10
  Context,
11
11
  Duration,
12
12
  Effect,
13
+ HashMap,
13
14
  Layer,
15
+ Metric,
16
+ Ref,
17
+ Schedule,
14
18
  Scope,
15
19
  Stream,
16
20
  } from "effect";
17
- import type { Presence, Primitive, Transaction } from "@voidhash/mimic";
21
+ import type { Primitive, Transaction } from "@voidhash/mimic";
18
22
  import type {
19
23
  MimicServerEngineConfig,
20
24
  PresenceEntry,
@@ -27,16 +31,25 @@ import { ColdStorageTag } from "./ColdStorage";
27
31
  import { HotStorageTag } from "./HotStorage";
28
32
  import { MimicAuthServiceTag } from "./MimicAuthService";
29
33
  import {
30
- DocumentManagerTag,
31
- DocumentManagerConfigTag,
32
- layer as documentManagerLayer,
34
+ DocumentInstance,
33
35
  type SubmitResult,
34
- type DocumentManagerError,
35
- } from "./DocumentManager";
36
+ type DocumentInstance as DocumentInstanceType,
37
+ } from "./DocumentInstance";
36
38
  import {
37
39
  PresenceManagerTag,
38
40
  layer as presenceManagerLayer,
39
41
  } from "./PresenceManager";
42
+ import * as Metrics from "./Metrics";
43
+ import type { ColdStorageError, HotStorageError } from "./Errors";
44
+
45
+ // =============================================================================
46
+ // Types
47
+ // =============================================================================
48
+
49
+ /**
50
+ * Error type for MimicServerEngine operations
51
+ */
52
+ export type MimicServerEngineError = ColdStorageError | HotStorageError;
40
53
 
41
54
  // =============================================================================
42
55
  // MimicServerEngine Interface
@@ -52,30 +65,30 @@ export interface MimicServerEngine {
52
65
  /**
53
66
  * Submit a transaction to a document.
54
67
  * Authorization is checked against the auth service.
55
- * May fail with DocumentManagerError if storage is unavailable.
68
+ * May fail with MimicServerEngineError if storage is unavailable.
56
69
  */
57
70
  readonly submit: (
58
71
  documentId: string,
59
72
  transaction: Transaction.Transaction
60
- ) => Effect.Effect<SubmitResult, DocumentManagerError>;
73
+ ) => Effect.Effect<SubmitResult, MimicServerEngineError>;
61
74
 
62
75
  /**
63
76
  * Get document snapshot (current state and version).
64
- * May fail with DocumentManagerError if storage is unavailable.
77
+ * May fail with MimicServerEngineError if storage is unavailable.
65
78
  */
66
79
  readonly getSnapshot: (
67
80
  documentId: string
68
- ) => Effect.Effect<{ state: unknown; version: number }, DocumentManagerError>;
81
+ ) => Effect.Effect<{ state: unknown; version: number }, MimicServerEngineError>;
69
82
 
70
83
  /**
71
84
  * Subscribe to document broadcasts (transactions).
72
85
  * Returns a stream of server messages.
73
86
  * Requires a Scope for cleanup when the subscription ends.
74
- * May fail with DocumentManagerError if storage is unavailable.
87
+ * May fail with MimicServerEngineError if storage is unavailable.
75
88
  */
76
89
  readonly subscribe: (
77
90
  documentId: string
78
- ) => Effect.Effect<Stream.Stream<Protocol.ServerMessage, never, never>, DocumentManagerError, Scope.Scope>;
91
+ ) => Effect.Effect<Stream.Stream<Protocol.ServerMessage, never, never>, MimicServerEngineError, Scope.Scope>;
79
92
 
80
93
  /**
81
94
  * Touch document to prevent idle garbage collection.
@@ -164,6 +177,18 @@ const resolveConfig = <TSchema extends Primitive.AnyPrimitive>(
164
177
  },
165
178
  });
166
179
 
180
+ // =============================================================================
181
+ // Internal Types
182
+ // =============================================================================
183
+
184
+ /**
185
+ * Store entry for a document instance with last activity time
186
+ */
187
+ interface StoreEntry<TSchema extends Primitive.AnyPrimitive> {
188
+ readonly instance: DocumentInstanceType<TSchema>;
189
+ readonly lastActivityTime: Ref.Ref<number>;
190
+ }
191
+
167
192
  // =============================================================================
168
193
  // Factory
169
194
  // =============================================================================
@@ -208,37 +233,148 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
208
233
  > => {
209
234
  const resolvedConfig = resolveConfig(config);
210
235
 
211
- // Create config layer for DocumentManager
212
- const configLayer = Layer.succeed(
213
- DocumentManagerConfigTag,
214
- resolvedConfig as ResolvedConfig<Primitive.AnyPrimitive>
215
- );
216
-
217
- // Create internal layers
218
- const internalLayers = Layer.mergeAll(
219
- documentManagerLayer.pipe(Layer.provide(configLayer)),
220
- presenceManagerLayer
221
- );
222
-
223
236
  return Layer.scoped(
224
237
  MimicServerEngineTag,
225
238
  Effect.gen(function* () {
226
- const documentManager = yield* DocumentManagerTag;
239
+ const coldStorage = yield* ColdStorageTag;
240
+ const hotStorage = yield* HotStorageTag;
227
241
  const presenceManager = yield* PresenceManagerTag;
228
242
 
243
+ // Store: documentId -> StoreEntry
244
+ const store = yield* Ref.make(
245
+ HashMap.empty<string, StoreEntry<TSchema>>()
246
+ );
247
+
248
+ /**
249
+ * Get or create a document instance
250
+ */
251
+ const getOrCreateDocument = Effect.fn("engine.document.get-or-create")(
252
+ function* (documentId: string) {
253
+ const current = yield* Ref.get(store);
254
+ const existing = HashMap.get(current, documentId);
255
+
256
+ if (existing._tag === "Some") {
257
+ // Update activity time
258
+ yield* Ref.set(existing.value.lastActivityTime, Date.now());
259
+ return existing.value.instance;
260
+ }
261
+
262
+ // Create new document instance
263
+ const instance = yield* DocumentInstance.make(
264
+ documentId,
265
+ {
266
+ schema: config.schema,
267
+ initial: config.initial,
268
+ maxTransactionHistory: resolvedConfig.maxTransactionHistory,
269
+ snapshot: resolvedConfig.snapshot,
270
+ },
271
+ coldStorage,
272
+ hotStorage
273
+ );
274
+
275
+ const lastActivityTime = yield* Ref.make(Date.now());
276
+
277
+ // Store it
278
+ yield* Ref.update(store, (map) =>
279
+ HashMap.set(map, documentId, { instance, lastActivityTime })
280
+ );
281
+
282
+ return instance;
283
+ }
284
+ );
285
+
286
+ /**
287
+ * Start background GC fiber
288
+ */
289
+ const startGCFiber = Effect.fn("engine.gc.start")(function* () {
290
+ const gcLoop = Effect.fn("engine.gc.loop")(function* () {
291
+ const current = yield* Ref.get(store);
292
+ const now = Date.now();
293
+ const maxIdleMs = Duration.toMillis(resolvedConfig.maxIdleTime);
294
+
295
+ for (const [documentId, entry] of current) {
296
+ const lastActivity = yield* Ref.get(entry.lastActivityTime);
297
+ if (now - lastActivity >= maxIdleMs) {
298
+ // Save final snapshot before eviction (best effort)
299
+ yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>
300
+ Effect.logError("Failed to save snapshot during eviction", {
301
+ documentId,
302
+ error: e,
303
+ })
304
+ );
305
+
306
+ // Remove from store
307
+ yield* Ref.update(store, (map) => HashMap.remove(map, documentId));
308
+
309
+ // Track eviction metrics
310
+ yield* Metric.increment(Metrics.documentsEvicted);
311
+ yield* Metric.incrementBy(Metrics.documentsActive, -1);
312
+
313
+ yield* Effect.logInfo("Document evicted due to idle timeout", {
314
+ documentId,
315
+ });
316
+ }
317
+ }
318
+ });
319
+
320
+ // Run GC every minute
321
+ yield* gcLoop().pipe(
322
+ Effect.repeat(Schedule.spaced("1 minute")),
323
+ Effect.fork
324
+ );
325
+ });
326
+
327
+ // Start GC fiber
328
+ yield* startGCFiber();
329
+
330
+ // Cleanup on shutdown
331
+ yield* Effect.addFinalizer(() =>
332
+ Effect.fn("engine.shutdown")(function* () {
333
+ const current = yield* Ref.get(store);
334
+ for (const [documentId, entry] of current) {
335
+ // Best effort save - don't fail shutdown if storage is unavailable
336
+ yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>
337
+ Effect.logError("Failed to save snapshot during shutdown", {
338
+ documentId,
339
+ error: e,
340
+ })
341
+ );
342
+ }
343
+ yield* Effect.logInfo("MimicServerEngine shutdown complete");
344
+ })()
345
+ );
346
+
229
347
  const engine: MimicServerEngine = {
230
348
  submit: (documentId, transaction) =>
231
- documentManager.submit(documentId, transaction),
349
+ Effect.gen(function* () {
350
+ const instance = yield* getOrCreateDocument(documentId);
351
+ return yield* instance.submit(transaction);
352
+ }),
232
353
 
233
- getSnapshot: (documentId) => documentManager.getSnapshot(documentId),
354
+ getSnapshot: (documentId) =>
355
+ Effect.gen(function* () {
356
+ const instance = yield* getOrCreateDocument(documentId);
357
+ return instance.getSnapshot();
358
+ }),
234
359
 
235
360
  subscribe: (documentId) =>
236
- documentManager.subscribe(documentId) as Effect.Effect<
237
- Stream.Stream<Protocol.ServerMessage, never, never>,
238
- never
239
- >,
240
-
241
- touch: (documentId) => documentManager.touch(documentId),
361
+ Effect.gen(function* () {
362
+ const instance = yield* getOrCreateDocument(documentId);
363
+ return Stream.fromPubSub(instance.pubsub) as Stream.Stream<
364
+ Protocol.ServerMessage,
365
+ never,
366
+ never
367
+ >;
368
+ }),
369
+
370
+ touch: (documentId) =>
371
+ Effect.gen(function* () {
372
+ const current = yield* Ref.get(store);
373
+ const existing = HashMap.get(current, documentId);
374
+ if (existing._tag === "Some") {
375
+ yield* Ref.set(existing.value.lastActivityTime, Date.now());
376
+ }
377
+ }),
242
378
 
243
379
  getPresenceSnapshot: (documentId) =>
244
380
  presenceManager.getSnapshot(documentId),
@@ -257,7 +393,7 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
257
393
 
258
394
  return engine;
259
395
  })
260
- ).pipe(Layer.provide(internalLayers));
396
+ ).pipe(Layer.provide(presenceManagerLayer));
261
397
  };
262
398
 
263
399
  // =============================================================================
@@ -106,30 +106,29 @@ export const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(
106
106
  /**
107
107
  * Get or create presence state for a document
108
108
  */
109
- const getOrCreateDocumentState = (
110
- documentId: string
111
- ): Effect.Effect<DocumentPresenceState> =>
112
- Effect.gen(function* () {
113
- const current = yield* Ref.get(store);
114
- const existing = HashMap.get(current, documentId);
115
- if (existing._tag === "Some") {
116
- return existing.value;
117
- }
118
-
119
- // Create new state for this document
120
- const pubsub = yield* PubSub.unbounded<PresenceEvent>();
121
- const state: DocumentPresenceState = {
122
- presences: HashMap.empty(),
123
- pubsub,
124
- };
125
-
126
- yield* Ref.update(store, (map) => HashMap.set(map, documentId, state));
127
- return state;
128
- });
109
+ const getOrCreateDocumentState = Effect.fn(
110
+ "presence.document-state.get-or-create"
111
+ )(function* (documentId: string) {
112
+ const current = yield* Ref.get(store);
113
+ const existing = HashMap.get(current, documentId);
114
+ if (existing._tag === "Some") {
115
+ return existing.value;
116
+ }
117
+
118
+ // Create new state for this document
119
+ const pubsub = yield* PubSub.unbounded<PresenceEvent>();
120
+ const state: DocumentPresenceState = {
121
+ presences: HashMap.empty(),
122
+ pubsub,
123
+ };
124
+
125
+ yield* Ref.update(store, (map) => HashMap.set(map, documentId, state));
126
+ return state;
127
+ });
129
128
 
130
129
  return {
131
- getSnapshot: (documentId) =>
132
- Effect.gen(function* () {
130
+ getSnapshot: Effect.fn("presence.snapshot.get")(
131
+ function* (documentId: string) {
133
132
  const current = yield* Ref.get(store);
134
133
  const existing = HashMap.get(current, documentId);
135
134
  if (existing._tag === "None") {
@@ -142,10 +141,15 @@ export const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(
142
141
  presences[id] = entry;
143
142
  }
144
143
  return { presences };
145
- }),
146
-
147
- set: (documentId, connectionId, entry) =>
148
- Effect.gen(function* () {
144
+ }
145
+ ),
146
+
147
+ set: Effect.fn("presence.set")(
148
+ function* (
149
+ documentId: string,
150
+ connectionId: string,
151
+ entry: PresenceEntry
152
+ ) {
149
153
  const state = yield* getOrCreateDocumentState(documentId);
150
154
 
151
155
  // Update presence in store
@@ -174,16 +178,20 @@ export const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(
174
178
  userId: entry.userId,
175
179
  };
176
180
  yield* PubSub.publish(state.pubsub, event);
177
- }),
181
+ }
182
+ ),
178
183
 
179
- remove: (documentId, connectionId) =>
180
- Effect.gen(function* () {
184
+ remove: Effect.fn("presence.remove")(
185
+ function* (documentId: string, connectionId: string) {
181
186
  const current = yield* Ref.get(store);
182
187
  const existing = HashMap.get(current, documentId);
183
188
  if (existing._tag === "None") return;
184
189
 
185
190
  // Check if presence exists before removing
186
- const hasPresence = HashMap.has(existing.value.presences, connectionId);
191
+ const hasPresence = HashMap.has(
192
+ existing.value.presences,
193
+ connectionId
194
+ );
187
195
  if (!hasPresence) return;
188
196
 
189
197
  // Remove presence from store
@@ -205,13 +213,15 @@ export const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(
205
213
  id: connectionId,
206
214
  };
207
215
  yield* PubSub.publish(existing.value.pubsub, event);
208
- }),
216
+ }
217
+ ),
209
218
 
210
- subscribe: (documentId) =>
211
- Effect.gen(function* () {
219
+ subscribe: Effect.fn("presence.subscribe")(
220
+ function* (documentId: string) {
212
221
  const state = yield* getOrCreateDocumentState(documentId);
213
222
  return Stream.fromPubSub(state.pubsub);
214
- }),
223
+ }
224
+ ),
215
225
  };
216
226
  })
217
227
  );
package/src/index.ts CHANGED
@@ -41,11 +41,10 @@ export * as Protocol from "./Protocol";
41
41
  // =============================================================================
42
42
 
43
43
  export {
44
- DocumentManager,
45
- DocumentManagerTag,
46
- DocumentManagerConfigTag,
44
+ DocumentInstance,
45
+ type DocumentInstance as DocumentInstanceInterface,
47
46
  type SubmitResult,
48
- } from "./DocumentManager";
47
+ } from "./DocumentInstance";
49
48
 
50
49
  export {
51
50
  PresenceManager,