@voidhash/mimic-effect 1.0.0-beta.16 → 1.0.0-beta.17

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.
Files changed (99) hide show
  1. package/dist/ColdStorage.cjs +1 -1
  2. package/dist/ColdStorage.d.cts +2 -2
  3. package/dist/ColdStorage.d.cts.map +1 -1
  4. package/dist/ColdStorage.d.mts +2 -2
  5. package/dist/ColdStorage.d.mts.map +1 -1
  6. package/dist/ColdStorage.mjs +2 -2
  7. package/dist/ColdStorage.mjs.map +1 -1
  8. package/dist/DocumentInstance.cjs +13 -13
  9. package/dist/DocumentInstance.mjs +13 -13
  10. package/dist/DocumentInstance.mjs.map +1 -1
  11. package/dist/Errors.d.cts +8 -8
  12. package/dist/Errors.d.cts.map +1 -1
  13. package/dist/Errors.d.mts +8 -8
  14. package/dist/Errors.d.mts.map +1 -1
  15. package/dist/HotStorage.cjs +1 -1
  16. package/dist/HotStorage.d.cts +2 -2
  17. package/dist/HotStorage.d.mts +2 -2
  18. package/dist/HotStorage.mjs +2 -2
  19. package/dist/HotStorage.mjs.map +1 -1
  20. package/dist/Metrics.cjs +6 -6
  21. package/dist/Metrics.d.cts +21 -23
  22. package/dist/Metrics.d.cts.map +1 -1
  23. package/dist/Metrics.d.mts +21 -23
  24. package/dist/Metrics.d.mts.map +1 -1
  25. package/dist/Metrics.mjs +7 -7
  26. package/dist/Metrics.mjs.map +1 -1
  27. package/dist/MimicAuthService.cjs +1 -1
  28. package/dist/MimicAuthService.d.cts +2 -2
  29. package/dist/MimicAuthService.d.cts.map +1 -1
  30. package/dist/MimicAuthService.d.mts +2 -2
  31. package/dist/MimicAuthService.d.mts.map +1 -1
  32. package/dist/MimicAuthService.mjs +2 -2
  33. package/dist/MimicAuthService.mjs.map +1 -1
  34. package/dist/MimicClusterServerEngine.cjs +38 -41
  35. package/dist/MimicClusterServerEngine.d.cts +1 -1
  36. package/dist/MimicClusterServerEngine.d.mts +1 -1
  37. package/dist/MimicClusterServerEngine.mjs +31 -34
  38. package/dist/MimicClusterServerEngine.mjs.map +1 -1
  39. package/dist/MimicServer.cjs +23 -23
  40. package/dist/MimicServer.d.cts +3 -3
  41. package/dist/MimicServer.d.cts.map +1 -1
  42. package/dist/MimicServer.d.mts +3 -3
  43. package/dist/MimicServer.d.mts.map +1 -1
  44. package/dist/MimicServer.mjs +22 -22
  45. package/dist/MimicServer.mjs.map +1 -1
  46. package/dist/MimicServerEngine.cjs +13 -13
  47. package/dist/MimicServerEngine.d.cts +2 -2
  48. package/dist/MimicServerEngine.d.mts +2 -2
  49. package/dist/MimicServerEngine.mjs +14 -14
  50. package/dist/MimicServerEngine.mjs.map +1 -1
  51. package/dist/PresenceManager.cjs +4 -4
  52. package/dist/PresenceManager.d.cts +2 -2
  53. package/dist/PresenceManager.d.mts +2 -2
  54. package/dist/PresenceManager.mjs +5 -5
  55. package/dist/PresenceManager.mjs.map +1 -1
  56. package/dist/Types.d.cts +1 -1
  57. package/dist/Types.d.mts +1 -1
  58. package/dist/testing/ColdStorageTestSuite.cjs +3 -3
  59. package/dist/testing/ColdStorageTestSuite.mjs +3 -3
  60. package/dist/testing/ColdStorageTestSuite.mjs.map +1 -1
  61. package/dist/testing/HotStorageTestSuite.cjs +13 -13
  62. package/dist/testing/HotStorageTestSuite.mjs +13 -13
  63. package/dist/testing/HotStorageTestSuite.mjs.map +1 -1
  64. package/dist/testing/StorageIntegrationTestSuite.cjs +3 -3
  65. package/dist/testing/StorageIntegrationTestSuite.mjs +3 -3
  66. package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -1
  67. package/dist/testing/types.d.cts +1 -1
  68. package/dist/testing/types.d.cts.map +1 -1
  69. package/dist/testing/types.d.mts +3 -3
  70. package/dist/testing/types.d.mts.map +1 -1
  71. package/package.json +17 -21
  72. package/src/ColdStorage.ts +4 -5
  73. package/src/DocumentInstance.ts +13 -13
  74. package/src/HotStorage.ts +3 -3
  75. package/src/Metrics.ts +22 -16
  76. package/src/MimicAuthService.ts +3 -3
  77. package/src/MimicClusterServerEngine.ts +35 -35
  78. package/src/MimicServer.ts +26 -30
  79. package/src/MimicServerEngine.ts +15 -15
  80. package/src/PresenceManager.ts +6 -6
  81. package/src/Types.ts +1 -1
  82. package/src/testing/ColdStorageTestSuite.ts +3 -3
  83. package/src/testing/HotStorageTestSuite.ts +17 -17
  84. package/src/testing/StorageIntegrationTestSuite.ts +3 -3
  85. package/.turbo/turbo-build.log +0 -154
  86. package/tests/ColdStorage.test.ts +0 -24
  87. package/tests/DocumentInstance.test.ts +0 -669
  88. package/tests/HotStorage.test.ts +0 -24
  89. package/tests/MimicAuthService.test.ts +0 -153
  90. package/tests/MimicClusterServerEngine.test.ts +0 -587
  91. package/tests/MimicServer.test.ts +0 -142
  92. package/tests/MimicServerEngine.test.ts +0 -547
  93. package/tests/PresenceManager.test.ts +0 -380
  94. package/tests/Protocol.test.ts +0 -190
  95. package/tests/StorageIntegration.test.ts +0 -259
  96. package/tsconfig.build.json +0 -24
  97. package/tsconfig.json +0 -8
  98. package/tsdown.config.ts +0 -18
  99. package/vitest.mts +0 -11
@@ -5,7 +5,7 @@ const require_Protocol = require('./Protocol.cjs');
5
5
  const require_MimicAuthService = require('./MimicAuthService.cjs');
6
6
  let effect = require("effect");
7
7
  let _voidhash_mimic = require("@voidhash/mimic");
8
- let _effect_platform = require("@effect/platform");
8
+ let effect_unstable_http = require("effect/unstable/http");
9
9
 
10
10
  //#region src/MimicServer.ts
