@voidhash/mimic-effect 1.0.0-beta.1 → 1.0.0-beta.10

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 (142) hide show
  1. package/.turbo/turbo-build.log +116 -74
  2. package/dist/ColdStorage.cjs +9 -5
  3. package/dist/ColdStorage.d.cts.map +1 -1
  4. package/dist/ColdStorage.d.mts.map +1 -1
  5. package/dist/ColdStorage.mjs +9 -5
  6. package/dist/ColdStorage.mjs.map +1 -1
  7. package/dist/DocumentInstance.cjs +263 -0
  8. package/dist/DocumentInstance.d.cts +78 -0
  9. package/dist/DocumentInstance.d.cts.map +1 -0
  10. package/dist/DocumentInstance.d.mts +78 -0
  11. package/dist/DocumentInstance.d.mts.map +1 -0
  12. package/dist/DocumentInstance.mjs +264 -0
  13. package/dist/DocumentInstance.mjs.map +1 -0
  14. package/dist/Errors.cjs +10 -1
  15. package/dist/Errors.d.cts +18 -3
  16. package/dist/Errors.d.cts.map +1 -1
  17. package/dist/Errors.d.mts +18 -3
  18. package/dist/Errors.d.mts.map +1 -1
  19. package/dist/Errors.mjs +9 -1
  20. package/dist/Errors.mjs.map +1 -1
  21. package/dist/HotStorage.cjs +39 -12
  22. package/dist/HotStorage.d.cts +17 -1
  23. package/dist/HotStorage.d.cts.map +1 -1
  24. package/dist/HotStorage.d.mts +17 -1
  25. package/dist/HotStorage.d.mts.map +1 -1
  26. package/dist/HotStorage.mjs +39 -12
  27. package/dist/HotStorage.mjs.map +1 -1
  28. package/dist/Metrics.cjs +29 -1
  29. package/dist/Metrics.d.cts +5 -0
  30. package/dist/Metrics.d.cts.map +1 -1
  31. package/dist/Metrics.d.mts +5 -0
  32. package/dist/Metrics.d.mts.map +1 -1
  33. package/dist/Metrics.mjs +26 -1
  34. package/dist/Metrics.mjs.map +1 -1
  35. package/dist/MimicClusterServerEngine.cjs +44 -139
  36. package/dist/MimicClusterServerEngine.d.cts.map +1 -1
  37. package/dist/MimicClusterServerEngine.d.mts +1 -1
  38. package/dist/MimicClusterServerEngine.d.mts.map +1 -1
  39. package/dist/MimicClusterServerEngine.mjs +46 -141
  40. package/dist/MimicClusterServerEngine.mjs.map +1 -1
  41. package/dist/MimicServer.cjs +20 -20
  42. package/dist/MimicServer.d.cts.map +1 -1
  43. package/dist/MimicServer.d.mts.map +1 -1
  44. package/dist/MimicServer.mjs +20 -20
  45. package/dist/MimicServer.mjs.map +1 -1
  46. package/dist/MimicServerEngine.cjs +92 -11
  47. package/dist/MimicServerEngine.d.cts +12 -4
  48. package/dist/MimicServerEngine.d.cts.map +1 -1
  49. package/dist/MimicServerEngine.d.mts +12 -4
  50. package/dist/MimicServerEngine.d.mts.map +1 -1
  51. package/dist/MimicServerEngine.mjs +94 -13
  52. package/dist/MimicServerEngine.mjs.map +1 -1
  53. package/dist/PresenceManager.cjs +5 -5
  54. package/dist/PresenceManager.d.cts.map +1 -1
  55. package/dist/PresenceManager.d.mts.map +1 -1
  56. package/dist/PresenceManager.mjs +5 -5
  57. package/dist/PresenceManager.mjs.map +1 -1
  58. package/dist/Protocol.d.cts +1 -1
  59. package/dist/Protocol.d.mts +1 -1
  60. package/dist/Types.d.cts +9 -2
  61. package/dist/Types.d.cts.map +1 -1
  62. package/dist/Types.d.mts +9 -2
  63. package/dist/Types.d.mts.map +1 -1
  64. package/dist/index.cjs +5 -6
  65. package/dist/index.d.cts +3 -3
  66. package/dist/index.d.mts +3 -3
  67. package/dist/index.mjs +3 -3
  68. package/dist/testing/ColdStorageTestSuite.cjs +508 -0
  69. package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
  70. package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
  71. package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
  72. package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
  73. package/dist/testing/ColdStorageTestSuite.mjs +508 -0
  74. package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
  75. package/dist/testing/FailingStorage.cjs +162 -0
  76. package/dist/testing/FailingStorage.d.cts +43 -0
  77. package/dist/testing/FailingStorage.d.cts.map +1 -0
  78. package/dist/testing/FailingStorage.d.mts +43 -0
  79. package/dist/testing/FailingStorage.d.mts.map +1 -0
  80. package/dist/testing/FailingStorage.mjs +163 -0
  81. package/dist/testing/FailingStorage.mjs.map +1 -0
  82. package/dist/testing/HotStorageTestSuite.cjs +820 -0
  83. package/dist/testing/HotStorageTestSuite.d.cts +42 -0
  84. package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
  85. package/dist/testing/HotStorageTestSuite.d.mts +42 -0
  86. package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
  87. package/dist/testing/HotStorageTestSuite.mjs +820 -0
  88. package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
  89. package/dist/testing/StorageIntegrationTestSuite.cjs +487 -0
  90. package/dist/testing/StorageIntegrationTestSuite.d.cts +37 -0
  91. package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
  92. package/dist/testing/StorageIntegrationTestSuite.d.mts +37 -0
  93. package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
  94. package/dist/testing/StorageIntegrationTestSuite.mjs +487 -0
  95. package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
  96. package/dist/testing/assertions.cjs +117 -0
  97. package/dist/testing/assertions.mjs +112 -0
  98. package/dist/testing/assertions.mjs.map +1 -0
  99. package/dist/testing/index.cjs +14 -0
  100. package/dist/testing/index.d.cts +6 -0
  101. package/dist/testing/index.d.mts +6 -0
  102. package/dist/testing/index.mjs +7 -0
  103. package/dist/testing/types.cjs +15 -0
  104. package/dist/testing/types.d.cts +90 -0
  105. package/dist/testing/types.d.cts.map +1 -0
  106. package/dist/testing/types.d.mts +90 -0
  107. package/dist/testing/types.d.mts.map +1 -0
  108. package/dist/testing/types.mjs +16 -0
  109. package/dist/testing/types.mjs.map +1 -0
  110. package/package.json +8 -3
  111. package/src/ColdStorage.ts +21 -12
  112. package/src/DocumentInstance.ts +527 -0
  113. package/src/Errors.ts +15 -1
  114. package/src/HotStorage.ts +115 -24
  115. package/src/Metrics.ts +30 -0
  116. package/src/MimicClusterServerEngine.ts +120 -275
  117. package/src/MimicServer.ts +83 -75
  118. package/src/MimicServerEngine.ts +230 -30
  119. package/src/PresenceManager.ts +44 -34
  120. package/src/Types.ts +9 -2
  121. package/src/index.ts +5 -35
  122. package/src/testing/ColdStorageTestSuite.ts +589 -0
  123. package/src/testing/FailingStorage.ts +338 -0
  124. package/src/testing/HotStorageTestSuite.ts +1105 -0
  125. package/src/testing/StorageIntegrationTestSuite.ts +736 -0
  126. package/src/testing/assertions.ts +188 -0
  127. package/src/testing/index.ts +83 -0
  128. package/src/testing/types.ts +100 -0
  129. package/tests/ColdStorage.test.ts +8 -120
  130. package/tests/DocumentInstance.test.ts +669 -0
  131. package/tests/HotStorage.test.ts +7 -126
  132. package/tests/StorageIntegration.test.ts +259 -0
  133. package/tsdown.config.ts +1 -1
  134. package/dist/DocumentManager.cjs +0 -229
  135. package/dist/DocumentManager.d.cts +0 -59
  136. package/dist/DocumentManager.d.cts.map +0 -1
  137. package/dist/DocumentManager.d.mts +0 -59
  138. package/dist/DocumentManager.d.mts.map +0 -1
  139. package/dist/DocumentManager.mjs +0 -227
  140. package/dist/DocumentManager.mjs.map +0 -1
  141. package/src/DocumentManager.ts +0 -506
  142. package/tests/DocumentManager.test.ts +0 -335
@@ -1,4 +1,7 @@
1
- const require_DocumentManager = require('./DocumentManager.cjs');
1
+ const require_ColdStorage = require('./ColdStorage.cjs');
2
+ const require_HotStorage = require('./HotStorage.cjs');
3
+ const require_Metrics = require('./Metrics.cjs');
4
+ const require_DocumentInstance = require('./DocumentInstance.cjs');
2
5
  const require_PresenceManager = require('./PresenceManager.cjs');
3
6
  let effect = require("effect");
4
7
 
@@ -19,11 +22,12 @@ const DEFAULT_MAX_IDLE_TIME = effect.Duration.minutes(5);
19
22
  const DEFAULT_MAX_TRANSACTION_HISTORY = 1e3;
20
23
  const DEFAULT_SNAPSHOT_INTERVAL = effect.Duration.minutes(5);
21
24
  const DEFAULT_SNAPSHOT_THRESHOLD = 100;
25
+ const DEFAULT_SNAPSHOT_IDLE_TIMEOUT = effect.Duration.seconds(30);
22
26
  /**
23
27
  * Resolve configuration with defaults
24
28
  */
