@voidhash/mimic-effect 0.0.9 → 1.0.0-beta.2
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 +136 -90
- package/README.md +385 -0
- package/dist/ColdStorage.cjs +60 -0
- package/dist/ColdStorage.d.cts +53 -0
- package/dist/ColdStorage.d.cts.map +1 -0
- package/dist/ColdStorage.d.mts +53 -0
- package/dist/ColdStorage.d.mts.map +1 -0
- package/dist/ColdStorage.mjs +60 -0
- package/dist/ColdStorage.mjs.map +1 -0
- package/dist/DocumentManager.cjs +263 -82
- package/dist/DocumentManager.d.cts +44 -22
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts +44 -22
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +259 -67
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/Errors.cjs +54 -0
- package/dist/Errors.d.cts +96 -0
- package/dist/Errors.d.cts.map +1 -0
- package/dist/Errors.d.mts +96 -0
- package/dist/Errors.d.mts.map +1 -0
- package/dist/Errors.mjs +48 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/HotStorage.cjs +100 -0
- package/dist/HotStorage.d.cts +70 -0
- package/dist/HotStorage.d.cts.map +1 -0
- package/dist/HotStorage.d.mts +70 -0
- package/dist/HotStorage.d.mts.map +1 -0
- package/dist/HotStorage.mjs +100 -0
- package/dist/HotStorage.mjs.map +1 -0
- package/dist/Metrics.cjs +143 -0
- package/dist/Metrics.d.cts +31 -0
- package/dist/Metrics.d.cts.map +1 -0
- package/dist/Metrics.d.mts +31 -0
- package/dist/Metrics.d.mts.map +1 -0
- package/dist/Metrics.mjs +126 -0
- package/dist/Metrics.mjs.map +1 -0
- package/dist/MimicAuthService.cjs +61 -45
- package/dist/MimicAuthService.d.cts +61 -48
- package/dist/MimicAuthService.d.cts.map +1 -1
- package/dist/MimicAuthService.d.mts +61 -48
- package/dist/MimicAuthService.d.mts.map +1 -1
- package/dist/MimicAuthService.mjs +60 -36
- package/dist/MimicAuthService.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +521 -0
- package/dist/MimicClusterServerEngine.d.cts +17 -0
- package/dist/MimicClusterServerEngine.d.cts.map +1 -0
- package/dist/MimicClusterServerEngine.d.mts +17 -0
- package/dist/MimicClusterServerEngine.d.mts.map +1 -0
- package/dist/MimicClusterServerEngine.mjs +523 -0
- package/dist/MimicClusterServerEngine.mjs.map +1 -0
- package/dist/MimicServer.cjs +205 -96
- package/dist/MimicServer.d.cts +9 -110
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +9 -110
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +206 -90
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +97 -0
- package/dist/MimicServerEngine.d.cts +78 -0
- package/dist/MimicServerEngine.d.cts.map +1 -0
- package/dist/MimicServerEngine.d.mts +78 -0
- package/dist/MimicServerEngine.d.mts.map +1 -0
- package/dist/MimicServerEngine.mjs +97 -0
- package/dist/MimicServerEngine.mjs.map +1 -0
- package/dist/PresenceManager.cjs +75 -91
- package/dist/PresenceManager.d.cts +17 -66
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts +17 -66
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +74 -78
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.cjs +146 -0
- package/dist/Protocol.d.cts +203 -0
- package/dist/Protocol.d.cts.map +1 -0
- package/dist/Protocol.d.mts +203 -0
- package/dist/Protocol.d.mts.map +1 -0
- package/dist/Protocol.mjs +132 -0
- package/dist/Protocol.mjs.map +1 -0
- package/dist/Types.d.cts +172 -0
- package/dist/Types.d.cts.map +1 -0
- package/dist/Types.d.mts +172 -0
- package/dist/Types.d.mts.map +1 -0
- package/dist/_virtual/rolldown_runtime.cjs +1 -25
- package/dist/_virtual/rolldown_runtime.mjs +4 -1
- package/dist/index.cjs +37 -75
- package/dist/index.d.cts +13 -12
- package/dist/index.d.mts +13 -12
- package/dist/index.mjs +12 -12
- package/dist/testing/ColdStorageTestSuite.cjs +508 -0
- package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.mjs +508 -0
- package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
- package/dist/testing/FailingStorage.cjs +135 -0
- package/dist/testing/FailingStorage.d.cts +43 -0
- package/dist/testing/FailingStorage.d.cts.map +1 -0
- package/dist/testing/FailingStorage.d.mts +43 -0
- package/dist/testing/FailingStorage.d.mts.map +1 -0
- package/dist/testing/FailingStorage.mjs +136 -0
- package/dist/testing/FailingStorage.mjs.map +1 -0
- package/dist/testing/HotStorageTestSuite.cjs +585 -0
- package/dist/testing/HotStorageTestSuite.d.cts +40 -0
- package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/HotStorageTestSuite.d.mts +40 -0
- package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/HotStorageTestSuite.mjs +585 -0
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.cjs +349 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts +35 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts +35 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs +349 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
- package/dist/testing/assertions.cjs +114 -0
- package/dist/testing/assertions.mjs +109 -0
- package/dist/testing/assertions.mjs.map +1 -0
- package/dist/testing/index.cjs +14 -0
- package/dist/testing/index.d.cts +6 -0
- package/dist/testing/index.d.mts +6 -0
- package/dist/testing/index.mjs +7 -0
- package/dist/testing/types.cjs +15 -0
- package/dist/testing/types.d.cts +90 -0
- package/dist/testing/types.d.cts.map +1 -0
- package/dist/testing/types.d.mts +90 -0
- package/dist/testing/types.d.mts.map +1 -0
- package/dist/testing/types.mjs +16 -0
- package/dist/testing/types.mjs.map +1 -0
- package/package.json +18 -3
- package/src/ColdStorage.ts +136 -0
- package/src/DocumentManager.ts +550 -190
- package/src/Errors.ts +114 -0
- package/src/HotStorage.ts +239 -0
- package/src/Metrics.ts +187 -0
- package/src/MimicAuthService.ts +126 -64
- package/src/MimicClusterServerEngine.ts +946 -0
- package/src/MimicServer.ts +448 -195
- package/src/MimicServerEngine.ts +276 -0
- package/src/PresenceManager.ts +169 -240
- package/src/Protocol.ts +350 -0
- package/src/Types.ts +231 -0
- package/src/index.ts +57 -23
- package/src/testing/ColdStorageTestSuite.ts +589 -0
- package/src/testing/FailingStorage.ts +286 -0
- package/src/testing/HotStorageTestSuite.ts +762 -0
- package/src/testing/StorageIntegrationTestSuite.ts +504 -0
- package/src/testing/assertions.ts +181 -0
- package/src/testing/index.ts +83 -0
- package/src/testing/types.ts +100 -0
- package/tests/ColdStorage.test.ts +24 -0
- package/tests/DocumentManager.test.ts +158 -287
- package/tests/HotStorage.test.ts +24 -0
- package/tests/MimicAuthService.test.ts +102 -134
- package/tests/MimicClusterServerEngine.test.ts +587 -0
- package/tests/MimicServer.test.ts +90 -226
- package/tests/MimicServerEngine.test.ts +521 -0
- package/tests/PresenceManager.test.ts +22 -63
- package/tests/Protocol.test.ts +190 -0
- package/tests/StorageIntegration.test.ts +259 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +1 -1
- package/dist/DocumentProtocol.cjs +0 -94
- package/dist/DocumentProtocol.d.cts +0 -113
- package/dist/DocumentProtocol.d.cts.map +0 -1
- package/dist/DocumentProtocol.d.mts +0 -113
- package/dist/DocumentProtocol.d.mts.map +0 -1
- package/dist/DocumentProtocol.mjs +0 -89
- package/dist/DocumentProtocol.mjs.map +0 -1
- package/dist/MimicConfig.cjs +0 -60
- package/dist/MimicConfig.d.cts +0 -141
- package/dist/MimicConfig.d.cts.map +0 -1
- package/dist/MimicConfig.d.mts +0 -141
- package/dist/MimicConfig.d.mts.map +0 -1
- package/dist/MimicConfig.mjs +0 -50
- package/dist/MimicConfig.mjs.map +0 -1
- package/dist/MimicDataStorage.cjs +0 -83
- package/dist/MimicDataStorage.d.cts +0 -113
- package/dist/MimicDataStorage.d.cts.map +0 -1
- package/dist/MimicDataStorage.d.mts +0 -113
- package/dist/MimicDataStorage.d.mts.map +0 -1
- package/dist/MimicDataStorage.mjs +0 -74
- package/dist/MimicDataStorage.mjs.map +0 -1
- package/dist/WebSocketHandler.cjs +0 -365
- package/dist/WebSocketHandler.d.cts +0 -34
- package/dist/WebSocketHandler.d.cts.map +0 -1
- package/dist/WebSocketHandler.d.mts +0 -34
- package/dist/WebSocketHandler.d.mts.map +0 -1
- package/dist/WebSocketHandler.mjs +0 -355
- package/dist/WebSocketHandler.mjs.map +0 -1
- package/dist/auth/NoAuth.cjs +0 -43
- package/dist/auth/NoAuth.d.cts +0 -22
- package/dist/auth/NoAuth.d.cts.map +0 -1
- package/dist/auth/NoAuth.d.mts +0 -22
- package/dist/auth/NoAuth.d.mts.map +0 -1
- package/dist/auth/NoAuth.mjs +0 -36
- package/dist/auth/NoAuth.mjs.map +0 -1
- package/dist/errors.cjs +0 -74
- package/dist/errors.d.cts +0 -89
- package/dist/errors.d.cts.map +0 -1
- package/dist/errors.d.mts +0 -89
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs +0 -67
- package/dist/errors.mjs.map +0 -1
- package/dist/storage/InMemoryDataStorage.cjs +0 -57
- package/dist/storage/InMemoryDataStorage.d.cts +0 -19
- package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
- package/dist/storage/InMemoryDataStorage.d.mts +0 -19
- package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
- package/dist/storage/InMemoryDataStorage.mjs +0 -48
- package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
- package/src/DocumentProtocol.ts +0 -112
- package/src/MimicConfig.ts +0 -211
- package/src/MimicDataStorage.ts +0 -157
- package/src/WebSocketHandler.ts +0 -735
- package/src/auth/NoAuth.ts +0 -46
- package/src/errors.ts +0 -113
- package/src/storage/InMemoryDataStorage.ts +0 -66
- package/tests/DocumentProtocol.test.ts +0 -113
- package/tests/InMemoryDataStorage.test.ts +0 -190
- package/tests/MimicConfig.test.ts +0 -290
- package/tests/MimicDataStorage.test.ts +0 -190
- package/tests/NoAuth.test.ts +0 -94
- package/tests/WebSocketHandler.test.ts +0 -321
- package/tests/errors.test.ts +0 -77
package/dist/MimicServer.mjs
CHANGED
|
@@ -1,119 +1,235 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { MissingDocumentIdError } from "./Errors.mjs";
|
|
2
|
+
import { connectionsActive, connectionsDuration, connectionsErrors, connectionsTotal } from "./Metrics.mjs";
|
|
3
|
+
import { MimicServerEngineTag } from "./MimicServerEngine.mjs";
|
|
4
|
+
import { authResultFailure, authResultSuccess, encodeServerMessage, errorMessage, parseClientMessage, pong, presenceRemoveMessage, presenceSnapshotMessage, presenceUpdateMessage, snapshotMessage } from "./Protocol.mjs";
|
|
4
5
|
import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { layerDefault as layerDefault$1 } from "./auth/NoAuth.mjs";
|
|
9
|
-
import * as Effect from "effect/Effect";
|
|
10
|
-
import * as Layer from "effect/Layer";
|
|
11
|
-
import { HttpLayerRouter, HttpServerRequest, HttpServerResponse } from "@effect/platform";
|
|
6
|
+
import { Duration, Effect, Fiber, Layer, Metric, Stream } from "effect";
|
|
7
|
+
import { Presence } from "@voidhash/mimic";
|
|
8
|
+
import { HttpLayerRouter, HttpServerResponse } from "@effect/platform";
|
|
12
9
|
|
|
13
10
|
//#region src/MimicServer.ts
|
|
14
11
|
/**
|
|
15
|
-
* @
|
|
16
|
-
*
|
|
12
|
+
* @voidhash/mimic-effect - MimicServer
|
|
13
|
+
*
|
|
14
|
+
* WebSocket route layer for MimicServerEngine.
|
|
15
|
+
* Creates routes compatible with HttpLayerRouter.
|
|
17
16
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const DEFAULT_PATH = "/mimic";
|
|
18
|
+
const DEFAULT_HEARTBEAT_INTERVAL = Duration.seconds(30);
|
|
19
|
+
const DEFAULT_HEARTBEAT_TIMEOUT = Duration.seconds(10);
|
|
20
|
+
/**
|
|
21
|
+
* Resolve route configuration with defaults
|
|
22
|
+
*/
|
|
23
|
+
const resolveRouteConfig = (config) => {
|
|
24
|
+
var _config$path;
|
|
25
|
+
return {
|
|
26
|
+
path: (_config$path = config === null || config === void 0 ? void 0 : config.path) !== null && _config$path !== void 0 ? _config$path : DEFAULT_PATH,
|
|
27
|
+
heartbeatInterval: (config === null || config === void 0 ? void 0 : config.heartbeatInterval) ? Duration.decode(config.heartbeatInterval) : DEFAULT_HEARTBEAT_INTERVAL,
|
|
28
|
+
heartbeatTimeout: (config === null || config === void 0 ? void 0 : config.heartbeatTimeout) ? Duration.decode(config.heartbeatTimeout) : DEFAULT_HEARTBEAT_TIMEOUT
|
|
29
|
+
};
|
|
30
|
+
};
|
|
22
31
|
/**
|
|
23
|
-
*
|
|
32
|
+
* Extract document ID from URL path.
|
|
33
|
+
* Expected format: /basePath/doc/{documentId}
|
|
24
34
|
*/
|
|
25
|
-
const
|
|
35
|
+
const extractDocumentId = (path) => {
|
|
36
|
+
const parts = path.replace(/^\/+/, "").split("/");
|
|
37
|
+
const docIndex = parts.lastIndexOf("doc");
|
|
38
|
+
const part = parts[docIndex + 1];
|
|
39
|
+
if (docIndex !== -1 && part) return Effect.succeed(decodeURIComponent(part));
|
|
40
|
+
return Effect.fail(new MissingDocumentIdError({ path }));
|
|
41
|
+
};
|
|
26
42
|
/**
|
|
27
|
-
*
|
|
28
|
-
* This handler:
|
|
29
|
-
* 1. Extracts the document ID from the URL path
|
|
30
|
-
* 2. Upgrades the HTTP connection to WebSocket
|
|
31
|
-
* 3. Delegates to the WebSocketHandler for document sync
|
|
43
|
+
* Handle a WebSocket connection for a document.
|
|
32
44
|
*/
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const handleWebSocketConnection = (socket, documentId, engine, authService, _routeConfig) => Effect.gen(function* () {
|
|
46
|
+
const connectionId = crypto.randomUUID();
|
|
47
|
+
const connectionStartTime = Date.now();
|
|
48
|
+
yield* Metric.increment(connectionsTotal);
|
|
49
|
+
yield* Metric.incrementBy(connectionsActive, 1);
|
|
50
|
+
const state = {
|
|
51
|
+
documentId,
|
|
52
|
+
connectionId,
|
|
53
|
+
authenticated: false,
|
|
54
|
+
hasPresence: false
|
|
55
|
+
};
|
|
56
|
+
const write = yield* socket.writer;
|
|
57
|
+
const sendMessage = (message) => write(encodeServerMessage(message));
|
|
58
|
+
const sendPresenceSnapshot = Effect.gen(function* () {
|
|
59
|
+
if (!engine.config.presence) return;
|
|
60
|
+
const snapshot = yield* engine.getPresenceSnapshot(documentId);
|
|
61
|
+
yield* sendMessage(presenceSnapshotMessage(connectionId, snapshot.presences));
|
|
62
|
+
});
|
|
63
|
+
const sendDocumentSnapshot = Effect.gen(function* () {
|
|
64
|
+
const snapshot = yield* engine.getSnapshot(documentId);
|
|
65
|
+
yield* sendMessage(snapshotMessage(snapshot.state, snapshot.version));
|
|
66
|
+
});
|
|
67
|
+
const handleAuth = (token) => Effect.gen(function* () {
|
|
68
|
+
const result = yield* Effect.either(authService.authenticate(token, documentId));
|
|
69
|
+
if (result._tag === "Right") {
|
|
70
|
+
state.authenticated = true;
|
|
71
|
+
state.authContext = result.right;
|
|
72
|
+
yield* sendMessage(authResultSuccess(result.right.userId, result.right.permission));
|
|
73
|
+
yield* sendDocumentSnapshot;
|
|
74
|
+
yield* sendPresenceSnapshot;
|
|
75
|
+
} else {
|
|
76
|
+
var _result$left$reason;
|
|
77
|
+
yield* Metric.increment(connectionsErrors);
|
|
78
|
+
yield* sendMessage(authResultFailure((_result$left$reason = result.left.reason) !== null && _result$left$reason !== void 0 ? _result$left$reason : "Authentication failed"));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const handlePresenceSet = (data) => Effect.gen(function* () {
|
|
82
|
+
if (!state.authenticated) return;
|
|
83
|
+
if (!state.authContext) return;
|
|
84
|
+
if (!engine.config.presence) return;
|
|
85
|
+
if (state.authContext.permission !== "write") {
|
|
86
|
+
yield* Effect.logWarning("Presence set rejected - read-only user", { connectionId });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const validated = Presence.validateSafe(engine.config.presence, data);
|
|
90
|
+
if (validated === void 0) {
|
|
91
|
+
yield* Effect.logWarning("Invalid presence data received", {
|
|
92
|
+
connectionId,
|
|
93
|
+
data
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
yield* engine.setPresence(documentId, connectionId, {
|
|
98
|
+
data: validated,
|
|
99
|
+
userId: state.authContext.userId
|
|
100
|
+
});
|
|
101
|
+
state.hasPresence = true;
|
|
102
|
+
});
|
|
103
|
+
const handlePresenceClear = Effect.gen(function* () {
|
|
104
|
+
if (!state.authenticated) return;
|
|
105
|
+
if (!engine.config.presence) return;
|
|
106
|
+
yield* engine.removePresence(documentId, connectionId);
|
|
107
|
+
state.hasPresence = false;
|
|
108
|
+
});
|
|
109
|
+
const handleMessage = (message) => Effect.gen(function* () {
|
|
110
|
+
yield* engine.touch(documentId);
|
|
111
|
+
switch (message.type) {
|
|
112
|
+
case "auth":
|
|
113
|
+
yield* handleAuth(message.token);
|
|
114
|
+
break;
|
|
115
|
+
case "ping":
|
|
116
|
+
yield* sendMessage(pong());
|
|
117
|
+
break;
|
|
118
|
+
case "submit":
|
|
119
|
+
var _state$authContext;
|
|
120
|
+
if (!state.authenticated) {
|
|
121
|
+
yield* sendMessage(errorMessage(message.transaction.id, "Not authenticated"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (((_state$authContext = state.authContext) === null || _state$authContext === void 0 ? void 0 : _state$authContext.permission) !== "write") {
|
|
125
|
+
yield* sendMessage(errorMessage(message.transaction.id, "Write permission required"));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const submitResult = yield* engine.submit(documentId, message.transaction);
|
|
129
|
+
if (!submitResult.success) yield* sendMessage(errorMessage(message.transaction.id, submitResult.reason));
|
|
130
|
+
break;
|
|
131
|
+
case "request_snapshot":
|
|
132
|
+
if (!state.authenticated) return;
|
|
133
|
+
const snapshot = yield* engine.getSnapshot(documentId);
|
|
134
|
+
yield* sendMessage(snapshotMessage(snapshot.state, snapshot.version));
|
|
135
|
+
break;
|
|
136
|
+
case "presence_set":
|
|
137
|
+
yield* handlePresenceSet(message.data);
|
|
138
|
+
break;
|
|
139
|
+
case "presence_clear":
|
|
140
|
+
yield* handlePresenceClear;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
const subscribeFiber = yield* Effect.fork(Effect.gen(function* () {
|
|
145
|
+
while (!state.authenticated) yield* Effect.sleep(Duration.millis(100));
|
|
146
|
+
const broadcastStream = yield* engine.subscribe(documentId);
|
|
147
|
+
yield* Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
|
|
148
|
+
}).pipe(Effect.scoped));
|
|
149
|
+
const presenceFiber = yield* Effect.fork(Effect.gen(function* () {
|
|
150
|
+
if (!engine.config.presence) return;
|
|
151
|
+
while (!state.authenticated) yield* Effect.sleep(Duration.millis(100));
|
|
152
|
+
const presenceStream = yield* engine.subscribePresence(documentId);
|
|
153
|
+
yield* Stream.runForEach(presenceStream, (event) => Effect.gen(function* () {
|
|
154
|
+
if (event.id === connectionId) return;
|
|
155
|
+
if (event.type === "presence_update") yield* sendMessage(presenceUpdateMessage(event.id, event.data, event.userId));
|
|
156
|
+
else if (event.type === "presence_remove") yield* sendMessage(presenceRemoveMessage(event.id));
|
|
157
|
+
}));
|
|
158
|
+
}).pipe(Effect.scoped));
|
|
159
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
160
|
+
const duration = Date.now() - connectionStartTime;
|
|
161
|
+
yield* Fiber.interrupt(subscribeFiber);
|
|
162
|
+
yield* Fiber.interrupt(presenceFiber);
|
|
163
|
+
if (state.hasPresence && engine.config.presence) yield* engine.removePresence(documentId, connectionId);
|
|
164
|
+
yield* Metric.incrementBy(connectionsActive, -1);
|
|
165
|
+
yield* Metric.update(connectionsDuration, duration);
|
|
166
|
+
yield* Effect.logDebug("WebSocket connection closed", {
|
|
167
|
+
connectionId,
|
|
168
|
+
documentId,
|
|
169
|
+
durationMs: duration
|
|
170
|
+
});
|
|
171
|
+
}));
|
|
172
|
+
yield* socket.runRaw((data) => Effect.gen(function* () {
|
|
173
|
+
yield* handleMessage(yield* parseClientMessage(data));
|
|
174
|
+
}).pipe(Effect.catchAll((error) => Effect.logError("Message handling error", error))));
|
|
48
175
|
});
|
|
49
176
|
/**
|
|
50
|
-
* Create a
|
|
177
|
+
* Create a route layer for MimicServerEngine.
|
|
51
178
|
*
|
|
52
|
-
* This
|
|
53
|
-
*
|
|
54
|
-
* 2. Handles WebSocket upgrades for document sync
|
|
55
|
-
* 3. Provides all required dependencies (config, auth, storage, document manager)
|
|
56
|
-
*
|
|
57
|
-
* By default, uses in-memory storage and no authentication.
|
|
58
|
-
* To override these defaults, provide custom layers before the defaults:
|
|
179
|
+
* This creates a WebSocket route that connects to the engine.
|
|
180
|
+
* Use Layer.mergeAll to compose with other routes.
|
|
59
181
|
*
|
|
60
182
|
* @example
|
|
61
183
|
* ```typescript
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* title: Primitive.String(),
|
|
68
|
-
* completed: Primitive.Boolean(),
|
|
69
|
-
* });
|
|
184
|
+
* // 1. Create the engine
|
|
185
|
+
* const Engine = MimicServerEngine.make({
|
|
186
|
+
* schema: DocSchema,
|
|
187
|
+
* initial: { title: "Untitled" },
|
|
188
|
+
* })
|
|
70
189
|
*
|
|
71
|
-
* // Create the
|
|
190
|
+
* // 2. Create the WebSocket route
|
|
72
191
|
* const MimicRoute = MimicServer.layerHttpLayerRouter({
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
192
|
+
* path: "/mimic",
|
|
193
|
+
* })
|
|
194
|
+
*
|
|
195
|
+
* // 3. Wire together
|
|
196
|
+
* const MimicLive = MimicRoute.pipe(
|
|
197
|
+
* Layer.provide(Engine),
|
|
198
|
+
* Layer.provide(ColdStorage.InMemory.make()),
|
|
199
|
+
* Layer.provide(HotStorage.InMemory.make()),
|
|
200
|
+
* Layer.provide(MimicAuthService.NoAuth.make()),
|
|
201
|
+
* )
|
|
76
202
|
*
|
|
77
|
-
* //
|
|
78
|
-
* const
|
|
79
|
-
* basePath: "/mimic/todo",
|
|
80
|
-
* schema: TodoSchema,
|
|
81
|
-
* authLayer: MimicAuthService.layer({
|
|
82
|
-
* authHandler: (token) => ({ success: true, userId: token })
|
|
83
|
-
* })
|
|
84
|
-
* });
|
|
203
|
+
* // 4. Compose with other routes
|
|
204
|
+
* const AllRoutes = Layer.mergeAll(MimicLive, DocsRoute, OtherRoutes)
|
|
85
205
|
*
|
|
86
|
-
* //
|
|
87
|
-
* const AllRoutes = Layer.mergeAll(MimicRoute, OtherRoutes);
|
|
206
|
+
* // 5. Serve
|
|
88
207
|
* HttpLayerRouter.serve(AllRoutes).pipe(
|
|
89
|
-
* Layer.provide(
|
|
208
|
+
* Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
|
|
90
209
|
* Layer.launch,
|
|
91
|
-
*
|
|
92
|
-
* )
|
|
210
|
+
* NodeRuntime.runMain
|
|
211
|
+
* )
|
|
93
212
|
* ```
|
|
94
213
|
*/
|
|
95
|
-
const layerHttpLayerRouter = (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
214
|
+
const layerHttpLayerRouter = (options) => {
|
|
215
|
+
const routeConfig = resolveRouteConfig(options);
|
|
216
|
+
const routePath = `${routeConfig.path}/doc/:documentId`;
|
|
217
|
+
return Layer.scopedDiscard(Effect.gen(function* () {
|
|
218
|
+
const router = yield* HttpLayerRouter.HttpRouter;
|
|
219
|
+
const engine = yield* MimicServerEngineTag;
|
|
220
|
+
const authService = yield* MimicAuthServiceTag;
|
|
221
|
+
const handler = (request) => Effect.gen(function* () {
|
|
222
|
+
const documentIdResult = yield* Effect.either(extractDocumentId(request.url));
|
|
223
|
+
if (documentIdResult._tag === "Left") return HttpServerResponse.text(`Missing document ID in path: ${request.url}`, { status: 400 });
|
|
224
|
+
const documentId = documentIdResult.right;
|
|
225
|
+
yield* handleWebSocketConnection(yield* request.upgrade, documentId, engine, authService, routeConfig).pipe(Effect.scoped, Effect.catchAll((error) => Effect.logError("WebSocket connection error", error)));
|
|
226
|
+
return HttpServerResponse.empty();
|
|
105
227
|
});
|
|
106
|
-
|
|
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));
|
|
228
|
+
yield* router.add("GET", routePath, handler);
|
|
114
229
|
}));
|
|
115
230
|
};
|
|
231
|
+
const MimicServer = { layerHttpLayerRouter };
|
|
116
232
|
|
|
117
233
|
//#endregion
|
|
118
|
-
export {
|
|
234
|
+
export { MimicServer };
|
|
119
235
|
//# sourceMappingURL=MimicServer.mjs.map
|
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<\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"}
|
|
1
|
+
{"version":3,"file":"MimicServer.mjs","names":["Metrics.connectionsTotal","Metrics.connectionsActive","state: ConnectionState","Protocol.encodeServerMessage","Protocol.presenceSnapshotMessage","Protocol.snapshotMessage","Protocol.authResultSuccess","Metrics.connectionsErrors","Protocol.authResultFailure","Protocol.pong","Protocol.errorMessage","Protocol.presenceUpdateMessage","Protocol.presenceRemoveMessage","Metrics.connectionsDuration","Protocol.parseClientMessage"],"sources":["../src/MimicServer.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServer\n *\n * WebSocket route layer for MimicServerEngine.\n * Creates routes compatible with HttpLayerRouter.\n */\nimport {\n Duration,\n Effect,\n Fiber,\n Layer,\n Metric,\n Scope,\n Stream,\n} from \"effect\";\nimport {\n HttpLayerRouter,\n HttpServerRequest,\n HttpServerResponse,\n} from \"@effect/platform\";\nimport type * as Socket from \"@effect/platform/Socket\";\nimport { Presence } from \"@voidhash/mimic\";\nimport type { MimicServerRouteConfig, ResolvedRouteConfig } from \"./Types\";\nimport * as Protocol from \"./Protocol\";\nimport { MissingDocumentIdError } from \"./Errors\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport { MimicAuthServiceTag, type MimicAuthService } from \"./MimicAuthService\";\nimport * as Metrics from \"./Metrics\";\nimport type { AuthContext } from \"./Types\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_PATH = \"/mimic\";\nconst DEFAULT_HEARTBEAT_INTERVAL = Duration.seconds(30);\nconst DEFAULT_HEARTBEAT_TIMEOUT = Duration.seconds(10);\n\n/**\n * Resolve route configuration with defaults\n */\nconst resolveRouteConfig = (\n config?: MimicServerRouteConfig\n): ResolvedRouteConfig => ({\n path: config?.path ?? DEFAULT_PATH,\n heartbeatInterval: config?.heartbeatInterval\n ? Duration.decode(config.heartbeatInterval)\n : DEFAULT_HEARTBEAT_INTERVAL,\n heartbeatTimeout: config?.heartbeatTimeout\n ? Duration.decode(config.heartbeatTimeout)\n : DEFAULT_HEARTBEAT_TIMEOUT,\n});\n\n// =============================================================================\n// URL Path Parsing\n// =============================================================================\n\n/**\n * Extract document ID from URL path.\n * Expected format: /basePath/doc/{documentId}\n */\nconst extractDocumentId = (\n path: string\n): Effect.Effect<string, MissingDocumentIdError> => {\n // Remove leading slash and split\n const parts = path.replace(/^\\/+/, \"\").split(\"/\");\n\n // Find the last occurrence of 'doc' in the path\n const docIndex = parts.lastIndexOf(\"doc\");\n const part = parts[docIndex + 1];\n if (docIndex !== -1 && part) {\n return Effect.succeed(decodeURIComponent(part));\n }\n return Effect.fail(new MissingDocumentIdError({ path }));\n};\n\n// =============================================================================\n// Connection State\n// =============================================================================\n\ninterface ConnectionState {\n readonly documentId: string;\n readonly connectionId: string;\n authenticated: boolean;\n authContext?: AuthContext;\n hasPresence: boolean;\n}\n\n// =============================================================================\n// WebSocket Connection Handler\n// =============================================================================\n\n/**\n * Handle a WebSocket connection for a document.\n */\nconst handleWebSocketConnection = (\n socket: Socket.Socket,\n documentId: string,\n engine: MimicServerEngine,\n authService: MimicAuthService,\n _routeConfig: ResolvedRouteConfig\n): Effect.Effect<void, Socket.SocketError, Scope.Scope> =>\n Effect.gen(function* () {\n const connectionId = crypto.randomUUID();\n const connectionStartTime = Date.now();\n\n // Track connection metrics\n yield* Metric.increment(Metrics.connectionsTotal);\n yield* Metric.incrementBy(Metrics.connectionsActive, 1);\n\n // Track connection state (mutable for simplicity)\n const state: ConnectionState = {\n documentId,\n connectionId,\n authenticated: false,\n hasPresence: false,\n };\n\n // Get the socket writer\n const write = yield* socket.writer;\n\n // Helper to send a message to the client\n const sendMessage = (message: Protocol.ServerMessage) =>\n write(Protocol.encodeServerMessage(message));\n\n // Send presence snapshot after auth\n const sendPresenceSnapshot = Effect.gen(function* () {\n if (!engine.config.presence) return;\n\n const snapshot = yield* engine.getPresenceSnapshot(documentId);\n yield* sendMessage(\n Protocol.presenceSnapshotMessage(connectionId, snapshot.presences)\n );\n });\n\n // Send document snapshot after auth\n const sendDocumentSnapshot = Effect.gen(function* () {\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n });\n\n // Handle authentication\n const handleAuth = (token: string) =>\n Effect.gen(function* () {\n const result = yield* Effect.either(\n authService.authenticate(token, documentId)\n );\n\n if (result._tag === \"Right\") {\n state.authenticated = true;\n state.authContext = result.right;\n\n yield* sendMessage(\n Protocol.authResultSuccess(\n result.right.userId,\n result.right.permission\n )\n );\n\n // Send document snapshot after successful auth\n yield* sendDocumentSnapshot;\n\n // Send presence snapshot after successful auth\n yield* sendPresenceSnapshot;\n } else {\n yield* Metric.increment(Metrics.connectionsErrors);\n yield* sendMessage(\n Protocol.authResultFailure(\n result.left.reason ?? \"Authentication failed\"\n )\n );\n }\n });\n\n // Handle presence set\n const handlePresenceSet = (data: unknown) =>\n Effect.gen(function* () {\n if (!state.authenticated) return;\n if (!state.authContext) return;\n if (!engine.config.presence) return;\n\n // Check write permission\n if (state.authContext.permission !== \"write\") {\n yield* Effect.logWarning(\"Presence set rejected - read-only user\", {\n connectionId,\n });\n return;\n }\n\n // Validate presence data against schema\n const validated = Presence.validateSafe(engine.config.presence, data);\n if (validated === undefined) {\n yield* Effect.logWarning(\"Invalid presence data received\", {\n connectionId,\n data,\n });\n return;\n }\n\n // Store in engine\n yield* engine.setPresence(documentId, connectionId, {\n data: validated,\n userId: state.authContext.userId,\n });\n\n state.hasPresence = true;\n });\n\n // Handle presence clear\n const handlePresenceClear = Effect.gen(function* () {\n if (!state.authenticated) return;\n if (!engine.config.presence) return;\n\n yield* engine.removePresence(documentId, connectionId);\n state.hasPresence = false;\n });\n\n // Handle a client message\n const handleMessage = (message: Protocol.ClientMessage) =>\n Effect.gen(function* () {\n // Touch document on any activity (prevents idle GC)\n yield* engine.touch(documentId);\n\n switch (message.type) {\n case \"auth\":\n yield* handleAuth(message.token);\n break;\n\n case \"ping\":\n yield* sendMessage(Protocol.pong());\n break;\n\n case \"submit\":\n if (!state.authenticated) {\n yield* sendMessage(\n Protocol.errorMessage(\n message.transaction.id,\n \"Not authenticated\"\n )\n );\n return;\n }\n\n // Check write permission\n if (state.authContext?.permission !== \"write\") {\n yield* sendMessage(\n Protocol.errorMessage(\n message.transaction.id,\n \"Write permission required\"\n )\n );\n return;\n }\n\n // Submit to the engine\n const submitResult = yield* engine.submit(\n documentId,\n message.transaction\n );\n\n // If rejected, send error (success is broadcast to all)\n if (!submitResult.success) {\n yield* sendMessage(\n Protocol.errorMessage(message.transaction.id, submitResult.reason)\n );\n }\n break;\n\n case \"request_snapshot\":\n if (!state.authenticated) {\n return;\n }\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n break;\n\n case \"presence_set\":\n yield* handlePresenceSet(message.data);\n break;\n\n case \"presence_clear\":\n yield* handlePresenceClear;\n break;\n }\n });\n\n // Subscribe to document broadcasts\n const subscribeFiber = yield* Effect.fork(\n Effect.gen(function* () {\n // Wait until authenticated before subscribing\n while (!state.authenticated) {\n yield* Effect.sleep(Duration.millis(100));\n }\n\n // Subscribe to the document\n const broadcastStream = yield* engine.subscribe(documentId);\n\n // Forward broadcasts to the WebSocket\n yield* Stream.runForEach(broadcastStream, (broadcast) =>\n sendMessage(broadcast as Protocol.ServerMessage)\n );\n }).pipe(Effect.scoped)\n );\n\n // Subscribe to presence events (if presence is enabled)\n const presenceFiber = yield* Effect.fork(\n Effect.gen(function* () {\n if (!engine.config.presence) return;\n\n // Wait until authenticated before subscribing\n while (!state.authenticated) {\n yield* Effect.sleep(Duration.millis(100));\n }\n\n // Subscribe to presence events\n const presenceStream = yield* engine.subscribePresence(documentId);\n\n // Forward presence events to the WebSocket, filtering out our own events (no-echo)\n yield* Stream.runForEach(presenceStream, (event) =>\n Effect.gen(function* () {\n // Don't echo our own presence events\n if (event.id === connectionId) return;\n\n if (event.type === \"presence_update\") {\n yield* sendMessage(\n Protocol.presenceUpdateMessage(event.id, event.data, event.userId)\n );\n } else if (event.type === \"presence_remove\") {\n yield* sendMessage(Protocol.presenceRemoveMessage(event.id));\n }\n })\n );\n }).pipe(Effect.scoped)\n );\n\n // Ensure cleanup on disconnect\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n // Calculate connection duration\n const duration = Date.now() - connectionStartTime;\n\n // Interrupt the subscribe fibers\n yield* Fiber.interrupt(subscribeFiber);\n yield* Fiber.interrupt(presenceFiber);\n\n // Remove presence if we had any\n if (state.hasPresence && engine.config.presence) {\n yield* engine.removePresence(documentId, connectionId);\n }\n\n // Update connection metrics\n yield* Metric.incrementBy(Metrics.connectionsActive, -1);\n yield* Metric.update(Metrics.connectionsDuration, duration);\n\n yield* Effect.logDebug(\"WebSocket connection closed\", {\n connectionId,\n documentId,\n durationMs: duration,\n });\n })\n );\n\n // Process incoming messages\n yield* socket.runRaw((data) =>\n Effect.gen(function* () {\n const message = yield* Protocol.parseClientMessage(data);\n yield* handleMessage(message);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.logError(\"Message handling error\", error)\n )\n )\n );\n });\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a route layer for MimicServerEngine.\n *\n * This creates a WebSocket route that connects to the engine.\n * Use Layer.mergeAll to compose with other routes.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.InMemory.make()),\n * Layer.provide(HotStorage.InMemory.make()),\n * Layer.provide(MimicAuthService.NoAuth.make()),\n * )\n *\n * // 4. Compose with other routes\n * const AllRoutes = Layer.mergeAll(MimicLive, DocsRoute, OtherRoutes)\n *\n * // 5. Serve\n * HttpLayerRouter.serve(AllRoutes).pipe(\n * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),\n * Layer.launch,\n * NodeRuntime.runMain\n * )\n * ```\n */\nexport const layerHttpLayerRouter = (\n options?: MimicServerRouteConfig\n) => {\n const routeConfig = resolveRouteConfig(options);\n\n // Build the route path pattern: {path}/doc/:documentId\n const routePath =\n `${routeConfig.path}/doc/:documentId` as HttpLayerRouter.PathInput;\n\n return Layer.scopedDiscard(\n Effect.gen(function* () {\n const router = yield* HttpLayerRouter.HttpRouter;\n // Capture engine and auth service at layer creation time\n const engine = yield* MimicServerEngineTag;\n const authService = yield* MimicAuthServiceTag;\n\n // Create the handler that receives the request\n // Engine and authService are captured in closure, not yielded per-request\n const handler = (request: HttpServerRequest.HttpServerRequest) =>\n Effect.gen(function* () {\n // Extract document ID from path\n const documentIdResult = yield* Effect.either(\n extractDocumentId(request.url)\n );\n if (documentIdResult._tag === \"Left\") {\n return HttpServerResponse.text(\n `Missing document ID in path: ${request.url}`,\n { status: 400 }\n );\n }\n const documentId = documentIdResult.right;\n\n // Upgrade to WebSocket\n const socket = yield* request.upgrade;\n\n // Handle the WebSocket connection\n yield* handleWebSocketConnection(\n socket,\n documentId,\n engine,\n authService,\n routeConfig\n ).pipe(\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 });\n\n yield* router.add(\"GET\", routePath, handler);\n })\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServer = {\n layerHttpLayerRouter,\n};\n\n// =============================================================================\n// Re-export types\n// =============================================================================\n\nexport type { MimicServerRouteConfig };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const require_DocumentManager = require('./DocumentManager.cjs');
|
|
2
|
+
const require_PresenceManager = require('./PresenceManager.cjs');
|
|
3
|
+
let effect = require("effect");
|
|
4
|
+
|
|
5
|
+
//#region src/MimicServerEngine.ts
|
|
6
|
+
/**
|
|
7
|
+
* @voidhash/mimic-effect - MimicServerEngine
|
|
8
|
+
*
|
|
9
|
+
* Core document management service for Mimic real-time collaboration.
|
|
10
|
+
* Handles document lifecycle, storage, presence, and transaction processing.
|
|
11
|
+
*
|
|
12
|
+
* This is the engine layer - for WebSocket routes, use MimicServer.layerHttpLayerRouter().
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Context tag for MimicServerEngine
|
|
16
|
+
*/
|
|
17
|
+
var MimicServerEngineTag = class extends effect.Context.Tag("@voidhash/mimic-effect/MimicServerEngine")() {};
|
|
18
|
+
const DEFAULT_MAX_IDLE_TIME = effect.Duration.minutes(5);
|
|
19
|
+
const DEFAULT_MAX_TRANSACTION_HISTORY = 1e3;
|
|
20
|
+
const DEFAULT_SNAPSHOT_INTERVAL = effect.Duration.minutes(5);
|
|
21
|
+
const DEFAULT_SNAPSHOT_THRESHOLD = 100;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve configuration with defaults
|
|
24
|
+
*/
|
|
25
|
+
const resolveConfig = (config) => {
|
|
26
|
+
var _config$maxTransactio, _config$snapshot, _config$snapshot$tran, _config$snapshot2;
|
|
27
|
+
return {
|
|
28
|
+
schema: config.schema,
|
|
29
|
+
initial: config.initial,
|
|
30
|
+
presence: config.presence,
|
|
31
|
+
maxIdleTime: config.maxIdleTime ? effect.Duration.decode(config.maxIdleTime) : DEFAULT_MAX_IDLE_TIME,
|
|
32
|
+
maxTransactionHistory: (_config$maxTransactio = config.maxTransactionHistory) !== null && _config$maxTransactio !== void 0 ? _config$maxTransactio : DEFAULT_MAX_TRANSACTION_HISTORY,
|
|
33
|
+
snapshot: {
|
|
34
|
+
interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? effect.Duration.decode(config.snapshot.interval) : DEFAULT_SNAPSHOT_INTERVAL,
|
|
35
|
+
transactionThreshold: (_config$snapshot$tran = (_config$snapshot2 = config.snapshot) === null || _config$snapshot2 === void 0 ? void 0 : _config$snapshot2.transactionThreshold) !== null && _config$snapshot$tran !== void 0 ? _config$snapshot$tran : DEFAULT_SNAPSHOT_THRESHOLD
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Create a MimicServerEngine layer.
|
|
41
|
+
*
|
|
42
|
+
* This creates the core document management service. To expose it via WebSocket,
|
|
43
|
+
* use MimicServer.layerHttpLayerRouter().
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // 1. Create the engine
|
|
48
|
+
* const Engine = MimicServerEngine.make({
|
|
49
|
+
* schema: DocSchema,
|
|
50
|
+
* initial: { title: "Untitled" },
|
|
51
|
+
* presence: CursorPresence,
|
|
52
|
+
* maxIdleTime: "5 minutes",
|
|
53
|
+
* snapshot: { interval: "5 minutes", transactionThreshold: 100 },
|
|
54
|
+
* })
|
|
55
|
+
*
|
|
56
|
+
* // 2. Create the WebSocket route
|
|
57
|
+
* const MimicRoute = MimicServer.layerHttpLayerRouter({
|
|
58
|
+
* path: "/mimic",
|
|
59
|
+
* })
|
|
60
|
+
*
|
|
61
|
+
* // 3. Wire together
|
|
62
|
+
* const MimicLive = MimicRoute.pipe(
|
|
63
|
+
* Layer.provide(Engine),
|
|
64
|
+
* Layer.provide(ColdStorage.InMemory.make()),
|
|
65
|
+
* Layer.provide(HotStorage.InMemory.make()),
|
|
66
|
+
* Layer.provide(MimicAuthService.NoAuth.make()),
|
|
67
|
+
* )
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
const make = (config) => {
|
|
71
|
+
const resolvedConfig = resolveConfig(config);
|
|
72
|
+
const configLayer = effect.Layer.succeed(require_DocumentManager.DocumentManagerConfigTag, resolvedConfig);
|
|
73
|
+
const internalLayers = effect.Layer.mergeAll(require_DocumentManager.layer.pipe(effect.Layer.provide(configLayer)), require_PresenceManager.layer);
|
|
74
|
+
return effect.Layer.scoped(MimicServerEngineTag, effect.Effect.gen(function* () {
|
|
75
|
+
const documentManager = yield* require_DocumentManager.DocumentManagerTag;
|
|
76
|
+
const presenceManager = yield* require_PresenceManager.PresenceManagerTag;
|
|
77
|
+
return {
|
|
78
|
+
submit: (documentId, transaction) => documentManager.submit(documentId, transaction),
|
|
79
|
+
getSnapshot: (documentId) => documentManager.getSnapshot(documentId),
|
|
80
|
+
subscribe: (documentId) => documentManager.subscribe(documentId),
|
|
81
|
+
touch: (documentId) => documentManager.touch(documentId),
|
|
82
|
+
getPresenceSnapshot: (documentId) => presenceManager.getSnapshot(documentId),
|
|
83
|
+
setPresence: (documentId, connectionId, entry) => presenceManager.set(documentId, connectionId, entry),
|
|
84
|
+
removePresence: (documentId, connectionId) => presenceManager.remove(documentId, connectionId),
|
|
85
|
+
subscribePresence: (documentId) => presenceManager.subscribe(documentId),
|
|
86
|
+
config: resolvedConfig
|
|
87
|
+
};
|
|
88
|
+
})).pipe(effect.Layer.provide(internalLayers));
|
|
89
|
+
};
|
|
90
|
+
const MimicServerEngine = {
|
|
91
|
+
Tag: MimicServerEngineTag,
|
|
92
|
+
make
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
exports.MimicServerEngine = MimicServerEngine;
|
|
97
|
+
exports.MimicServerEngineTag = MimicServerEngineTag;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MimicServerEngineConfig, PresenceEntry, PresenceEvent, PresenceSnapshot, ResolvedConfig } from "./Types.cjs";
|
|
2
|
+
import { ServerMessage } from "./Protocol.cjs";
|
|
3
|
+
import { ColdStorageTag } from "./ColdStorage.cjs";
|
|
4
|
+
import { HotStorageTag } from "./HotStorage.cjs";
|
|
5
|
+
import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
|
|
6
|
+
import { DocumentManagerError, SubmitResult } from "./DocumentManager.cjs";
|
|
7
|
+
import { Context, Effect, Layer, Scope, Stream } from "effect";
|
|
8
|
+
import { Primitive, Transaction } from "@voidhash/mimic";
|
|
9
|
+
|
|
10
|
+
//#region src/MimicServerEngine.d.ts
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MimicServerEngine service interface.
|
|
14
|
+
*
|
|
15
|
+
* Provides document management operations for Mimic collaboration.
|
|
16
|
+
* Use MimicServer.layerHttpLayerRouter() to create WebSocket routes.
|
|
17
|
+
*/
|
|
18
|
+
interface MimicServerEngine {
|
|
19
|
+
/**
|
|
20
|
+
* Submit a transaction to a document.
|
|
21
|
+
* Authorization is checked against the auth service.
|
|
22
|
+
* May fail with DocumentManagerError if storage is unavailable.
|
|
23
|
+
*/
|
|
24
|
+
readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, DocumentManagerError>;
|
|
25
|
+
/**
|
|
26
|
+
* Get document snapshot (current state and version).
|
|
27
|
+
* May fail with DocumentManagerError if storage is unavailable.
|
|
28
|
+
*/
|
|
29
|
+
readonly getSnapshot: (documentId: string) => Effect.Effect<{
|
|
30
|
+
state: unknown;
|
|
31
|
+
version: number;
|
|
32
|
+
}, DocumentManagerError>;
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to document broadcasts (transactions).
|
|
35
|
+
* Returns a stream of server messages.
|
|
36
|
+
* Requires a Scope for cleanup when the subscription ends.
|
|
37
|
+
* May fail with DocumentManagerError if storage is unavailable.
|
|
38
|
+
*/
|
|
39
|
+
readonly subscribe: (documentId: string) => Effect.Effect<Stream.Stream<ServerMessage, never, never>, DocumentManagerError, Scope.Scope>;
|
|
40
|
+
/**
|
|
41
|
+
* Touch document to prevent idle garbage collection.
|
|
42
|
+
*/
|
|
43
|
+
readonly touch: (documentId: string) => Effect.Effect<void, never>;
|
|
44
|
+
/**
|
|
45
|
+
* Get presence snapshot for a document.
|
|
46
|
+
*/
|
|
47
|
+
readonly getPresenceSnapshot: (documentId: string) => Effect.Effect<PresenceSnapshot, never>;
|
|
48
|
+
/**
|
|
49
|
+
* Set presence for a connection.
|
|
50
|
+
*/
|
|
51
|
+
readonly setPresence: (documentId: string, connectionId: string, entry: PresenceEntry) => Effect.Effect<void, never>;
|
|
52
|
+
/**
|
|
53
|
+
* Remove presence for a connection.
|
|
54
|
+
*/
|
|
55
|
+
readonly removePresence: (documentId: string, connectionId: string) => Effect.Effect<void, never>;
|
|
56
|
+
/**
|
|
57
|
+
* Subscribe to presence events for a document.
|
|
58
|
+
* Requires a Scope for cleanup when the subscription ends.
|
|
59
|
+
*/
|
|
60
|
+
readonly subscribePresence: (documentId: string) => Effect.Effect<Stream.Stream<PresenceEvent, never, never>, never, Scope.Scope>;
|
|
61
|
+
/**
|
|
62
|
+
* Resolved engine configuration.
|
|
63
|
+
* Used by route layer to access schema, presence config, etc.
|
|
64
|
+
*/
|
|
65
|
+
readonly config: ResolvedConfig<Primitive.AnyPrimitive>;
|
|
66
|
+
}
|
|
67
|
+
declare const MimicServerEngineTag_base: Context.TagClass<MimicServerEngineTag, "@voidhash/mimic-effect/MimicServerEngine", MimicServerEngine>;
|
|
68
|
+
/**
|
|
69
|
+
* Context tag for MimicServerEngine
|
|
70
|
+
*/
|
|
71
|
+
declare class MimicServerEngineTag extends MimicServerEngineTag_base {}
|
|
72
|
+
declare const MimicServerEngine: {
|
|
73
|
+
Tag: typeof MimicServerEngineTag;
|
|
74
|
+
make: <TSchema extends Primitive.AnyPrimitive>(config: MimicServerEngineConfig<TSchema>) => Layer.Layer<MimicServerEngineTag, never, ColdStorageTag | HotStorageTag | MimicAuthServiceTag>;
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { MimicServerEngine, MimicServerEngineTag };
|
|
78
|
+
//# sourceMappingURL=MimicServerEngine.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MimicServerEngine.d.cts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AAkF0C,UAhCzB,iBAAA,CAgCgC;EAO5B;;;;;EAyBc,SAAA,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAxDlB,WAAA,CAAY,WAwDM,EAAA,GAvD5B,MAAA,CAAO,MAuDqB,CAvDd,YAuDc,EAvDA,oBAuDA,CAAA;EAAd;;;;EAMF,SAAA,WAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GArDZ,MAAA,CAAO,MAqDK,CAAA;IAAc,KAAA,EAAA,OAAA;IAChC,OAAA,EAAA,MAAA;KAtDyD;;;;AA+D1D;AAwIA;;EAjEqC,SAAU,SAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GA5HxC,MAAA,CAAO,MA4HiC,CA5H1B,MAAA,CAAO,MA4HmB,CA5HZ,aA4HY,EAAA,KAAA,EAAA,KAAA,CAAA,EA5H2B,oBA4H3B,EA5HiD,KAAA,CAAM,KA4HvD,CAAA;EACb;;;EAIhC,SAAA,KAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GA5HwC,MAAA,CAAO,MA4H/C,CAAA,IAAA,EAAA,KAAA,CAAA;EAAiB;;;EAHL,SAAA,mBAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAlHP,MAAA,CAAO,MAkHA,CAlHO,gBAkHP,EAAA,KAAA,CAAA;;;;0EA1GH,kBACJ,MAAA,CAAO;;;;yEAQP,MAAA,CAAO;;;;;sDAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,qCAAqC,KAAA,CAAM;;;;;mBAM3D,eAAe,SAAA,CAAU;;cAC3C;;;;cASY,oBAAA,SAA6B,yBAAA;cAwI7B;;yBAjEwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
|