11
11
  /**
@@ -24,8 +24,8 @@ const resolveRouteConfig = (config) => {
24
24
  var _config$path;
25
25
  return {
26
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) ? effect.Duration.decode(config.heartbeatInterval) : DEFAULT_HEARTBEAT_INTERVAL,
28
- heartbeatTimeout: (config === null || config === void 0 ? void 0 : config.heartbeatTimeout) ? effect.Duration.decode(config.heartbeatTimeout) : DEFAULT_HEARTBEAT_TIMEOUT
27
+ heartbeatInterval: (config === null || config === void 0 ? void 0 : config.heartbeatInterval) ? effect.Duration.fromInputUnsafe(config.heartbeatInterval) : DEFAULT_HEARTBEAT_INTERVAL,
28
+ heartbeatTimeout: (config === null || config === void 0 ? void 0 : config.heartbeatTimeout) ? effect.Duration.fromInputUnsafe(config.heartbeatTimeout) : DEFAULT_HEARTBEAT_TIMEOUT
29
29
  };
30
30
  };
31
31
  /**
@@ -45,8 +45,8 @@ const extractDocumentId = (path) => {
45
45
  const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle")(function* (socket, documentId, engine, authService, _routeConfig) {
46
46
  const connectionId = crypto.randomUUID();
47
47
  const connectionStartTime = Date.now();
48
- yield* effect.Metric.increment(require_Metrics.connectionsTotal);
49
- yield* effect.Metric.incrementBy(require_Metrics.connectionsActive, 1);
48
+ yield* effect.Metric.update(require_Metrics.connectionsTotal, 1);
49
+ yield* effect.Metric.update(require_Metrics.connectionsActive, 1);
50
50
  const state = {
51
51
  documentId,
52
52
  connectionId,
@@ -65,17 +65,17 @@ const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle"
65
65
  yield* sendMessage(require_Protocol.snapshotMessage(snapshot.state, snapshot.version));
66
66
  });
67
67
  const handleAuth = effect.Effect.fn("auth.handle")(function* (token) {
68
- const result = yield* effect.Effect.either(authService.authenticate(token, documentId));
69
- if (result._tag === "Right") {
68
+ const result = yield* effect.Effect.result(authService.authenticate(token, documentId));
69
+ if (result._tag === "Success") {
70
70
  state.authenticated = true;
71
- state.authContext = result.right;
72
- yield* sendMessage(require_Protocol.authResultSuccess(result.right.userId, result.right.permission));
71
+ state.authContext = result.success;
72
+ yield* sendMessage(require_Protocol.authResultSuccess(result.success.userId, result.success.permission));
73
73
  yield* sendDocumentSnapshot();
74
74
  yield* sendPresenceSnapshot();
75
75
  } else {
76
- var _result$left$reason;
77
- yield* effect.Metric.increment(require_Metrics.connectionsErrors);
78
- yield* sendMessage(require_Protocol.authResultFailure((_result$left$reason = result.left.reason) !== null && _result$left$reason !== void 0 ? _result$left$reason : "Authentication failed"));
76
+ var _result$failure$reaso;
77
+ yield* effect.Metric.update(require_Metrics.connectionsErrors, 1);
78
+ yield* sendMessage(require_Protocol.authResultFailure((_result$failure$reaso = result.failure.reason) !== null && _result$failure$reaso !== void 0 ? _result$failure$reaso : "Authentication failed"));
79
79
  }
80
80
  });
81
81
  const handlePresenceSet = effect.Effect.fn("presence.set.handle")(function* (data) {
@@ -141,12 +141,12 @@ const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle"
141
141
  break;
142
142
  }
143
143
  });
144
- const subscribeFiber = yield* effect.Effect.fork(effect.Effect.fn("subscriptions.document.start")(function* () {
144
+ const subscribeFiber = yield* effect.Effect.forkChild(effect.Effect.fn("subscriptions.document.start")(function* () {
145
145
  while (!state.authenticated) yield* effect.Effect.sleep(effect.Duration.millis(100));
146
146
  const broadcastStream = yield* engine.subscribe(documentId);
147
147
  yield* effect.Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
148
148
  })().pipe(effect.Effect.scoped));
149
- const presenceFiber = yield* effect.Effect.fork(effect.Effect.fn("subscriptions.presence.start")(function* () {
149
+ const presenceFiber = yield* effect.Effect.forkChild(effect.Effect.fn("subscriptions.presence.start")(function* () {
150
150
  if (!engine.config.presence) return;
151
151
  while (!state.authenticated) yield* effect.Effect.sleep(effect.Duration.millis(100));
152
152
  const presenceStream = yield* engine.subscribePresence(documentId);
@@ -161,7 +161,7 @@ const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle"
161
161
  yield* effect.Fiber.interrupt(subscribeFiber);
162
162
  yield* effect.Fiber.interrupt(presenceFiber);
163
163
  if (state.hasPresence && engine.config.presence) yield* engine.removePresence(documentId, connectionId);
164
- yield* effect.Metric.incrementBy(require_Metrics.connectionsActive, -1);
164
+ yield* effect.Metric.update(require_Metrics.connectionsActive, -1);
165
165
  yield* effect.Metric.update(require_Metrics.connectionsDuration, duration);
166
166
  yield* effect.Effect.logDebug("WebSocket connection closed", {
167
167
  connectionId,
@@ -171,7 +171,7 @@ const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle"
171
171
  })());
172
172
  yield* socket.runRaw((data) => effect.Effect.fn("message.process")(function* () {
173
173
  yield* handleMessage(yield* require_Protocol.parseClientMessage(data));
174
- })().pipe(effect.Effect.catchAll((error) => effect.Effect.logError("Message handling error", error))));
174
+ })().pipe(effect.Effect["catch"]((error) => effect.Effect.logError("Message handling error", error))));
175
175
  });
176
176
  /**
177
177
  * Create a route layer for MimicServerEngine.
@@ -214,16 +214,16 @@ const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle"
214
214
  const layerHttpLayerRouter = (options) => {
215
215
  const routeConfig = resolveRouteConfig(options);
216
216
  const routePath = `${routeConfig.path}/doc/:documentId`;
217
- return effect.Layer.scopedDiscard(effect.Effect.gen(function* () {
218
- const router = yield* _effect_platform.HttpLayerRouter.HttpRouter;
217
+ return effect.Layer.effectDiscard(effect.Effect.gen(function* () {
218
+ const router = yield* effect_unstable_http.HttpRouter.HttpRouter;
219
219
  const engine = yield* require_MimicServerEngine.MimicServerEngineTag;
220
220
  const authService = yield* require_MimicAuthService.MimicAuthServiceTag;
221
221
  const handler = effect.Effect.fn("websocket.route.handler")(function* (request) {
222
- const documentIdResult = yield* effect.Effect.either(extractDocumentId(request.url));
223
- if (documentIdResult._tag === "Left") return _effect_platform.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.Effect.scoped, effect.Effect.catchAll((error) => effect.Effect.logError("WebSocket connection error", error)));
226
- return _effect_platform.HttpServerResponse.empty();
222
+ const documentIdResult = yield* effect.Effect.result(extractDocumentId(request.url));
223
+ if (documentIdResult._tag === "Failure") return effect_unstable_http.HttpServerResponse.text(`Missing document ID in path: ${request.url}`, { status: 400 });
224
+ const documentId = documentIdResult.success;
225
+ yield* handleWebSocketConnection(yield* request.upgrade, documentId, engine, authService, routeConfig).pipe(effect.Effect.scoped, effect.Effect["catch"]((error) => effect.Effect.logError("WebSocket connection error", error)));
226
+ return effect_unstable_http.HttpServerResponse.empty();
227
227
  });
228
228
  yield* router.add("GET", routePath, handler);
229
229
  }));
@@ -2,13 +2,13 @@ import { MimicServerRouteConfig } from "./Types.cjs";
2
2
  import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
3
3
  import { MimicServerEngineTag } from "./MimicServerEngine.cjs";
4
4
  import { Layer } from "effect";
5
- import * as _effect_platform_HttpServerError0 from "@effect/platform/HttpServerError";
6
- import { HttpLayerRouter } from "@effect/platform";
5
+ import * as effect_unstable_http_HttpServerError0 from "effect/unstable/http/HttpServerError";
6
+ import { HttpRouter } from "effect/unstable/http";
7
7
 
8
8
  //#region src/MimicServer.d.ts
9
9
 
10
10
  declare const MimicServer: {
11
- layerHttpLayerRouter: (options?: MimicServerRouteConfig) => Layer.Layer<never, never, MimicServerEngineTag | MimicAuthServiceTag | HttpLayerRouter.HttpRouter | HttpLayerRouter.Request<"Error", _effect_platform_HttpServerError0.RequestError>>;
11
+ layerHttpLayerRouter: (options?: MimicServerRouteConfig) => Layer.Layer<never, never, MimicServerEngineTag | MimicAuthServiceTag | HttpRouter.HttpRouter | HttpRouter.Request<"Error", effect_unstable_http_HttpServerError0.HttpServerError>>;
12
12
  };
13
13
  //#endregion
14
14
  export { MimicServer };
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServer.d.cts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cA2ea;mCA9DD,2BAAsB,KAAA,CAAA,oBAAA,uBAAA,sBAAA,eAAA,CAAA,aAAA,eAAA,CAAA,iBAAA,iCAAA,CAAA,YAAA"}
1
+ {"version":3,"file":"MimicServer.d.cts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAuea;mCA9DD,2BAAsB,KAAA,CAAA,oBAAA,uBAAA,sBAAA,UAAA,CAAA,aAAA,UAAA,CAAA,iBAAA,qCAAA,CAAA,eAAA"}
@@ -2,13 +2,13 @@ import { MimicServerRouteConfig } from "./Types.mjs";
2
2
  import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
3
3
  import { MimicServerEngineTag } from "./MimicServerEngine.mjs";
4
4
  import { Layer } from "effect";
5
- import { HttpLayerRouter } from "@effect/platform";
6
- import * as _effect_platform_HttpServerError0 from "@effect/platform/HttpServerError";
5
+ import { HttpRouter } from "effect/unstable/http";
6
+ import * as effect_unstable_http_HttpServerError0 from "effect/unstable/http/HttpServerError";
7
7
 
8
8
  //#region src/MimicServer.d.ts
9
9
 
10
10
  declare const MimicServer: {
11
- layerHttpLayerRouter: (options?: MimicServerRouteConfig) => Layer.Layer<never, never, MimicServerEngineTag | MimicAuthServiceTag | HttpLayerRouter.HttpRouter | HttpLayerRouter.Request<"Error", _effect_platform_HttpServerError0.RequestError>>;
11
+ layerHttpLayerRouter: (options?: MimicServerRouteConfig) => Layer.Layer<never, never, MimicServerEngineTag | MimicAuthServiceTag | HttpRouter.HttpRouter | HttpRouter.Request<"Error", effect_unstable_http_HttpServerError0.HttpServerError>>;
12
12
  };
13
13
  //#endregion
14
14
  export { MimicServer };
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServer.d.mts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cA2ea;mCA9DD,2BAAsB,KAAA,CAAA,oBAAA,uBAAA,sBAAA,eAAA,CAAA,aAAA,eAAA,CAAA,iBAAA,iCAAA,CAAA,YAAA"}
1
+ {"version":3,"file":"MimicServer.d.mts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAuea;mCA9DD,2BAAsB,KAAA,CAAA,oBAAA,uBAAA,sBAAA,UAAA,CAAA,aAAA,UAAA,CAAA,iBAAA,qCAAA,CAAA,eAAA"}
@@ -5,7 +5,7 @@ import { authResultFailure, authResultSuccess, encodeServerMessage, errorMessage
5
5
  import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
6
6
  import { Duration, Effect, Fiber, Layer, Metric, Stream } from "effect";
7
7
  import { Presence } from "@voidhash/mimic";
8
- import { HttpLayerRouter, HttpServerResponse } from "@effect/platform";
8
+ import { HttpRouter, HttpServerResponse } from "effect/unstable/http";
9
9
 
10
10
  //#region src/MimicServer.ts
11
11
  /**
@@ -24,8 +24,8 @@ const resolveRouteConfig = (config) => {
24
24
  var _config$path;
25
25
  return {
26
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
27
+ heartbeatInterval: (config === null || config === void 0 ? void 0 : config.heartbeatInterval) ? Duration.fromInputUnsafe(config.heartbeatInterval) : DEFAULT_HEARTBEAT_INTERVAL,
28
+ heartbeatTimeout: (config === null || config === void 0 ? void 0 : config.heartbeatTimeout) ? Duration.fromInputUnsafe(config.heartbeatTimeout) : DEFAULT_HEARTBEAT_TIMEOUT
29
29
  };
30
30
  };
31
31
  /**
@@ -45,8 +45,8 @@ const extractDocumentId = (path) => {
45
45
  const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(function* (socket, documentId, engine, authService, _routeConfig) {
46
46
  const connectionId = crypto.randomUUID();
47
47
  const connectionStartTime = Date.now();
48
- yield* Metric.increment(connectionsTotal);
49
- yield* Metric.incrementBy(connectionsActive, 1);
48
+ yield* Metric.update(connectionsTotal, 1);
49
+ yield* Metric.update(connectionsActive, 1);
50
50
  const state = {
51
51
  documentId,
52
52
  connectionId,
@@ -65,17 +65,17 @@ const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(funct
65
65
  yield* sendMessage(snapshotMessage(snapshot.state, snapshot.version));
66
66
  });
67
67
  const handleAuth = Effect.fn("auth.handle")(function* (token) {
68
- const result = yield* Effect.either(authService.authenticate(token, documentId));
69
- if (result._tag === "Right") {
68
+ const result = yield* Effect.result(authService.authenticate(token, documentId));
69
+ if (result._tag === "Success") {
70
70
  state.authenticated = true;
71
- state.authContext = result.right;
72
- yield* sendMessage(authResultSuccess(result.right.userId, result.right.permission));
71
+ state.authContext = result.success;
72
+ yield* sendMessage(authResultSuccess(result.success.userId, result.success.permission));
73
73
  yield* sendDocumentSnapshot();
74
74
  yield* sendPresenceSnapshot();
75
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"));
76
+ var _result$failure$reaso;
77
+ yield* Metric.update(connectionsErrors, 1);
78
+ yield* sendMessage(authResultFailure((_result$failure$reaso = result.failure.reason) !== null && _result$failure$reaso !== void 0 ? _result$failure$reaso : "Authentication failed"));
79
79
  }
80
80
  });
81
81
  const handlePresenceSet = Effect.fn("presence.set.handle")(function* (data) {
@@ -141,12 +141,12 @@ const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(funct
141
141
  break;
142
142
  }
143
143
  });
144
- const subscribeFiber = yield* Effect.fork(Effect.fn("subscriptions.document.start")(function* () {
144
+ const subscribeFiber = yield* Effect.forkChild(Effect.fn("subscriptions.document.start")(function* () {
145
145
  while (!state.authenticated) yield* Effect.sleep(Duration.millis(100));
146
146
  const broadcastStream = yield* engine.subscribe(documentId);
147
147
  yield* Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
148
148
  })().pipe(Effect.scoped));
149
- const presenceFiber = yield* Effect.fork(Effect.fn("subscriptions.presence.start")(function* () {
149
+ const presenceFiber = yield* Effect.forkChild(Effect.fn("subscriptions.presence.start")(function* () {
150
150
  if (!engine.config.presence) return;
151
151
  while (!state.authenticated) yield* Effect.sleep(Duration.millis(100));
152
152
  const presenceStream = yield* engine.subscribePresence(documentId);
@@ -161,7 +161,7 @@ const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(funct
161
161
  yield* Fiber.interrupt(subscribeFiber);
162
162
  yield* Fiber.interrupt(presenceFiber);
163
163
  if (state.hasPresence && engine.config.presence) yield* engine.removePresence(documentId, connectionId);
164
- yield* Metric.incrementBy(connectionsActive, -1);
164
+ yield* Metric.update(connectionsActive, -1);
165
165
  yield* Metric.update(connectionsDuration, duration);
166
166
  yield* Effect.logDebug("WebSocket connection closed", {
167
167
  connectionId,
@@ -171,7 +171,7 @@ const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(funct
171
171
  })());
172
172
  yield* socket.runRaw((data) => Effect.fn("message.process")(function* () {
173
173
  yield* handleMessage(yield* parseClientMessage(data));
174
- })().pipe(Effect.catchAll((error) => Effect.logError("Message handling error", error))));
174
+ })().pipe(Effect["catch"]((error) => Effect.logError("Message handling error", error))));
175
175
  });
176
176
  /**
177
177
  * Create a route layer for MimicServerEngine.
@@ -214,15 +214,15 @@ const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(funct
214
214
  const layerHttpLayerRouter = (options) => {
215
215
  const routeConfig = resolveRouteConfig(options);
216
216
  const routePath = `${routeConfig.path}/doc/:documentId`;
217
- return Layer.scopedDiscard(Effect.gen(function* () {
218
- const router = yield* HttpLayerRouter.HttpRouter;
217
+ return Layer.effectDiscard(Effect.gen(function* () {
218
+ const router = yield* HttpRouter.HttpRouter;
219
219
  const engine = yield* MimicServerEngineTag;
220
220
  const authService = yield* MimicAuthServiceTag;
221
221
  const handler = Effect.fn("websocket.route.handler")(function* (request) {
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)));
222
+ const documentIdResult = yield* Effect.result(extractDocumentId(request.url));
223
+ if (documentIdResult._tag === "Failure") return HttpServerResponse.text(`Missing document ID in path: ${request.url}`, { status: 400 });
224
+ const documentId = documentIdResult.success;
225
+ yield* handleWebSocketConnection(yield* request.upgrade, documentId, engine, authService, routeConfig).pipe(Effect.scoped, Effect["catch"]((error) => Effect.logError("WebSocket connection error", error)));
226
226
  return HttpServerResponse.empty();
227
227
  });
228
228
  yield* router.add("GET", routePath, handler);
@@ -1 +1 @@
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 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 = Effect.fn(\"websocket.connection.handle\")(\n function* (\n socket: Socket.Socket,\n documentId: string,\n engine: MimicServerEngine,\n authService: MimicAuthService,\n _routeConfig: ResolvedRouteConfig\n ) {\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.fn(\"presence.snapshot.send\")(\n 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\n // Send document snapshot after auth\n const sendDocumentSnapshot = Effect.fn(\"document.snapshot.send\")(\n function* () {\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n }\n );\n\n // Handle authentication\n const handleAuth = Effect.fn(\"auth.handle\")(function* (token: string) {\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 = Effect.fn(\"presence.set.handle\")(\n function* (data: unknown) {\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\n // Handle presence clear\n const handlePresenceClear = Effect.fn(\"presence.clear.handle\")(\n 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\n // Handle a client message\n const handleMessage = Effect.fn(\"message.handle\")(\n function* (message: Protocol.ClientMessage) {\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\n // Subscribe to document broadcasts\n const subscribeFiber = yield* Effect.fork(\n Effect.fn(\"subscriptions.document.start\")(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.fn(\"subscriptions.presence.start\")(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.fn(\"connection.cleanup\")(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.fn(\"message.process\")(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// =============================================================================\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 = Effect.fn(\"websocket.route.handler\")(\n function* (request: HttpServerRequest.HttpServerRequest) {\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\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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"}
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 Stream,\n} from \"effect\";\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from \"effect/unstable/http\";\nimport type * as Socket from \"effect/unstable/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.fromInputUnsafe(config.heartbeatInterval)\n : DEFAULT_HEARTBEAT_INTERVAL,\n heartbeatTimeout: config?.heartbeatTimeout\n ? Duration.fromInputUnsafe(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 = Effect.fn(\"websocket.connection.handle\")(\n function* (\n socket: Socket.Socket.Socket,\n documentId: string,\n engine: MimicServerEngine,\n authService: MimicAuthService,\n _routeConfig: ResolvedRouteConfig\n ) {\n const connectionId = crypto.randomUUID();\n const connectionStartTime = Date.now();\n\n // Track connection metrics\n yield* Metric.update(Metrics.connectionsTotal, 1);\n yield* Metric.update(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.fn(\"presence.snapshot.send\")(\n 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\n // Send document snapshot after auth\n const sendDocumentSnapshot = Effect.fn(\"document.snapshot.send\")(\n function* () {\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n }\n );\n\n // Handle authentication\n const handleAuth = Effect.fn(\"auth.handle\")(function* (token: string) {\n const result = yield* Effect.result(\n authService.authenticate(token, documentId)\n );\n\n if (result._tag === \"Success\") {\n state.authenticated = true;\n state.authContext = result.success;\n\n yield* sendMessage(\n Protocol.authResultSuccess(\n result.success.userId,\n result.success.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.update(Metrics.connectionsErrors, 1);\n yield* sendMessage(\n Protocol.authResultFailure(\n result.failure.reason ?? \"Authentication failed\"\n )\n );\n }\n });\n\n // Handle presence set\n const handlePresenceSet = Effect.fn(\"presence.set.handle\")(\n function* (data: unknown) {\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\n // Handle presence clear\n const handlePresenceClear = Effect.fn(\"presence.clear.handle\")(\n 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\n // Handle a client message\n const handleMessage = Effect.fn(\"message.handle\")(\n function* (message: Protocol.ClientMessage) {\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\n // Subscribe to document broadcasts\n const subscribeFiber = yield* Effect.forkChild(\n Effect.fn(\"subscriptions.document.start\")(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.forkChild(\n Effect.fn(\"subscriptions.presence.start\")(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.fn(\"connection.cleanup\")(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.update(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: string | Uint8Array) =>\n Effect.fn(\"message.process\")(function* () {\n const message = yield* Protocol.parseClientMessage(data);\n yield* handleMessage(message);\n })().pipe(\n Effect[\"catch\"]((error) =>\n Effect.logError(\"Message handling error\", error)\n )\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 HttpRouter.PathInput;\n\n return Layer.effectDiscard(\n Effect.gen(function* () {\n const router = yield* HttpRouter.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 = Effect.fn(\"websocket.route.handler\")(\n function* (request: HttpServerRequest.HttpServerRequest) {\n // Extract document ID from path\n const documentIdResult = yield* Effect.result(\n extractDocumentId(request.url)\n );\n if (documentIdResult._tag === \"Failure\") {\n return HttpServerResponse.text(\n `Missing document ID in path: ${request.url}`,\n { status: 400 }\n );\n }\n const documentId = documentIdResult.success;\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[\"catch\"]((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\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":";;;;;;;;;;;;;;;;AA6BA,MAAM,eAAe;AACrB,MAAM,6BAA6B,SAAS,QAAQ,GAAG;AACvD,MAAM,4BAA4B,SAAS,QAAQ,GAAG;;;;AAKtD,MAAM,sBACJ,WACwB;;QAAC;EACzB,sEAAM,OAAQ,2DAAQ;EACtB,oEAAmB,OAAQ,qBACvB,SAAS,gBAAgB,OAAO,kBAAkB,GAClD;EACJ,mEAAkB,OAAQ,oBACtB,SAAS,gBAAgB,OAAO,iBAAiB,GACjD;EACL;;;;;;AAUD,MAAM,qBACJ,SACkD;CAElD,MAAM,QAAQ,KAAK,QAAQ,QAAQ,GAAG,CAAC,MAAM,IAAI;CAGjD,MAAM,WAAW,MAAM,YAAY,MAAM;CACzC,MAAM,OAAO,MAAM,WAAW;AAC9B,KAAI,aAAa,MAAM,KACrB,QAAO,OAAO,QAAQ,mBAAmB,KAAK,CAAC;AAEjD,QAAO,OAAO,KAAK,IAAI,uBAAuB,EAAE,MAAM,CAAC,CAAC;;;;;AAsB1D,MAAM,4BAA4B,OAAO,GAAG,8BAA8B,CACxE,WACE,QACA,YACA,QACA,aACA,cACA;CACA,MAAM,eAAe,OAAO,YAAY;CACxC,MAAM,sBAAsB,KAAK,KAAK;AAGtC,QAAO,OAAO,OAAOA,kBAA0B,EAAE;AACjD,QAAO,OAAO,OAAOC,mBAA2B,EAAE;CAGlD,MAAMC,QAAyB;EAC7B;EACA;EACA,eAAe;EACf,aAAa;EACd;CAGD,MAAM,QAAQ,OAAO,OAAO;CAG5B,MAAM,eAAe,YACnB,MAAMC,oBAA6B,QAAQ,CAAC;CAG9C,MAAM,uBAAuB,OAAO,GAAG,yBAAyB,CAC9D,aAAa;AACX,MAAI,CAAC,OAAO,OAAO,SAAU;EAE7B,MAAM,WAAW,OAAO,OAAO,oBAAoB,WAAW;AAC9D,SAAO,YACLC,wBAAiC,cAAc,SAAS,UAAU,CACnE;GAEJ;CAGD,MAAM,uBAAuB,OAAO,GAAG,yBAAyB,CAC9D,aAAa;EACX,MAAM,WAAW,OAAO,OAAO,YAAY,WAAW;AACtD,SAAO,YACLC,gBAAyB,SAAS,OAAO,SAAS,QAAQ,CAC3D;GAEJ;CAGD,MAAM,aAAa,OAAO,GAAG,cAAc,CAAC,WAAW,OAAe;EACpE,MAAM,SAAS,OAAO,OAAO,OAC3B,YAAY,aAAa,OAAO,WAAW,CAC5C;AAED,MAAI,OAAO,SAAS,WAAW;AAC7B,SAAM,gBAAgB;AACtB,SAAM,cAAc,OAAO;AAE3B,UAAO,YACLC,kBACE,OAAO,QAAQ,QACf,OAAO,QAAQ,WAChB,CACF;AAGD,UAAO,sBAAsB;AAG7B,UAAO,sBAAsB;SACxB;;AACL,UAAO,OAAO,OAAOC,mBAA2B,EAAE;AAClD,UAAO,YACLC,2CACE,OAAO,QAAQ,+EAAU,wBAC1B,CACF;;GAEH;CAGF,MAAM,oBAAoB,OAAO,GAAG,sBAAsB,CACxD,WAAW,MAAe;AACxB,MAAI,CAAC,MAAM,cAAe;AAC1B,MAAI,CAAC,MAAM,YAAa;AACxB,MAAI,CAAC,OAAO,OAAO,SAAU;AAG7B,MAAI,MAAM,YAAY,eAAe,SAAS;AAC5C,UAAO,OAAO,WAAW,0CAA0C,EACjE,cACD,CAAC;AACF;;EAIF,MAAM,YAAY,SAAS,aAAa,OAAO,OAAO,UAAU,KAAK;AACrE,MAAI,cAAc,QAAW;AAC3B,UAAO,OAAO,WAAW,kCAAkC;IACzD;IACA;IACD,CAAC;AACF;;AAIF,SAAO,OAAO,YAAY,YAAY,cAAc;GAClD,MAAM;GACN,QAAQ,MAAM,YAAY;GAC3B,CAAC;AAEF,QAAM,cAAc;GAEvB;CAGD,MAAM,sBAAsB,OAAO,GAAG,wBAAwB,CAC5D,aAAa;AACX,MAAI,CAAC,MAAM,cAAe;AAC1B,MAAI,CAAC,OAAO,OAAO,SAAU;AAE7B,SAAO,OAAO,eAAe,YAAY,aAAa;AACtD,QAAM,cAAc;GAEvB;CAGD,MAAM,gBAAgB,OAAO,GAAG,iBAAiB,CAC/C,WAAW,SAAiC;AAE1C,SAAO,OAAO,MAAM,WAAW;AAE/B,UAAQ,QAAQ,MAAhB;GACE,KAAK;AACH,WAAO,WAAW,QAAQ,MAAM;AAChC;GAEF,KAAK;AACH,WAAO,YAAYC,MAAe,CAAC;AACnC;GAEF,KAAK;;AACH,QAAI,CAAC,MAAM,eAAe;AACxB,YAAO,YACLC,aACE,QAAQ,YAAY,IACpB,oBACD,CACF;AACD;;AAIF,+BAAI,MAAM,qFAAa,gBAAe,SAAS;AAC7C,YAAO,YACLA,aACE,QAAQ,YAAY,IACpB,4BACD,CACF;AACD;;IAIF,MAAM,eAAe,OAAO,OAAO,OACjC,YACA,QAAQ,YACT;AAGD,QAAI,CAAC,aAAa,QAChB,QAAO,YACLA,aAAsB,QAAQ,YAAY,IAAI,aAAa,OAAO,CACnE;AAEH;GAEF,KAAK;AACH,QAAI,CAAC,MAAM,cACT;IAEF,MAAM,WAAW,OAAO,OAAO,YAAY,WAAW;AACtD,WAAO,YACLL,gBAAyB,SAAS,OAAO,SAAS,QAAQ,CAC3D;AACD;GAEF,KAAK;AACH,WAAO,kBAAkB,QAAQ,KAAK;AACtC;GAEF,KAAK;AACH,WAAO,qBAAqB;AAC5B;;GAGP;CAGD,MAAM,iBAAiB,OAAO,OAAO,UACnC,OAAO,GAAG,+BAA+B,CAAC,aAAa;AAErD,SAAO,CAAC,MAAM,cACZ,QAAO,OAAO,MAAM,SAAS,OAAO,IAAI,CAAC;EAI3C,MAAM,kBAAkB,OAAO,OAAO,UAAU,WAAW;AAG3D,SAAO,OAAO,WAAW,kBAAkB,cACzC,YAAY,UAAoC,CACjD;GACD,EAAE,CAAC,KAAK,OAAO,OAAO,CACzB;CAGD,MAAM,gBAAgB,OAAO,OAAO,UAClC,OAAO,GAAG,+BAA+B,CAAC,aAAa;AACrD,MAAI,CAAC,OAAO,OAAO,SAAU;AAG7B,SAAO,CAAC,MAAM,cACZ,QAAO,OAAO,MAAM,SAAS,OAAO,IAAI,CAAC;EAI3C,MAAM,iBAAiB,OAAO,OAAO,kBAAkB,WAAW;AAGlE,SAAO,OAAO,WAAW,iBAAiB,UACxC,OAAO,IAAI,aAAa;AAEtB,OAAI,MAAM,OAAO,aAAc;AAE/B,OAAI,MAAM,SAAS,kBACjB,QAAO,YACLM,sBAA+B,MAAM,IAAI,MAAM,MAAM,MAAM,OAAO,CACnE;YACQ,MAAM,SAAS,kBACxB,QAAO,YAAYC,sBAA+B,MAAM,GAAG,CAAC;IAE9D,CACH;GACD,EAAE,CAAC,KAAK,OAAO,OAAO,CACzB;AAGD,QAAO,OAAO,mBACZ,OAAO,GAAG,qBAAqB,CAAC,aAAa;EAE3C,MAAM,WAAW,KAAK,KAAK,GAAG;AAG9B,SAAO,MAAM,UAAU,eAAe;AACtC,SAAO,MAAM,UAAU,cAAc;AAGrC,MAAI,MAAM,eAAe,OAAO,OAAO,SACrC,QAAO,OAAO,eAAe,YAAY,aAAa;AAIxD,SAAO,OAAO,OAAOX,mBAA2B,GAAG;AACnD,SAAO,OAAO,OAAOY,qBAA6B,SAAS;AAE3D,SAAO,OAAO,SAAS,+BAA+B;GACpD;GACA;GACA,YAAY;GACb,CAAC;GACF,EAAE,CACL;AAGD,QAAO,OAAO,QAAQ,SACpB,OAAO,GAAG,kBAAkB,CAAC,aAAa;AAExC,SAAO,cADS,OAAOC,mBAA4B,KAAK,CAC3B;GAC7B,EAAE,CAAC,KACH,OAAO,UAAU,UACf,OAAO,SAAS,0BAA0B,MAAM,CACjD,CACF,CACF;EAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CD,MAAa,wBACX,YACG;CACH,MAAM,cAAc,mBAAmB,QAAQ;CAG/C,MAAM,YACJ,GAAG,YAAY,KAAK;AAEtB,QAAO,MAAM,cACX,OAAO,IAAI,aAAa;EACtB,MAAM,SAAS,OAAO,WAAW;EAEjC,MAAM,SAAS,OAAO;EACtB,MAAM,cAAc,OAAO;EAI3B,MAAM,UAAU,OAAO,GAAG,0BAA0B,CAClD,WAAW,SAA8C;GAEvD,MAAM,mBAAmB,OAAO,OAAO,OACrC,kBAAkB,QAAQ,IAAI,CAC/B;AACD,OAAI,iBAAiB,SAAS,UAC5B,QAAO,mBAAmB,KACxB,gCAAgC,QAAQ,OACxC,EAAE,QAAQ,KAAK,CAChB;GAEH,MAAM,aAAa,iBAAiB;AAMpC,UAAO,0BAHQ,OAAO,QAAQ,SAK5B,YACA,QACA,aACA,YACD,CAAC,KACA,OAAO,QACP,OAAO,UAAU,UACf,OAAO,SAAS,8BAA8B,MAAM,CACrD,CACF;AAGD,UAAO,mBAAmB,OAAO;IAEpC;AAED,SAAO,OAAO,IAAI,OAAO,WAAW,QAAQ;GAC5C,CACH;;AAOH,MAAa,cAAc,EACzB,sBACD"}
@@ -17,7 +17,7 @@ let effect = require("effect");
17
17
  /**
18
18
  * Context tag for MimicServerEngine
19
19
  */