25
29
  const resolveConfig = (config) => {
26
- var _config$maxTransactio, _config$snapshot, _config$snapshot$tran, _config$snapshot2;
30
+ var _config$maxTransactio, _config$snapshot, _config$snapshot$tran, _config$snapshot2, _config$snapshot3;
27
31
  return {
28
32
  schema: config.schema,
29
33
  initial: config.initial,
@@ -32,7 +36,8 @@ const resolveConfig = (config) => {
32
36
  maxTransactionHistory: (_config$maxTransactio = config.maxTransactionHistory) !== null && _config$maxTransactio !== void 0 ? _config$maxTransactio : DEFAULT_MAX_TRANSACTION_HISTORY,
33
37
  snapshot: {
34
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,
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
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
36
41
  }
37
42
  };
38
43
  };
@@ -69,23 +74,99 @@ const resolveConfig = (config) => {
69
74
  */
70
75
  const make = (config) => {
71
76
  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
77
  return effect.Layer.scoped(MimicServerEngineTag, effect.Effect.gen(function* () {
75
- const documentManager = yield* require_DocumentManager.DocumentManagerTag;
78
+ const coldStorage = yield* require_ColdStorage.ColdStorageTag;
79
+ const hotStorage = yield* require_HotStorage.HotStorageTag;
76
80
  const presenceManager = yield* require_PresenceManager.PresenceManagerTag;
81
+ const store = yield* effect.Ref.make(effect.HashMap.empty());
82
+ /**
83
+ * Get or create a document instance
84
+ */
85
+ const getOrCreateDocument = effect.Effect.fn("engine.document.get-or-create")(function* (documentId) {
86
+ const current = yield* effect.Ref.get(store);
87
+ const existing = effect.HashMap.get(current, documentId);
88
+ if (existing._tag === "Some") {
89
+ yield* effect.Ref.set(existing.value.lastActivityTime, Date.now());
90
+ return existing.value.instance;
91
+ }
92
+ const instance = yield* require_DocumentInstance.DocumentInstance.make(documentId, {
93
+ schema: config.schema,
94
+ initial: config.initial,
95
+ maxTransactionHistory: resolvedConfig.maxTransactionHistory,
96
+ snapshot: resolvedConfig.snapshot
97
+ }, coldStorage, hotStorage);
98
+ const lastActivityTime = yield* effect.Ref.make(Date.now());
99
+ yield* effect.Ref.update(store, (map) => effect.HashMap.set(map, documentId, {
100
+ instance,
101
+ lastActivityTime
102
+ }));
103
+ return instance;
104
+ });
105
+ yield* effect.Effect.fn("engine.gc.start")(function* () {
106
+ yield* effect.Effect.fn("engine.gc.loop")(function* () {
107
+ const current = yield* effect.Ref.get(store);
108
+ const now = Date.now();
109
+ const maxIdleMs = effect.Duration.toMillis(resolvedConfig.maxIdleTime);
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", {
112
+ documentId,
113
+ error: e
114
+ }));
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);
118
+ yield* effect.Effect.logInfo("Document evicted due to idle timeout", { documentId });
119
+ }
120
+ })().pipe(effect.Effect.repeat(effect.Schedule.spaced("1 minute")), effect.Effect.fork);
121
+ })();
122
+ yield* effect.Effect.fn("engine.snapshot.fiber.start")(function* () {
123
+ const idleTimeoutMs = effect.Duration.toMillis(resolvedConfig.snapshot.idleTimeout);
124
+ if (idleTimeoutMs <= 0) return;
125
+ yield* effect.Effect.fn("engine.snapshot.loop")(function* () {
126
+ const current = yield* effect.Ref.get(store);
127
+ const now = Date.now();
128
+ for (const [documentId, entry] of current) {
129
+ if (now - (yield* effect.Ref.get(entry.lastActivityTime)) < idleTimeoutMs) continue;
130
+ if (!(yield* entry.instance.needsSnapshot())) continue;
131
+ yield* effect.Effect.catchAll(entry.instance.saveSnapshot(), (e) => effect.Effect.logWarning("Periodic snapshot save failed", {
132
+ documentId,
133
+ error: e
134
+ }));
135
+ yield* effect.Metric.increment(require_Metrics.storageIdleSnapshots);
136
+ }
137
+ })().pipe(effect.Effect.repeat(effect.Schedule.spaced("10 seconds")), effect.Effect.fork);
138
+ })();
139
+ yield* effect.Effect.addFinalizer(() => effect.Effect.fn("engine.shutdown")(function* () {
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", {
142
+ documentId,
143
+ error: e
144
+ }));
145
+ yield* effect.Effect.logInfo("MimicServerEngine shutdown complete");
146
+ })());
77
147
  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),
148
+ submit: (documentId, transaction) => effect.Effect.gen(function* () {
149
+ return yield* (yield* getOrCreateDocument(documentId)).submit(transaction);
150
+ }),
151
+ getSnapshot: (documentId) => effect.Effect.gen(function* () {
152
+ return (yield* getOrCreateDocument(documentId)).getSnapshot();
153
+ }),
154
+ subscribe: (documentId) => effect.Effect.gen(function* () {
155
+ const instance = yield* getOrCreateDocument(documentId);
156
+ return effect.Stream.fromPubSub(instance.pubsub);
157
+ }),
158
+ touch: (documentId) => effect.Effect.gen(function* () {
159
+ const current = yield* effect.Ref.get(store);
160
+ const existing = effect.HashMap.get(current, documentId);
161
+ if (existing._tag === "Some") yield* effect.Ref.set(existing.value.lastActivityTime, Date.now());
162
+ }),
82
163
  getPresenceSnapshot: (documentId) => presenceManager.getSnapshot(documentId),
83
164
  setPresence: (documentId, connectionId, entry) => presenceManager.set(documentId, connectionId, entry),
84
165
  removePresence: (documentId, connectionId) => presenceManager.remove(documentId, connectionId),
85
166
  subscribePresence: (documentId) => presenceManager.subscribe(documentId),
86
167
  config: resolvedConfig
87
168
  };
88
- })).pipe(effect.Layer.provide(internalLayers));
169
+ })).pipe(effect.Layer.provide(require_PresenceManager.layer));
89
170
  };
90
171
  const MimicServerEngine = {
91
172
  Tag: MimicServerEngineTag,
@@ -1,14 +1,19 @@
1
1
  import { MimicServerEngineConfig, PresenceEntry, PresenceEvent, PresenceSnapshot, ResolvedConfig } from "./Types.cjs";
2
+ import { ColdStorageError, HotStorageError } from "./Errors.cjs";
2
3
  import { ServerMessage } from "./Protocol.cjs";
3
4
  import { ColdStorageTag } from "./ColdStorage.cjs";
4
5
  import { HotStorageTag } from "./HotStorage.cjs";
5
6
  import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
6
- import { SubmitResult } from "./DocumentManager.cjs";
7
+ import { SubmitResult } from "./DocumentInstance.cjs";
7
8
  import { Context, Effect, Layer, Scope, Stream } from "effect";
8
9
  import { Primitive, Transaction } from "@voidhash/mimic";
9
10
 
10
11
  //#region src/MimicServerEngine.d.ts
11
12
 
13
+ /**
14
+ * Error type for MimicServerEngine operations
15
+ */
16
+ type MimicServerEngineError = ColdStorageError | HotStorageError;
12
17
  /**
13
18
  * MimicServerEngine service interface.
14
19
  *
@@ -19,21 +24,24 @@ interface MimicServerEngine {
19
24
  /**
20
25
  * Submit a transaction to a document.
21
26
  * Authorization is checked against the auth service.
27
+ * May fail with MimicServerEngineError if storage is unavailable.
22
28
  */
23
- readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, never>;
29
+ readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, MimicServerEngineError>;
24
30
  /**
25
31
  * Get document snapshot (current state and version).
32
+ * May fail with MimicServerEngineError if storage is unavailable.
26
33
  */
27
34
  readonly getSnapshot: (documentId: string) => Effect.Effect<{
28
35
  state: unknown;
29
36
  version: number;
30
- }, never>;
37
+ }, MimicServerEngineError>;
31
38
  /**
32
39
  * Subscribe to document broadcasts (transactions).
33
40
  * Returns a stream of server messages.
34
41
  * Requires a Scope for cleanup when the subscription ends.
42
+ * May fail with MimicServerEngineError if storage is unavailable.
35
43
  */
