@voidhash/mimic-effect 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import * as Context from "effect/Context";
6
6
  import * as Duration from "effect/Duration";
7
7
  import type { DurationInput } from "effect/Duration";
8
8
  import * as Layer from "effect/Layer";
9
- import type { Primitive, Presence } from "@voidhash/mimic";
9
+ import { Primitive, Presence } from "@voidhash/mimic";
10
10
 
11
11
  // =============================================================================
12
12
  // Mimic Server Configuration
@@ -54,6 +54,13 @@ export interface MimicServerConfig<TSchema extends Primitive.AnyPrimitive = Prim
54
54
  * @default undefined (presence disabled)
55
55
  */
56
56
  readonly presence: Presence.AnyPresence | undefined;
57
+
58
+ /**
59
+ * Initial state for new documents.
60
+ * Used when a document is created and no existing state is found in storage.
61
+ * @default undefined (documents start empty)
62
+ */
63
+ readonly initial: Primitive.InferState<TSchema> | undefined;
57
64
  }
58
65
 
59
66
  /**
@@ -95,6 +102,17 @@ export interface MimicServerConfigOptions<TSchema extends Primitive.AnyPrimitive
95
102
  * @default undefined (presence disabled)
96
103
  */
97
104
  readonly presence?: Presence.AnyPresence;
105
+
106
+ /**
107
+ * Initial state for new documents.
108
+ * Used when a document is created and no existing state is found in storage.
109
+ *
110
+ * Type-safe: required fields (without defaults) must be provided,
111
+ * while optional fields and fields with defaults can be omitted.
112
+ *
113
+ * @default undefined (documents start empty or use schema defaults)
114
+ */
115
+ readonly initial?: Primitive.InferSetInput<TSchema>;
98
116
  }
99
117
 
100
118
  /**
@@ -109,6 +127,9 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
109
127
  heartbeatInterval: Duration.decode(options.heartbeatInterval ?? "30 seconds"),
110
128
  heartbeatTimeout: Duration.decode(options.heartbeatTimeout ?? "10 seconds"),
111
129
  presence: options.presence,
130
+ initial: options.initial !== undefined
131
+ ? Primitive.applyDefaults(options.schema, options.initial as Partial<Primitive.InferState<TSchema>>)
132
+ : undefined,
112
133
  });
113
134
 
114
135
  // =============================================================================
@@ -20,20 +20,6 @@ import * as NoAuth from "./auth/NoAuth.js";
20
20
  import { HttpLayerRouter, HttpServerRequest, HttpServerResponse } from "@effect/platform";
21
21
  import { PathInput } from "@effect/platform/HttpRouter";
22
22
 
23
- // =============================================================================
24
- // Handler Tag
25
- // =============================================================================
26
-
27
- /**
28
- * Tag for the WebSocket handler function.
29
- */
30
- export class MimicWebSocketHandler extends Context.Tag(
31
- "@voidhash/mimic-server-effect/MimicWebSocketHandler"
32
- )<
33
- MimicWebSocketHandler,
34
- (socket: Socket.Socket, documentId: string) => Effect.Effect<void, unknown>
35
- >() {}
36
-
37
23
  // =============================================================================
38
24
  // Layer Composition Options
39
25
  // =============================================================================
@@ -61,121 +47,18 @@ export interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {
61
47
  * When provided, enables presence features on WebSocket connections.
62
48
  */
63
49
  readonly presence?: Presence.AnyPresence;
50
+ /**
51
+ * Initial state for new documents.
52
+ * Used when a document is created and no existing state is found in storage.
53
+ *
54
+ * Type-safe: required fields (without defaults) must be provided,
55
+ * while optional fields and fields with defaults can be omitted.
56
+ *
57
+ * @default undefined (documents start empty or use schema defaults)
58
+ */
59
+ readonly initial?: Primitive.InferSetInput<TSchema>;
64
60
  }
65
61
 