20
- var MimicServerEngineTag = class extends effect.Context.Tag("@voidhash/mimic-effect/MimicServerEngine")() {};
20
+ var MimicServerEngineTag = class extends effect.ServiceMap.Service()("@voidhash/mimic-effect/MimicServerEngine") {};
21
21
  const DEFAULT_MAX_IDLE_TIME = effect.Duration.minutes(5);
22
22
  const DEFAULT_MAX_TRANSACTION_HISTORY = 1e3;
23
23
  const DEFAULT_SNAPSHOT_INTERVAL = effect.Duration.minutes(5);
@@ -32,12 +32,12 @@ const resolveConfig = (config) => {
32
32
  schema: config.schema,
33
33
  initial: config.initial,
34
34
  presence: config.presence,
35
- maxIdleTime: config.maxIdleTime ? effect.Duration.decode(config.maxIdleTime) : DEFAULT_MAX_IDLE_TIME,
35
+ maxIdleTime: config.maxIdleTime ? effect.Duration.fromInputUnsafe(config.maxIdleTime) : DEFAULT_MAX_IDLE_TIME,
36
36
  maxTransactionHistory: (_config$maxTransactio = config.maxTransactionHistory) !== null && _config$maxTransactio !== void 0 ? _config$maxTransactio : DEFAULT_MAX_TRANSACTION_HISTORY,
37
37
  snapshot: {
38
- interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? effect.Duration.decode(config.snapshot.interval) : DEFAULT_SNAPSHOT_INTERVAL,
38
+ interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? effect.Duration.fromInputUnsafe(config.snapshot.interval) : DEFAULT_SNAPSHOT_INTERVAL,
39
39
  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,
40
- idleTimeout: ((_config$snapshot3 = config.snapshot) === null || _config$snapshot3 === void 0 ? void 0 : _config$snapshot3.idleTimeout) ? effect.Duration.decode(config.snapshot.idleTimeout) : DEFAULT_SNAPSHOT_IDLE_TIMEOUT
40
+ idleTimeout: ((_config$snapshot3 = config.snapshot) === null || _config$snapshot3 === void 0 ? void 0 : _config$snapshot3.idleTimeout) ? effect.Duration.fromInputUnsafe(config.snapshot.idleTimeout) : DEFAULT_SNAPSHOT_IDLE_TIMEOUT
41
41
  }
42
42
  };
