@voidhash/mimic-effect 0.0.9 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/.turbo/turbo-build.log +136 -90
  2. package/README.md +385 -0
  3. package/dist/ColdStorage.cjs +60 -0
  4. package/dist/ColdStorage.d.cts +53 -0
  5. package/dist/ColdStorage.d.cts.map +1 -0
  6. package/dist/ColdStorage.d.mts +53 -0
  7. package/dist/ColdStorage.d.mts.map +1 -0
  8. package/dist/ColdStorage.mjs +60 -0
  9. package/dist/ColdStorage.mjs.map +1 -0
  10. package/dist/DocumentManager.cjs +263 -82
  11. package/dist/DocumentManager.d.cts +44 -22
  12. package/dist/DocumentManager.d.cts.map +1 -1
  13. package/dist/DocumentManager.d.mts +44 -22
  14. package/dist/DocumentManager.d.mts.map +1 -1
  15. package/dist/DocumentManager.mjs +259 -67
  16. package/dist/DocumentManager.mjs.map +1 -1
  17. package/dist/Errors.cjs +54 -0
  18. package/dist/Errors.d.cts +96 -0
  19. package/dist/Errors.d.cts.map +1 -0
  20. package/dist/Errors.d.mts +96 -0
  21. package/dist/Errors.d.mts.map +1 -0
  22. package/dist/Errors.mjs +48 -0
  23. package/dist/Errors.mjs.map +1 -0
  24. package/dist/HotStorage.cjs +100 -0
  25. package/dist/HotStorage.d.cts +70 -0
  26. package/dist/HotStorage.d.cts.map +1 -0
  27. package/dist/HotStorage.d.mts +70 -0
  28. package/dist/HotStorage.d.mts.map +1 -0
  29. package/dist/HotStorage.mjs +100 -0
  30. package/dist/HotStorage.mjs.map +1 -0
  31. package/dist/Metrics.cjs +143 -0
  32. package/dist/Metrics.d.cts +31 -0
  33. package/dist/Metrics.d.cts.map +1 -0
  34. package/dist/Metrics.d.mts +31 -0
  35. package/dist/Metrics.d.mts.map +1 -0
  36. package/dist/Metrics.mjs +126 -0
  37. package/dist/Metrics.mjs.map +1 -0
  38. package/dist/MimicAuthService.cjs +61 -45
  39. package/dist/MimicAuthService.d.cts +61 -48
  40. package/dist/MimicAuthService.d.cts.map +1 -1
  41. package/dist/MimicAuthService.d.mts +61 -48
  42. package/dist/MimicAuthService.d.mts.map +1 -1
  43. package/dist/MimicAuthService.mjs +60 -36
  44. package/dist/MimicAuthService.mjs.map +1 -1
  45. package/dist/MimicClusterServerEngine.cjs +521 -0
  46. package/dist/MimicClusterServerEngine.d.cts +17 -0
  47. package/dist/MimicClusterServerEngine.d.cts.map +1 -0
  48. package/dist/MimicClusterServerEngine.d.mts +17 -0
  49. package/dist/MimicClusterServerEngine.d.mts.map +1 -0
  50. package/dist/MimicClusterServerEngine.mjs +523 -0
  51. package/dist/MimicClusterServerEngine.mjs.map +1 -0
  52. package/dist/MimicServer.cjs +205 -96
  53. package/dist/MimicServer.d.cts +9 -110
  54. package/dist/MimicServer.d.cts.map +1 -1
  55. package/dist/MimicServer.d.mts +9 -110
  56. package/dist/MimicServer.d.mts.map +1 -1
  57. package/dist/MimicServer.mjs +206 -90
  58. package/dist/MimicServer.mjs.map +1 -1
  59. package/dist/MimicServerEngine.cjs +97 -0
  60. package/dist/MimicServerEngine.d.cts +78 -0
  61. package/dist/MimicServerEngine.d.cts.map +1 -0
  62. package/dist/MimicServerEngine.d.mts +78 -0
  63. package/dist/MimicServerEngine.d.mts.map +1 -0
  64. package/dist/MimicServerEngine.mjs +97 -0
  65. package/dist/MimicServerEngine.mjs.map +1 -0
  66. package/dist/PresenceManager.cjs +75 -91
  67. package/dist/PresenceManager.d.cts +17 -66
  68. package/dist/PresenceManager.d.cts.map +1 -1
  69. package/dist/PresenceManager.d.mts +17 -66
  70. package/dist/PresenceManager.d.mts.map +1 -1
  71. package/dist/PresenceManager.mjs +74 -78
  72. package/dist/PresenceManager.mjs.map +1 -1
  73. package/dist/Protocol.cjs +146 -0
  74. package/dist/Protocol.d.cts +203 -0
  75. package/dist/Protocol.d.cts.map +1 -0
  76. package/dist/Protocol.d.mts +203 -0
  77. package/dist/Protocol.d.mts.map +1 -0
  78. package/dist/Protocol.mjs +132 -0
  79. package/dist/Protocol.mjs.map +1 -0
  80. package/dist/Types.d.cts +172 -0
  81. package/dist/Types.d.cts.map +1 -0
  82. package/dist/Types.d.mts +172 -0
  83. package/dist/Types.d.mts.map +1 -0
  84. package/dist/_virtual/rolldown_runtime.cjs +1 -25
  85. package/dist/_virtual/rolldown_runtime.mjs +4 -1
  86. package/dist/index.cjs +37 -75
  87. package/dist/index.d.cts +13 -12
  88. package/dist/index.d.mts +13 -12
  89. package/dist/index.mjs +12 -12
  90. package/dist/testing/ColdStorageTestSuite.cjs +508 -0
  91. package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
  92. package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
  93. package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
  94. package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
  95. package/dist/testing/ColdStorageTestSuite.mjs +508 -0
  96. package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
  97. package/dist/testing/FailingStorage.cjs +135 -0
  98. package/dist/testing/FailingStorage.d.cts +43 -0
  99. package/dist/testing/FailingStorage.d.cts.map +1 -0
  100. package/dist/testing/FailingStorage.d.mts +43 -0
  101. package/dist/testing/FailingStorage.d.mts.map +1 -0
  102. package/dist/testing/FailingStorage.mjs +136 -0
  103. package/dist/testing/FailingStorage.mjs.map +1 -0
  104. package/dist/testing/HotStorageTestSuite.cjs +585 -0
  105. package/dist/testing/HotStorageTestSuite.d.cts +40 -0
  106. package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
  107. package/dist/testing/HotStorageTestSuite.d.mts +40 -0
  108. package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
  109. package/dist/testing/HotStorageTestSuite.mjs +585 -0
  110. package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
  111. package/dist/testing/StorageIntegrationTestSuite.cjs +349 -0
  112. package/dist/testing/StorageIntegrationTestSuite.d.cts +35 -0
  113. package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
  114. package/dist/testing/StorageIntegrationTestSuite.d.mts +35 -0
  115. package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
  116. package/dist/testing/StorageIntegrationTestSuite.mjs +349 -0
  117. package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
  118. package/dist/testing/assertions.cjs +114 -0
  119. package/dist/testing/assertions.mjs +109 -0
  120. package/dist/testing/assertions.mjs.map +1 -0
  121. package/dist/testing/index.cjs +14 -0
  122. package/dist/testing/index.d.cts +6 -0
  123. package/dist/testing/index.d.mts +6 -0
  124. package/dist/testing/index.mjs +7 -0
  125. package/dist/testing/types.cjs +15 -0
  126. package/dist/testing/types.d.cts +90 -0
  127. package/dist/testing/types.d.cts.map +1 -0
  128. package/dist/testing/types.d.mts +90 -0
  129. package/dist/testing/types.d.mts.map +1 -0
  130. package/dist/testing/types.mjs +16 -0
  131. package/dist/testing/types.mjs.map +1 -0
  132. package/package.json +18 -3
  133. package/src/ColdStorage.ts +136 -0
  134. package/src/DocumentManager.ts +550 -190
  135. package/src/Errors.ts +114 -0
  136. package/src/HotStorage.ts +239 -0
  137. package/src/Metrics.ts +187 -0
  138. package/src/MimicAuthService.ts +126 -64
  139. package/src/MimicClusterServerEngine.ts +946 -0
  140. package/src/MimicServer.ts +448 -195
  141. package/src/MimicServerEngine.ts +276 -0
  142. package/src/PresenceManager.ts +169 -240
  143. package/src/Protocol.ts +350 -0
  144. package/src/Types.ts +231 -0
  145. package/src/index.ts +57 -23
  146. package/src/testing/ColdStorageTestSuite.ts +589 -0
  147. package/src/testing/FailingStorage.ts +286 -0
  148. package/src/testing/HotStorageTestSuite.ts +762 -0
  149. package/src/testing/StorageIntegrationTestSuite.ts +504 -0
  150. package/src/testing/assertions.ts +181 -0
  151. package/src/testing/index.ts +83 -0
  152. package/src/testing/types.ts +100 -0
  153. package/tests/ColdStorage.test.ts +24 -0
  154. package/tests/DocumentManager.test.ts +158 -287
  155. package/tests/HotStorage.test.ts +24 -0
  156. package/tests/MimicAuthService.test.ts +102 -134
  157. package/tests/MimicClusterServerEngine.test.ts +587 -0
  158. package/tests/MimicServer.test.ts +90 -226
  159. package/tests/MimicServerEngine.test.ts +521 -0
  160. package/tests/PresenceManager.test.ts +22 -63
  161. package/tests/Protocol.test.ts +190 -0
  162. package/tests/StorageIntegration.test.ts +259 -0
  163. package/tsconfig.json +1 -1
  164. package/tsdown.config.ts +1 -1
  165. package/dist/DocumentProtocol.cjs +0 -94
  166. package/dist/DocumentProtocol.d.cts +0 -113
  167. package/dist/DocumentProtocol.d.cts.map +0 -1
  168. package/dist/DocumentProtocol.d.mts +0 -113
  169. package/dist/DocumentProtocol.d.mts.map +0 -1
  170. package/dist/DocumentProtocol.mjs +0 -89
  171. package/dist/DocumentProtocol.mjs.map +0 -1
  172. package/dist/MimicConfig.cjs +0 -60
  173. package/dist/MimicConfig.d.cts +0 -141
  174. package/dist/MimicConfig.d.cts.map +0 -1
  175. package/dist/MimicConfig.d.mts +0 -141
  176. package/dist/MimicConfig.d.mts.map +0 -1
  177. package/dist/MimicConfig.mjs +0 -50
  178. package/dist/MimicConfig.mjs.map +0 -1
  179. package/dist/MimicDataStorage.cjs +0 -83
  180. package/dist/MimicDataStorage.d.cts +0 -113
  181. package/dist/MimicDataStorage.d.cts.map +0 -1
  182. package/dist/MimicDataStorage.d.mts +0 -113
  183. package/dist/MimicDataStorage.d.mts.map +0 -1
  184. package/dist/MimicDataStorage.mjs +0 -74
  185. package/dist/MimicDataStorage.mjs.map +0 -1
  186. package/dist/WebSocketHandler.cjs +0 -365
  187. package/dist/WebSocketHandler.d.cts +0 -34
  188. package/dist/WebSocketHandler.d.cts.map +0 -1
  189. package/dist/WebSocketHandler.d.mts +0 -34
  190. package/dist/WebSocketHandler.d.mts.map +0 -1
  191. package/dist/WebSocketHandler.mjs +0 -355
  192. package/dist/WebSocketHandler.mjs.map +0 -1
  193. package/dist/auth/NoAuth.cjs +0 -43
  194. package/dist/auth/NoAuth.d.cts +0 -22
  195. package/dist/auth/NoAuth.d.cts.map +0 -1
  196. package/dist/auth/NoAuth.d.mts +0 -22
  197. package/dist/auth/NoAuth.d.mts.map +0 -1
  198. package/dist/auth/NoAuth.mjs +0 -36
  199. package/dist/auth/NoAuth.mjs.map +0 -1
  200. package/dist/errors.cjs +0 -74
  201. package/dist/errors.d.cts +0 -89
  202. package/dist/errors.d.cts.map +0 -1
  203. package/dist/errors.d.mts +0 -89
  204. package/dist/errors.d.mts.map +0 -1
  205. package/dist/errors.mjs +0 -67
  206. package/dist/errors.mjs.map +0 -1
  207. package/dist/storage/InMemoryDataStorage.cjs +0 -57
  208. package/dist/storage/InMemoryDataStorage.d.cts +0 -19
  209. package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
  210. package/dist/storage/InMemoryDataStorage.d.mts +0 -19
  211. package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
  212. package/dist/storage/InMemoryDataStorage.mjs +0 -48
  213. package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
  214. package/src/DocumentProtocol.ts +0 -112
  215. package/src/MimicConfig.ts +0 -211
  216. package/src/MimicDataStorage.ts +0 -157
  217. package/src/WebSocketHandler.ts +0 -735
  218. package/src/auth/NoAuth.ts +0 -46
  219. package/src/errors.ts +0 -113
  220. package/src/storage/InMemoryDataStorage.ts +0 -66
  221. package/tests/DocumentProtocol.test.ts +0 -113
  222. package/tests/InMemoryDataStorage.test.ts +0 -190
  223. package/tests/MimicConfig.test.ts +0 -290
  224. package/tests/MimicDataStorage.test.ts +0 -190
  225. package/tests/NoAuth.test.ts +0 -94
  226. package/tests/WebSocketHandler.test.ts +0 -321
  227. package/tests/errors.test.ts +0 -77