66
- // =============================================================================
67
- // Layer Composition
68
- // =============================================================================
69
-
70
- /**
71
- * Create a Mimic WebSocket handler layer.
72
- *
73
- * This layer provides a handler function that can be used with any WebSocket server
74
- * implementation. The handler takes a socket and document ID and manages the
75
- * document synchronization.
76
- *
77
- * By default, uses in-memory storage and no authentication.
78
- * Override these by providing MimicDataStorage and MimicAuthService layers.
79
- *
80
- * @example
81
- * ```typescript
82
- * import { MimicServer, MimicAuthService } from "@voidhash/mimic-effect";
83
- * import { Primitive } from "@voidhash/mimic";
84
- *
85
- * const TodoSchema = Primitive.Struct({
86
- * title: Primitive.String(),
87
- * completed: Primitive.Boolean(),
88
- * });
89
- *
90
- * // Create the handler layer with defaults
91
- * const HandlerLayer = MimicServer.layer({
92
- * basePath: "/mimic/todo",
93
- * schema: TodoSchema
94
- * });
95
- *
96
- * // Or with custom auth
97
- * const HandlerLayerWithAuth = MimicServer.layer({
98
- * basePath: "/mimic/todo",
99
- * schema: TodoSchema
100
- * }).pipe(
101
- * Layer.provideMerge(MimicAuthService.layer({
102
- * authHandler: (token) => ({ success: true, userId: "user-123" })
103
- * }))
104
- * );
105
- * ```
106
- */
107
- export const layer = <TSchema extends Primitive.AnyPrimitive>(
108
- options: MimicLayerOptions<TSchema>
109
- ): Layer.Layer<MimicWebSocketHandler | DocumentManager.DocumentManagerTag> => {
110
- const configLayer = MimicConfig.layer({
111
- schema: options.schema,
112
- maxTransactionHistory: options.maxTransactionHistory,
113
- presence: options.presence,
114
- });
115
-
116
- return Layer.merge(
117
- // Handler layer
118
- Layer.effect(MimicWebSocketHandler, WebSocketHandler.makeHandler).pipe(
119
- Layer.provide(DocumentManager.layer),
120
- Layer.provide(PresenceManager.layer),
121
- Layer.provide(configLayer)
122
- ),
123
- // Document manager layer
124
- DocumentManager.layer.pipe(Layer.provide(configLayer))
125
- ).pipe(
126
- // Provide defaults if not overridden
127
- Layer.provide(InMemoryDataStorage.layerDefault),
128
- Layer.provide(NoAuth.layerDefault)
129
- );
130
- };
131
-
132
- /**
133
- * Create the Mimic server handler layer.
134
- * This layer provides the WebSocket handler that can be used with any WebSocket server.
135
- *
136
- * @example
137
- * ```typescript
138
- * import { MimicServer } from "@voidhash/mimic-server-effect";
139
- * import { SocketServer } from "@effect/platform/SocketServer";
140
- * import { Primitive } from "@voidhash/mimic";
141
- *
142
- * // Define your document schema
143
- * const TodoSchema = Primitive.Struct({
144
- * title: Primitive.String(),
145
- * completed: Primitive.Boolean(),
146
- * });
147
- *
148
- * // Create the server layer
149
- * const serverLayer = MimicServer.handlerLayer({
150
- * schema: TodoSchema,
151
- * });
152
- *
153
- * // Run with your socket server
154
- * Effect.gen(function* () {
155
- * const handler = yield* MimicServer.MimicWebSocketHandler;
156
- * const server = yield* SocketServer;
157
- *
158
- * yield* server.run((socket) =>
159
- * // Extract document ID from request and call handler
160
- * handler(socket, "my-document-id")
161
- * );
162
- * }).pipe(
163
- * Effect.provide(serverLayer),
164
- * Effect.provide(YourSocketServerLayer),
165
- * );
166
- * ```
167
- */
168
- export const handlerLayer = <TSchema extends Primitive.AnyPrimitive>(
169
- options: MimicConfig.MimicServerConfigOptions<TSchema>
170
- ): Layer.Layer<MimicWebSocketHandler> =>
171
- Layer.effect(MimicWebSocketHandler, WebSocketHandler.makeHandler).pipe(
172
- Layer.provide(DocumentManager.layer),
173
- Layer.provide(PresenceManager.layer),
174
- Layer.provide(MimicConfig.layer(options)),
175
- // Provide defaults
176
- Layer.provide(InMemoryDataStorage.layerDefault),
177
- Layer.provide(NoAuth.layerDefault)
178
- );
179
62
 
