@voidhash/mimic-effect 0.0.3 → 0.0.5
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.
- package/.turbo/turbo-build.log +25 -25
- package/dist/DocumentManager.cjs +3 -3
- package/dist/DocumentManager.d.cts +1 -1
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +2 -2
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/MimicConfig.cjs +12 -2
- package/dist/MimicConfig.d.cts +45 -3
- package/dist/MimicConfig.d.cts.map +1 -1
- package/dist/MimicConfig.d.mts +44 -2
- package/dist/MimicConfig.d.mts.map +1 -1
- package/dist/MimicConfig.mjs +11 -2
- package/dist/MimicConfig.mjs.map +1 -1
- package/dist/MimicDataStorage.d.cts +5 -5
- package/dist/MimicServer.cjs +21 -126
- package/dist/MimicServer.d.cts +25 -101
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +25 -101
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +26 -130
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/PresenceManager.cjs +2 -2
- package/dist/PresenceManager.mjs +1 -1
- package/dist/WebSocketHandler.cjs +2 -3
- package/dist/WebSocketHandler.d.cts +1 -1
- package/dist/WebSocketHandler.d.mts +1 -1
- package/dist/WebSocketHandler.mjs +2 -2
- package/dist/errors.d.cts +9 -9
- package/package.json +3 -3
- package/src/DocumentManager.ts +5 -3
- package/src/MimicConfig.ts +92 -12
- package/src/MimicServer.ts +76 -200
- package/tests/DocumentManager.test.ts +124 -0
- package/tests/MimicConfig.test.ts +115 -0
- package/tests/MimicServer.test.ts +55 -162
package/src/MimicServer.ts
CHANGED
|
@@ -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
|
// =============================================================================
|
|
@@ -41,7 +27,9 @@ export class MimicWebSocketHandler extends Context.Tag(
|
|
|
41
27
|
/**
|
|
42
28
|
* Options for creating a Mimic server layer.
|
|
43
29
|
*/
|
|
44
|
-
export interface MimicLayerOptions<
|
|
30
|
+
export interface MimicLayerOptions<
|
|
31
|
+
TSchema extends Primitive.AnyPrimitive,
|
|
32
|
+
> {
|
|
45
33
|
/**
|
|
46
34
|
* Base path for document routes (used for path matching).
|
|
47
35
|
* @example "/mimic/todo" - documents accessed at "/mimic/todo/:documentId"
|
|
@@ -61,121 +49,23 @@ export interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
|
61
49
|
* When provided, enables presence features on WebSocket connections.
|
|
62
50
|
*/
|
|
63
51
|
readonly presence?: Presence.AnyPresence;
|
|
52
|
+
/**
|
|
53
|
+
* Initial state for new documents.
|
|
54
|
+
* Can be either:
|
|
55
|
+
* - A plain object with the initial state values
|
|
56
|
+
* - A function that receives context (with documentId) and returns an Effect producing the initial state
|
|
57
|
+
*
|
|
58
|
+
* When using a function that requires Effect services (has R requirements),
|
|
59
|
+
* you must also provide `initialLayer` to supply those dependencies.
|
|
60
|
+
*
|
|
61
|
+
* Type-safe: required fields (without defaults) must be provided,
|
|
62
|
+
* while optional fields and fields with defaults can be omitted.
|
|
63
|
+
*
|
|
64
|
+
* @default undefined (documents start empty or use schema defaults)
|
|
65
|
+
*/
|
|
66
|
+
readonly initial?: Primitive.InferSetInput<TSchema> | MimicConfig.InitialFn<TSchema>;
|
|
64
67
|
}
|
|
65
68
|
|
|
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
69
|
|
|
180
70
|
/**
|
|
181
71
|
* Create the document manager layer.
|
|
@@ -190,40 +80,6 @@ export const documentManagerLayer = <TSchema extends Primitive.AnyPrimitive>(
|
|
|
190
80
|
Layer.provide(NoAuth.layerDefault)
|
|
191
81
|
);
|
|
192
82
|
|
|
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
83
|
/**
|
|
228
84
|
* Create the HTTP handler effect for WebSocket upgrade.
|
|
229
85
|
* This handler:
|
|
@@ -275,6 +131,17 @@ const makeMimicHandler = Effect.gen(function* () {
|
|
|
275
131
|
|
|
276
132
|
|
|
277
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Options for layerHttpLayerRouter including optional custom layers.
|
|
136
|
+
*/
|
|
137
|
+
export interface MimicLayerRouterOptions<TSchema extends Primitive.AnyPrimitive>
|
|
138
|
+
extends MimicLayerOptions<TSchema> {
|
|
139
|
+
/** Custom auth layer. Defaults to NoAuth (all connections allowed). */
|
|
140
|
+
readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;
|
|
141
|
+
/** Custom storage layer. Defaults to InMemoryDataStorage. */
|
|
142
|
+
readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;
|
|
143
|
+
}
|
|
144
|
+
|
|
278
145
|
/**
|
|
279
146
|
* Create a Mimic server layer that integrates with HttpLayerRouter.
|
|
280
147
|
*
|
|
@@ -321,43 +188,52 @@ const makeMimicHandler = Effect.gen(function* () {
|
|
|
321
188
|
* );
|
|
322
189
|
* ```
|
|
323
190
|
*/
|
|
324
|
-
export const layerHttpLayerRouter = <
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
191
|
+
export const layerHttpLayerRouter = <
|
|
192
|
+
TSchema extends Primitive.AnyPrimitive,
|
|
193
|
+
TError,
|
|
194
|
+
TRequirements
|
|
195
|
+
>(
|
|
196
|
+
optionsEf: Effect.Effect<MimicLayerRouterOptions<TSchema>, TError, TRequirements>
|
|
197
|
+
): Layer.Layer<never, TError, TRequirements | HttpLayerRouter.HttpRouter> => {
|
|
198
|
+
return Layer.unwrapScoped(
|
|
199
|
+
Effect.gen(function* () {
|
|
200
|
+
const options = yield* optionsEf;
|
|
201
|
+
|
|
202
|
+
// Build the base path pattern for WebSocket routes
|
|
203
|
+
// Append /doc/* to match /basePath/doc/{documentId}
|
|
204
|
+
const basePath = options.basePath ?? "/mimic";
|
|
205
|
+
const wsPath: PathInput = `${basePath}/doc/*` as PathInput;
|
|
206
|
+
|
|
207
|
+
// Create the config layer with properly typed initial function
|
|
208
|
+
const configLayer = MimicConfig.layer<TSchema>({
|
|
209
|
+
schema: options.schema,
|
|
210
|
+
maxTransactionHistory: options.maxTransactionHistory,
|
|
211
|
+
presence: options.presence,
|
|
212
|
+
initial: options.initial,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Use provided layers or defaults
|
|
216
|
+
const authLayer = options.authLayer ?? NoAuth.layerDefault;
|
|
217
|
+
const storageLayer = options.storageLayer ?? InMemoryDataStorage.layerDefault;
|
|
218
|
+
|
|
219
|
+
// Combine all dependency layers
|
|
220
|
+
const depsLayer = Layer.mergeAll(configLayer, authLayer, storageLayer);
|
|
221
|
+
|
|
222
|
+
// Create the route registration layer
|
|
223
|
+
const routeLayer = Layer.scopedDiscard(
|
|
224
|
+
Effect.gen(function* () {
|
|
225
|
+
const router = yield* HttpLayerRouter.HttpRouter;
|
|
226
|
+
const handler = yield* makeMimicHandler;
|
|
227
|
+
yield* router.add("GET", wsPath, handler);
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Build the complete layer with all dependencies provided
|
|
232
|
+
return routeLayer.pipe(
|
|
233
|
+
Layer.provide(DocumentManager.layer),
|
|
234
|
+
Layer.provide(PresenceManager.layer),
|
|
235
|
+
Layer.provide(depsLayer),
|
|
236
|
+
);
|
|
237
|
+
})
|
|
362
238
|
);
|
|
363
239
|
};
|
|
@@ -337,4 +337,128 @@ 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
|
+
const makeTestLayerWithInitialFn = (
|
|
356
|
+
initialFn: (ctx: { documentId: string }) => Effect.Effect<{ title?: string; count?: number }>
|
|
357
|
+
) => {
|
|
358
|
+
const configLayer = MimicConfig.layer({
|
|
359
|
+
schema: TestSchema,
|
|
360
|
+
maxTransactionHistory: 100,
|
|
361
|
+
initial: initialFn,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return DocumentManager.layer.pipe(
|
|
365
|
+
Layer.provide(configLayer),
|
|
366
|
+
Layer.provide(InMemoryDataStorage.layer)
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
it("should use initial state for new documents", async () => {
|
|
371
|
+
const result = await Effect.runPromise(
|
|
372
|
+
Effect.gen(function* () {
|
|
373
|
+
const manager = yield* DocumentManager.DocumentManagerTag;
|
|
374
|
+
return yield* manager.getSnapshot("new-doc");
|
|
375
|
+
}).pipe(Effect.provide(makeTestLayerWithInitial({ title: "Initial Title", count: 42 })))
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(result.type).toBe("snapshot");
|
|
379
|
+
expect(result.version).toBe(0);
|
|
380
|
+
expect(result.state).toEqual({ title: "Initial Title", count: 42 });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should apply defaults for omitted fields in initial state", async () => {
|
|
384
|
+
const result = await Effect.runPromise(
|
|
385
|
+
Effect.gen(function* () {
|
|
386
|
+
const manager = yield* DocumentManager.DocumentManagerTag;
|
|
387
|
+
return yield* manager.getSnapshot("new-doc");
|
|
388
|
+
}).pipe(Effect.provide(makeTestLayerWithInitial({ title: "Only Title" })))
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
expect(result.type).toBe("snapshot");
|
|
392
|
+
// count should be 0 (default)
|
|
393
|
+
expect(result.state).toEqual({ title: "Only Title", count: 0 });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("should prefer stored state over initial state", async () => {
|
|
397
|
+
const result = await Effect.runPromise(
|
|
398
|
+
Effect.gen(function* () {
|
|
399
|
+
const manager = yield* DocumentManager.DocumentManagerTag;
|
|
400
|
+
|
|
401
|
+
// Apply a transaction to modify the document
|
|
402
|
+
const tx = createValidTransaction("tx-1", "Modified Title");
|
|
403
|
+
yield* manager.submit("doc-1", tx);
|
|
404
|
+
|
|
405
|
+
// Get snapshot - should show modified state, not initial
|
|
406
|
+
return yield* manager.getSnapshot("doc-1");
|
|
407
|
+
}).pipe(Effect.provide(makeTestLayerWithInitial({ title: "Initial Title", count: 42 })))
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(result.type).toBe("snapshot");
|
|
411
|
+
expect((result.state as any).title).toBe("Modified Title");
|
|
412
|
+
// count should still be 42 since we only modified title
|
|
413
|
+
expect((result.state as any).count).toBe(42);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should use initial state function with documentId for new documents", async () => {
|
|
417
|
+
const result = await Effect.runPromise(
|
|
418
|
+
Effect.gen(function* () {
|
|
419
|
+
const manager = yield* DocumentManager.DocumentManagerTag;
|
|
420
|
+
return yield* manager.getSnapshot("my-special-doc");
|
|
421
|
+
}).pipe(Effect.provide(makeTestLayerWithInitialFn(
|
|
422
|
+
({ documentId }) => Effect.succeed({ title: `Doc: ${documentId}`, count: documentId.length })
|
|
423
|
+
)))
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(result.type).toBe("snapshot");
|
|
427
|
+
expect(result.version).toBe(0);
|
|
428
|
+
expect(result.state).toEqual({ title: "Doc: my-special-doc", count: 14 });
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should call initial function with different documentIds", async () => {
|
|
432
|
+
const layer = makeTestLayerWithInitialFn(
|
|
433
|
+
({ documentId }) => Effect.succeed({ title: documentId, count: documentId.length })
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const result = await Effect.runPromise(
|
|
437
|
+
Effect.gen(function* () {
|
|
438
|
+
const manager = yield* DocumentManager.DocumentManagerTag;
|
|
439
|
+
const snap1 = yield* manager.getSnapshot("short");
|
|
440
|
+
const snap2 = yield* manager.getSnapshot("longer-document-id");
|
|
441
|
+
return { snap1, snap2 };
|
|
442
|
+
}).pipe(Effect.provide(layer))
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
expect(result.snap1.state).toEqual({ title: "short", count: 5 });
|
|
446
|
+
expect(result.snap2.state).toEqual({ title: "longer-document-id", count: 18 });
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should apply defaults to initial function result", async () => {
|
|
450
|
+
const result = await Effect.runPromise(
|
|
451
|
+
Effect.gen(function* () {
|
|
452
|
+
const manager = yield* DocumentManager.DocumentManagerTag;
|
|
453
|
+
return yield* manager.getSnapshot("test-doc");
|
|
454
|
+
}).pipe(Effect.provide(makeTestLayerWithInitialFn(
|
|
455
|
+
({ documentId }) => Effect.succeed({ title: documentId }) // count omitted
|
|
456
|
+
)))
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
expect(result.type).toBe("snapshot");
|
|
460
|
+
// count should be 0 (default)
|
|
461
|
+
expect(result.state).toEqual({ title: "test-doc", count: 0 });
|
|
462
|
+
});
|
|
463
|
+
});
|
|
340
464
|
});
|
|
@@ -172,4 +172,119 @@ 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 object and convert to function", async () => {
|
|
186
|
+
const config = MimicConfig.make({
|
|
187
|
+
schema: TestSchema,
|
|
188
|
+
initial: { title: "My Document", count: 42 },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(typeof config.initial).toBe("function");
|
|
192
|
+
// The function should return the initial state when called
|
|
193
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test-doc" }));
|
|
194
|
+
expect(result).toEqual({ title: "My Document", count: 42 });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should apply defaults for omitted fields in initial state object", async () => {
|
|
198
|
+
const config = MimicConfig.make({
|
|
199
|
+
schema: TestSchema,
|
|
200
|
+
initial: { title: "My Document" }, // count has default of 0
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test-doc" }));
|
|
204
|
+
expect(result).toEqual({ title: "My Document", count: 0 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should accept initial state function", async () => {
|
|
208
|
+
const config = MimicConfig.make({
|
|
209
|
+
schema: TestSchema,
|
|
210
|
+
initial: ({ documentId }) => Effect.succeed({ title: `Doc ${documentId}`, count: 123 }),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(typeof config.initial).toBe("function");
|
|
214
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "my-doc-id" }));
|
|
215
|
+
expect(result).toEqual({ title: "Doc my-doc-id", count: 123 });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should apply defaults to initial state function result", async () => {
|
|
219
|
+
const config = MimicConfig.make({
|
|
220
|
+
schema: TestSchema,
|
|
221
|
+
initial: ({ documentId }) => Effect.succeed({ title: documentId }), // count omitted
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test" }));
|
|
225
|
+
expect(result).toEqual({ title: "test", count: 0 });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should provide initial function through layer", async () => {
|
|
229
|
+
const testLayer = MimicConfig.layer({
|
|
230
|
+
schema: TestSchema,
|
|
231
|
+
initial: { title: "From Layer", count: 100 },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const result = await Effect.runPromise(
|
|
235
|
+
Effect.gen(function* () {
|
|
236
|
+
const config = yield* MimicConfig.MimicServerConfigTag;
|
|
237
|
+
return yield* config.initial!({ documentId: "layer-doc" });
|
|
238
|
+
}).pipe(Effect.provide(testLayer))
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(result).toEqual({ title: "From Layer", count: 100 });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should work with schema that has required fields without defaults", async () => {
|
|
245
|
+
const SchemaWithRequired = Primitive.Struct({
|
|
246
|
+
name: Primitive.String().required(),
|
|
247
|
+
optional: Primitive.String().default("default"),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const config = MimicConfig.make({
|
|
251
|
+
schema: SchemaWithRequired,
|
|
252
|
+
initial: { name: "Required Name" },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test" }));
|
|
256
|
+
expect(result).toEqual({ name: "Required Name", optional: "default" });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should work with all options including initial object", async () => {
|
|
260
|
+
const config = MimicConfig.make({
|
|
261
|
+
schema: TestSchema,
|
|
262
|
+
maxIdleTime: "10 minutes",
|
|
263
|
+
maxTransactionHistory: 500,
|
|
264
|
+
initial: { title: "Full Options", count: 999 },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(config.schema).toBe(TestSchema);
|
|
268
|
+
expect(Duration.toMillis(config.maxIdleTime)).toBe(10 * 60 * 1000);
|
|
269
|
+
expect(config.maxTransactionHistory).toBe(500);
|
|
270
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test" }));
|
|
271
|
+
expect(result).toEqual({ title: "Full Options", count: 999 });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should work with initial function that uses documentId", async () => {
|
|
275
|
+
const config = MimicConfig.make({
|
|
276
|
+
schema: TestSchema,
|
|
277
|
+
initial: ({ documentId }) => Effect.succeed({
|
|
278
|
+
title: `Document: ${documentId}`,
|
|
279
|
+
count: documentId.length,
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const result1 = await Effect.runPromise(config.initial!({ documentId: "short" }));
|
|
284
|
+
expect(result1).toEqual({ title: "Document: short", count: 5 });
|
|
285
|
+
|
|
286
|
+
const result2 = await Effect.runPromise(config.initial!({ documentId: "longer-id" }));
|
|
287
|
+
expect(result2).toEqual({ title: "Document: longer-id", count: 9 });
|
|
288
|
+
});
|
|
289
|
+
});
|
|
175
290
|
});
|