@@ -1,99 +1,39 @@
1
1
  /**
2
- * @since 0.0.1
3
- * Presence manager for ephemeral per-connection state.
4
- * Handles in-memory storage and broadcasting of presence updates.
2
+ * @voidhash/mimic-effect - PresenceManager
3
+ *
4
+ * Internal service for managing presence state per document.
5
5
  */
6
- import * as Effect from "effect/Effect";
7
- import * as Layer from "effect/Layer";
8
- import * as PubSub from "effect/PubSub";
9
- import * as Ref from "effect/Ref";
10
- import * as HashMap from "effect/HashMap";
11
- import * as Context from "effect/Context";
12
- import * as Scope from "effect/Scope";
13
- import * as Stream from "effect/Stream";
14
- import type { Presence } from "@voidhash/mimic";
6
+ import {
7
+ Context,
8
+ Effect,
9
+ HashMap,
10
+ Layer,
11
+ Metric,
12
+ PubSub,
13
+ Ref,
14
+ Scope,
15
+ Stream,
16
+ } from "effect";
17
+ import type {
18
+ PresenceEntry,
19
+ PresenceEvent,
20
+ PresenceSnapshot,
21
+ } from "./Types";
22
+ import * as Metrics from "./Metrics";
15
23
 
16
24
  // =============================================================================