180
63
  /**
181
64
  * Create the document manager layer.
@@ -190,40 +73,6 @@ export const documentManagerLayer = <TSchema extends Primitive.AnyPrimitive>(
190
73
  Layer.provide(NoAuth.layerDefault)
191
74
  );
192
75
 
193
- // =============================================================================
194
- // Convenience Functions
195
- // =============================================================================
196
-
197
- /**
198
- * Run a Mimic WebSocket server with the provided handler.
199
- *
200
- * This is a helper that:
201
- * 1. Gets the WebSocket handler from context
202
- * 2. Runs the socket server with the handler
203
- *
204
- * Note: The document ID extraction from socket is implementation-specific.
205
- * You may need to customize this based on your socket server.
206
- */
207
- export const run = (
208
- extractDocumentId: (socket: Socket.Socket) => Effect.Effect<string>
209
- ) =>
210
- Effect.gen(function* () {
211
- const handler = yield* MimicWebSocketHandler;
212
- const server = yield* SocketServer;
213
-
214
- yield* server.run((socket) =>
215
- Effect.gen(function* () {
216
- const documentId = yield* extractDocumentId(socket);
217
- yield* handler(socket, documentId);
218
- }).pipe(
219
- Effect.catchAll((error) =>
220
- Effect.logError("Connection error", error)
221
- )
222
- )
223
- );
224
- });
225
-
226
-
227
76
  /**
228
77
  * Create the HTTP handler effect for WebSocket upgrade.
229
78
  * This handler:
@@ -339,6 +188,7 @@ export const layerHttpLayerRouter = <TSchema extends Primitive.AnyPrimitive>(
339
188
  schema: options.schema,
340
189
  maxTransactionHistory: options.maxTransactionHistory,
341
190
  presence: options.presence,
191
+ initial: options.initial,
342
192
  });
343
193
 
344
194
  // Use provided layers or defaults
@@ -337,4 +337,65 @@ describe("DocumentManager", () => {
337
337
  expect(result).toBe(true);
338
338
  });
339
339
  });
340
+
341
+ describe("initial state", () => {
342
+ const makeTestLayerWithInitial = (initial: { title?: string; count?: number }) => {
343
+ const configLayer = MimicConfig.layer({
344
+ schema: TestSchema,
345
+ maxTransactionHistory: 100,
346
+ initial,
347
+ });
348
+
349
+ return DocumentManager.layer.pipe(
350
+ Layer.provide(configLayer),
351
+ Layer.provide(InMemoryDataStorage.layer)
352
+ );
353
+ };
354
+
355
+ it("should use initial state for new documents", async () => {
356
+ const result = await Effect.runPromise(
357
+ Effect.gen(function* () {
358
+ const manager = yield* DocumentManager.DocumentManagerTag;
359
+ return yield* manager.getSnapshot("new-doc");
360
+ }).pipe(Effect.provide(makeTestLayerWithInitial({ title: "Initial Title", count: 42 })))
361
+ );
362
+
363
+ expect(result.type).toBe("snapshot");
364
+ expect(result.version).toBe(0);
365
+ expect(result.state).toEqual({ title: "Initial Title", count: 42 });
366
+ });
367
+
368
+ it("should apply defaults for omitted fields in initial state", async () => {
369
+ const result = await Effect.runPromise(
370
+ Effect.gen(function* () {
371
+ const manager = yield* DocumentManager.DocumentManagerTag;
372
+ return yield* manager.getSnapshot("new-doc");
373
+ }).pipe(Effect.provide(makeTestLayerWithInitial({ title: "Only Title" })))
374
+ );
375
+
376
+ expect(result.type).toBe("snapshot");
377
+ // count should be 0 (default)
378
+ expect(result.state).toEqual({ title: "Only Title", count: 0 });
379
+ });
380
+
381
+ it("should prefer stored state over initial state", async () => {
382
+ const result = await Effect.runPromise(
383
+ Effect.gen(function* () {
384
+ const manager = yield* DocumentManager.DocumentManagerTag;
385
+
386
+ // Apply a transaction to modify the document
387
+ const tx = createValidTransaction("tx-1", "Modified Title");
388
+ yield* manager.submit("doc-1", tx);
389
+
390
+ // Get snapshot - should show modified state, not initial
391
+ return yield* manager.getSnapshot("doc-1");
392
+ }).pipe(Effect.provide(makeTestLayerWithInitial({ title: "Initial Title", count: 42 })))
393
+ );
394
+
395
+ expect(result.type).toBe("snapshot");
396
+ expect((result.state as any).title).toBe("Modified Title");
397
+ // count should still be 42 since we only modified title
398
+ expect((result.state as any).count).toBe(42);
399
+ });
400
+ });
340
401
  });
@@ -172,4 +172,76 @@ describe("MimicConfig", () => {
172
172
  expect(config.presence).toBe(CursorPresence);
173
173
  });
174
174
  });
175
+
176
+ describe("initial state configuration", () => {
177
+ it("should have undefined initial state by default", () => {
178
+ const config = MimicConfig.make({
179
+ schema: TestSchema,
180
+ });
181
+
182
+ expect(config.initial).toBeUndefined();
183
+ });
184
+
185
+ it("should accept initial state option", () => {
186
+ const config = MimicConfig.make({
187
+ schema: TestSchema,
188
+ initial: { title: "My Document", count: 42 },
189
+ });
190
+
191
+ expect(config.initial).toEqual({ title: "My Document", count: 42 });
192
+ });
193
+
194
+ it("should apply defaults for omitted fields in initial state", () => {
195
+ const config = MimicConfig.make({
196
+ schema: TestSchema,
197
+ initial: { title: "My Document" }, // count has default of 0
198
+ });
199
+
200
+ expect(config.initial).toEqual({ title: "My Document", count: 0 });
201
+ });
202
+
203
+ it("should provide initial state through layer", async () => {
204
+ const testLayer = MimicConfig.layer({
205
+ schema: TestSchema,
206
+ initial: { title: "From Layer", count: 100 },
207
+ });
208
+
209
+ const result = await Effect.runPromise(
210
+ Effect.gen(function* () {
211
+ const config = yield* MimicConfig.MimicServerConfigTag;
212
+ return config.initial;
213
+ }).pipe(Effect.provide(testLayer))
214
+ );
215
+
216
+ expect(result).toEqual({ title: "From Layer", count: 100 });
217
+ });
218
+
219
+ it("should work with schema that has required fields without defaults", () => {
220
+ const SchemaWithRequired = Primitive.Struct({
221
+ name: Primitive.String().required(),
222
+ optional: Primitive.String().default("default"),
223
+ });
224
+
225
+ const config = MimicConfig.make({
226
+ schema: SchemaWithRequired,
227
+ initial: { name: "Required Name" },
228
+ });
229
+
230
+ expect(config.initial).toEqual({ name: "Required Name", optional: "default" });
231
+ });
232
+
233
+ it("should work with all options including initial", () => {
234
+ const config = MimicConfig.make({
235
+ schema: TestSchema,
236
+ maxIdleTime: "10 minutes",
237
+ maxTransactionHistory: 500,
238
+ initial: { title: "Full Options", count: 999 },
239
+ });
240
+
241
+ expect(config.schema).toBe(TestSchema);
242
+ expect(Duration.toMillis(config.maxIdleTime)).toBe(10 * 60 * 1000);
243
+ expect(config.maxTransactionHistory).toBe(500);
244
+ expect(config.initial).toEqual({ title: "Full Options", count: 999 });
245
+ });
246
+ });
175
247
  });
@@ -21,84 +21,6 @@ const TestSchema = Primitive.Struct({
21
21
  // =============================================================================
22
22
 
23
23
  describe("MimicServer", () => {
24
- describe("layer", () => {
25
- it("should create a layer with default auth and storage", async () => {
26
- const testLayer = MimicServer.layer({
27
- basePath: "/mimic/test",
28
- schema: TestSchema,
29
- });
30
-
31
- // Verify the layer provides the expected services
32
- const result = await Effect.runPromise(
33
- Effect.gen(function* () {
34
- const handler = yield* MimicServer.MimicWebSocketHandler;
35
- return typeof handler === "function";
36
- }).pipe(Effect.provide(testLayer))
37
- );
38
-
39
- expect(result).toBe(true);
40
- });
41
-
42
- it("should allow custom auth layer to be provided", async () => {
43
- let authCalled = false;
44
-
45
- const customAuthLayer = MimicAuthService.layer({
46
- authHandler: (token) => {
47
- authCalled = true;
48
- return { success: true, userId: token };
49
- },
50
- });
51
-
52
- const testLayer = MimicServer.layer({
53
- basePath: "/mimic/test",
54
- schema: TestSchema,
55
- }).pipe(Layer.provideMerge(customAuthLayer));
56
-
57
- // Verify the layer compiles with custom auth
58
- const result = await Effect.runPromise(
59
- Effect.gen(function* () {
60
- const handler = yield* MimicServer.MimicWebSocketHandler;
61
- return typeof handler === "function";
62
- }).pipe(Effect.provide(testLayer))
63
- );
64
-
65
- expect(result).toBe(true);
66
- });
67
-
68
- it("should support maxTransactionHistory option", async () => {
69
- const testLayer = MimicServer.layer({
70
- basePath: "/mimic/test",
71
- schema: TestSchema,
72
- maxTransactionHistory: 500,
73
- });
74
-
75
- const result = await Effect.runPromise(
76
- Effect.gen(function* () {
77
- const handler = yield* MimicServer.MimicWebSocketHandler;
78
- return typeof handler === "function";
79
- }).pipe(Effect.provide(testLayer))
80
- );
81
-
82
- expect(result).toBe(true);
83
- });
84
- });
85
-
86
- describe("handlerLayer", () => {
87
- it("should create a layer that provides MimicWebSocketHandler", async () => {
88
- const testLayer = MimicServer.handlerLayer({
89
- schema: TestSchema,
90
- });
91
-
92
- const result = await Effect.runPromise(
93
- Effect.gen(function* () {
94
- const handler = yield* MimicServer.MimicWebSocketHandler;
95
- return typeof handler === "function";
96
- }).pipe(Effect.provide(testLayer))
97
- );
98
-
99
- expect(result).toBe(true);
100
- });
101
- });
102
24
 
103
25
  describe("documentManagerLayer", () => {
104
26
  it("should create a layer that provides DocumentManager", async () => {
@@ -187,14 +109,6 @@ describe("MimicServer", () => {
187
109
  });
188
110
  });
189
111
 
190
- describe("MimicWebSocketHandler tag", () => {
191
- it("should have the correct tag identifier", () => {
192
- expect(MimicServer.MimicWebSocketHandler.key).toBe(
193
- "@voidhash/mimic-server-effect/MimicWebSocketHandler"
194
- );
195
- });
196
- });
197
-
198
112
  describe("MimicLayerOptions", () => {
199
113
  it("should accept all optional properties", () => {
200
114
  // TypeScript compile-time check - if this compiles, the interface is correct
@@ -229,82 +143,6 @@ describe("MimicServer", () => {
229
143
  }),
230
144
  });
231
145
 
232
- describe("layer", () => {
233
- it("should accept presence option", async () => {
234
- const testLayer = MimicServer.layer({
235
- basePath: "/mimic/test",
236
- schema: TestSchema,
237
- presence: CursorPresence,
238
- });
239
-
240
- const result = await Effect.runPromise(
241
- Effect.gen(function* () {
242
- const handler = yield* MimicServer.MimicWebSocketHandler;
243
- return typeof handler === "function";
244
- }).pipe(Effect.provide(testLayer))
245
- );
246
-
247
- expect(result).toBe(true);
248
- });
249
-
250
- it("should work with presence and custom auth", async () => {
251
- const customAuthLayer = MimicAuthService.layer({
252
- authHandler: (token) => ({ success: true, userId: token }),
253
- });
254
-
255
- const testLayer = MimicServer.layer({
256
- basePath: "/mimic/test",
257
- schema: TestSchema,
258
- presence: CursorPresence,
259
- }).pipe(Layer.provideMerge(customAuthLayer));
260
-
261
- const result = await Effect.runPromise(
262
- Effect.gen(function* () {
263
- const handler = yield* MimicServer.MimicWebSocketHandler;
264
- return typeof handler === "function";
265
- }).pipe(Effect.provide(testLayer))
266
- );
267
-
268
- expect(result).toBe(true);
269
- });
270
-
271
- it("should work with presence and maxTransactionHistory", async () => {
272
- const testLayer = MimicServer.layer({
273
- basePath: "/mimic/test",
274
- schema: TestSchema,
275
- presence: CursorPresence,
276
- maxTransactionHistory: 500,
277
- });
278
-
279
- const result = await Effect.runPromise(
280
- Effect.gen(function* () {
281
- const handler = yield* MimicServer.MimicWebSocketHandler;
282
- return typeof handler === "function";
283
- }).pipe(Effect.provide(testLayer))
284
- );
285
-
286
- expect(result).toBe(true);
287
- });
288
- });
289
-
290
- describe("handlerLayer", () => {
291
- it("should accept presence option", async () => {
292
- const testLayer = MimicServer.handlerLayer({
293
- schema: TestSchema,
294
- presence: CursorPresence,
295
- });
296
-
297
- const result = await Effect.runPromise(
298
- Effect.gen(function* () {
299
- const handler = yield* MimicServer.MimicWebSocketHandler;
300
- return typeof handler === "function";
301
- }).pipe(Effect.provide(testLayer))
302
- );
303
-
304
- expect(result).toBe(true);
305
- });
306
- });
307
-
308
146
  describe("documentManagerLayer", () => {
309
147
  it("should accept presence option", async () => {
310
148
  const testLayer = MimicServer.documentManagerLayer({
@@ -382,4 +220,59 @@ describe("MimicServer", () => {
382
220
  });
383
221
  });
384
222
  });
223
+
224
+ describe("initial state support", () => {
225
+ describe("layerHttpLayerRouter", () => {
226
+ it("should accept initial option", () => {
227
+ const routeLayer = MimicServer.layerHttpLayerRouter({
228
+ basePath: "/mimic/test",
229
+ schema: TestSchema,
230
+ initial: { title: "My Document", completed: true },
231
+ });
232
+
233
+ expect(routeLayer).toBeDefined();
234
+ });
235
+
236
+ it("should work with initial and all other options", () => {
237
+ const customAuthLayer = MimicAuthService.layer({
238
+ authHandler: (token) => ({ success: true, userId: token }),
239
+ });
240
+
241
+ const routeLayer = MimicServer.layerHttpLayerRouter({
242
+ basePath: "/mimic/test",
243
+ schema: TestSchema,
244
+ initial: { title: "Full Options" },
245
+ maxTransactionHistory: 500,
246
+ authLayer: customAuthLayer,
247
+ storageLayer: InMemoryDataStorage.layer,
248
+ });
249
+
250
+ expect(routeLayer).toBeDefined();
251
+ });
252
+ });
253
+
254
+ describe("MimicLayerOptions with initial", () => {
255
+ it("should accept initial in options", () => {
256
+ const options: MimicServer.MimicLayerOptions<typeof TestSchema> = {
257
+ schema: TestSchema,
258
+ basePath: "/custom/path",
259
+ initial: { title: "Initial State", completed: true },
260
+ };
261
+
262
+ expect(options.schema).toBe(TestSchema);
263
+ expect(options.basePath).toBe("/custom/path");
264
+ expect(options.initial).toEqual({ title: "Initial State", completed: true });
265
+ });
266
+
267
+ it("should allow omitting optional fields in initial (type safety)", () => {
268
+ // This test verifies that TypeScript allows omitting fields with defaults
269
+ const options: MimicServer.MimicLayerOptions<typeof TestSchema> = {
270
+ schema: TestSchema,
271
+ initial: { title: "Only Title" }, // completed is optional because it has a default
272
+ };
273
+
274
+ expect(options.initial).toEqual({ title: "Only Title" });
275
+ });
276
+ });
277
+ });
385
278
  });