36
- readonly subscribe: (documentId: string) => Effect.Effect<Stream.Stream<ServerMessage, never, never>, never, Scope.Scope>;
44
+ readonly subscribe: (documentId: string) => Effect.Effect<Stream.Stream<ServerMessage, never, never>, MimicServerEngineError, Scope.Scope>;
37
45
  /**
38
46
  * Touch document to prevent idle garbage collection.
39
47
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServerEngine.d.cts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AA6FW,UA5CM,iBAAA,CA4CN;EACJ;;;;EAgBiE,SAAM,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAtD7D,WAAA,CAAY,WAsDiD,EAAA,GArDvE,MAAA,CAAO,MAqDgE,CArDzD,YAqDyD,EAAA,KAAA,CAAA;EAAvE;;;EAM0B,SAAA,WAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GApD1B,MAAA,CAAO,MAoDmB,CAAA;IAChC,KAAA,EAAA,OAAA;;;;;AASD;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,EAAA,KAAA,EA5HkC,KAAA,CAAM,KA4HxC,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"}
1
+ {"version":3,"file":"MimicServerEngine.d.cts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;AA0FqB,KAvCT,sBAAA,GAAyB,gBAuCT,GAvC4B,eAuC5B;;;;;;;AAoBjB,UA/CM,iBAAA,CA+CN;EACJ;;;;;EAgBA,SAAO,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAxDG,WAAA,CAAY,WAwDf,EAAA,GAvDP,MAAA,CAAO,MAuDA,CAvDO,YAuDP,EAvDqB,sBAuDrB,CAAA;EAMoB;;;AACjC;gDAtDM,MAAA,CAAO;;;KAA4C;EA+D7C;AA+Tb;;;;;EArOE,SAAA,SAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GA/IK,MAAA,CAAO,MA+IZ,CA/ImB,MAAA,CAAO,MA+I1B,CA/IiC,aA+IjC,EAAA,KAAA,EAAA,KAAA,CAAA,EA/IwE,sBA+IxE,EA/IgG,KAAA,CAAM,KA+ItG,CAAA;EAEA;;;EAHC,SAAM,KAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAzIiC,MAAA,CAAO,MAyIxC,CAAA,IAAA,EAAA,KAAA,CAAA;EAAK;;;wDAlIP,MAAA,CAAO,OAAO;;;;0EAQV,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;cA+T7B;;yBAxOwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
@@ -1,14 +1,19 @@
1
1
  import { MimicServerEngineConfig, PresenceEntry, PresenceEvent, PresenceSnapshot, ResolvedConfig } from "./Types.mjs";
2
+ import { ColdStorageError, HotStorageError } from "./Errors.mjs";
2
3
  import { ServerMessage } from "./Protocol.mjs";
3
4
  import { ColdStorageTag } from "./ColdStorage.mjs";
4
5
  import { HotStorageTag } from "./HotStorage.mjs";
5
6
  import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
6
- import { SubmitResult } from "./DocumentManager.mjs";
7
+ import { SubmitResult } from "./DocumentInstance.mjs";
7
8
  import { Context, Effect, Layer, Scope, Stream } from "effect";
8
9
  import { Primitive, Transaction } from "@voidhash/mimic";
9
10
 
10
11
  //#region src/MimicServerEngine.d.ts
11
12
 
13
+ /**
14
+ * Error type for MimicServerEngine operations
15
+ */
16
+ type MimicServerEngineError = ColdStorageError | HotStorageError;
12
17
  /**
13
18
  * MimicServerEngine service interface.
14
19
  *
@@ -19,21 +24,24 @@ interface MimicServerEngine {
19
24
  /**
20
25
  * Submit a transaction to a document.
21
26
  * Authorization is checked against the auth service.
27
+ * May fail with MimicServerEngineError if storage is unavailable.
22
28
  */
23
- readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, never>;
29
+ readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult, MimicServerEngineError>;
24
30
  /**
25
31
  * Get document snapshot (current state and version).
32
+ * May fail with MimicServerEngineError if storage is unavailable.
26
33
  */
27
34
  readonly getSnapshot: (documentId: string) => Effect.Effect<{
28
35
  state: unknown;
29
36
  version: number;
30
- }, never>;
37
+ }, MimicServerEngineError>;
31
38
  /**
32
39
  * Subscribe to document broadcasts (transactions).
33
40
  * Returns a stream of server messages.
34
41
  * Requires a Scope for cleanup when the subscription ends.
42
+ * May fail with MimicServerEngineError if storage is unavailable.
35
43
  */
36
- readonly subscribe: (documentId: string) => Effect.Effect<Stream.Stream<ServerMessage, never, never>, never, Scope.Scope>;
44
+ readonly subscribe: (documentId: string) => Effect.Effect<Stream.Stream<ServerMessage, never, never>, MimicServerEngineError, Scope.Scope>;
37
45
  /**
38
46
  * Touch document to prevent idle garbage collection.
39
47
  */
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServerEngine.d.mts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AA6FW,UA5CM,iBAAA,CA4CN;EACJ;;;;EAgBiE,SAAM,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAtD7D,WAAA,CAAY,WAsDiD,EAAA,GArDvE,MAAA,CAAO,MAqDgE,CArDzD,YAqDyD,EAAA,KAAA,CAAA;EAAvE;;;EAM0B,SAAA,WAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GApD1B,MAAA,CAAO,MAoDmB,CAAA;IAChC,KAAA,EAAA,OAAA;;;;;AASD;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,EAAA,KAAA,EA5HkC,KAAA,CAAM,KA4HxC,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"}
1
+ {"version":3,"file":"MimicServerEngine.d.mts","names":[],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;AA0FqB,KAvCT,sBAAA,GAAyB,gBAuCT,GAvC4B,eAuC5B;;;;;;;AAoBjB,UA/CM,iBAAA,CA+CN;EACJ;;;;;EAgBA,SAAO,MAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,WAAA,EAxDG,WAAA,CAAY,WAwDf,EAAA,GAvDP,MAAA,CAAO,MAuDA,CAvDO,YAuDP,EAvDqB,sBAuDrB,CAAA;EAMoB;;;AACjC;gDAtDM,MAAA,CAAO;;;KAA4C;EA+D7C;AA+Tb;;;;;EArOE,SAAA,SAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GA/IK,MAAA,CAAO,MA+IZ,CA/ImB,MAAA,CAAO,MA+I1B,CA/IiC,aA+IjC,EAAA,KAAA,EAAA,KAAA,CAAA,EA/IwE,sBA+IxE,EA/IgG,KAAA,CAAM,KA+ItG,CAAA;EAEA;;;EAHC,SAAM,KAAA,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,GAzIiC,MAAA,CAAO,MAyIxC,CAAA,IAAA,EAAA,KAAA,CAAA;EAAK;;;wDAlIP,MAAA,CAAO,OAAO;;;;0EAQV,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;cA+T7B;;yBAxOwB,SAAA,CAAU,sBACrC,wBAAwB,aAC/B,KAAA,CAAM,MACP,6BAEA,iBAAiB,gBAAgB"}
@@ -1,6 +1,9 @@
1
- import { DocumentManagerConfigTag, DocumentManagerTag, layer } from "./DocumentManager.mjs";
2
- import { PresenceManagerTag, layer as layer$1 } from "./PresenceManager.mjs";
3
- import { Context, Duration, Effect, Layer } from "effect";
1
+ import { ColdStorageTag } from "./ColdStorage.mjs";
2
+ import { HotStorageTag } from "./HotStorage.mjs";
3
+ import { documentsActive, documentsEvicted, storageIdleSnapshots } from "./Metrics.mjs";
4
+ import { DocumentInstance } from "./DocumentInstance.mjs";
5
+ import { PresenceManagerTag, layer } from "./PresenceManager.mjs";
6
+ import { Context, Duration, Effect, HashMap, Layer, Metric, Ref, Schedule, Stream } from "effect";
4
7
 
5
8
  //#region src/MimicServerEngine.ts
6
9
  /**
@@ -19,11 +22,12 @@ const DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);
19
22
  const DEFAULT_MAX_TRANSACTION_HISTORY = 1e3;
20
23
  const DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);
21
24
  const DEFAULT_SNAPSHOT_THRESHOLD = 100;
25
+ const DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);
22
26
  /**
23
27
  * Resolve configuration with defaults
24
28
  */
25
29
  const resolveConfig = (config) => {
26
- var _config$maxTransactio, _config$snapshot, _config$snapshot$tran, _config$snapshot2;
30
+ var _config$maxTransactio, _config$snapshot, _config$snapshot$tran, _config$snapshot2, _config$snapshot3;
27
31
  return {
28
32
  schema: config.schema,
29
33
  initial: config.initial,
@@ -32,7 +36,8 @@ const resolveConfig = (config) => {
32
36
  maxTransactionHistory: (_config$maxTransactio = config.maxTransactionHistory) !== null && _config$maxTransactio !== void 0 ? _config$maxTransactio : DEFAULT_MAX_TRANSACTION_HISTORY,
33
37
  snapshot: {
34
38
  interval: ((_config$snapshot = config.snapshot) === null || _config$snapshot === void 0 ? void 0 : _config$snapshot.interval) ? 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
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
36
41
  }
37
42
  };
38
43
  };
@@ -69,23 +74,99 @@ const resolveConfig = (config) => {
69
74
  */
70
75
  const make = (config) => {
71
76
  const resolvedConfig = resolveConfig(config);
72
- const configLayer = Layer.succeed(DocumentManagerConfigTag, resolvedConfig);
73
- const internalLayers = Layer.mergeAll(layer.pipe(Layer.provide(configLayer)), layer$1);
74
77
  return Layer.scoped(MimicServerEngineTag, Effect.gen(function* () {
75
- const documentManager = yield* DocumentManagerTag;
78
+ const coldStorage = yield* ColdStorageTag;
79
+ const hotStorage = yield* HotStorageTag;
76
80
  const presenceManager = yield* PresenceManagerTag;
81
+ const store = yield* Ref.make(HashMap.empty());
82
+ /**
83
+ * Get or create a document instance
84
+ */
85
+ const getOrCreateDocument = Effect.fn("engine.document.get-or-create")(function* (documentId) {
86
+ const current = yield* Ref.get(store);
87
+ const existing = HashMap.get(current, documentId);
88
+ if (existing._tag === "Some") {
89
+ yield* Ref.set(existing.value.lastActivityTime, Date.now());
90
+ return existing.value.instance;
91
+ }
92
+ const instance = yield* DocumentInstance.make(documentId, {
93
+ schema: config.schema,
94
+ initial: config.initial,
95
+ maxTransactionHistory: resolvedConfig.maxTransactionHistory,
96
+ snapshot: resolvedConfig.snapshot
97
+ }, coldStorage, hotStorage);
98
+ const lastActivityTime = yield* Ref.make(Date.now());
99
+ yield* Ref.update(store, (map) => HashMap.set(map, documentId, {
100
+ instance,
101
+ lastActivityTime
102
+ }));
103
+ return instance;
104
+ });
105
+ yield* Effect.fn("engine.gc.start")(function* () {
106
+ yield* Effect.fn("engine.gc.loop")(function* () {
107
+ const current = yield* Ref.get(store);
108
+ const now = Date.now();
109
+ const maxIdleMs = Duration.toMillis(resolvedConfig.maxIdleTime);
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", {
112
+ documentId,
113
+ error: e
114
+ }));
115
+ yield* Ref.update(store, (map) => HashMap.remove(map, documentId));
116
+ yield* Metric.increment(documentsEvicted);
117
+ yield* Metric.incrementBy(documentsActive, -1);
118
+ yield* Effect.logInfo("Document evicted due to idle timeout", { documentId });
119
+ }
120
+ })().pipe(Effect.repeat(Schedule.spaced("1 minute")), Effect.fork);
121
+ })();
122
+ yield* Effect.fn("engine.snapshot.fiber.start")(function* () {
123
+ const idleTimeoutMs = Duration.toMillis(resolvedConfig.snapshot.idleTimeout);
124
+ if (idleTimeoutMs <= 0) return;
125
+ yield* Effect.fn("engine.snapshot.loop")(function* () {
126
+ const current = yield* Ref.get(store);
127
+ const now = Date.now();
128
+ for (const [documentId, entry] of current) {
129
+ if (now - (yield* Ref.get(entry.lastActivityTime)) < idleTimeoutMs) continue;
130
+ if (!(yield* entry.instance.needsSnapshot())) continue;
131
+ yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) => Effect.logWarning("Periodic snapshot save failed", {
132
+ documentId,
133
+ error: e
134
+ }));
135
+ yield* Metric.increment(storageIdleSnapshots);
136
+ }
137
+ })().pipe(Effect.repeat(Schedule.spaced("10 seconds")), Effect.fork);
138
+ })();
139
+ yield* Effect.addFinalizer(() => Effect.fn("engine.shutdown")(function* () {
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", {
142
+ documentId,
143
+ error: e
144
+ }));
145
+ yield* Effect.logInfo("MimicServerEngine shutdown complete");
146
+ })());
77
147
  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),