43
43
  };
@@ -74,7 +74,7 @@ const resolveConfig = (config) => {
74
74
  */
75
75
  const make = (config) => {
76
76
  const resolvedConfig = resolveConfig(config);
77
- return effect.Layer.scoped(MimicServerEngineTag, effect.Effect.gen(function* () {
77
+ return effect.Layer.effect(MimicServerEngineTag, effect.Effect.gen(function* () {
78
78
  const coldStorage = yield* require_ColdStorage.ColdStorageTag;
79
79
  const hotStorage = yield* require_HotStorage.HotStorageTag;
80
80
  const presenceManager = yield* require_PresenceManager.PresenceManagerTag;
@@ -108,16 +108,16 @@ const make = (config) => {
108
108
  const now = Date.now();
109
109
  const maxIdleMs = effect.Duration.toMillis(resolvedConfig.maxIdleTime);
110
110
  for (const [documentId, entry] of current) if (now - (yield* effect.Ref.get(entry.lastActivityTime)) >= maxIdleMs) {
111
- yield* effect.Effect.catchAll(entry.instance.saveSnapshot(), (e) => effect.Effect.logError("Failed to save snapshot during eviction", {
111
+ yield* effect.Effect["catch"](entry.instance.saveSnapshot(), (e) => effect.Effect.logError("Failed to save snapshot during eviction", {
112
112
  documentId,
113
113
  error: e
114
114
  }));
115
115
  yield* effect.Ref.update(store, (map) => effect.HashMap.remove(map, documentId));
116
- yield* effect.Metric.increment(require_Metrics.documentsEvicted);
117
- yield* effect.Metric.incrementBy(require_Metrics.documentsActive, -1);
116
+ yield* effect.Metric.update(require_Metrics.documentsEvicted, 1);
117
+ yield* effect.Metric.update(require_Metrics.documentsActive, -1);
118
118
  yield* effect.Effect.logInfo("Document evicted due to idle timeout", { documentId });
119
119
  }
120
- })().pipe(effect.Effect.repeat(effect.Schedule.spaced("1 minute")), effect.Effect.fork);
120
+ })().pipe(effect.Effect.repeat(effect.Schedule.spaced("1 minute")), effect.Effect.forkChild);
121
121
  })();
122
122
  yield* effect.Effect.fn("engine.snapshot.fiber.start")(function* () {
123
123
  const idleTimeoutMs = effect.Duration.toMillis(resolvedConfig.snapshot.idleTimeout);
@@ -128,17 +128,17 @@ const make = (config) => {
128
128
  for (const [documentId, entry] of current) {
129
129
  if (now - (yield* effect.Ref.get(entry.lastActivityTime)) < idleTimeoutMs) continue;
130
130
  if (!(yield* entry.instance.needsSnapshot())) continue;
131
- yield* effect.Effect.catchAll(entry.instance.saveSnapshot(), (e) => effect.Effect.logWarning("Periodic snapshot save failed", {
131
+ yield* effect.Effect["catch"](entry.instance.saveSnapshot(), (e) => effect.Effect.logWarning("Periodic snapshot save failed", {
132
132
  documentId,
133
133
  error: e
134
134
  }));
135
- yield* effect.Metric.increment(require_Metrics.storageIdleSnapshots);
135
+ yield* effect.Metric.update(require_Metrics.storageIdleSnapshots, 1);
136
136
  }
137
- })().pipe(effect.Effect.repeat(effect.Schedule.spaced("10 seconds")), effect.Effect.fork);
137
+ })().pipe(effect.Effect.repeat(effect.Schedule.spaced("10 seconds")), effect.Effect.forkChild);
138
138
  })();
139
139
  yield* effect.Effect.addFinalizer(() => effect.Effect.fn("engine.shutdown")(function* () {
140
140
  const current = yield* effect.Ref.get(store);
141
- for (const [documentId, entry] of current) yield* effect.Effect.catchAll(entry.instance.saveSnapshot(), (e) => effect.Effect.logError("Failed to save snapshot during shutdown", {
141
+ for (const [documentId, entry] of current) yield* effect.Effect["catch"](entry.instance.saveSnapshot(), (e) => effect.Effect.logError("Failed to save snapshot during shutdown", {
142
142
  documentId,
143
143
  error: e
144
144
  }));
@@ -5,7 +5,7 @@ import { ColdStorageTag } from "./ColdStorage.cjs";
5
5
  import { HotStorageTag } from "./HotStorage.cjs";
6
6
  import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
7
7
  import { SubmitResult } from "./DocumentInstance.cjs";
8
- import { Context, Effect, Layer, Scope, Stream } from "effect";
8
+ import { Effect, Layer, Scope, ServiceMap, Stream } from "effect";
9
9
  import { Primitive, Transaction } from "@voidhash/mimic";
10
10
 
11
11
  //#region src/MimicServerEngine.d.ts
@@ -77,7 +77,7 @@ interface MimicServerEngine {
77
77
  */
78
78
  readonly config: ResolvedConfig<Primitive.AnyPrimitive>;
79
79
  }
80
- declare const MimicServerEngineTag_base: Context.TagClass<MimicServerEngineTag, "@voidhash/mimic-effect/MimicServerEngine", MimicServerEngine>;
80
+ declare const MimicServerEngineTag_base: ServiceMap.ServiceClass<MimicServerEngineTag, "@voidhash/mimic-effect/MimicServerEngine", MimicServerEngine>;
81
81
  /**
82
82
  * Context tag for MimicServerEngine
83
83
  */
@@ -5,7 +5,7 @@ import { ColdStorageTag } from "./ColdStorage.mjs";
5
5
  import { HotStorageTag } from "./HotStorage.mjs";
6
6
  import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
7
7
  import { SubmitResult } from "./DocumentInstance.mjs";
8
- import { Context, Effect, Layer, Scope, Stream } from "effect";
8
+ import { Effect, Layer, Scope, ServiceMap, Stream } from "effect";
9
9
  import { Primitive, Transaction } from "@voidhash/mimic";
10
10
 
11
11
  //#region src/MimicServerEngine.d.ts
@@ -77,7 +77,7 @@ interface MimicServerEngine {
77
77
  */
78
78
  readonly config: ResolvedConfig<Primitive.AnyPrimitive>;
79
79
  }
80
- declare const MimicServerEngineTag_base: Context.TagClass<MimicServerEngineTag, "@voidhash/mimic-effect/MimicServerEngine", MimicServerEngine>;
80
+ declare const MimicServerEngineTag_base: ServiceMap.ServiceClass<MimicServerEngineTag, "@voidhash/mimic-effect/MimicServerEngine", MimicServerEngine>;
81
81
  /**
82
82
  * Context tag for MimicServerEngine
83
83
  */
@@ -3,7 +3,7 @@ import { HotStorageTag } from "./HotStorage.mjs";
3
3
  import { documentsActive, documentsEvicted, storageIdleSnapshots } from "./Metrics.mjs";
4
4
  import { DocumentInstance } from "./DocumentInstance.mjs";
5
5
  import { PresenceManagerTag, layer } from "./PresenceManager.mjs";
6
- import { Context, Duration, Effect, HashMap, Layer, Metric, Ref, Schedule, Stream } from "effect";
6
+ import { Duration, Effect, HashMap, Layer, Metric, Ref, Schedule, ServiceMap, Stream } from "effect";
7
7
 
8
8
  //#region src/MimicServerEngine.ts
9
9
  /**
@@ -17,7 +17,7 @@ import { Context, Duration, Effect, HashMap, Layer, Metric, Ref, Schedule, Strea
17
17
  /**
18
18
  * Context tag for MimicServerEngine
19
19
  */
20
- var MimicServerEngineTag = class extends Context.Tag("@voidhash/mimic-effect/MimicServerEngine")() {};
20
+ var MimicServerEngineTag = class extends ServiceMap.Service()("@voidhash/mimic-effect/MimicServerEngine") {};
21
21
  const DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);
22
22
  const DEFAULT_MAX_TRANSACTION_HISTORY = 1e3;
23
23
  const DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);
@@ -32,12 +32,12 @@ const resolveConfig = (config) => {
32
32
  schema: config.schema,
33
33
  initial: config.initial,
34
34
  presence: config.presence,
35
- maxIdleTime: config.maxIdleTime ? Duration.decode(config.maxIdleTime) : DEFAULT_MAX_IDLE_TIME,
35
+ maxIdleTime: config.maxIdleTime ? Duration.fromInputUnsafe(config.maxIdleTime) : DEFAULT_MAX_IDLE_TIME,
36
36
  maxTransactionHistory: (_config$maxTransactio = config.maxTransactionHistory) !== null && _config$maxTransactio !== void 0 ? _config$maxTransactio : DEFAULT_MAX_TRANSACTION_HISTORY,
37
37
  snapshot: {
38
- interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? Duration.decode(config.snapshot.interval) : DEFAULT_SNAPSHOT_INTERVAL,
38
+ interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? Duration.fromInputUnsafe(config.snapshot.interval) : DEFAULT_SNAPSHOT_INTERVAL,
39
39
  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,
40
- idleTimeout: ((_config$snapshot3 = config.snapshot) === null || _config$snapshot3 === void 0 ? void 0 : _config$snapshot3.idleTimeout) ? Duration.decode(config.snapshot.idleTimeout) : DEFAULT_SNAPSHOT_IDLE_TIMEOUT
40
+ idleTimeout: ((_config$snapshot3 = config.snapshot) === null || _config$snapshot3 === void 0 ? void 0 : _config$snapshot3.idleTimeout) ? Duration.fromInputUnsafe(config.snapshot.idleTimeout) : DEFAULT_SNAPSHOT_IDLE_TIMEOUT
41
41
  }
42
42
  };
43
43
  };
@@ -74,7 +74,7 @@ const resolveConfig = (config) => {
74
74
  */
75
75
  const make = (config) => {
76
76
  const resolvedConfig = resolveConfig(config);
77
- return Layer.scoped(MimicServerEngineTag, Effect.gen(function* () {
77
+ return Layer.effect(MimicServerEngineTag, Effect.gen(function* () {
78
78
  const coldStorage = yield* ColdStorageTag;
79
79
  const hotStorage = yield* HotStorageTag;
80
80
  const presenceManager = yield* PresenceManagerTag;
@@ -108,16 +108,16 @@ const make = (config) => {
108
108
  const now = Date.now();
109
109
  const maxIdleMs = Duration.toMillis(resolvedConfig.maxIdleTime);
110
110
  for (const [documentId, entry] of current) if (now - (yield* Ref.get(entry.lastActivityTime)) >= maxIdleMs) {
111
- yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) => Effect.logError("Failed to save snapshot during eviction", {
111
+ yield* Effect["catch"](entry.instance.saveSnapshot(), (e) => Effect.logError("Failed to save snapshot during eviction", {
112
112
  documentId,
113
113
  error: e
114
114
  }));
115
115
  yield* Ref.update(store, (map) => HashMap.remove(map, documentId));
116
- yield* Metric.increment(documentsEvicted);
117
- yield* Metric.incrementBy(documentsActive, -1);
116
+ yield* Metric.update(documentsEvicted, 1);
117
+ yield* Metric.update(documentsActive, -1);
118
118
  yield* Effect.logInfo("Document evicted due to idle timeout", { documentId });
119
119
  }
120
- })().pipe(Effect.repeat(Schedule.spaced("1 minute")), Effect.fork);
120
+ })().pipe(Effect.repeat(Schedule.spaced("1 minute")), Effect.forkChild);
121
121
  })();
122
122
  yield* Effect.fn("engine.snapshot.fiber.start")(function* () {
123
123
  const idleTimeoutMs = Duration.toMillis(resolvedConfig.snapshot.idleTimeout);
@@ -128,17 +128,17 @@ const make = (config) => {
128
128
  for (const [documentId, entry] of current) {
129
129
  if (now - (yield* Ref.get(entry.lastActivityTime)) < idleTimeoutMs) continue;
130
130
  if (!(yield* entry.instance.needsSnapshot())) continue;
131
- yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) => Effect.logWarning("Periodic snapshot save failed", {
131
+ yield* Effect["catch"](entry.instance.saveSnapshot(), (e) => Effect.logWarning("Periodic snapshot save failed", {
132
132
  documentId,
133
133
  error: e
134
134
  }));
135
- yield* Metric.increment(storageIdleSnapshots);
135
+ yield* Metric.update(storageIdleSnapshots, 1);
136
136
  }
137
- })().pipe(Effect.repeat(Schedule.spaced("10 seconds")), Effect.fork);
137
+ })().pipe(Effect.repeat(Schedule.spaced("10 seconds")), Effect.forkChild);
138
138
  })();
139
139
  yield* Effect.addFinalizer(() => Effect.fn("engine.shutdown")(function* () {
140
140
  const current = yield* Ref.get(store);
141
- for (const [documentId, entry] of current) yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) => Effect.logError("Failed to save snapshot during shutdown", {
141
+ for (const [documentId, entry] of current) yield* Effect["catch"](entry.instance.saveSnapshot(), (e) => Effect.logError("Failed to save snapshot during shutdown", {
142
142
  documentId,
143
143
  error: e
144
144
  }));