@voidhash/mimic-effect 0.0.4 → 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 +23 -23
- package/dist/DocumentManager.cjs +1 -1
- 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 +1 -1
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/MimicAuthService.d.cts +1 -1
- package/dist/MimicConfig.cjs +10 -2
- package/dist/MimicConfig.d.cts +34 -8
- package/dist/MimicConfig.d.cts.map +1 -1
- package/dist/MimicConfig.d.mts +34 -8
- package/dist/MimicConfig.d.mts.map +1 -1
- package/dist/MimicConfig.mjs +9 -2
- package/dist/MimicConfig.mjs.map +1 -1
- package/dist/MimicDataStorage.d.cts +6 -6
- package/dist/MimicServer.cjs +20 -17
- package/dist/MimicServer.d.cts +20 -10
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +20 -10
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +20 -17
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/PresenceManager.d.cts +1 -1
- package/dist/errors.d.cts +9 -9
- package/package.json +3 -3
- package/src/DocumentManager.ts +5 -3
- package/src/MimicConfig.ts +79 -20
- package/src/MimicServer.ts +68 -42
- package/tests/DocumentManager.test.ts +63 -0
- package/tests/MimicConfig.test.ts +53 -10
package/src/MimicServer.ts
CHANGED
|
@@ -27,7 +27,9 @@ import { PathInput } from "@effect/platform/HttpRouter";
|
|
|
27
27
|
/**
|
|
28
28
|
* Options for creating a Mimic server layer.
|
|
29
29
|
*/
|
|
30
|
-
export interface MimicLayerOptions<
|
|
30
|
+
export interface MimicLayerOptions<
|
|
31
|
+
TSchema extends Primitive.AnyPrimitive,
|
|
32
|
+
> {
|
|
31
33
|
/**
|
|
32
34
|
* Base path for document routes (used for path matching).
|
|
33
35
|
* @example "/mimic/todo" - documents accessed at "/mimic/todo/:documentId"
|
|
@@ -49,14 +51,19 @@ export interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
|
49
51
|
readonly presence?: Presence.AnyPresence;
|
|
50
52
|
/**
|
|
51
53
|
* Initial state for new documents.
|
|
52
|
-
*
|
|
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.
|
|
53
60
|
*
|
|
54
61
|
* Type-safe: required fields (without defaults) must be provided,
|
|
55
62
|
* while optional fields and fields with defaults can be omitted.
|
|
56
63
|
*
|
|
57
64
|
* @default undefined (documents start empty or use schema defaults)
|
|
58
65
|
*/
|
|
59
|
-
readonly initial?: Primitive.InferSetInput<TSchema>;
|
|
66
|
+
readonly initial?: Primitive.InferSetInput<TSchema> | MimicConfig.InitialFn<TSchema>;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
|
|
@@ -124,6 +131,17 @@ const makeMimicHandler = Effect.gen(function* () {
|
|
|
124
131
|
|
|
125
132
|
|
|
126
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
|
+
|
|
127
145
|
/**
|
|
128
146
|
* Create a Mimic server layer that integrates with HttpLayerRouter.
|
|
129
147
|
*
|
|
@@ -170,44 +188,52 @@ const makeMimicHandler = Effect.gen(function* () {
|
|
|
170
188
|
* );
|
|
171
189
|
* ```
|
|
172
190
|
*/
|
|
173
|
-
export const layerHttpLayerRouter = <
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
})
|
|
212
238
|
);
|
|
213
239
|
};
|
|
@@ -352,6 +352,21 @@ describe("DocumentManager", () => {
|
|
|
352
352
|
);
|
|
353
353
|
};
|
|
354
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
|
+
|
|
355
370
|
it("should use initial state for new documents", async () => {
|
|
356
371
|
const result = await Effect.runPromise(
|
|
357
372
|
Effect.gen(function* () {
|
|
@@ -397,5 +412,53 @@ describe("DocumentManager", () => {
|
|
|
397
412
|
// count should still be 42 since we only modified title
|
|
398
413
|
expect((result.state as any).count).toBe(42);
|
|
399
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
|
+
});
|
|
400
463
|
});
|
|
401
464
|
});
|
|
@@ -182,25 +182,50 @@ describe("MimicConfig", () => {
|
|
|
182
182
|
expect(config.initial).toBeUndefined();
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
it("should accept initial state
|
|
185
|
+
it("should accept initial state object and convert to function", async () => {
|
|
186
186
|
const config = MimicConfig.make({
|
|
187
187
|
schema: TestSchema,
|
|
188
188
|
initial: { title: "My Document", count: 42 },
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
expect(config.initial).
|
|
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 });
|
|
192
195
|
});
|
|
193
196
|
|
|
194
|
-
it("should apply defaults for omitted fields in initial state", () => {
|
|
197
|
+
it("should apply defaults for omitted fields in initial state object", async () => {
|
|
195
198
|
const config = MimicConfig.make({
|
|
196
199
|
schema: TestSchema,
|
|
197
200
|
initial: { title: "My Document" }, // count has default of 0
|
|
198
201
|
});
|
|
199
202
|
|
|
200
|
-
|
|
203
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test-doc" }));
|
|
204
|
+
expect(result).toEqual({ title: "My Document", count: 0 });
|
|
201
205
|
});
|
|
202
206
|
|
|
203
|
-
it("should
|
|
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 () => {
|
|
204
229
|
const testLayer = MimicConfig.layer({
|
|
205
230
|
schema: TestSchema,
|
|
206
231
|
initial: { title: "From Layer", count: 100 },
|
|
@@ -209,14 +234,14 @@ describe("MimicConfig", () => {
|
|
|
209
234
|
const result = await Effect.runPromise(
|
|
210
235
|
Effect.gen(function* () {
|
|
211
236
|
const config = yield* MimicConfig.MimicServerConfigTag;
|
|
212
|
-
return config.initial;
|
|
237
|
+
return yield* config.initial!({ documentId: "layer-doc" });
|
|
213
238
|
}).pipe(Effect.provide(testLayer))
|
|
214
239
|
);
|
|
215
240
|
|
|
216
241
|
expect(result).toEqual({ title: "From Layer", count: 100 });
|
|
217
242
|
});
|
|
218
243
|
|
|
219
|
-
it("should work with schema that has required fields without defaults", () => {
|
|
244
|
+
it("should work with schema that has required fields without defaults", async () => {
|
|
220
245
|
const SchemaWithRequired = Primitive.Struct({
|
|
221
246
|
name: Primitive.String().required(),
|
|
222
247
|
optional: Primitive.String().default("default"),
|
|
@@ -227,10 +252,11 @@ describe("MimicConfig", () => {
|
|
|
227
252
|
initial: { name: "Required Name" },
|
|
228
253
|
});
|
|
229
254
|
|
|
230
|
-
|
|
255
|
+
const result = await Effect.runPromise(config.initial!({ documentId: "test" }));
|
|
256
|
+
expect(result).toEqual({ name: "Required Name", optional: "default" });
|
|
231
257
|
});
|
|
232
258
|
|
|
233
|
-
it("should work with all options including initial", () => {
|
|
259
|
+
it("should work with all options including initial object", async () => {
|
|
234
260
|
const config = MimicConfig.make({
|
|
235
261
|
schema: TestSchema,
|
|
236
262
|
maxIdleTime: "10 minutes",
|
|
@@ -241,7 +267,24 @@ describe("MimicConfig", () => {
|
|
|
241
267
|
expect(config.schema).toBe(TestSchema);
|
|
242
268
|
expect(Duration.toMillis(config.maxIdleTime)).toBe(10 * 60 * 1000);
|
|
243
269
|
expect(config.maxTransactionHistory).toBe(500);
|
|
244
|
-
|
|
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 });
|
|
245
288
|
});
|
|
246
289
|
});
|
|
247
290
|
});
|