@voidhash/mimic-effect 0.0.1-alpha.1
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/README.md +0 -0
- package/package.json +40 -0
- package/src/DocumentManager.ts +252 -0
- package/src/DocumentProtocol.ts +112 -0
- package/src/MimicAuthService.ts +103 -0
- package/src/MimicConfig.ts +131 -0
- package/src/MimicDataStorage.ts +157 -0
- package/src/MimicServer.ts +363 -0
- package/src/PresenceManager.ts +297 -0
- package/src/WebSocketHandler.ts +735 -0
- package/src/auth/NoAuth.ts +46 -0
- package/src/errors.ts +113 -0
- package/src/index.ts +48 -0
- package/src/storage/InMemoryDataStorage.ts +66 -0
- package/tests/DocumentManager.test.ts +340 -0
- package/tests/DocumentProtocol.test.ts +113 -0
- package/tests/InMemoryDataStorage.test.ts +190 -0
- package/tests/MimicAuthService.test.ts +185 -0
- package/tests/MimicConfig.test.ts +175 -0
- package/tests/MimicDataStorage.test.ts +190 -0
- package/tests/MimicServer.test.ts +385 -0
- package/tests/NoAuth.test.ts +94 -0
- package/tests/PresenceManager.test.ts +421 -0
- package/tests/WebSocketHandler.test.ts +321 -0
- package/tests/errors.test.ts +77 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 0.0.1
|
|
3
|
+
* Data storage service interface for Mimic documents.
|
|
4
|
+
* Provides pluggable storage adapters with load/save hooks for data transformation.
|
|
5
|
+
*/
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Context from "effect/Context";
|
|
8
|
+
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as Data from "effect/Data";
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Error Types
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Error when loading a document from storage fails.
|
|
17
|
+
*/
|
|
18
|
+
export class StorageLoadError extends Data.TaggedError("StorageLoadError")<{
|
|
19
|
+
readonly documentId: string;
|
|
20
|
+
readonly cause: unknown;
|
|
21
|
+
}> {
|
|
22
|
+
get message(): string {
|
|
23
|
+
return `Failed to load document ${this.documentId}: ${String(this.cause)}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Error when saving a document to storage fails.
|
|
29
|
+
*/
|
|
30
|
+
export class StorageSaveError extends Data.TaggedError("StorageSaveError")<{
|
|
31
|
+
readonly documentId: string;
|
|
32
|
+
readonly cause: unknown;
|
|
33
|
+
}> {
|
|
34
|
+
get message(): string {
|
|
35
|
+
return `Failed to save document ${this.documentId}: ${String(this.cause)}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Error when deleting a document from storage fails.
|
|
41
|
+
*/
|
|
42
|
+
export class StorageDeleteError extends Data.TaggedError("StorageDeleteError")<{
|
|
43
|
+
readonly documentId: string;
|
|
44
|
+
readonly cause: unknown;
|
|
45
|
+
}> {
|
|
46
|
+
get message(): string {
|
|
47
|
+
return `Failed to delete document ${this.documentId}: ${String(this.cause)}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Union of all storage errors.
|
|
53
|
+
*/
|
|
54
|
+
export type StorageError = StorageLoadError | StorageSaveError | StorageDeleteError;
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Storage Service Interface
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Data storage service interface.
|
|
62
|
+
* Implementations can persist documents to various backends (memory, S3, database, etc.)
|
|
63
|
+
*/
|
|
64
|
+
export interface MimicDataStorage {
|
|
65
|
+
/**
|
|
66
|
+
* Load a document's state from storage.
|
|
67
|
+
* @param documentId - The unique identifier for the document
|
|
68
|
+
* @returns The document state, or undefined if not found
|
|
69
|
+
*/
|
|
70
|
+
readonly load: (
|
|
71
|
+
documentId: string
|
|
72
|
+
) => Effect.Effect<unknown | undefined, StorageLoadError>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Save a document's state to storage.
|
|
76
|
+
* @param documentId - The unique identifier for the document
|
|
77
|
+
* @param state - The document state to persist
|
|
78
|
+
*/
|
|
79
|
+
readonly save: (
|
|
80
|
+
documentId: string,
|
|
81
|
+
state: unknown
|
|
82
|
+
) => Effect.Effect<void, StorageSaveError>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Delete a document from storage.
|
|
86
|
+
* @param documentId - The unique identifier for the document
|
|
87
|
+
*/
|
|
88
|
+
readonly delete: (
|
|
89
|
+
documentId: string
|
|
90
|
+
) => Effect.Effect<void, StorageDeleteError>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Transform data after loading from storage.
|
|
94
|
+
* Useful for migrations, decryption, decompression, etc.
|
|
95
|
+
* @param state - The raw state loaded from storage
|
|
96
|
+
* @returns The transformed state
|
|
97
|
+
*/
|
|
98
|
+
readonly onLoad: (state: unknown) => Effect.Effect<unknown>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Transform/validate data before saving to storage.
|
|
102
|
+
* Useful for encryption, compression, validation, etc.
|
|
103
|
+
* @param state - The state to be saved
|
|
104
|
+
* @returns The transformed state
|
|
105
|
+
*/
|
|
106
|
+
readonly onSave: (state: unknown) => Effect.Effect<unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Context Tag
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Context tag for MimicDataStorage service.
|
|
115
|
+
*/
|
|
116
|
+
export class MimicDataStorageTag extends Context.Tag(
|
|
117
|
+
"@voidhash/mimic-server-effect/MimicDataStorage"
|
|
118
|
+
)<MimicDataStorageTag, MimicDataStorage>() {}
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// Layer Constructors
|
|
122
|
+
// =============================================================================
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create a MimicDataStorage layer from a storage implementation.
|
|
126
|
+
*/
|
|
127
|
+
export const layer = (storage: MimicDataStorage): Layer.Layer<MimicDataStorageTag> =>
|
|
128
|
+
Layer.succeed(MimicDataStorageTag, storage);
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a MimicDataStorage layer from an Effect that produces a storage implementation.
|
|
132
|
+
*/
|
|
133
|
+
export const layerEffect = <E, R>(
|
|
134
|
+
effect: Effect.Effect<MimicDataStorage, E, R>
|
|
135
|
+
): Layer.Layer<MimicDataStorageTag, E, R> =>
|
|
136
|
+
Layer.effect(MimicDataStorageTag, effect);
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// Helper Functions
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a simple storage implementation with minimal configuration.
|
|
144
|
+
*/
|
|
145
|
+
export const make = (options: {
|
|
146
|
+
readonly load: (documentId: string) => Effect.Effect<unknown | undefined, StorageLoadError>;
|
|
147
|
+
readonly save: (documentId: string, state: unknown) => Effect.Effect<void, StorageSaveError>;
|
|
148
|
+
readonly delete?: (documentId: string) => Effect.Effect<void, StorageDeleteError>;
|
|
149
|
+
readonly onLoad?: (state: unknown) => Effect.Effect<unknown>;
|
|
150
|
+
readonly onSave?: (state: unknown) => Effect.Effect<unknown>;
|
|
151
|
+
}): MimicDataStorage => ({
|
|
152
|
+
load: options.load,
|
|
153
|
+
save: options.save,
|
|
154
|
+
delete: options.delete ?? (() => Effect.void),
|
|
155
|
+
onLoad: options.onLoad ?? ((state) => Effect.succeed(state)),
|
|
156
|
+
onSave: options.onSave ?? ((state) => Effect.succeed(state)),
|
|
157
|
+
});
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 0.0.1
|
|
3
|
+
* Mimic server layer composition.
|
|
4
|
+
*/
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Layer from "effect/Layer";
|
|
7
|
+
import * as Context from "effect/Context";
|
|
8
|
+
import type * as Socket from "@effect/platform/Socket";
|
|
9
|
+
import { SocketServer } from "@effect/platform/SocketServer";
|
|
10
|
+
import type { Primitive, Presence } from "@voidhash/mimic";
|
|
11
|
+
|
|
12
|
+
import * as DocumentManager from "./DocumentManager.js";
|
|
13
|
+
import * as WebSocketHandler from "./WebSocketHandler.js";
|
|
14
|
+
import * as MimicConfig from "./MimicConfig.js";
|
|
15
|
+
import { MimicDataStorageTag } from "./MimicDataStorage.js";
|
|
16
|
+
import { MimicAuthServiceTag } from "./MimicAuthService.js";
|
|
17
|
+
import * as PresenceManager from "./PresenceManager.js";
|
|
18
|
+
import * as InMemoryDataStorage from "./storage/InMemoryDataStorage.js";
|
|
19
|
+
import * as NoAuth from "./auth/NoAuth.js";
|
|
20
|
+
import { HttpLayerRouter, HttpServerRequest, HttpServerResponse } from "@effect/platform";
|
|
21
|
+
import { PathInput } from "@effect/platform/HttpRouter";
|
|
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
|
+
// =============================================================================
|
|
38
|
+
// Layer Composition Options
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options for creating a Mimic server layer.
|
|
43
|
+
*/
|
|
44
|
+
export interface MimicLayerOptions<TSchema extends Primitive.AnyPrimitive> {
|
|
45
|
+
/**
|
|
46
|
+
* Base path for document routes (used for path matching).
|
|
47
|
+
* @example "/mimic/todo" - documents accessed at "/mimic/todo/:documentId"
|
|
48
|
+
*/
|
|
49
|
+
readonly basePath?: PathInput;
|
|
50
|
+
/**
|
|
51
|
+
* The schema defining the document structure.
|
|
52
|
+
*/
|
|
53
|
+
readonly schema: TSchema;
|
|
54
|
+
/**
|
|
55
|
+
* Maximum number of processed transaction IDs to track for deduplication.
|
|
56
|
+
* @default 1000
|
|
57
|
+
*/
|
|
58
|
+
readonly maxTransactionHistory?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Optional presence schema for ephemeral per-user data.
|
|
61
|
+
* When provided, enables presence features on WebSocket connections.
|
|
62
|
+
*/
|
|
63
|
+
readonly presence?: Presence.AnyPresence;
|
|
64
|
+
}
|
|
65
|
+
|
|
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
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create the document manager layer.
|
|
182
|
+
*/
|
|
183
|
+
export const documentManagerLayer = <TSchema extends Primitive.AnyPrimitive>(
|
|
184
|
+
options: MimicConfig.MimicServerConfigOptions<TSchema>
|
|
185
|
+
): Layer.Layer<DocumentManager.DocumentManagerTag> =>
|
|
186
|
+
DocumentManager.layer.pipe(
|
|
187
|
+
Layer.provide(MimicConfig.layer(options)),
|
|
188
|
+
// Provide defaults
|
|
189
|
+
Layer.provide(InMemoryDataStorage.layerDefault),
|
|
190
|
+
Layer.provide(NoAuth.layerDefault)
|
|
191
|
+
);
|
|
192
|
+
|
|
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
|
+
/**
|
|
228
|
+
* Create the HTTP handler effect for WebSocket upgrade.
|
|
229
|
+
* This handler:
|
|
230
|
+
* 1. Extracts the document ID from the URL path
|
|
231
|
+
* 2. Upgrades the HTTP connection to WebSocket
|
|
232
|
+
* 3. Delegates to the WebSocketHandler for document sync
|
|
233
|
+
*/
|
|
234
|
+
const makeMimicHandler = Effect.gen(function* () {
|
|
235
|
+
const config = yield* MimicConfig.MimicServerConfigTag;
|
|
236
|
+
const authService = yield* MimicAuthServiceTag;
|
|
237
|
+
const documentManager = yield* DocumentManager.DocumentManagerTag;
|
|
238
|
+
const presenceManager = yield* PresenceManager.PresenceManagerTag;
|
|
239
|
+
|
|
240
|
+
return Effect.gen(function* () {
|
|
241
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
242
|
+
|
|
243
|
+
// Extract document ID from the URL path
|
|
244
|
+
// Expected format: /basePath/doc/{documentId}
|
|
245
|
+
const documentId = yield* WebSocketHandler.extractDocumentId(request.url);
|
|
246
|
+
|
|
247
|
+
// Upgrade to WebSocket
|
|
248
|
+
const socket = yield* request.upgrade;
|
|
249
|
+
|
|
250
|
+
// Handle the WebSocket connection
|
|
251
|
+
yield* WebSocketHandler.handleConnection(socket, request.url).pipe(
|
|
252
|
+
Effect.provideService(MimicConfig.MimicServerConfigTag, config),
|
|
253
|
+
Effect.provideService(MimicAuthServiceTag, authService),
|
|
254
|
+
Effect.provideService(DocumentManager.DocumentManagerTag, documentManager),
|
|
255
|
+
Effect.provideService(PresenceManager.PresenceManagerTag, presenceManager),
|
|
256
|
+
Effect.scoped,
|
|
257
|
+
Effect.catchAll((error) =>
|
|
258
|
+
Effect.logError("WebSocket connection error", error)
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Return empty response - the WebSocket upgrade handles the connection
|
|
263
|
+
return HttpServerResponse.empty();
|
|
264
|
+
}).pipe(
|
|
265
|
+
Effect.catchAll((error) =>
|
|
266
|
+
Effect.gen(function* () {
|
|
267
|
+
yield* Effect.logWarning("WebSocket upgrade failed", error);
|
|
268
|
+
return HttpServerResponse.text("WebSocket upgrade failed", {
|
|
269
|
+
status: 400,
|
|
270
|
+
});
|
|
271
|
+
})
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create a Mimic server layer that integrates with HttpLayerRouter.
|
|
280
|
+
*
|
|
281
|
+
* This function creates a layer that:
|
|
282
|
+
* 1. Registers a WebSocket route at the specified base path
|
|
283
|
+
* 2. Handles WebSocket upgrades for document sync
|
|
284
|
+
* 3. Provides all required dependencies (config, auth, storage, document manager)
|
|
285
|
+
*
|
|
286
|
+
* By default, uses in-memory storage and no authentication.
|
|
287
|
+
* To override these defaults, provide custom layers before the defaults:
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```typescript
|
|
291
|
+
* import { MimicServer, MimicAuthService } from "@voidhash/mimic-effect";
|
|
292
|
+
* import { HttpLayerRouter } from "@effect/platform";
|
|
293
|
+
* import { Primitive } from "@voidhash/mimic";
|
|
294
|
+
*
|
|
295
|
+
* const TodoSchema = Primitive.Struct({
|
|
296
|
+
* title: Primitive.String(),
|
|
297
|
+
* completed: Primitive.Boolean(),
|
|
298
|
+
* });
|
|
299
|
+
*
|
|
300
|
+
* // Create the Mimic route layer with defaults
|
|
301
|
+
* const MimicRoute = MimicServer.layerHttpLayerRouter({
|
|
302
|
+
* basePath: "/mimic/todo",
|
|
303
|
+
* schema: TodoSchema
|
|
304
|
+
* });
|
|
305
|
+
*
|
|
306
|
+
* // Or with custom auth - use Layer.provide to inject before defaults
|
|
307
|
+
* const MimicRouteWithAuth = MimicServer.layerHttpLayerRouter({
|
|
308
|
+
* basePath: "/mimic/todo",
|
|
309
|
+
* schema: TodoSchema,
|
|
310
|
+
* authLayer: MimicAuthService.layer({
|
|
311
|
+
* authHandler: (token) => ({ success: true, userId: token })
|
|
312
|
+
* })
|
|
313
|
+
* });
|
|
314
|
+
*
|
|
315
|
+
* // Merge with other routes and serve
|
|
316
|
+
* const AllRoutes = Layer.mergeAll(MimicRoute, OtherRoutes);
|
|
317
|
+
* HttpLayerRouter.serve(AllRoutes).pipe(
|
|
318
|
+
* Layer.provide(BunHttpServer.layer({ port: 3000 })),
|
|
319
|
+
* Layer.launch,
|
|
320
|
+
* BunRuntime.runMain
|
|
321
|
+
* );
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
export const layerHttpLayerRouter = <TSchema extends Primitive.AnyPrimitive>(
|
|
325
|
+
options: MimicLayerOptions<TSchema> & {
|
|
326
|
+
/** Custom auth layer. Defaults to NoAuth (all connections allowed). */
|
|
327
|
+
readonly authLayer?: Layer.Layer<MimicAuthServiceTag>;
|
|
328
|
+
/** Custom storage layer. Defaults to InMemoryDataStorage. */
|
|
329
|
+
readonly storageLayer?: Layer.Layer<MimicDataStorageTag>;
|
|
330
|
+
}
|
|
331
|
+
): Layer.Layer<never, never, HttpLayerRouter.HttpRouter> => {
|
|
332
|
+
// Build the base path pattern for WebSocket routes
|
|
333
|
+
// Append /doc/* to match /basePath/doc/{documentId}
|
|
334
|
+
const basePath = options.basePath ?? "/mimic";
|
|
335
|
+
const wsPath: PathInput = `${basePath}/doc/*` as PathInput;
|
|
336
|
+
|
|
337
|
+
// Create the config layer
|
|
338
|
+
const configLayer = MimicConfig.layer({
|
|
339
|
+
schema: options.schema,
|
|
340
|
+
maxTransactionHistory: options.maxTransactionHistory,
|
|
341
|
+
presence: options.presence,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Use provided layers or defaults
|
|
345
|
+
const authLayer = options.authLayer ?? NoAuth.layerDefault;
|
|
346
|
+
const storageLayer = options.storageLayer ?? InMemoryDataStorage.layerDefault;
|
|
347
|
+
|
|
348
|
+
// Create the route registration effect
|
|
349
|
+
const registerRoute = Effect.gen(function* () {
|
|
350
|
+
const router = yield* HttpLayerRouter.HttpRouter;
|
|
351
|
+
const handler = yield* makeMimicHandler;
|
|
352
|
+
yield* router.add("GET", wsPath, handler);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Build the layer with all dependencies
|
|
356
|
+
return Layer.scopedDiscard(registerRoute).pipe(
|
|
357
|
+
Layer.provide(DocumentManager.layer),
|
|
358
|
+
Layer.provide(PresenceManager.layer),
|
|
359
|
+
Layer.provide(configLayer),
|
|
360
|
+
Layer.provide(storageLayer),
|
|
361
|
+
Layer.provide(authLayer)
|
|
362
|
+
);
|
|
363
|
+
};
|