@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.
- package/.turbo/turbo-build.log +24 -24
- package/dist/DocumentManager.cjs +3 -3
- package/dist/DocumentManager.d.cts +2 -2
- package/dist/DocumentManager.mjs +2 -2
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/MimicAuthService.d.cts +1 -1
- package/dist/MimicConfig.cjs +3 -1
- package/dist/MimicConfig.d.cts +17 -1
- package/dist/MimicConfig.d.cts.map +1 -1
- package/dist/MimicConfig.d.mts +16 -0
- package/dist/MimicConfig.d.mts.map +1 -1
- package/dist/MimicConfig.mjs +3 -1
- package/dist/MimicConfig.mjs.map +1 -1
- package/dist/MimicDataStorage.d.cts +1 -1
- package/dist/MimicServer.cjs +3 -111
- package/dist/MimicServer.d.cts +11 -97
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +11 -97
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +10 -117
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/PresenceManager.cjs +2 -2
- package/dist/PresenceManager.d.cts +1 -1
- 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/package.json +3 -3
- package/src/DocumentManager.ts +2 -2
- package/src/MimicConfig.ts +22 -1
- package/src/MimicServer.ts +11 -161
- package/tests/DocumentManager.test.ts +61 -0
- package/tests/MimicConfig.test.ts +72 -0
- package/tests/MimicServer.test.ts +55 -162
package/src/MimicConfig.ts
CHANGED
|
@@ -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
|
|
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
|
// =============================================================================
|
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
|
// =============================================================================
|
|
@@ -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
|
});
|