@voidhash/mimic-effect 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,297 @@
1
+ /**
2
+ * @since 0.0.1
3
+ * Presence manager for ephemeral per-connection state.
4
+ * Handles in-memory storage and broadcasting of presence updates.
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";
15
+
16
+ // =============================================================================
17
+ // Presence Entry Types
18
+ // =============================================================================
19
+
20
+ /**
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.
93
+ */
94
+ export interface PresenceManager {
95
+ /**
96
+ * Get a snapshot of all presences for a document.
97
+ */
98
+ readonly getSnapshot: (
99
+ documentId: string
100
+ ) => Effect.Effect<PresenceSnapshot>;
101
+
102
+ /**
103
+ * Set/update presence for a connection.
104
+ * Broadcasts the update to all subscribers.
105
+ */
106
+ readonly set: (
107
+ documentId: string,
108
+ connectionId: string,
109
+ entry: PresenceEntry
110
+ ) => Effect.Effect<void>;
111
+
112
+ /**
113
+ * Remove presence for a connection (e.g., on disconnect).
114
+ * Broadcasts the removal to all subscribers.
115
+ */
116
+ readonly remove: (
117
+ documentId: string,
118
+ connectionId: string
119
+ ) => Effect.Effect<void>;
120
+
121
+ /**
122
+ * Subscribe to presence events for a document.
123
+ * Returns a Stream of presence events.
124
+ */
125
+ readonly subscribe: (
126
+ documentId: string
127
+ ) => Effect.Effect<
128
+ Stream.Stream<PresenceEvent>,
129
+ never,
130
+ Scope.Scope
131
+ >;
132
+ }
133
+
134
+ /**
135
+ * Context tag for PresenceManager.
136
+ */
137
+ export class PresenceManagerTag extends Context.Tag(
138
+ "@voidhash/mimic-server-effect/PresenceManager"
139
+ )<PresenceManagerTag, PresenceManager>() {}
140
+
141
+ // =============================================================================
142
+ // Presence Manager Implementation
143
+ // =============================================================================
144
+
145
+ /**
146
+ * Create the PresenceManager service.
147
+ */
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
+ };
278
+
279
+ return manager;
280
+ });
281
+
282
+ /**
283
+ * Layer that provides PresenceManager.
284
+ */
285
+ export const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(
286
+ PresenceManagerTag,
287
+ makePresenceManager
288
+ );
289
+
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));
297
+