17
- // Presence Entry Types
25
+ // PresenceManager Interface
18
26
  // =============================================================================
19
27
 
20
28
  /**
21
- * A presence entry stored in the manager.
22
- */
23
- export interface PresenceEntry {
24
- /** The presence data */
25
- readonly data: unknown;
26
- /** Optional user ID from authentication */
27
- readonly userId?: string;
28
- }
29
-
30
- // =============================================================================
31
- // Presence Events
32
- // =============================================================================
33
-
34
- /**
35
- * Event emitted when a presence is updated.
36
- */
37
- export interface PresenceUpdateEvent {
38
- readonly type: "presence_update";
39
- /** The connection ID of the user who updated */
40
- readonly id: string;
41
- /** The presence data */
42
- readonly data: unknown;
43
- /** Optional user ID from authentication */
44
- readonly userId?: string;
45
- }
46
-
47
- /**
48
- * Event emitted when a presence is removed (user disconnected).
49
- */
50
- export interface PresenceRemoveEvent {
51
- readonly type: "presence_remove";
52
- /** The connection ID of the user who disconnected */
53
- readonly id: string;
54
- }
55
-
56
- /**
57
- * Union of all presence events.
58
- */
59
- export type PresenceEvent = PresenceUpdateEvent | PresenceRemoveEvent;
60
-
61
- // =============================================================================
62
- // Presence Snapshot
63
- // =============================================================================
64
-
65
- /**
66
- * A snapshot of all presence entries for a document.
67
- */
68
- export interface PresenceSnapshot {
69
- /** Map of connectionId to presence entry */
70
- readonly presences: Record<string, PresenceEntry>;
71
- }
72
-
73
- // =============================================================================
74
- // Document Presence Instance
75
- // =============================================================================
76
-
77
- /**
78
- * Per-document presence state.
79
- */
80
- interface DocumentPresence {
81
- /** Map of connectionId to presence entry */
82
- readonly entries: Ref.Ref<HashMap.HashMap<string, PresenceEntry>>;
83
- /** PubSub for broadcasting presence events */
84
- readonly pubsub: PubSub.PubSub<PresenceEvent>;
85
- }
86
-
87
- // =============================================================================
88
- // Presence Manager Service
89
- // =============================================================================
90
-
91
- /**
92
- * Service interface for the PresenceManager.
29
+ * Internal service for managing presence state per document.
30
+ *
31
+ * Presence is ephemeral state associated with connections, not persisted.
32
+ * Each document has its own set of presences, keyed by connectionId.
93
33
  */