148
+ submit: (documentId, transaction) => Effect.gen(function* () {
149
+ return yield* (yield* getOrCreateDocument(documentId)).submit(transaction);
150
+ }),
151
+ getSnapshot: (documentId) => Effect.gen(function* () {
152
+ return (yield* getOrCreateDocument(documentId)).getSnapshot();
153
+ }),
154
+ subscribe: (documentId) => Effect.gen(function* () {
155
+ const instance = yield* getOrCreateDocument(documentId);
156
+ return Stream.fromPubSub(instance.pubsub);
157
+ }),
158
+ touch: (documentId) => Effect.gen(function* () {
159
+ const current = yield* Ref.get(store);
160
+ const existing = HashMap.get(current, documentId);
161
+ if (existing._tag === "Some") yield* Ref.set(existing.value.lastActivityTime, Date.now());
162
+ }),
82
163
  getPresenceSnapshot: (documentId) => presenceManager.getSnapshot(documentId),
83
164
  setPresence: (documentId, connectionId, entry) => presenceManager.set(documentId, connectionId, entry),
84
165
  removePresence: (documentId, connectionId) => presenceManager.remove(documentId, connectionId),
85
166
  subscribePresence: (documentId) => presenceManager.subscribe(documentId),
86
167
  config: resolvedConfig
87
168
  };
88
- })).pipe(Layer.provide(internalLayers));
169
+ })).pipe(Layer.provide(layer));
89
170
  };
