@voidhash/mimic-effect 0.0.4 → 0.0.6
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 +1 -1
- 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/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/dist/MimicServer.d.cts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { MimicServerConfigOptions } from "./MimicConfig.cjs";
|
|
1
|
+
import { InitialFn, MimicServerConfigOptions } from "./MimicConfig.cjs";
|
|
2
2
|
import { MimicDataStorageTag } from "./MimicDataStorage.cjs";
|
|
3
3
|
import { DocumentManagerTag } from "./DocumentManager.cjs";
|
|
4
4
|
import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
5
6
|
import * as Layer from "effect/Layer";
|
|
6
7
|
import { Presence, Primitive } from "@voidhash/mimic";
|
|
7
8
|
import { HttpLayerRouter } from "@effect/platform";
|
|
@@ -9,7 +10,7 @@ import { PathInput } from "@effect/platform/HttpRouter";
|
|
|
9
10
|
|
|
10
11
|
//#region src/MimicServer.d.ts
|
|
11
12
|
declare namespace MimicServer_d_exports {
|
|
12
|
-
export { MimicLayerOptions, documentManagerLayer, layerHttpLayerRouter };
|
|
13
|
+
export { MimicLayerOptions, MimicLayerRouterOptions, documentManagerLayer, layerHttpLayerRouter };
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Options for creating a Mimic server layer.
|
|
@@ -36,19 +37,33 @@ interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
|
36
37
|
readonly presence?: Presence.AnyPresence;
|
|
37
38
|
/**
|
|
38
39
|
* Initial state for new documents.
|
|
39
|
-
*
|
|
40
|
+
* Can be either:
|
|
41
|
+
* - A plain object with the initial state values
|
|
42
|
+
* - A function that receives context (with documentId) and returns an Effect producing the initial state
|
|
43
|
+
*
|
|
44
|
+
* When using a function that requires Effect services (has R requirements),
|
|
45
|
+
* you must also provide `initialLayer` to supply those dependencies.
|
|
40
46
|
*
|
|
41
47
|
* Type-safe: required fields (without defaults) must be provided,
|
|
42
48
|
* while optional fields and fields with defaults can be omitted.
|
|
43
49
|
*
|
|
44
50
|
* @default undefined (documents start empty or use schema defaults)
|
|
45
51
|
*/
|
|
46
|
-
readonly initial?: Primitive.InferSetInput<TSchema>;
|
|
52
|
+
readonly initial?: Primitive.InferSetInput<TSchema> | InitialFn<TSchema>;
|
|
47
53
|
}
|
|
48
54
|
/**
|
|
49
55
|
* Create the document manager layer.
|
|
50
56
|
*/
|
|
51
57
|
declare const documentManagerLayer: <TSchema extends Primitive.AnyPrimitive>(options: MimicServerConfigOptions<TSchema>) => Layer.Layer<DocumentManagerTag>;
|
|
58
|
+
/**
|
|
59
|
+
* Options for layerHttpLayerRouter including optional custom layers.
|
|
60
|
+
*/
|
|
61
|
+
interface MimicLayerRouterOptions<TSchema extends Primitive.AnyPrimitive> extends MimicLayerOptions<TSchema> {
|
|
62
|
+
/** Custom auth layer. Defaults to NoAuth (all connections allowed). */
|
|
63
|
+
readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;
|
|
64
|
+
/** Custom storage layer. Defaults to InMemoryDataStorage. */
|
|
65
|
+
readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;
|
|
66
|
+
}
|
|
52
67
|
/**
|
|
53
68
|
* Create a Mimic server layer that integrates with HttpLayerRouter.
|
|
54
69
|
*
|
|
@@ -95,12 +110,7 @@ declare const documentManagerLayer: <TSchema extends Primitive.AnyPrimitive>(opt
|
|
|
95
110
|
* );
|
|
96
111
|
* ```
|
|
97
112
|
*/
|
|
98
|
-
declare const layerHttpLayerRouter: <TSchema extends Primitive.AnyPrimitive>(
|
|
99
|
-
/** Custom auth layer. Defaults to NoAuth (all connections allowed). */
|
|
100
|
-
readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;
|
|
101
|
-
/** Custom storage layer. Defaults to InMemoryDataStorage. */
|
|
102
|
-
readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;
|
|
103
|
-
}) => Layer.Layer<never, never, HttpLayerRouter.HttpRouter>;
|
|
113
|
+
declare const layerHttpLayerRouter: <TSchema extends Primitive.AnyPrimitive, TError, TRequirements>(optionsEf: Effect.Effect<MimicLayerRouterOptions<TSchema>, TError, TRequirements>) => Layer.Layer<never, TError, TRequirements | HttpLayerRouter.HttpRouter>;
|
|
104
114
|
//#endregion
|
|
105
115
|
export { MimicServer_d_exports };
|
|
106
116
|
//# sourceMappingURL=MimicServer.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicServer.d.cts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MimicServer.d.cts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;UA6BiB,kCACC,SAAA,CAAU;;AAD5B;;;EAWmB,SAAA,QAAA,CAAA,EAJG,SAIH;EAUG;;;EAewD,SAAA,MAAA,EAzB3D,OAyB2D;EAAtB;;AAOxD;;EACgD,SAAA,qBAAA,CAAA,EAAA,MAAA;EAArC;;;;EA+DM,SAAA,QAAA,CAAA,EAtFK,QAAA,CAAS,WAsFS;EAAiB;;;;;;;;AAsDzD;;;;;;EAKa,SAAO,OAAA,CAAA,EAlIC,SAAA,CAAU,aAkIX,CAlIyB,OAkIzB,CAAA,GAlIoC,SAkIpC,CAlI0D,OAkI1D,CAAA;;;;;AACN,cA5HD,oBA4HC,EAAA,CAAA,gBA5HuC,SAAA,CAAU,YA4HjD,CAAA,CAAA,OAAA,EA3HH,wBA2HG,CA3HkC,OA2HlC,CAAA,EAAA,GA1HX,KAAA,CAAM,KA0HK,CA1HC,kBA0HD,CAAA;;;;UA5DG,wCAAwC,SAAA,CAAU,sBACzD,kBAAkB;;uBAEL,KAAA,CAAM,MAAM;;0BAET,KAAA,CAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiDzB,uCACK,SAAA,CAAU,gDAIf,MAAA,CAAO,OAAO,wBAAwB,UAAU,QAAQ,mBAClE,KAAA,CAAM,aAAa,QAAQ,gBAAgB,eAAA,CAAgB"}
|
package/dist/MimicServer.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { MimicServerConfigOptions } from "./MimicConfig.mjs";
|
|
1
|
+
import { InitialFn, MimicServerConfigOptions } from "./MimicConfig.mjs";
|
|
2
2
|
import { MimicDataStorageTag } from "./MimicDataStorage.mjs";
|
|
3
3
|
import { DocumentManagerTag } from "./DocumentManager.mjs";
|
|
4
4
|
import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
5
6
|
import * as Layer from "effect/Layer";
|
|
6
7
|
import { Presence, Primitive } from "@voidhash/mimic";
|
|
7
8
|
import { HttpLayerRouter } from "@effect/platform";
|
|
@@ -9,7 +10,7 @@ import { PathInput } from "@effect/platform/HttpRouter";
|
|
|
9
10
|
|
|
10
11
|
//#region src/MimicServer.d.ts
|
|
11
12
|
declare namespace MimicServer_d_exports {
|
|
12
|
-
export { MimicLayerOptions, documentManagerLayer, layerHttpLayerRouter };
|
|
13
|
+
export { MimicLayerOptions, MimicLayerRouterOptions, documentManagerLayer, layerHttpLayerRouter };
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Options for creating a Mimic server layer.
|
|
@@ -36,19 +37,33 @@ interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
|
36
37
|
readonly presence?: Presence.AnyPresence;
|
|
37
38
|
/**
|
|
38
39
|
* Initial state for new documents.
|
|
39
|
-
*
|
|
40
|
+
* Can be either:
|
|
41
|
+
* - A plain object with the initial state values
|
|
42
|
+
* - A function that receives context (with documentId) and returns an Effect producing the initial state
|
|
43
|
+
*
|
|
44
|
+
* When using a function that requires Effect services (has R requirements),
|
|
45
|
+
* you must also provide `initialLayer` to supply those dependencies.
|
|
40
46
|
*
|
|
41
47
|
* Type-safe: required fields (without defaults) must be provided,
|
|
42
48
|
* while optional fields and fields with defaults can be omitted.
|
|
43
49
|
*
|
|
44
50
|
* @default undefined (documents start empty or use schema defaults)
|
|
45
51
|
*/
|
|
46
|
-
readonly initial?: Primitive.InferSetInput<TSchema>;
|
|
52
|
+
readonly initial?: Primitive.InferSetInput<TSchema> | InitialFn<TSchema>;
|
|
47
53
|
}
|
|
48
54
|
/**
|
|
49
55
|
* Create the document manager layer.
|
|
50
56
|
*/
|
|
51
57
|
declare const documentManagerLayer: <TSchema extends Primitive.AnyPrimitive>(options: MimicServerConfigOptions<TSchema>) => Layer.Layer<DocumentManagerTag>;
|
|
58
|
+
/**
|
|
59
|
+
* Options for layerHttpLayerRouter including optional custom layers.
|
|
60
|
+
*/
|
|
61
|
+
interface MimicLayerRouterOptions<TSchema extends Primitive.AnyPrimitive> extends MimicLayerOptions<TSchema> {
|
|
62
|
+
/** Custom auth layer. Defaults to NoAuth (all connections allowed). */
|
|
63
|
+
readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;
|
|
64
|
+
/** Custom storage layer. Defaults to InMemoryDataStorage. */
|
|
65
|
+
readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;
|
|
66
|
+
}
|
|
52
67
|
/**
|
|
53
68
|
* Create a Mimic server layer that integrates with HttpLayerRouter.
|
|
54
69
|
*
|
|
@@ -95,12 +110,7 @@ declare const documentManagerLayer: <TSchema extends Primitive.AnyPrimitive>(opt
|
|
|
95
110
|
* );
|
|
96
111
|
* ```
|
|
97
112
|
*/
|
|
98
|
-
declare const layerHttpLayerRouter: <TSchema extends Primitive.AnyPrimitive>(
|
|
99
|
-
/** Custom auth layer. Defaults to NoAuth (all connections allowed). */
|
|
100
|
-
readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;
|
|
101
|
-
/** Custom storage layer. Defaults to InMemoryDataStorage. */
|
|
102
|
-
readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;
|
|
103
|
-
}) => Layer.Layer<never, never, HttpLayerRouter.HttpRouter>;
|
|
113
|
+
declare const layerHttpLayerRouter: <TSchema extends Primitive.AnyPrimitive, TError, TRequirements>(optionsEf: Effect.Effect<MimicLayerRouterOptions<TSchema>, TError, TRequirements>) => Layer.Layer<never, TError, TRequirements | HttpLayerRouter.HttpRouter>;
|
|
104
114
|
//#endregion
|
|
105
115
|
export { MimicServer_d_exports };
|
|
106
116
|
//# sourceMappingURL=MimicServer.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicServer.d.mts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MimicServer.d.mts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;UA6BiB,kCACC,SAAA,CAAU;;AAD5B;;;EAWmB,SAAA,QAAA,CAAA,EAJG,SAIH;EAUG;;;EAewD,SAAA,MAAA,EAzB3D,OAyB2D;EAAtB;;AAOxD;;EACgD,SAAA,qBAAA,CAAA,EAAA,MAAA;EAArC;;;;EA+DM,SAAA,QAAA,CAAA,EAtFK,QAAA,CAAS,WAsFS;EAAiB;;;;;;;;AAsDzD;;;;;;EAKa,SAAO,OAAA,CAAA,EAlIC,SAAA,CAAU,aAkIX,CAlIyB,OAkIzB,CAAA,GAlIoC,SAkIpC,CAlI0D,OAkI1D,CAAA;;;;;AACN,cA5HD,oBA4HC,EAAA,CAAA,gBA5HuC,SAAA,CAAU,YA4HjD,CAAA,CAAA,OAAA,EA3HH,wBA2HG,CA3HkC,OA2HlC,CAAA,EAAA,GA1HX,KAAA,CAAM,KA0HK,CA1HC,kBA0HD,CAAA;;;;UA5DG,wCAAwC,SAAA,CAAU,sBACzD,kBAAkB;;uBAEL,KAAA,CAAM,MAAM;;0BAET,KAAA,CAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiDzB,uCACK,SAAA,CAAU,gDAIf,MAAA,CAAO,OAAO,wBAAwB,UAAU,QAAQ,mBAClE,KAAA,CAAM,aAAa,QAAQ,gBAAgB,eAAA,CAAgB"}
|
package/dist/MimicServer.mjs
CHANGED
|
@@ -92,23 +92,26 @@ const makeMimicHandler = Effect.gen(function* () {
|
|
|
92
92
|
* );
|
|
93
93
|
* ```
|
|
94
94
|
*/
|
|
95
|
-
const layerHttpLayerRouter = (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
const layerHttpLayerRouter = (optionsEf) => {
|
|
96
|
+
return Layer.unwrapScoped(Effect.gen(function* () {
|
|
97
|
+
var _options$basePath, _options$authLayer, _options$storageLayer;
|
|
98
|
+
const options = yield* optionsEf;
|
|
99
|
+
const wsPath = `${(_options$basePath = options.basePath) !== null && _options$basePath !== void 0 ? _options$basePath : "/mimic"}/doc/*`;
|
|
100
|
+
const configLayer = layer({
|
|
101
|
+
schema: options.schema,
|
|
102
|
+
maxTransactionHistory: options.maxTransactionHistory,
|
|
103
|
+
presence: options.presence,
|
|
104
|
+
initial: options.initial
|
|
105
|
+
});
|
|
106
|
+
const authLayer = (_options$authLayer = options.authLayer) !== null && _options$authLayer !== void 0 ? _options$authLayer : layerDefault$1;
|
|
107
|
+
const storageLayer = (_options$storageLayer = options.storageLayer) !== null && _options$storageLayer !== void 0 ? _options$storageLayer : layerDefault;
|
|
108
|
+
const depsLayer = Layer.mergeAll(configLayer, authLayer, storageLayer);
|
|
109
|
+
return Layer.scopedDiscard(Effect.gen(function* () {
|
|
110
|
+
const router = yield* HttpLayerRouter.HttpRouter;
|
|
111
|
+
const handler = yield* makeMimicHandler;
|
|
112
|
+
yield* router.add("GET", wsPath, handler);
|
|
113
|
+
})).pipe(Layer.provide(layer$1), Layer.provide(layer$2), Layer.provide(depsLayer));
|
|
114
|
+
}));
|
|
112
115
|
};
|
|
113
116
|
|
|
114
117
|
//#endregion
|
package/dist/MimicServer.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicServer.mjs","names":["MimicConfig.layer","InMemoryDataStorage.layerDefault","NoAuth.layerDefault","MimicConfig.MimicServerConfigTag","DocumentManager.DocumentManagerTag","PresenceManager.PresenceManagerTag","WebSocketHandler.extractDocumentId","WebSocketHandler.handleConnection","wsPath: PathInput","DocumentManager.layer","PresenceManager.layer"],"sources":["../src/MimicServer.ts"],"sourcesContent":["/**\n * @since 0.0.1\n * Mimic server layer composition.\n */\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as Context from \"effect/Context\";\nimport type * as Socket from \"@effect/platform/Socket\";\nimport { SocketServer } from \"@effect/platform/SocketServer\";\nimport type { Primitive, Presence } from \"@voidhash/mimic\";\n\nimport * as DocumentManager from \"./DocumentManager.js\";\nimport * as WebSocketHandler from \"./WebSocketHandler.js\";\nimport * as MimicConfig from \"./MimicConfig.js\";\nimport { MimicDataStorageTag } from \"./MimicDataStorage.js\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService.js\";\nimport * as PresenceManager from \"./PresenceManager.js\";\nimport * as InMemoryDataStorage from \"./storage/InMemoryDataStorage.js\";\nimport * as NoAuth from \"./auth/NoAuth.js\";\nimport { HttpLayerRouter, HttpServerRequest, HttpServerResponse } from \"@effect/platform\";\nimport { PathInput } from \"@effect/platform/HttpRouter\";\n\n// =============================================================================\n// Layer Composition Options\n// =============================================================================\n\n/**\n * Options for creating a Mimic server layer.\n */\nexport interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {\n /**\n * Base path for document routes (used for path matching).\n * @example \"/mimic/todo\" - documents accessed at \"/mimic/todo/:documentId\"\n */\n readonly basePath?: PathInput;\n /**\n * The schema defining the document structure.\n */\n readonly schema: TSchema;\n /**\n * Maximum number of processed transaction IDs to track for deduplication.\n * @default 1000\n */\n readonly maxTransactionHistory?: number;\n /**\n * Optional presence schema for ephemeral per-user data.\n * When provided, enables presence features on WebSocket connections.\n */\n readonly presence?: Presence.AnyPresence;\n /**\n * Initial state for new documents.\n * Used when a document is created and no existing state is found in storage.\n *\n * Type-safe: required fields (without defaults) must be provided,\n * while optional fields and fields with defaults can be omitted.\n *\n * @default undefined (documents start empty or use schema defaults)\n */\n readonly initial?: Primitive.InferSetInput<TSchema>;\n}\n\n\n/**\n * Create the document manager layer.\n */\nexport const documentManagerLayer = <TSchema extends Primitive.AnyPrimitive>(\n options: MimicConfig.MimicServerConfigOptions<TSchema>\n): Layer.Layer<DocumentManager.DocumentManagerTag> =>\n DocumentManager.layer.pipe(\n Layer.provide(MimicConfig.layer(options)),\n // Provide defaults\n Layer.provide(InMemoryDataStorage.layerDefault),\n Layer.provide(NoAuth.layerDefault)\n );\n\n/**\n * Create the HTTP handler effect for WebSocket upgrade.\n * This handler:\n * 1. Extracts the document ID from the URL path\n * 2. Upgrades the HTTP connection to WebSocket\n * 3. Delegates to the WebSocketHandler for document sync\n */\nconst makeMimicHandler = Effect.gen(function* () {\n const config = yield* MimicConfig.MimicServerConfigTag;\n const authService = yield* MimicAuthServiceTag;\n const documentManager = yield* DocumentManager.DocumentManagerTag;\n const presenceManager = yield* PresenceManager.PresenceManagerTag;\n\n return Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n\n // Extract document ID from the URL path\n // Expected format: /basePath/doc/{documentId}\n const documentId = yield* WebSocketHandler.extractDocumentId(request.url);\n\n // Upgrade to WebSocket\n const socket = yield* request.upgrade;\n\n // Handle the WebSocket connection\n yield* WebSocketHandler.handleConnection(socket, request.url).pipe(\n Effect.provideService(MimicConfig.MimicServerConfigTag, config),\n Effect.provideService(MimicAuthServiceTag, authService),\n Effect.provideService(DocumentManager.DocumentManagerTag, documentManager),\n Effect.provideService(PresenceManager.PresenceManagerTag, presenceManager),\n Effect.scoped,\n Effect.catchAll((error) =>\n Effect.logError(\"WebSocket connection error\", error)\n )\n );\n\n // Return empty response - the WebSocket upgrade handles the connection\n return HttpServerResponse.empty();\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logWarning(\"WebSocket upgrade failed\", error);\n return HttpServerResponse.text(\"WebSocket upgrade failed\", {\n status: 400,\n });\n })\n )\n );\n});\n\n\n\n/**\n * Create a Mimic server layer that integrates with HttpLayerRouter.\n *\n * This function creates a layer that:\n * 1. Registers a WebSocket route at the specified base path\n * 2. Handles WebSocket upgrades for document sync\n * 3. Provides all required dependencies (config, auth, storage, document manager)\n *\n * By default, uses in-memory storage and no authentication.\n * To override these defaults, provide custom layers before the defaults:\n *\n * @example\n * ```typescript\n * import { MimicServer, MimicAuthService } from \"@voidhash/mimic-effect\";\n * import { HttpLayerRouter } from \"@effect/platform\";\n * import { Primitive } from \"@voidhash/mimic\";\n *\n * const TodoSchema = Primitive.Struct({\n * title: Primitive.String(),\n * completed: Primitive.Boolean(),\n * });\n *\n * // Create the Mimic route layer with defaults\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * basePath: \"/mimic/todo\",\n * schema: TodoSchema\n * });\n *\n * // Or with custom auth - use Layer.provide to inject before defaults\n * const MimicRouteWithAuth = MimicServer.layerHttpLayerRouter({\n * basePath: \"/mimic/todo\",\n * schema: TodoSchema,\n * authLayer: MimicAuthService.layer({\n * authHandler: (token) => ({ success: true, userId: token })\n * })\n * });\n *\n * // Merge with other routes and serve\n * const AllRoutes = Layer.mergeAll(MimicRoute, OtherRoutes);\n * HttpLayerRouter.serve(AllRoutes).pipe(\n * Layer.provide(BunHttpServer.layer({ port: 3000 })),\n * Layer.launch,\n * BunRuntime.runMain\n * );\n * ```\n */\nexport const layerHttpLayerRouter = <TSchema extends Primitive.AnyPrimitive>(\n options: MimicLayerOptions<TSchema> & {\n /** Custom auth layer. Defaults to NoAuth (all connections allowed). */\n readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;\n /** Custom storage layer. Defaults to InMemoryDataStorage. */\n readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;\n }\n) => {\n // Build the base path pattern for WebSocket routes\n // Append /doc/* to match /basePath/doc/{documentId}\n const basePath = options.basePath ?? \"/mimic\";\n const wsPath: PathInput = `${basePath}/doc/*` as PathInput;\n\n // Create the config layer\n const configLayer = MimicConfig.layer({\n schema: options.schema,\n maxTransactionHistory: options.maxTransactionHistory,\n presence: options.presence,\n initial: options.initial,\n });\n\n // Use provided layers or defaults\n const authLayer = options.authLayer ?? NoAuth.layerDefault;\n const storageLayer = options.storageLayer ?? InMemoryDataStorage.layerDefault;\n\n // Create the route registration effect\n const registerRoute = Effect.gen(function* () {\n const router = yield* HttpLayerRouter.HttpRouter;\n const handler = yield* makeMimicHandler;\n yield* router.add(\"GET\", wsPath, handler);\n });\n\n // Build the layer with all dependencies\n return Layer.scopedDiscard(registerRoute).pipe(\n Layer.provide(DocumentManager.layer),\n Layer.provide(PresenceManager.layer),\n Layer.provide(configLayer),\n Layer.provide(storageLayer),\n Layer.provide(authLayer)\n );\n};"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAiEA,MAAa,wBACX,oBAEsB,KACpB,MAAM,QAAQA,MAAkB,QAAQ,CAAC,EAEzC,MAAM,QAAQC,aAAiC,EAC/C,MAAM,QAAQC,eAAoB,CACnC;;;;;;;;AASH,MAAM,mBAAmB,OAAO,IAAI,aAAa;CAC/C,MAAM,SAAS,OAAOC;CACtB,MAAM,cAAc,OAAO;CAC3B,MAAM,kBAAkB,OAAOC;CAC/B,MAAM,kBAAkB,OAAOC;AAE/B,QAAO,OAAO,IAAI,aAAa;EAC7B,MAAM,UAAU,OAAO,kBAAkB;AAItB,SAAOC,kBAAmC,QAAQ,IAAI;EAGzE,MAAM,SAAS,OAAO,QAAQ;AAG9B,SAAOC,iBAAkC,QAAQ,QAAQ,IAAI,CAAC,KAC5D,OAAO,eAAeJ,sBAAkC,OAAO,EAC/D,OAAO,eAAe,qBAAqB,YAAY,EACvD,OAAO,eAAeC,oBAAoC,gBAAgB,EAC1E,OAAO,eAAeC,oBAAoC,gBAAgB,EAC1E,OAAO,QACP,OAAO,UAAU,UACf,OAAO,SAAS,8BAA8B,MAAM,CACrD,CACF;AAGD,SAAO,mBAAmB,OAAO;GACjC,CAAC,KACD,OAAO,UAAU,UACf,OAAO,IAAI,aAAa;AACtB,SAAO,OAAO,WAAW,4BAA4B,MAAM;AAC3D,SAAO,mBAAmB,KAAK,4BAA4B,EACzD,QAAQ,KACT,CAAC;GACF,CACH,CACF;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDF,MAAa,wBACX,YAMG;;CAIH,MAAMG,SAAoB,wBADT,QAAQ,yEAAY,SACC;CAGtC,MAAM,cAAcR,MAAkB;EACpC,QAAQ,QAAQ;EAChB,uBAAuB,QAAQ;EAC/B,UAAU,QAAQ;EAClB,SAAS,QAAQ;EAClB,CAAC;CAGF,MAAM,kCAAY,QAAQ,4EAAaE;CACvC,MAAM,wCAAe,QAAQ,qFAAgBD;CAG7C,MAAM,gBAAgB,OAAO,IAAI,aAAa;EAC5C,MAAM,SAAS,OAAO,gBAAgB;EACtC,MAAM,UAAU,OAAO;AACvB,SAAO,OAAO,IAAI,OAAO,QAAQ,QAAQ;GACzC;AAGF,QAAO,MAAM,cAAc,cAAc,CAAC,KACxC,MAAM,QAAQQ,QAAsB,EACpC,MAAM,QAAQC,QAAsB,EACpC,MAAM,QAAQ,YAAY,EAC1B,MAAM,QAAQ,aAAa,EAC3B,MAAM,QAAQ,UAAU,CACzB"}
|
|
1
|
+
{"version":3,"file":"MimicServer.mjs","names":["MimicConfig.layer","InMemoryDataStorage.layerDefault","NoAuth.layerDefault","MimicConfig.MimicServerConfigTag","DocumentManager.DocumentManagerTag","PresenceManager.PresenceManagerTag","WebSocketHandler.extractDocumentId","WebSocketHandler.handleConnection","wsPath: PathInput","DocumentManager.layer","PresenceManager.layer"],"sources":["../src/MimicServer.ts"],"sourcesContent":["/**\n * @since 0.0.1\n * Mimic server layer composition.\n */\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as Context from \"effect/Context\";\nimport type * as Socket from \"@effect/platform/Socket\";\nimport { SocketServer } from \"@effect/platform/SocketServer\";\nimport type { Primitive, Presence } from \"@voidhash/mimic\";\n\nimport * as DocumentManager from \"./DocumentManager.js\";\nimport * as WebSocketHandler from \"./WebSocketHandler.js\";\nimport * as MimicConfig from \"./MimicConfig.js\";\nimport { MimicDataStorageTag } from \"./MimicDataStorage.js\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService.js\";\nimport * as PresenceManager from \"./PresenceManager.js\";\nimport * as InMemoryDataStorage from \"./storage/InMemoryDataStorage.js\";\nimport * as NoAuth from \"./auth/NoAuth.js\";\nimport { HttpLayerRouter, HttpServerRequest, HttpServerResponse } from \"@effect/platform\";\nimport { PathInput } from \"@effect/platform/HttpRouter\";\n\n// =============================================================================\n// Layer Composition Options\n// =============================================================================\n\n/**\n * Options for creating a Mimic server layer.\n */\nexport interface MimicLayerOptions<\n TSchema extends Primitive.AnyPrimitive,\n> {\n /**\n * Base path for document routes (used for path matching).\n * @example \"/mimic/todo\" - documents accessed at \"/mimic/todo/:documentId\"\n */\n readonly basePath?: PathInput;\n /**\n * The schema defining the document structure.\n */\n readonly schema: TSchema;\n /**\n * Maximum number of processed transaction IDs to track for deduplication.\n * @default 1000\n */\n readonly maxTransactionHistory?: number;\n /**\n * Optional presence schema for ephemeral per-user data.\n * When provided, enables presence features on WebSocket connections.\n */\n readonly presence?: Presence.AnyPresence;\n /**\n * Initial state for new documents.\n * Can be either:\n * - A plain object with the initial state values\n * - A function that receives context (with documentId) and returns an Effect producing the initial state\n *\n * When using a function that requires Effect services (has R requirements),\n * you must also provide `initialLayer` to supply those dependencies.\n *\n * Type-safe: required fields (without defaults) must be provided,\n * while optional fields and fields with defaults can be omitted.\n *\n * @default undefined (documents start empty or use schema defaults)\n */\n readonly initial?: Primitive.InferSetInput<TSchema> | MimicConfig.InitialFn<TSchema>;\n}\n\n\n/**\n * Create the document manager layer.\n */\nexport const documentManagerLayer = <TSchema extends Primitive.AnyPrimitive>(\n options: MimicConfig.MimicServerConfigOptions<TSchema>\n): Layer.Layer<DocumentManager.DocumentManagerTag> =>\n DocumentManager.layer.pipe(\n Layer.provide(MimicConfig.layer(options)),\n // Provide defaults\n Layer.provide(InMemoryDataStorage.layerDefault),\n Layer.provide(NoAuth.layerDefault)\n );\n\n/**\n * Create the HTTP handler effect for WebSocket upgrade.\n * This handler:\n * 1. Extracts the document ID from the URL path\n * 2. Upgrades the HTTP connection to WebSocket\n * 3. Delegates to the WebSocketHandler for document sync\n */\nconst makeMimicHandler = Effect.gen(function* () {\n const config = yield* MimicConfig.MimicServerConfigTag;\n const authService = yield* MimicAuthServiceTag;\n const documentManager = yield* DocumentManager.DocumentManagerTag;\n const presenceManager = yield* PresenceManager.PresenceManagerTag;\n\n return Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n\n // Extract document ID from the URL path\n // Expected format: /basePath/doc/{documentId}\n const documentId = yield* WebSocketHandler.extractDocumentId(request.url);\n\n // Upgrade to WebSocket\n const socket = yield* request.upgrade;\n\n // Handle the WebSocket connection\n yield* WebSocketHandler.handleConnection(socket, request.url).pipe(\n Effect.provideService(MimicConfig.MimicServerConfigTag, config),\n Effect.provideService(MimicAuthServiceTag, authService),\n Effect.provideService(DocumentManager.DocumentManagerTag, documentManager),\n Effect.provideService(PresenceManager.PresenceManagerTag, presenceManager),\n Effect.scoped,\n Effect.catchAll((error) =>\n Effect.logError(\"WebSocket connection error\", error)\n )\n );\n\n // Return empty response - the WebSocket upgrade handles the connection\n return HttpServerResponse.empty();\n }).pipe(\n Effect.catchAll((error) =>\n Effect.gen(function* () {\n yield* Effect.logWarning(\"WebSocket upgrade failed\", error);\n return HttpServerResponse.text(\"WebSocket upgrade failed\", {\n status: 400,\n });\n })\n )\n );\n});\n\n\n\n/**\n * Options for layerHttpLayerRouter including optional custom layers.\n */\nexport interface MimicLayerRouterOptions<TSchema extends Primitive.AnyPrimitive>\n extends MimicLayerOptions<TSchema> {\n /** Custom auth layer. Defaults to NoAuth (all connections allowed). */\n readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;\n /** Custom storage layer. Defaults to InMemoryDataStorage. */\n readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;\n}\n\n/**\n * Create a Mimic server layer that integrates with HttpLayerRouter.\n *\n * This function creates a layer that:\n * 1. Registers a WebSocket route at the specified base path\n * 2. Handles WebSocket upgrades for document sync\n * 3. Provides all required dependencies (config, auth, storage, document manager)\n *\n * By default, uses in-memory storage and no authentication.\n * To override these defaults, provide custom layers before the defaults:\n *\n * @example\n * ```typescript\n * import { MimicServer, MimicAuthService } from \"@voidhash/mimic-effect\";\n * import { HttpLayerRouter } from \"@effect/platform\";\n * import { Primitive } from \"@voidhash/mimic\";\n *\n * const TodoSchema = Primitive.Struct({\n * title: Primitive.String(),\n * completed: Primitive.Boolean(),\n * });\n *\n * // Create the Mimic route layer with defaults\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * basePath: \"/mimic/todo\",\n * schema: TodoSchema\n * });\n *\n * // Or with custom auth - use Layer.provide to inject before defaults\n * const MimicRouteWithAuth = MimicServer.layerHttpLayerRouter({\n * basePath: \"/mimic/todo\",\n * schema: TodoSchema,\n * authLayer: MimicAuthService.layer({\n * authHandler: (token) => ({ success: true, userId: token })\n * })\n * });\n *\n * // Merge with other routes and serve\n * const AllRoutes = Layer.mergeAll(MimicRoute, OtherRoutes);\n * HttpLayerRouter.serve(AllRoutes).pipe(\n * Layer.provide(BunHttpServer.layer({ port: 3000 })),\n * Layer.launch,\n * BunRuntime.runMain\n * );\n * ```\n */\nexport const layerHttpLayerRouter = <\n TSchema extends Primitive.AnyPrimitive,\n TError,\n TRequirements\n>(\n optionsEf: Effect.Effect<MimicLayerRouterOptions<TSchema>, TError, TRequirements>\n): Layer.Layer<never, TError, TRequirements | HttpLayerRouter.HttpRouter> => {\n return Layer.unwrapScoped(\n Effect.gen(function* () {\n const options = yield* optionsEf;\n\n // Build the base path pattern for WebSocket routes\n // Append /doc/* to match /basePath/doc/{documentId}\n const basePath = options.basePath ?? \"/mimic\";\n const wsPath: PathInput = `${basePath}/doc/*` as PathInput;\n\n // Create the config layer with properly typed initial function\n const configLayer = MimicConfig.layer<TSchema>({\n schema: options.schema,\n maxTransactionHistory: options.maxTransactionHistory,\n presence: options.presence,\n initial: options.initial,\n });\n\n // Use provided layers or defaults\n const authLayer = options.authLayer ?? NoAuth.layerDefault;\n const storageLayer = options.storageLayer ?? InMemoryDataStorage.layerDefault;\n\n // Combine all dependency layers\n const depsLayer = Layer.mergeAll(configLayer, authLayer, storageLayer);\n\n // Create the route registration layer\n const routeLayer = Layer.scopedDiscard(\n Effect.gen(function* () {\n const router = yield* HttpLayerRouter.HttpRouter;\n const handler = yield* makeMimicHandler;\n yield* router.add(\"GET\", wsPath, handler);\n })\n );\n\n // Build the complete layer with all dependencies provided\n return routeLayer.pipe(\n Layer.provide(DocumentManager.layer),\n Layer.provide(PresenceManager.layer),\n Layer.provide(depsLayer),\n );\n })\n );\n};"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwEA,MAAa,wBACX,oBAEsB,KACpB,MAAM,QAAQA,MAAkB,QAAQ,CAAC,EAEzC,MAAM,QAAQC,aAAiC,EAC/C,MAAM,QAAQC,eAAoB,CACnC;;;;;;;;AASH,MAAM,mBAAmB,OAAO,IAAI,aAAa;CAC/C,MAAM,SAAS,OAAOC;CACtB,MAAM,cAAc,OAAO;CAC3B,MAAM,kBAAkB,OAAOC;CAC/B,MAAM,kBAAkB,OAAOC;AAE/B,QAAO,OAAO,IAAI,aAAa;EAC7B,MAAM,UAAU,OAAO,kBAAkB;AAItB,SAAOC,kBAAmC,QAAQ,IAAI;EAGzE,MAAM,SAAS,OAAO,QAAQ;AAG9B,SAAOC,iBAAkC,QAAQ,QAAQ,IAAI,CAAC,KAC5D,OAAO,eAAeJ,sBAAkC,OAAO,EAC/D,OAAO,eAAe,qBAAqB,YAAY,EACvD,OAAO,eAAeC,oBAAoC,gBAAgB,EAC1E,OAAO,eAAeC,oBAAoC,gBAAgB,EAC1E,OAAO,QACP,OAAO,UAAU,UACf,OAAO,SAAS,8BAA8B,MAAM,CACrD,CACF;AAGD,SAAO,mBAAmB,OAAO;GACjC,CAAC,KACD,OAAO,UAAU,UACf,OAAO,IAAI,aAAa;AACtB,SAAO,OAAO,WAAW,4BAA4B,MAAM;AAC3D,SAAO,mBAAmB,KAAK,4BAA4B,EACzD,QAAQ,KACT,CAAC;GACF,CACH,CACF;EACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DF,MAAa,wBAKX,cAC2E;AAC3E,QAAO,MAAM,aACX,OAAO,IAAI,aAAa;;EACtB,MAAM,UAAU,OAAO;EAKvB,MAAMG,SAAoB,wBADT,QAAQ,yEAAY,SACC;EAGtC,MAAM,cAAcR,MAA2B;GAC7C,QAAQ,QAAQ;GAChB,uBAAuB,QAAQ;GAC/B,UAAU,QAAQ;GAClB,SAAS,QAAQ;GAClB,CAAC;EAGF,MAAM,kCAAY,QAAQ,4EAAaE;EACvC,MAAM,wCAAe,QAAQ,qFAAgBD;EAG7C,MAAM,YAAY,MAAM,SAAS,aAAa,WAAW,aAAa;AAYtE,SATmB,MAAM,cACvB,OAAO,IAAI,aAAa;GACtB,MAAM,SAAS,OAAO,gBAAgB;GACtC,MAAM,UAAU,OAAO;AACvB,UAAO,OAAO,IAAI,OAAO,QAAQ,QAAQ;IACzC,CACH,CAGiB,KAChB,MAAM,QAAQQ,QAAsB,EACpC,MAAM,QAAQC,QAAsB,EACpC,MAAM,QAAQ,UAAU,CACzB;GACD,CACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidhash/mimic-effect",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"typescript": "5.8.3",
|
|
25
25
|
"vite-tsconfig-paths": "^5.1.4",
|
|
26
26
|
"vitest": "^3.2.4",
|
|
27
|
-
"@voidhash/tsconfig": "0.0.
|
|
27
|
+
"@voidhash/tsconfig": "0.0.6"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"effect": "^3.19.12",
|
|
31
|
-
"@voidhash/mimic": "0.0.
|
|
31
|
+
"@voidhash/mimic": "0.0.6"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsdown",
|
package/src/DocumentManager.ts
CHANGED
|
@@ -113,10 +113,12 @@ const makeDocumentManager = Effect.gen(function* () {
|
|
|
113
113
|
() => Effect.succeed(undefined)
|
|
114
114
|
);
|
|
115
115
|
|
|
116
|
-
// Transform loaded state with onLoad hook, or
|
|
116
|
+
// Transform loaded state with onLoad hook, or compute initial state for new docs
|
|
117
117
|
const initialState = rawState !== undefined
|
|
118
118
|
? yield* storage.onLoad(rawState)
|
|
119
|
-
: config.initial
|
|
119
|
+
: config.initial !== undefined
|
|
120
|
+
? yield* config.initial({ documentId })
|
|
121
|
+
: undefined;
|
|
120
122
|
|
|
121
123
|
// Create PubSub for broadcasting
|
|
122
124
|
const pubsub = yield* PubSub.unbounded<Protocol.ServerBroadcast>();
|
|
@@ -124,7 +126,7 @@ const makeDocumentManager = Effect.gen(function* () {
|
|
|
124
126
|
// Create ServerDocument with broadcast callback
|
|
125
127
|
const serverDocument = ServerDocument.make({
|
|
126
128
|
schema: config.schema,
|
|
127
|
-
initialState: initialState as Primitive.
|
|
129
|
+
initialState: initialState as Primitive.InferSetInput<typeof config.schema> | undefined,
|
|
128
130
|
maxTransactionHistory: config.maxTransactionHistory,
|
|
129
131
|
onBroadcast: (transactionMessage) => {
|
|
130
132
|
// Get current state and save to storage
|
package/src/MimicConfig.ts
CHANGED
|
@@ -5,9 +5,32 @@
|
|
|
5
5
|
import * as Context from "effect/Context";
|
|
6
6
|
import * as Duration from "effect/Duration";
|
|
7
7
|
import type { DurationInput } from "effect/Duration";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
8
9
|
import * as Layer from "effect/Layer";
|
|
9
10
|
import { Primitive, Presence } from "@voidhash/mimic";
|
|
10
11
|
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Initial State Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Context available when computing initial state for a document.
|
|
18
|
+
*/
|
|
19
|
+
export interface InitialContext {
|
|
20
|
+
/**
|
|
21
|
+
* The document ID being initialized.
|
|
22
|
+
*/
|
|
23
|
+
readonly documentId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Function that computes initial state for a document.
|
|
28
|
+
* Receives context with the document ID and returns an Effect that produces the initial state.
|
|
29
|
+
*/
|
|
30
|
+
export type InitialFn<TSchema extends Primitive.AnyPrimitive> = (
|
|
31
|
+
context: InitialContext
|
|
32
|
+
) => Effect.Effect<Primitive.InferSetInput<TSchema>>;
|
|
33
|
+
|
|
11
34
|
// =============================================================================
|
|
12
35
|
// Mimic Server Configuration
|
|
13
36
|
// =============================================================================
|
|
@@ -18,7 +41,9 @@ import { Primitive, Presence } from "@voidhash/mimic";
|
|
|
18
41
|
* Note: Authentication and persistence are now handled by injectable services
|
|
19
42
|
* (MimicAuthService and MimicDataStorage) rather than config options.
|
|
20
43
|
*/
|
|
21
|
-
export interface MimicServerConfig<
|
|
44
|
+
export interface MimicServerConfig<
|
|
45
|
+
TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive,
|
|
46
|
+
> {
|
|
22
47
|
/**
|
|
23
48
|
* The schema defining the document structure.
|
|
24
49
|
*/
|
|
@@ -56,17 +81,20 @@ export interface MimicServerConfig<TSchema extends Primitive.AnyPrimitive = Prim
|
|
|
56
81
|
readonly presence: Presence.AnyPresence | undefined;
|
|
57
82
|
|
|
58
83
|
/**
|
|
59
|
-
* Initial state for new documents.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
84
|
+
* Initial state function for new documents.
|
|
85
|
+
* Called when a document is created and no existing state is found in storage.
|
|
86
|
+
* Receives the document ID and returns an Effect that produces the initial state.
|
|
87
|
+
* @default undefined (documents start empty or use schema defaults)
|
|
62
88
|
*/
|
|
63
|
-
readonly initial:
|
|
89
|
+
readonly initial: InitialFn<TSchema> | undefined;
|
|
64
90
|
}
|
|
65
91
|
|
|
66
92
|
/**
|
|
67
93
|
* Options for creating a MimicServerConfig.
|
|
68
94
|
*/
|
|
69
|
-
export interface MimicServerConfigOptions<
|
|
95
|
+
export interface MimicServerConfigOptions<
|
|
96
|
+
TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive,
|
|
97
|
+
> {
|
|
70
98
|
/**
|
|
71
99
|
* The schema defining the document structure.
|
|
72
100
|
*/
|
|
@@ -105,32 +133,63 @@ export interface MimicServerConfigOptions<TSchema extends Primitive.AnyPrimitive
|
|
|
105
133
|
|
|
106
134
|
/**
|
|
107
135
|
* Initial state for new documents.
|
|
108
|
-
*
|
|
136
|
+
* Can be either:
|
|
137
|
+
* - A plain object with the initial state values
|
|
138
|
+
* - A function that receives context (with documentId) and returns an Effect producing the initial state
|
|
109
139
|
*
|
|
110
140
|
* Type-safe: required fields (without defaults) must be provided,
|
|
111
141
|
* while optional fields and fields with defaults can be omitted.
|
|
112
142
|
*
|
|
143
|
+
* @example
|
|
144
|
+
* // Plain object
|
|
145
|
+
* initial: { title: "New Document", count: 0 }
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* // Function returning Effect
|
|
149
|
+
* initial: ({ documentId }) => Effect.succeed({ title: `Doc ${documentId}`, count: 0 })
|
|
150
|
+
*
|
|
113
151
|
* @default undefined (documents start empty or use schema defaults)
|
|
114
152
|
*/
|
|
115
|
-
readonly initial?: Primitive.InferSetInput<TSchema>;
|
|
153
|
+
readonly initial?: Primitive.InferSetInput<TSchema> | InitialFn<TSchema>;
|
|
116
154
|
}
|
|
117
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Check if a value is an InitialFn (function) rather than a plain object.
|
|
158
|
+
*/
|
|
159
|
+
const isInitialFn = <TSchema extends Primitive.AnyPrimitive>(
|
|
160
|
+
value: Primitive.InferSetInput<TSchema> | InitialFn<TSchema> | undefined
|
|
161
|
+
): value is InitialFn<TSchema> => typeof value === "function";
|
|
162
|
+
|
|
118
163
|
/**
|
|
119
164
|
* Create a MimicServerConfig from options.
|
|
120
165
|
*/
|
|
121
166
|
export const make = <TSchema extends Primitive.AnyPrimitive>(
|
|
122
167
|
options: MimicServerConfigOptions<TSchema>
|
|
123
|
-
): MimicServerConfig<TSchema> =>
|
|
124
|
-
schema
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
): MimicServerConfig<TSchema> => {
|
|
169
|
+
const { initial, schema } = options;
|
|
170
|
+
|
|
171
|
+
// Convert initial to a function that applies defaults
|
|
172
|
+
const initialFn: InitialFn<TSchema> | undefined = initial === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: isInitialFn<TSchema>(initial)
|
|
175
|
+
? (context) => Effect.map(
|
|
176
|
+
initial(context),
|
|
177
|
+
(state) => Primitive.applyDefaults(schema, state as Partial<Primitive.InferState<TSchema>>)
|
|
178
|
+
) as Effect.Effect<Primitive.InferSetInput<TSchema>>
|
|
179
|
+
: () => Effect.succeed(
|
|
180
|
+
Primitive.applyDefaults(schema, initial as Partial<Primitive.InferState<TSchema>>)
|
|
181
|
+
) as Effect.Effect<Primitive.InferSetInput<TSchema>>;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
schema,
|
|
185
|
+
maxIdleTime: Duration.decode(options.maxIdleTime ?? "5 minutes"),
|
|
186
|
+
maxTransactionHistory: options.maxTransactionHistory ?? 1000,
|
|
187
|
+
heartbeatInterval: Duration.decode(options.heartbeatInterval ?? "30 seconds"),
|
|
188
|
+
heartbeatTimeout: Duration.decode(options.heartbeatTimeout ?? "10 seconds"),
|
|
189
|
+
presence: options.presence,
|
|
190
|
+
initial: initialFn,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
134
193
|
|
|
135
194
|
// =============================================================================
|
|
136
195
|
// Context Tag
|
|
@@ -149,4 +208,4 @@ export class MimicServerConfigTag extends Context.Tag(
|
|
|
149
208
|
export const layer = <TSchema extends Primitive.AnyPrimitive>(
|
|
150
209
|
options: MimicServerConfigOptions<TSchema>
|
|
151
210
|
): Layer.Layer<MimicServerConfigTag> =>
|
|
152
|
-
Layer.succeed(MimicServerConfigTag, make(options));
|
|
211
|
+
Layer.succeed(MimicServerConfigTag, make(options) as unknown as MimicServerConfig);
|
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
|
};
|