94
34
  export interface PresenceManager {
95
35
  /**
96
- * Get a snapshot of all presences for a document.
36
+ * Get snapshot of all presences for a document.
97
37
  */
98
38
  readonly getSnapshot: (
99
39
  documentId: string
@@ -101,7 +41,6 @@ export interface PresenceManager {
101
41
 
102
42
  /**
103
43
  * Set/update presence for a connection.
104
- * Broadcasts the update to all subscribers.
105
44
  */
106
45
  readonly set: (
107
46
  documentId: string,
@@ -110,8 +49,7 @@ export interface PresenceManager {
110
49
  ) => Effect.Effect<void>;
111
50
 
112
51
  /**
113
- * Remove presence for a connection (e.g., on disconnect).
114
- * Broadcasts the removal to all subscribers.
52
+ * Remove presence for a connection (on disconnect).
115
53
  */
116
54
  readonly remove: (
117
55
  documentId: string,
@@ -120,178 +58,169 @@ export interface PresenceManager {
120
58
 
121
59
  /**
122
60
  * Subscribe to presence events for a document.
123
- * Returns a Stream of presence events.
61
+ * Returns a stream of presence update/remove events.
124
62
  */
125
63
  readonly subscribe: (
126
64
  documentId: string
127
- ) => Effect.Effect<
128
- Stream.Stream<PresenceEvent>,
129
- never,
130
- Scope.Scope
131
- >;
65
+ ) => Effect.Effect<Stream.Stream<PresenceEvent>, never, Scope.Scope>;
132
66
  }
133
67
 
68
+ // =============================================================================
69
+ // Context Tag
70
+ // =============================================================================
71
+
134
72
  /**
135
- * Context tag for PresenceManager.
73
+ * Context tag for PresenceManager service
136
74
  */
137
75
  export class PresenceManagerTag extends Context.Tag(
138
- "@voidhash/mimic-server-effect/PresenceManager"
76
+ "@voidhash/mimic-effect/PresenceManager"
139
77
  )<PresenceManagerTag, PresenceManager>() {}
140
78
 
141
79
  // =============================================================================
142
- // Presence Manager Implementation
80
+ // Internal Types
143
81
  // =============================================================================
144
82
 
145
83
  /**
146
- * Create the PresenceManager service.
84
+ * Per-document presence state
147
85
  */
148
- const makePresenceManager = Effect.gen(function* () {
149
- // Map of document ID to document presence state
150
- const documents = yield* Ref.make(
151
- HashMap.empty<string, DocumentPresence>()
152
- );
153
-
154
- // Get or create a document presence instance
155
- const getOrCreateDocument = (
156
- documentId: string
157
- ): Effect.Effect<DocumentPresence> =>
158
- Effect.gen(function* () {
159
- const current = yield* Ref.get(documents);
160
- const existing = HashMap.get(current, documentId);
161
-
162
- if (existing._tag === "Some") {
163
- return existing.value;
164
- }
165
-
166
- // Create new document presence
167
- const entries = yield* Ref.make(
168
- HashMap.empty<string, PresenceEntry>()
169
- );
170
- const pubsub = yield* PubSub.unbounded<PresenceEvent>();
171
-
172
- const docPresence: DocumentPresence = {
173
- entries,
174
- pubsub,
175
- };
176
-
177
- // Store in map
178
- yield* Ref.update(documents, (map) =>
179
- HashMap.set(map, documentId, docPresence)
180
- );
181
-
182
- return docPresence;
183
- });
184
-
185
- // Get snapshot of all presences for a document
186
- const getSnapshot = (documentId: string): Effect.Effect<PresenceSnapshot> =>
187
- Effect.gen(function* () {
188
- const docPresence = yield* getOrCreateDocument(documentId);
189
- const entriesMap = yield* Ref.get(docPresence.entries);
190
-
191
- // Convert HashMap to Record
192
- const presences: Record<string, PresenceEntry> = {};
193
- for (const [id, entry] of entriesMap) {
194
- presences[id] = entry;
195
- }
196
-
197
- return { presences };
198
- });
199
-
200
- // Set/update presence for a connection
201
- const set = (
202
- documentId: string,
203
- connectionId: string,
204
- entry: PresenceEntry
205
- ): Effect.Effect<void> =>
206
- Effect.gen(function* () {
207
- const docPresence = yield* getOrCreateDocument(documentId);
208
-
209
- // Update the entry
210
- yield* Ref.update(docPresence.entries, (map) =>
211
- HashMap.set(map, connectionId, entry)
212
- );
213
-
214
- // Broadcast the update
215
- yield* PubSub.publish(docPresence.pubsub, {
216
- type: "presence_update",
217
- id: connectionId,
218
- data: entry.data,
219
- userId: entry.userId,
220
- });
221
- });
222
-
223
- // Remove presence for a connection
224
- const remove = (
225
- documentId: string,
226
- connectionId: string
227
- ): Effect.Effect<void> =>
228
- Effect.gen(function* () {
229
- const current = yield* Ref.get(documents);
230
- const existing = HashMap.get(current, documentId);
231
-
232
- if (existing._tag === "None") {
233
- return; // Document doesn't exist, nothing to remove
234
- }
235
-
236
- const docPresence = existing.value;
237
-
238
- // Check if the connection has a presence
239
- const entries = yield* Ref.get(docPresence.entries);
240
- const hasEntry = HashMap.has(entries, connectionId);
241
-
242
- if (!hasEntry) {
243
- return; // No presence to remove
244
- }
245
-
246
- // Remove the entry
247
- yield* Ref.update(docPresence.entries, (map) =>
248
- HashMap.remove(map, connectionId)
249
- );
250
-
251
- // Broadcast the removal
252
- yield* PubSub.publish(docPresence.pubsub, {
253
- type: "presence_remove",
254
- id: connectionId,
255
- });
256
- });
257
-
258
- // Subscribe to presence events
259
- const subscribe = (
260
- documentId: string
261
- ): Effect.Effect<Stream.Stream<PresenceEvent>, never, Scope.Scope> =>
262
- Effect.gen(function* () {
263
- const docPresence = yield* getOrCreateDocument(documentId);
264
-
265
- // Subscribe to the PubSub
266
- const queue = yield* PubSub.subscribe(docPresence.pubsub);
267
-
268
- // Convert queue to stream
269
- return Stream.fromQueue(queue);
270
- });
271
-
272
- const manager: PresenceManager = {
273
- getSnapshot,
274
- set,
275
- remove,
276
- subscribe,
277
- };
86
+ interface DocumentPresenceState {
87
+ readonly presences: HashMap.HashMap<string, PresenceEntry>;
88
+ readonly pubsub: PubSub.PubSub<PresenceEvent>;
89
+ }
278
90
 
279
- return manager;
280
- });
91
+ // =============================================================================
92
+ // Layer Implementation
93
+ // =============================================================================
281
94
 
282
95
  /**
283
- * Layer that provides PresenceManager.
96
+ * Create the PresenceManager layer.
284
97
  */
285
98
  export const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(
286
99
  PresenceManagerTag,
287
- makePresenceManager
100
+ Effect.gen(function* () {
101
+ // Store: documentId -> DocumentPresenceState
102
+ const store = yield* Ref.make(
103
+ HashMap.empty<string, DocumentPresenceState>()
104
+ );
105
+
106
+ /**
107
+ * Get or create presence state for a document
108
+ */
109
+ const getOrCreateDocumentState = (
110
+ documentId: string
111
+ ): Effect.Effect<DocumentPresenceState> =>
112
+ Effect.gen(function* () {
113
+ const current = yield* Ref.get(store);
114
+ const existing = HashMap.get(current, documentId);
115
+ if (existing._tag === "Some") {
116
+ return existing.value;
117
+ }
118
+
119
+ // Create new state for this document
120
+ const pubsub = yield* PubSub.unbounded<PresenceEvent>();
121
+ const state: DocumentPresenceState = {
122
+ presences: HashMap.empty(),
123
+ pubsub,
124
+ };
125
+
126
+ yield* Ref.update(store, (map) => HashMap.set(map, documentId, state));
127
+ return state;
128
+ });
129
+
130
+ return {
131
+ getSnapshot: (documentId) =>
132
+ Effect.gen(function* () {
133
+ const current = yield* Ref.get(store);
134
+ const existing = HashMap.get(current, documentId);
135
+ if (existing._tag === "None") {
136
+ return { presences: {} };
137
+ }
138
+
139
+ // Convert HashMap to Record
140
+ const presences: Record<string, PresenceEntry> = {};
141
+ for (const [id, entry] of existing.value.presences) {
142
+ presences[id] = entry;
143
+ }
144
+ return { presences };
145
+ }),
146
+
147
+ set: (documentId, connectionId, entry) =>
148
+ Effect.gen(function* () {
149
+ const state = yield* getOrCreateDocumentState(documentId);
150
+
151
+ // Update presence in store
152
+ yield* Ref.update(store, (map) => {
153
+ const existing = HashMap.get(map, documentId);
154
+ if (existing._tag === "None") return map;
155
+ return HashMap.set(map, documentId, {
156
+ ...existing.value,
157
+ presences: HashMap.set(
158
+ existing.value.presences,
159
+ connectionId,
160
+ entry
161
+ ),
162
+ });
163
+ });
164
+
165
+ // Track metrics
166
+ yield* Metric.increment(Metrics.presenceUpdates);
167
+ yield* Metric.incrementBy(Metrics.presenceActive, 1);
168
+
169
+ // Broadcast update event
170
+ const event: PresenceEvent = {
171
+ type: "presence_update",
172
+ id: connectionId,
173
+ data: entry.data,
174
+ userId: entry.userId,
175
+ };
176
+ yield* PubSub.publish(state.pubsub, event);
177
+ }),
178
+
179
+ remove: (documentId, connectionId) =>
180
+ Effect.gen(function* () {
181
+ const current = yield* Ref.get(store);
182
+ const existing = HashMap.get(current, documentId);
183
+ if (existing._tag === "None") return;
184
+
185
+ // Check if presence exists before removing
186
+ const hasPresence = HashMap.has(existing.value.presences, connectionId);
187
+ if (!hasPresence) return;
188
+
189
+ // Remove presence from store
190
+ yield* Ref.update(store, (map) => {
191
+ const docState = HashMap.get(map, documentId);
192
+ if (docState._tag === "None") return map;
193
+ return HashMap.set(map, documentId, {
194
+ ...docState.value,
195
+ presences: HashMap.remove(docState.value.presences, connectionId),
196
+ });
197
+ });
198
+
199
+ // Track metrics
200
+ yield* Metric.incrementBy(Metrics.presenceActive, -1);
201
+
202
+ // Broadcast remove event
203
+ const event: PresenceEvent = {
204
+ type: "presence_remove",
205
+ id: connectionId,
206
+ };
207
+ yield* PubSub.publish(existing.value.pubsub, event);
208
+ }),
209
+
210
+ subscribe: (documentId) =>
211
+ Effect.gen(function* () {
212
+ const state = yield* getOrCreateDocumentState(documentId);
213
+ return Stream.fromPubSub(state.pubsub);
214
+ }),
215
+ };
216
+ })
288
217
  );
289
218
 
290
- /**
291
- * Default layer that provides PresenceManager.
292
- * Uses the default priority for layer composition.
293
- */
294
- export const layerDefault: Layer.Layer<PresenceManagerTag> = Layer.effectDiscard(
295
- Effect.succeed(undefined)
296
- ).pipe(Layer.provideMerge(layer));
219
+ // =============================================================================
220
+ // Re-export namespace
221
+ // =============================================================================
297
222
 
223
+ export const PresenceManager = {
224
+ Tag: PresenceManagerTag,
225
+ layer,
226
+ };