90
171
  const MimicServerEngine = {
91
172
  Tag: MimicServerEngineTag,
@@ -1 +1 @@
1
- {"version":3,"file":"MimicServerEngine.mjs","names":["documentManagerLayer","presenceManagerLayer"],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServerEngine\n *\n * Core document management service for Mimic real-time collaboration.\n * Handles document lifecycle, storage, presence, and transaction processing.\n *\n * This is the engine layer - for WebSocket routes, use MimicServer.layerHttpLayerRouter().\n */\nimport {\n Context,\n Duration,\n Effect,\n Layer,\n Scope,\n Stream,\n} from \"effect\";\nimport type { Presence, Primitive, Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n ResolvedConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag } from \"./ColdStorage\";\nimport { HotStorageTag } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport {\n DocumentManagerTag,\n DocumentManagerConfigTag,\n layer as documentManagerLayer,\n type SubmitResult,\n} from \"./DocumentManager\";\nimport {\n PresenceManagerTag,\n layer as presenceManagerLayer,\n} from \"./PresenceManager\";\n\n// =============================================================================\n// MimicServerEngine Interface\n// =============================================================================\n\n/**\n * MimicServerEngine service interface.\n *\n * Provides document management operations for Mimic collaboration.\n * Use MimicServer.layerHttpLayerRouter() to create WebSocket routes.\n */\nexport interface MimicServerEngine {\n /**\n * Submit a transaction to a document.\n * Authorization is checked against the auth service.\n */\n readonly submit: (\n documentId: string,\n transaction: Transaction.Transaction\n ) => Effect.Effect<SubmitResult, never>;\n\n /**\n * Get document snapshot (current state and version).\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<{ state: unknown; version: number }, never>;\n\n /**\n * Subscribe to document broadcasts (transactions).\n * Returns a stream of server messages.\n * Requires a Scope for cleanup when the subscription ends.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<Protocol.ServerMessage, never, never>, never, Scope.Scope>;\n\n /**\n * Touch document to prevent idle garbage collection.\n */\n readonly touch: (documentId: string) => Effect.Effect<void, never>;\n\n /**\n * Get presence snapshot for a document.\n */\n readonly getPresenceSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot, never>;\n\n /**\n * Set presence for a connection.\n */\n readonly setPresence: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void, never>;\n\n /**\n * Remove presence for a connection.\n */\n readonly removePresence: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void, never>;\n\n /**\n * Subscribe to presence events for a document.\n * Requires a Scope for cleanup when the subscription ends.\n */\n readonly subscribePresence: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<PresenceEvent, never, never>, never, Scope.Scope>;\n\n /**\n * Resolved engine configuration.\n * Used by route layer to access schema, presence config, etc.\n */\n readonly config: ResolvedConfig<Primitive.AnyPrimitive>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for MimicServerEngine\n */\nexport class MimicServerEngineTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicServerEngine\"\n)<MimicServerEngineTag, MimicServerEngine>() {}\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\n\n/**\n * Resolve configuration with defaults\n */\nconst resolveConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): ResolvedConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n },\n});\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicServerEngine layer.\n *\n * This creates the core document management service. To expose it via WebSocket,\n * use MimicServer.layerHttpLayerRouter().\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\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 */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag\n> => {\n const resolvedConfig = resolveConfig(config);\n\n // Create config layer for DocumentManager\n const configLayer = Layer.succeed(\n DocumentManagerConfigTag,\n resolvedConfig as ResolvedConfig<Primitive.AnyPrimitive>\n );\n\n // Create internal layers\n const internalLayers = Layer.mergeAll(\n documentManagerLayer.pipe(Layer.provide(configLayer)),\n presenceManagerLayer\n );\n\n return Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n const documentManager = yield* DocumentManagerTag;\n const presenceManager = yield* PresenceManagerTag;\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n documentManager.submit(documentId, transaction),\n\n getSnapshot: (documentId) => documentManager.getSnapshot(documentId),\n\n subscribe: (documentId) =>\n documentManager.subscribe(documentId) as Effect.Effect<\n Stream.Stream<Protocol.ServerMessage, never, never>,\n never\n >,\n\n touch: (documentId) => documentManager.touch(documentId),\n\n getPresenceSnapshot: (documentId) =>\n presenceManager.getSnapshot(documentId),\n\n setPresence: (documentId, connectionId, entry) =>\n presenceManager.set(documentId, connectionId, entry),\n\n removePresence: (documentId, connectionId) =>\n presenceManager.remove(documentId, connectionId),\n\n subscribePresence: (documentId) =>\n presenceManager.subscribe(documentId),\n\n config: resolvedConfig as ResolvedConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n ).pipe(Layer.provide(internalLayers));\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServerEngine = {\n Tag: MimicServerEngineTag,\n make,\n};\n\n// =============================================================================\n// Re-export SubmitResult type\n// =============================================================================\n\nexport type { SubmitResult };\n"],"mappings":";;;;;;;;;;;;;;;;AA8HA,IAAa,uBAAb,cAA0C,QAAQ,IAChD,2CACD,EAA2C,CAAC;AAM7C,MAAM,wBAAwB,SAAS,QAAQ,EAAE;AACjD,MAAM,kCAAkC;AACxC,MAAM,4BAA4B,SAAS,QAAQ,EAAE;AACrD,MAAM,6BAA6B;;;;AAKnC,MAAM,iBACJ,WAC4B;;QAAC;EAC7B,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,UAAU,OAAO;EACjB,aAAa,OAAO,cAChB,SAAS,OAAO,OAAO,YAAY,GACnC;EACJ,gDACE,OAAO,8FAAyB;EAClC,UAAU;GACR,+BAAU,OAAO,8EAAU,YACvB,SAAS,OAAO,OAAO,SAAS,SAAS,GACzC;GACJ,oEACE,OAAO,gFAAU,6FAAwB;GAC5C;EACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCD,MAAa,QACX,WAKG;CACH,MAAM,iBAAiB,cAAc,OAAO;CAG5C,MAAM,cAAc,MAAM,QACxB,0BACA,eACD;CAGD,MAAM,iBAAiB,MAAM,SAC3BA,MAAqB,KAAK,MAAM,QAAQ,YAAY,CAAC,EACrDC,QACD;AAED,QAAO,MAAM,OACX,sBACA,OAAO,IAAI,aAAa;EACtB,MAAM,kBAAkB,OAAO;EAC/B,MAAM,kBAAkB,OAAO;AA+B/B,SA7BkC;GAChC,SAAS,YAAY,gBACnB,gBAAgB,OAAO,YAAY,YAAY;GAEjD,cAAc,eAAe,gBAAgB,YAAY,WAAW;GAEpE,YAAY,eACV,gBAAgB,UAAU,WAAW;GAKvC,QAAQ,eAAe,gBAAgB,MAAM,WAAW;GAExD,sBAAsB,eACpB,gBAAgB,YAAY,WAAW;GAEzC,cAAc,YAAY,cAAc,UACtC,gBAAgB,IAAI,YAAY,cAAc,MAAM;GAEtD,iBAAiB,YAAY,iBAC3B,gBAAgB,OAAO,YAAY,aAAa;GAElD,oBAAoB,eAClB,gBAAgB,UAAU,WAAW;GAEvC,QAAQ;GACT;GAGD,CACH,CAAC,KAAK,MAAM,QAAQ,eAAe,CAAC;;AAOvC,MAAa,oBAAoB;CAC/B,KAAK;CACL;CACD"}
1
+ {"version":3,"file":"MimicServerEngine.mjs","names":["Metrics.documentsEvicted","Metrics.documentsActive","Metrics.storageIdleSnapshots","presenceManagerLayer"],"sources":["../src/MimicServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServerEngine\n *\n * Core document management service for Mimic real-time collaboration.\n * Handles document lifecycle, storage, presence, and transaction processing.\n *\n * This is the engine layer - for WebSocket routes, use MimicServer.layerHttpLayerRouter().\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n Ref,\n Schedule,\n Scope,\n Stream,\n} from \"effect\";\nimport type { Primitive, Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n ResolvedConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag } from \"./ColdStorage\";\nimport { HotStorageTag } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport {\n DocumentInstance,\n type SubmitResult,\n type DocumentInstance as DocumentInstanceType,\n} from \"./DocumentInstance\";\nimport {\n PresenceManagerTag,\n layer as presenceManagerLayer,\n} from \"./PresenceManager\";\nimport * as Metrics from \"./Metrics\";\nimport type { ColdStorageError, HotStorageError } from \"./Errors\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\n/**\n * Error type for MimicServerEngine operations\n */\nexport type MimicServerEngineError = ColdStorageError | HotStorageError;\n\n// =============================================================================\n// MimicServerEngine Interface\n// =============================================================================\n\n/**\n * MimicServerEngine service interface.\n *\n * Provides document management operations for Mimic collaboration.\n * Use MimicServer.layerHttpLayerRouter() to create WebSocket routes.\n */\nexport interface MimicServerEngine {\n /**\n * Submit a transaction to a document.\n * Authorization is checked against the auth service.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly submit: (\n documentId: string,\n transaction: Transaction.Transaction\n ) => Effect.Effect<SubmitResult, MimicServerEngineError>;\n\n /**\n * Get document snapshot (current state and version).\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<{ state: unknown; version: number }, MimicServerEngineError>;\n\n /**\n * Subscribe to document broadcasts (transactions).\n * Returns a stream of server messages.\n * Requires a Scope for cleanup when the subscription ends.\n * May fail with MimicServerEngineError if storage is unavailable.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<Protocol.ServerMessage, never, never>, MimicServerEngineError, Scope.Scope>;\n\n /**\n * Touch document to prevent idle garbage collection.\n */\n readonly touch: (documentId: string) => Effect.Effect<void, never>;\n\n /**\n * Get presence snapshot for a document.\n */\n readonly getPresenceSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot, never>;\n\n /**\n * Set presence for a connection.\n */\n readonly setPresence: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void, never>;\n\n /**\n * Remove presence for a connection.\n */\n readonly removePresence: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void, never>;\n\n /**\n * Subscribe to presence events for a document.\n * Requires a Scope for cleanup when the subscription ends.\n */\n readonly subscribePresence: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<PresenceEvent, never, never>, never, Scope.Scope>;\n\n /**\n * Resolved engine configuration.\n * Used by route layer to access schema, presence config, etc.\n */\n readonly config: ResolvedConfig<Primitive.AnyPrimitive>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for MimicServerEngine\n */\nexport class MimicServerEngineTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicServerEngine\"\n)<MimicServerEngineTag, MimicServerEngine>() {}\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);\n\n/**\n * Resolve configuration with defaults\n */\nconst resolveConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): ResolvedConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n idleTimeout: config.snapshot?.idleTimeout\n ? Duration.decode(config.snapshot.idleTimeout)\n : DEFAULT_SNAPSHOT_IDLE_TIMEOUT,\n },\n});\n\n// =============================================================================\n// Internal Types\n// =============================================================================\n\n/**\n * Store entry for a document instance with last activity time\n */\ninterface StoreEntry<TSchema extends Primitive.AnyPrimitive> {\n readonly instance: DocumentInstanceType<TSchema>;\n readonly lastActivityTime: Ref.Ref<number>;\n}\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicServerEngine layer.\n *\n * This creates the core document management service. To expose it via WebSocket,\n * use MimicServer.layerHttpLayerRouter().\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\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 */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag\n> => {\n const resolvedConfig = resolveConfig(config);\n\n return Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n const presenceManager = yield* PresenceManagerTag;\n\n // Store: documentId -> StoreEntry\n const store = yield* Ref.make(\n HashMap.empty<string, StoreEntry<TSchema>>()\n );\n\n /**\n * Get or create a document instance\n */\n const getOrCreateDocument = Effect.fn(\"engine.document.get-or-create\")(\n function* (documentId: string) {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n\n if (existing._tag === \"Some\") {\n // Update activity time\n yield* Ref.set(existing.value.lastActivityTime, Date.now());\n return existing.value.instance;\n }\n\n // Create new document instance\n const instance = yield* DocumentInstance.make(\n documentId,\n {\n schema: config.schema,\n initial: config.initial,\n maxTransactionHistory: resolvedConfig.maxTransactionHistory,\n snapshot: resolvedConfig.snapshot,\n },\n coldStorage,\n hotStorage\n );\n\n const lastActivityTime = yield* Ref.make(Date.now());\n\n // Store it\n yield* Ref.update(store, (map) =>\n HashMap.set(map, documentId, { instance, lastActivityTime })\n );\n\n return instance;\n }\n );\n\n /**\n * Start background GC fiber\n */\n const startGCFiber = Effect.fn(\"engine.gc.start\")(function* () {\n const gcLoop = Effect.fn(\"engine.gc.loop\")(function* () {\n const current = yield* Ref.get(store);\n const now = Date.now();\n const maxIdleMs = Duration.toMillis(resolvedConfig.maxIdleTime);\n\n for (const [documentId, entry] of current) {\n const lastActivity = yield* Ref.get(entry.lastActivityTime);\n if (now - lastActivity >= maxIdleMs) {\n // Save final snapshot before eviction (best effort)\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during eviction\", {\n documentId,\n error: e,\n })\n );\n\n // Remove from store\n yield* Ref.update(store, (map) => HashMap.remove(map, documentId));\n\n // Track eviction metrics\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n\n yield* Effect.logInfo(\"Document evicted due to idle timeout\", {\n documentId,\n });\n }\n }\n });\n\n // Run GC every minute\n yield* gcLoop().pipe(\n Effect.repeat(Schedule.spaced(\"1 minute\")),\n Effect.fork\n );\n });\n\n // Start GC fiber\n yield* startGCFiber();\n\n /**\n * Start background snapshot fiber for idle documents.\n * This ensures documents with unsnapshot transactions get persisted\n * even without new transaction activity.\n */\n const startSnapshotFiber = Effect.fn(\"engine.snapshot.fiber.start\")(function* () {\n const idleTimeoutMs = Duration.toMillis(resolvedConfig.snapshot.idleTimeout);\n\n // Skip if idle snapshots are disabled\n if (idleTimeoutMs <= 0) {\n return;\n }\n\n const snapshotLoop = Effect.fn(\"engine.snapshot.loop\")(function* () {\n const current = yield* Ref.get(store);\n const now = Date.now();\n\n for (const [documentId, entry] of current) {\n // Check if document has been idle long enough\n const lastActivity = yield* Ref.get(entry.lastActivityTime);\n const idleDuration = now - lastActivity;\n\n if (idleDuration < idleTimeoutMs) {\n // Document not idle long enough, skip\n continue;\n }\n\n // Check if document has unsnapshot transactions\n const needs = yield* entry.instance.needsSnapshot();\n if (!needs) {\n continue;\n }\n\n // Save snapshot (with error handling)\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logWarning(\"Periodic snapshot save failed\", {\n documentId,\n error: e,\n })\n );\n\n // Track metric\n yield* Metric.increment(Metrics.storageIdleSnapshots);\n }\n });\n\n // Run snapshot check every 10 seconds\n yield* snapshotLoop().pipe(\n Effect.repeat(Schedule.spaced(\"10 seconds\")),\n Effect.fork\n );\n });\n\n // Start snapshot fiber\n yield* startSnapshotFiber();\n\n // Cleanup on shutdown\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"engine.shutdown\")(function* () {\n const current = yield* Ref.get(store);\n for (const [documentId, entry] of current) {\n // Best effort save - don't fail shutdown if storage is unavailable\n yield* Effect.catchAll(entry.instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during shutdown\", {\n documentId,\n error: e,\n })\n );\n }\n yield* Effect.logInfo(\"MimicServerEngine shutdown complete\");\n })()\n );\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return yield* instance.submit(transaction);\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return instance.getSnapshot();\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const instance = yield* getOrCreateDocument(documentId);\n return Stream.fromPubSub(instance.pubsub) as Stream.Stream<\n Protocol.ServerMessage,\n never,\n never\n >;\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n yield* Ref.set(existing.value.lastActivityTime, Date.now());\n }\n }),\n\n getPresenceSnapshot: (documentId) =>\n presenceManager.getSnapshot(documentId),\n\n setPresence: (documentId, connectionId, entry) =>\n presenceManager.set(documentId, connectionId, entry),\n\n removePresence: (documentId, connectionId) =>\n presenceManager.remove(documentId, connectionId),\n\n subscribePresence: (documentId) =>\n presenceManager.subscribe(documentId),\n\n config: resolvedConfig as ResolvedConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n ).pipe(Layer.provide(presenceManagerLayer));\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServerEngine = {\n Tag: MimicServerEngineTag,\n make,\n};\n\n// =============================================================================\n// Re-export SubmitResult type\n// =============================================================================\n\nexport type { SubmitResult };\n"],"mappings":"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"}
@@ -20,7 +20,7 @@ const layer = effect.Layer.effect(PresenceManagerTag, effect.Effect.gen(function
20
20
  /**
21
21
  * Get or create presence state for a document
22
22
  */
23
- const getOrCreateDocumentState = (documentId) => effect.Effect.gen(function* () {
23
+ const getOrCreateDocumentState = effect.Effect.fn("presence.document-state.get-or-create")(function* (documentId) {
24
24
  const current = yield* effect.Ref.get(store);
25
25
  const existing = effect.HashMap.get(current, documentId);
26
26
  if (existing._tag === "Some") return existing.value;
@@ -33,7 +33,7 @@ const layer = effect.Layer.effect(PresenceManagerTag, effect.Effect.gen(function
33
33
  return state;
34
34
  });
35
35
  return {
36
- getSnapshot: (documentId) => effect.Effect.gen(function* () {
36
+ getSnapshot: effect.Effect.fn("presence.snapshot.get")(function* (documentId) {
37
37
  const current = yield* effect.Ref.get(store);
38
38
  const existing = effect.HashMap.get(current, documentId);
39
39
  if (existing._tag === "None") return { presences: {} };
@@ -41,7 +41,7 @@ const layer = effect.Layer.effect(PresenceManagerTag, effect.Effect.gen(function
41
41
  for (const [id, entry] of existing.value.presences) presences[id] = entry;
42
42
  return { presences };
43
43
  }),
44
- set: (documentId, connectionId, entry) => effect.Effect.gen(function* () {
44
+ set: effect.Effect.fn("presence.set")(function* (documentId, connectionId, entry) {
45
45
  const state = yield* getOrCreateDocumentState(documentId);
46
46
  yield* effect.Ref.update(store, (map) => {
47
47
  const existing = effect.HashMap.get(map, documentId);
@@ -58,7 +58,7 @@ const layer = effect.Layer.effect(PresenceManagerTag, effect.Effect.gen(function
58
58
  };
59
59
  yield* effect.PubSub.publish(state.pubsub, event);
60
60
  }),
61
- remove: (documentId, connectionId) => effect.Effect.gen(function* () {
61
+ remove: effect.Effect.fn("presence.remove")(function* (documentId, connectionId) {
62
62
  const current = yield* effect.Ref.get(store);
63
63
  const existing = effect.HashMap.get(current, documentId);
64
64
  if (existing._tag === "None") return;
@@ -75,7 +75,7 @@ const layer = effect.Layer.effect(PresenceManagerTag, effect.Effect.gen(function
75
75
  };
76
76
  yield* effect.PubSub.publish(existing.value.pubsub, event);
77
77
  }),
78
- subscribe: (documentId) => effect.Effect.gen(function* () {
78
+ subscribe: effect.Effect.fn("presence.subscribe")(function* (documentId) {
79
79
  const state = yield* getOrCreateDocumentState(documentId);
80
80
  return effect.Stream.fromPubSub(state.pubsub);
81
81
  })
@@ -1 +1 @@
1
- {"version":3,"file":"PresenceManager.d.cts","names":[],"sources":["../src/PresenceManager.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAgEO,UA/BU,eAAA,CA+BH;EAAM;AACnB;;gDA1BM,MAAA,CAAO,OAAO;;;AAmCrB;EAoJa,SAAA,GAAA,EAAA,CAAA,UAGZ,EAAA,MAAA,EAAA,YAAA,EAAA,MAAA,EAAA,KAAA,EAlLU,aAkLV,EAAA,GAjLM,MAAA,CAAO,MAiLb,CAAA,IAAA,CAAA;;;;iEAzKM,MAAA,CAAO;;;;;8CAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,uBAAuB,KAAA,CAAM;;cAC/D;;;;cASY,kBAAA,SAA2B,uBAAA;cAoJ3B"}
1
+ {"version":3,"file":"PresenceManager.d.cts","names":[],"sources":["../src/PresenceManager.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAgEO,UA/BU,eAAA,CA+BH;EAAM;AACnB;;gDA1BM,MAAA,CAAO,OAAO;;;AAmCrB;EA8Ja,SAAA,GAAA,EAAA,CAAA,UAGZ,EAAA,MAAA,EAAA,YAAA,EAAA,MAAA,EAAA,KAAA,EA5LU,aA4LV,EAAA,GA3LM,MAAA,CAAO,MA2Lb,CAAA,IAAA,CAAA;;;;iEAnLM,MAAA,CAAO;;;;;8CAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,uBAAuB,KAAA,CAAM;;cAC/D;;;;cASY,kBAAA,SAA2B,uBAAA;cA8J3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"PresenceManager.d.mts","names":[],"sources":["../src/PresenceManager.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAgEO,UA/BU,eAAA,CA+BH;EAAM;AACnB;;gDA1BM,MAAA,CAAO,OAAO;;;AAmCrB;EAoJa,SAAA,GAAA,EAAA,CAAA,UAGZ,EAAA,MAAA,EAAA,YAAA,EAAA,MAAA,EAAA,KAAA,EAlLU,aAkLV,EAAA,GAjLM,MAAA,CAAO,MAiLb,CAAA,IAAA,CAAA;;;;iEAzKM,MAAA,CAAO;;;;;8CAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,uBAAuB,KAAA,CAAM;;cAC/D;;;;cASY,kBAAA,SAA2B,uBAAA;cAoJ3B"}
1
+ {"version":3,"file":"PresenceManager.d.mts","names":[],"sources":["../src/PresenceManager.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;AAgEO,UA/BU,eAAA,CA+BH;EAAM;AACnB;;gDA1BM,MAAA,CAAO,OAAO;;;AAmCrB;EA8Ja,SAAA,GAAA,EAAA,CAAA,UAGZ,EAAA,MAAA,EAAA,YAAA,EAAA,MAAA,EAAA,KAAA,EA5LU,aA4LV,EAAA,GA3LM,MAAA,CAAO,MA2Lb,CAAA,IAAA,CAAA;;;;iEAnLM,MAAA,CAAO;;;;;8CAQP,MAAA,CAAO,OAAO,MAAA,CAAO,OAAO,uBAAuB,KAAA,CAAM;;cAC/D;;;;cASY,kBAAA,SAA2B,uBAAA;cA8J3B"}
@@ -20,7 +20,7 @@ const layer = Layer.effect(PresenceManagerTag, Effect.gen(function* () {
20
20
  /**
21
21
  * Get or create presence state for a document
22
22
  */
23
- const getOrCreateDocumentState = (documentId) => Effect.gen(function* () {
23
+ const getOrCreateDocumentState = Effect.fn("presence.document-state.get-or-create")(function* (documentId) {
24
24
  const current = yield* Ref.get(store);
25
25
  const existing = HashMap.get(current, documentId);
26
26
  if (existing._tag === "Some") return existing.value;
@@ -33,7 +33,7 @@ const layer = Layer.effect(PresenceManagerTag, Effect.gen(function* () {
33
33
  return state;
34
34
  });
35
35
  return {
36
- getSnapshot: (documentId) => Effect.gen(function* () {
36
+ getSnapshot: Effect.fn("presence.snapshot.get")(function* (documentId) {
37
37
  const current = yield* Ref.get(store);
38
38
  const existing = HashMap.get(current, documentId);
39
39
  if (existing._tag === "None") return { presences: {} };
@@ -41,7 +41,7 @@ const layer = Layer.effect(PresenceManagerTag, Effect.gen(function* () {
41
41
  for (const [id, entry] of existing.value.presences) presences[id] = entry;
42
42
  return { presences };
43
43
  }),
44
- set: (documentId, connectionId, entry) => Effect.gen(function* () {
44
+ set: Effect.fn("presence.set")(function* (documentId, connectionId, entry) {
45
45
  const state = yield* getOrCreateDocumentState(documentId);
46
46
  yield* Ref.update(store, (map) => {
47
47
  const existing = HashMap.get(map, documentId);
@@ -58,7 +58,7 @@ const layer = Layer.effect(PresenceManagerTag, Effect.gen(function* () {
58
58
  };
59
59
  yield* PubSub.publish(state.pubsub, event);
60
60
  }),
61
- remove: (documentId, connectionId) => Effect.gen(function* () {
61
+ remove: Effect.fn("presence.remove")(function* (documentId, connectionId) {
62
62
  const current = yield* Ref.get(store);
63
63
  const existing = HashMap.get(current, documentId);
64
64
  if (existing._tag === "None") return;
@@ -75,7 +75,7 @@ const layer = Layer.effect(PresenceManagerTag, Effect.gen(function* () {
75
75
  };
76
76
  yield* PubSub.publish(existing.value.pubsub, event);
77
77
  }),
78
- subscribe: (documentId) => Effect.gen(function* () {
78
+ subscribe: Effect.fn("presence.subscribe")(function* (documentId) {
79
79
  const state = yield* getOrCreateDocumentState(documentId);
80
80
  return Stream.fromPubSub(state.pubsub);
81
81
  })
@@ -1 +1 @@
1
- {"version":3,"file":"PresenceManager.mjs","names":["layer: Layer.Layer<PresenceManagerTag>","state: DocumentPresenceState","presences: Record<string, PresenceEntry>","Metrics.presenceUpdates","Metrics.presenceActive","event: PresenceEvent"],"sources":["../src/PresenceManager.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - PresenceManager\n *\n * Internal service for managing presence state per document.\n */\nimport {\n Context,\n Effect,\n HashMap,\n Layer,\n Metric,\n PubSub,\n Ref,\n Scope,\n Stream,\n} from \"effect\";\nimport type {\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n} from \"./Types\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// PresenceManager Interface\n// =============================================================================\n\n/**\n * Internal service for managing presence state per document.\n *\n * Presence is ephemeral state associated with connections, not persisted.\n * Each document has its own set of presences, keyed by connectionId.\n */\nexport interface PresenceManager {\n /**\n * Get snapshot of all presences for a document.\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot>;\n\n /**\n * Set/update presence for a connection.\n */\n readonly set: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void>;\n\n /**\n * Remove presence for a connection (on disconnect).\n */\n readonly remove: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void>;\n\n /**\n * Subscribe to presence events for a document.\n * Returns a stream of presence update/remove events.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<PresenceEvent>, never, Scope.Scope>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for PresenceManager service\n */\nexport class PresenceManagerTag extends Context.Tag(\n \"@voidhash/mimic-effect/PresenceManager\"\n)<PresenceManagerTag, PresenceManager>() {}\n\n// =============================================================================\n// Internal Types\n// =============================================================================\n\n/**\n * Per-document presence state\n */\ninterface DocumentPresenceState {\n readonly presences: HashMap.HashMap<string, PresenceEntry>;\n readonly pubsub: PubSub.PubSub<PresenceEvent>;\n}\n\n// =============================================================================\n// Layer Implementation\n// =============================================================================\n\n/**\n * Create the PresenceManager layer.\n */\nexport const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(\n PresenceManagerTag,\n Effect.gen(function* () {\n // Store: documentId -> DocumentPresenceState\n const store = yield* Ref.make(\n HashMap.empty<string, DocumentPresenceState>()\n );\n\n /**\n * Get or create presence state for a document\n */\n const getOrCreateDocumentState = (\n documentId: string\n ): Effect.Effect<DocumentPresenceState> =>\n Effect.gen(function* () {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n // Create new state for this document\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n const state: DocumentPresenceState = {\n presences: HashMap.empty(),\n pubsub,\n };\n\n yield* Ref.update(store, (map) => HashMap.set(map, documentId, state));\n return state;\n });\n\n return {\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"None\") {\n return { presences: {} };\n }\n\n // Convert HashMap to Record\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of existing.value.presences) {\n presences[id] = entry;\n }\n return { presences };\n }),\n\n set: (documentId, connectionId, entry) =>\n Effect.gen(function* () {\n const state = yield* getOrCreateDocumentState(documentId);\n\n // Update presence in store\n yield* Ref.update(store, (map) => {\n const existing = HashMap.get(map, documentId);\n if (existing._tag === \"None\") return map;\n return HashMap.set(map, documentId, {\n ...existing.value,\n presences: HashMap.set(\n existing.value.presences,\n connectionId,\n entry\n ),\n });\n });\n\n // Track metrics\n yield* Metric.increment(Metrics.presenceUpdates);\n yield* Metric.incrementBy(Metrics.presenceActive, 1);\n\n // Broadcast update event\n const event: PresenceEvent = {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n };\n yield* PubSub.publish(state.pubsub, event);\n }),\n\n remove: (documentId, connectionId) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"None\") return;\n\n // Check if presence exists before removing\n const hasPresence = HashMap.has(existing.value.presences, connectionId);\n if (!hasPresence) return;\n\n // Remove presence from store\n yield* Ref.update(store, (map) => {\n const docState = HashMap.get(map, documentId);\n if (docState._tag === \"None\") return map;\n return HashMap.set(map, documentId, {\n ...docState.value,\n presences: HashMap.remove(docState.value.presences, connectionId),\n });\n });\n\n // Track metrics\n yield* Metric.incrementBy(Metrics.presenceActive, -1);\n\n // Broadcast remove event\n const event: PresenceEvent = {\n type: \"presence_remove\",\n id: connectionId,\n };\n yield* PubSub.publish(existing.value.pubsub, event);\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const state = yield* getOrCreateDocumentState(documentId);\n return Stream.fromPubSub(state.pubsub);\n }),\n };\n })\n);\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const PresenceManager = {\n Tag: PresenceManagerTag,\n layer,\n};\n"],"mappings":";;;;;;;;;;;;;AA0EA,IAAa,qBAAb,cAAwC,QAAQ,IAC9C,yCACD,EAAuC,CAAC;;;;AAqBzC,MAAaA,QAAyC,MAAM,OAC1D,oBACA,OAAO,IAAI,aAAa;CAEtB,MAAM,QAAQ,OAAO,IAAI,KACvB,QAAQ,OAAsC,CAC/C;;;;CAKD,MAAM,4BACJ,eAEA,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;EACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,MAAI,SAAS,SAAS,OACpB,QAAO,SAAS;EAIlB,MAAM,SAAS,OAAO,OAAO,WAA0B;EACvD,MAAMC,QAA+B;GACnC,WAAW,QAAQ,OAAO;GAC1B;GACD;AAED,SAAO,IAAI,OAAO,QAAQ,QAAQ,QAAQ,IAAI,KAAK,YAAY,MAAM,CAAC;AACtE,SAAO;GACP;AAEJ,QAAO;EACL,cAAc,eACZ,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,OAAI,SAAS,SAAS,OACpB,QAAO,EAAE,WAAW,EAAE,EAAE;GAI1B,MAAMC,YAA2C,EAAE;AACnD,QAAK,MAAM,CAAC,IAAI,UAAU,SAAS,MAAM,UACvC,WAAU,MAAM;AAElB,UAAO,EAAE,WAAW;IACpB;EAEJ,MAAM,YAAY,cAAc,UAC9B,OAAO,IAAI,aAAa;GACtB,MAAM,QAAQ,OAAO,yBAAyB,WAAW;AAGzD,UAAO,IAAI,OAAO,QAAQ,QAAQ;IAChC,MAAM,WAAW,QAAQ,IAAI,KAAK,WAAW;AAC7C,QAAI,SAAS,SAAS,OAAQ,QAAO;AACrC,WAAO,QAAQ,IAAI,KAAK,8CACnB,SAAS,cACZ,WAAW,QAAQ,IACjB,SAAS,MAAM,WACf,cACA,MACD,IACD;KACF;AAGF,UAAO,OAAO,UAAUC,gBAAwB;AAChD,UAAO,OAAO,YAAYC,gBAAwB,EAAE;GAGpD,MAAMC,QAAuB;IAC3B,MAAM;IACN,IAAI;IACJ,MAAM,MAAM;IACZ,QAAQ,MAAM;IACf;AACD,UAAO,OAAO,QAAQ,MAAM,QAAQ,MAAM;IAC1C;EAEJ,SAAS,YAAY,iBACnB,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,OAAI,SAAS,SAAS,OAAQ;AAI9B,OAAI,CADgB,QAAQ,IAAI,SAAS,MAAM,WAAW,aAAa,CACrD;AAGlB,UAAO,IAAI,OAAO,QAAQ,QAAQ;IAChC,MAAM,WAAW,QAAQ,IAAI,KAAK,WAAW;AAC7C,QAAI,SAAS,SAAS,OAAQ,QAAO;AACrC,WAAO,QAAQ,IAAI,KAAK,8CACnB,SAAS,cACZ,WAAW,QAAQ,OAAO,SAAS,MAAM,WAAW,aAAa,IACjE;KACF;AAGF,UAAO,OAAO,YAAYD,gBAAwB,GAAG;GAGrD,MAAMC,QAAuB;IAC3B,MAAM;IACN,IAAI;IACL;AACD,UAAO,OAAO,QAAQ,SAAS,MAAM,QAAQ,MAAM;IACnD;EAEJ,YAAY,eACV,OAAO,IAAI,aAAa;GACtB,MAAM,QAAQ,OAAO,yBAAyB,WAAW;AACzD,UAAO,OAAO,WAAW,MAAM,OAAO;IACtC;EACL;EACD,CACH;AAMD,MAAa,kBAAkB;CAC7B,KAAK;CACL;CACD"}
1
+ {"version":3,"file":"PresenceManager.mjs","names":["layer: Layer.Layer<PresenceManagerTag>","state: DocumentPresenceState","presences: Record<string, PresenceEntry>","Metrics.presenceUpdates","Metrics.presenceActive","event: PresenceEvent"],"sources":["../src/PresenceManager.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - PresenceManager\n *\n * Internal service for managing presence state per document.\n */\nimport {\n Context,\n Effect,\n HashMap,\n Layer,\n Metric,\n PubSub,\n Ref,\n Scope,\n Stream,\n} from \"effect\";\nimport type {\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n} from \"./Types\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// PresenceManager Interface\n// =============================================================================\n\n/**\n * Internal service for managing presence state per document.\n *\n * Presence is ephemeral state associated with connections, not persisted.\n * Each document has its own set of presences, keyed by connectionId.\n */\nexport interface PresenceManager {\n /**\n * Get snapshot of all presences for a document.\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot>;\n\n /**\n * Set/update presence for a connection.\n */\n readonly set: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void>;\n\n /**\n * Remove presence for a connection (on disconnect).\n */\n readonly remove: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void>;\n\n /**\n * Subscribe to presence events for a document.\n * Returns a stream of presence update/remove events.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<Stream.Stream<PresenceEvent>, never, Scope.Scope>;\n}\n\n// =============================================================================\n// Context Tag\n// =============================================================================\n\n/**\n * Context tag for PresenceManager service\n */\nexport class PresenceManagerTag extends Context.Tag(\n \"@voidhash/mimic-effect/PresenceManager\"\n)<PresenceManagerTag, PresenceManager>() {}\n\n// =============================================================================\n// Internal Types\n// =============================================================================\n\n/**\n * Per-document presence state\n */\ninterface DocumentPresenceState {\n readonly presences: HashMap.HashMap<string, PresenceEntry>;\n readonly pubsub: PubSub.PubSub<PresenceEvent>;\n}\n\n// =============================================================================\n// Layer Implementation\n// =============================================================================\n\n/**\n * Create the PresenceManager layer.\n */\nexport const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(\n PresenceManagerTag,\n Effect.gen(function* () {\n // Store: documentId -> DocumentPresenceState\n const store = yield* Ref.make(\n HashMap.empty<string, DocumentPresenceState>()\n );\n\n /**\n * Get or create presence state for a document\n */\n const getOrCreateDocumentState = Effect.fn(\n \"presence.document-state.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n // Create new state for this document\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n const state: DocumentPresenceState = {\n presences: HashMap.empty(),\n pubsub,\n };\n\n yield* Ref.update(store, (map) => HashMap.set(map, documentId, state));\n return state;\n });\n\n return {\n getSnapshot: Effect.fn(\"presence.snapshot.get\")(\n function* (documentId: string) {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"None\") {\n return { presences: {} };\n }\n\n // Convert HashMap to Record\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of existing.value.presences) {\n presences[id] = entry;\n }\n return { presences };\n }\n ),\n\n set: Effect.fn(\"presence.set\")(\n function* (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) {\n const state = yield* getOrCreateDocumentState(documentId);\n\n // Update presence in store\n yield* Ref.update(store, (map) => {\n const existing = HashMap.get(map, documentId);\n if (existing._tag === \"None\") return map;\n return HashMap.set(map, documentId, {\n ...existing.value,\n presences: HashMap.set(\n existing.value.presences,\n connectionId,\n entry\n ),\n });\n });\n\n // Track metrics\n yield* Metric.increment(Metrics.presenceUpdates);\n yield* Metric.incrementBy(Metrics.presenceActive, 1);\n\n // Broadcast update event\n const event: PresenceEvent = {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n };\n yield* PubSub.publish(state.pubsub, event);\n }\n ),\n\n remove: Effect.fn(\"presence.remove\")(\n function* (documentId: string, connectionId: string) {\n const current = yield* Ref.get(store);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"None\") return;\n\n // Check if presence exists before removing\n const hasPresence = HashMap.has(\n existing.value.presences,\n connectionId\n );\n if (!hasPresence) return;\n\n // Remove presence from store\n yield* Ref.update(store, (map) => {\n const docState = HashMap.get(map, documentId);\n if (docState._tag === \"None\") return map;\n return HashMap.set(map, documentId, {\n ...docState.value,\n presences: HashMap.remove(docState.value.presences, connectionId),\n });\n });\n\n // Track metrics\n yield* Metric.incrementBy(Metrics.presenceActive, -1);\n\n // Broadcast remove event\n const event: PresenceEvent = {\n type: \"presence_remove\",\n id: connectionId,\n };\n yield* PubSub.publish(existing.value.pubsub, event);\n }\n ),\n\n subscribe: Effect.fn(\"presence.subscribe\")(\n function* (documentId: string) {\n const state = yield* getOrCreateDocumentState(documentId);\n return Stream.fromPubSub(state.pubsub);\n }\n ),\n };\n })\n);\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const PresenceManager = {\n Tag: PresenceManagerTag,\n layer,\n};\n"],"mappings":";;;;;;;;;;;;;AA0EA,IAAa,qBAAb,cAAwC,QAAQ,IAC9C,yCACD,EAAuC,CAAC;;;;AAqBzC,MAAaA,QAAyC,MAAM,OAC1D,oBACA,OAAO,IAAI,aAAa;CAEtB,MAAM,QAAQ,OAAO,IAAI,KACvB,QAAQ,OAAsC,CAC/C;;;;CAKD,MAAM,2BAA2B,OAAO,GACtC,wCACD,CAAC,WAAW,YAAoB;EAC/B,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;EACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,MAAI,SAAS,SAAS,OACpB,QAAO,SAAS;EAIlB,MAAM,SAAS,OAAO,OAAO,WAA0B;EACvD,MAAMC,QAA+B;GACnC,WAAW,QAAQ,OAAO;GAC1B;GACD;AAED,SAAO,IAAI,OAAO,QAAQ,QAAQ,QAAQ,IAAI,KAAK,YAAY,MAAM,CAAC;AACtE,SAAO;GACP;AAEF,QAAO;EACL,aAAa,OAAO,GAAG,wBAAwB,CAC7C,WAAW,YAAoB;GAC7B,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,OAAI,SAAS,SAAS,OACpB,QAAO,EAAE,WAAW,EAAE,EAAE;GAI1B,MAAMC,YAA2C,EAAE;AACnD,QAAK,MAAM,CAAC,IAAI,UAAU,SAAS,MAAM,UACvC,WAAU,MAAM;AAElB,UAAO,EAAE,WAAW;IAEvB;EAED,KAAK,OAAO,GAAG,eAAe,CAC5B,WACE,YACA,cACA,OACA;GACA,MAAM,QAAQ,OAAO,yBAAyB,WAAW;AAGzD,UAAO,IAAI,OAAO,QAAQ,QAAQ;IAChC,MAAM,WAAW,QAAQ,IAAI,KAAK,WAAW;AAC7C,QAAI,SAAS,SAAS,OAAQ,QAAO;AACrC,WAAO,QAAQ,IAAI,KAAK,8CACnB,SAAS,cACZ,WAAW,QAAQ,IACjB,SAAS,MAAM,WACf,cACA,MACD,IACD;KACF;AAGF,UAAO,OAAO,UAAUC,gBAAwB;AAChD,UAAO,OAAO,YAAYC,gBAAwB,EAAE;GAGpD,MAAMC,QAAuB;IAC3B,MAAM;IACN,IAAI;IACJ,MAAM,MAAM;IACZ,QAAQ,MAAM;IACf;AACD,UAAO,OAAO,QAAQ,MAAM,QAAQ,MAAM;IAE7C;EAED,QAAQ,OAAO,GAAG,kBAAkB,CAClC,WAAW,YAAoB,cAAsB;GACnD,MAAM,UAAU,OAAO,IAAI,IAAI,MAAM;GACrC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AACjD,OAAI,SAAS,SAAS,OAAQ;AAO9B,OAAI,CAJgB,QAAQ,IAC1B,SAAS,MAAM,WACf,aACD,CACiB;AAGlB,UAAO,IAAI,OAAO,QAAQ,QAAQ;IAChC,MAAM,WAAW,QAAQ,IAAI,KAAK,WAAW;AAC7C,QAAI,SAAS,SAAS,OAAQ,QAAO;AACrC,WAAO,QAAQ,IAAI,KAAK,8CACnB,SAAS,cACZ,WAAW,QAAQ,OAAO,SAAS,MAAM,WAAW,aAAa,IACjE;KACF;AAGF,UAAO,OAAO,YAAYD,gBAAwB,GAAG;GAGrD,MAAMC,QAAuB;IAC3B,MAAM;IACN,IAAI;IACL;AACD,UAAO,OAAO,QAAQ,SAAS,MAAM,QAAQ,MAAM;IAEtD;EAED,WAAW,OAAO,GAAG,qBAAqB,CACxC,WAAW,YAAoB;GAC7B,MAAM,QAAQ,OAAO,yBAAyB,WAAW;AACzD,UAAO,OAAO,WAAW,MAAM,OAAO;IAEzC;EACF;EACD,CACH;AAMD,MAAa,kBAAkB;CAC7B,KAAK;CACL;CACD"}