@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,421 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Stream from "effect/Stream";
4
+ import * as Chunk from "effect/Chunk";
5
+ import * as Fiber from "effect/Fiber";
6
+ import * as PresenceManager from "../src/PresenceManager";
7
+
8
+ // =============================================================================
9
+ // PresenceManager Tests
10
+ // =============================================================================
11
+
12
+ describe("PresenceManager", () => {
13
+ describe("getSnapshot", () => {
14
+ it("should return empty snapshot for unknown document", async () => {
15
+ const result = await Effect.runPromise(
16
+ Effect.gen(function* () {
17
+ const pm = yield* PresenceManager.PresenceManagerTag;
18
+ return yield* pm.getSnapshot("unknown-doc");
19
+ }).pipe(Effect.provide(PresenceManager.layer))
20
+ );
21
+
22
+ expect(result.presences).toEqual({});
23
+ });
24
+
25
+ it("should return existing presences after set", async () => {
26
+ const result = await Effect.runPromise(
27
+ Effect.gen(function* () {
28
+ const pm = yield* PresenceManager.PresenceManagerTag;
29
+
30
+ yield* pm.set("doc-1", "conn-1", {
31
+ data: { x: 10, y: 20 },
32
+ userId: "user-1",
33
+ });
34
+
35
+ return yield* pm.getSnapshot("doc-1");
36
+ }).pipe(Effect.provide(PresenceManager.layer))
37
+ );
38
+
39
+ expect(result.presences).toEqual({
40
+ "conn-1": { data: { x: 10, y: 20 }, userId: "user-1" },
41
+ });
42
+ });
43
+
44
+ it("should return multiple presences", async () => {
45
+ const result = await Effect.runPromise(
46
+ Effect.gen(function* () {
47
+ const pm = yield* PresenceManager.PresenceManagerTag;
48
+
49
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10, y: 20 } });
50
+ yield* pm.set("doc-1", "conn-2", {
51
+ data: { x: 30, y: 40 },
52
+ userId: "user-2",
53
+ });
54
+ yield* pm.set("doc-1", "conn-3", { data: { x: 50, y: 60 } });
55
+
56
+ return yield* pm.getSnapshot("doc-1");
57
+ }).pipe(Effect.provide(PresenceManager.layer))
58
+ );
59
+
60
+ expect(Object.keys(result.presences).length).toBe(3);
61
+ expect(result.presences["conn-1"]).toEqual({ data: { x: 10, y: 20 } });
62
+ expect(result.presences["conn-2"]).toEqual({
63
+ data: { x: 30, y: 40 },
64
+ userId: "user-2",
65
+ });
66
+ expect(result.presences["conn-3"]).toEqual({ data: { x: 50, y: 60 } });
67
+ });
68
+ });
69
+
70
+ describe("set", () => {
71
+ it("should store presence entry", async () => {
72
+ const result = await Effect.runPromise(
73
+ Effect.gen(function* () {
74
+ const pm = yield* PresenceManager.PresenceManagerTag;
75
+
76
+ yield* pm.set("doc-1", "conn-1", {
77
+ data: { cursor: { x: 100, y: 200 } },
78
+ });
79
+
80
+ return yield* pm.getSnapshot("doc-1");
81
+ }).pipe(Effect.provide(PresenceManager.layer))
82
+ );
83
+
84
+ expect(result.presences["conn-1"]).toEqual({
85
+ data: { cursor: { x: 100, y: 200 } },
86
+ });
87
+ });
88
+
89
+ it("should update existing presence entry", async () => {
90
+ const result = await Effect.runPromise(
91
+ Effect.gen(function* () {
92
+ const pm = yield* PresenceManager.PresenceManagerTag;
93
+
94
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10, y: 20 } });
95
+ yield* pm.set("doc-1", "conn-1", { data: { x: 100, y: 200 } });
96
+
97
+ return yield* pm.getSnapshot("doc-1");
98
+ }).pipe(Effect.provide(PresenceManager.layer))
99
+ );
100
+
101
+ expect(result.presences["conn-1"]).toEqual({ data: { x: 100, y: 200 } });
102
+ });
103
+
104
+ it("should broadcast presence_update event", async () => {
105
+ const result = await Effect.runPromise(
106
+ Effect.scoped(
107
+ Effect.gen(function* () {
108
+ const pm = yield* PresenceManager.PresenceManagerTag;
109
+
110
+ // Subscribe first
111
+ const eventStream = yield* pm.subscribe("doc-1");
112
+
113
+ // Collect events in background
114
+ const eventsFiber = yield* Effect.fork(
115
+ Stream.runCollect(Stream.take(eventStream, 1))
116
+ );
117
+
118
+ // Small delay to ensure subscription is ready
119
+ yield* Effect.sleep("10 millis");
120
+
121
+ // Set presence
122
+ yield* pm.set("doc-1", "conn-1", {
123
+ data: { x: 10, y: 20 },
124
+ userId: "user-1",
125
+ });
126
+
127
+ // Wait for events
128
+ const events = yield* Fiber.join(eventsFiber);
129
+
130
+ return Chunk.toArray(events);
131
+ })
132
+ ).pipe(Effect.provide(PresenceManager.layer))
133
+ );
134
+
135
+ expect(result.length).toBe(1);
136
+ expect(result[0]!.type).toBe("presence_update");
137
+ if (result[0]!.type === "presence_update") {
138
+ expect(result[0]!.id).toBe("conn-1");
139
+ expect(result[0]!.data).toEqual({ x: 10, y: 20 });
140
+ expect(result[0]!.userId).toBe("user-1");
141
+ }
142
+ });
143
+ });
144
+
145
+ describe("remove", () => {
146
+ it("should remove presence entry", async () => {
147
+ const result = await Effect.runPromise(
148
+ Effect.gen(function* () {
149
+ const pm = yield* PresenceManager.PresenceManagerTag;
150
+
151
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10, y: 20 } });
152
+ yield* pm.remove("doc-1", "conn-1");
153
+
154
+ return yield* pm.getSnapshot("doc-1");
155
+ }).pipe(Effect.provide(PresenceManager.layer))
156
+ );
157
+
158
+ expect(result.presences).toEqual({});
159
+ });
160
+
161
+ it("should not error when removing non-existent connection", async () => {
162
+ await expect(
163
+ Effect.runPromise(
164
+ Effect.gen(function* () {
165
+ const pm = yield* PresenceManager.PresenceManagerTag;
166
+ yield* pm.remove("doc-1", "non-existent-conn");
167
+ }).pipe(Effect.provide(PresenceManager.layer))
168
+ )
169
+ ).resolves.toBeUndefined();
170
+ });
171
+
172
+ it("should not error when removing from non-existent document", async () => {
173
+ await expect(
174
+ Effect.runPromise(
175
+ Effect.gen(function* () {
176
+ const pm = yield* PresenceManager.PresenceManagerTag;
177
+ yield* pm.remove("non-existent-doc", "conn-1");
178
+ }).pipe(Effect.provide(PresenceManager.layer))
179
+ )
180
+ ).resolves.toBeUndefined();
181
+ });
182
+
183
+ it("should broadcast presence_remove event", async () => {
184
+ const result = await Effect.runPromise(
185
+ Effect.scoped(
186
+ Effect.gen(function* () {
187
+ const pm = yield* PresenceManager.PresenceManagerTag;
188
+
189
+ // Set presence first
190
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10, y: 20 } });
191
+
192
+ // Subscribe
193
+ const eventStream = yield* pm.subscribe("doc-1");
194
+
195
+ // Collect events in background
196
+ const eventsFiber = yield* Effect.fork(
197
+ Stream.runCollect(Stream.take(eventStream, 1))
198
+ );
199
+
200
+ // Small delay to ensure subscription is ready
201
+ yield* Effect.sleep("10 millis");
202
+
203
+ // Remove presence
204
+ yield* pm.remove("doc-1", "conn-1");
205
+
206
+ // Wait for events
207
+ const events = yield* Fiber.join(eventsFiber);
208
+
209
+ return Chunk.toArray(events);
210
+ })
211
+ ).pipe(Effect.provide(PresenceManager.layer))
212
+ );
213
+
214
+ expect(result.length).toBe(1);
215
+ expect(result[0]!.type).toBe("presence_remove");
216
+ if (result[0]!.type === "presence_remove") {
217
+ expect(result[0]!.id).toBe("conn-1");
218
+ }
219
+ });
220
+
221
+ it("should not broadcast event when removing non-existent presence", async () => {
222
+ const result = await Effect.runPromise(
223
+ Effect.scoped(
224
+ Effect.gen(function* () {
225
+ const pm = yield* PresenceManager.PresenceManagerTag;
226
+
227
+ // Subscribe to doc that has no presences
228
+ const eventStream = yield* pm.subscribe("doc-1");
229
+
230
+ // Collect events in background with a timeout
231
+ const eventsFiber = yield* Effect.fork(
232
+ Stream.runCollect(
233
+ Stream.take(eventStream, 1).pipe(Stream.timeout("50 millis"))
234
+ )
235
+ );
236
+
237
+ // Small delay to ensure subscription is ready
238
+ yield* Effect.sleep("10 millis");
239
+
240
+ // Remove non-existent presence
241
+ yield* pm.remove("doc-1", "non-existent-conn");
242
+
243
+ // Wait for events (should timeout with no events)
244
+ const events = yield* Fiber.join(eventsFiber);
245
+
246
+ return Chunk.toArray(events);
247
+ })
248
+ ).pipe(Effect.provide(PresenceManager.layer))
249
+ );
250
+
251
+ expect(result.length).toBe(0);
252
+ });
253
+ });
254
+
255
+ describe("subscribe", () => {
256
+ it("should receive update events from stream", async () => {
257
+ const result = await Effect.runPromise(
258
+ Effect.scoped(
259
+ Effect.gen(function* () {
260
+ const pm = yield* PresenceManager.PresenceManagerTag;
261
+
262
+ const eventStream = yield* pm.subscribe("doc-1");
263
+
264
+ const eventsFiber = yield* Effect.fork(
265
+ Stream.runCollect(Stream.take(eventStream, 2))
266
+ );
267
+
268
+ yield* Effect.sleep("10 millis");
269
+
270
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10 } });
271
+ yield* pm.set("doc-1", "conn-2", { data: { x: 20 } });
272
+
273
+ const events = yield* Fiber.join(eventsFiber);
274
+ return Chunk.toArray(events);
275
+ })
276
+ ).pipe(Effect.provide(PresenceManager.layer))
277
+ );
278
+
279
+ expect(result.length).toBe(2);
280
+ expect(result[0]!.type).toBe("presence_update");
281
+ expect(result[1]!.type).toBe("presence_update");
282
+ });
283
+
284
+ it("should receive mixed update and remove events", async () => {
285
+ const result = await Effect.runPromise(
286
+ Effect.scoped(
287
+ Effect.gen(function* () {
288
+ const pm = yield* PresenceManager.PresenceManagerTag;
289
+
290
+ // Set up initial presence
291
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10 } });
292
+
293
+ const eventStream = yield* pm.subscribe("doc-1");
294
+
295
+ const eventsFiber = yield* Effect.fork(
296
+ Stream.runCollect(Stream.take(eventStream, 3))
297
+ );
298
+
299
+ yield* Effect.sleep("10 millis");
300
+
301
+ yield* pm.set("doc-1", "conn-2", { data: { x: 20 } });
302
+ yield* pm.remove("doc-1", "conn-1");
303
+ yield* pm.set("doc-1", "conn-3", { data: { x: 30 } });
304
+
305
+ const events = yield* Fiber.join(eventsFiber);
306
+ return Chunk.toArray(events);
307
+ })
308
+ ).pipe(Effect.provide(PresenceManager.layer))
309
+ );
310
+
311
+ expect(result.length).toBe(3);
312
+ expect(result[0]!.type).toBe("presence_update");
313
+ expect(result[1]!.type).toBe("presence_remove");
314
+ expect(result[2]!.type).toBe("presence_update");
315
+ });
316
+ });
317
+
318
+ describe("document isolation", () => {
319
+ it("should isolate presences between documents", async () => {
320
+ const result = await Effect.runPromise(
321
+ Effect.gen(function* () {
322
+ const pm = yield* PresenceManager.PresenceManagerTag;
323
+
324
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10 } });
325
+ yield* pm.set("doc-2", "conn-2", { data: { x: 20 } });
326
+
327
+ const snapshot1 = yield* pm.getSnapshot("doc-1");
328
+ const snapshot2 = yield* pm.getSnapshot("doc-2");
329
+
330
+ return { snapshot1, snapshot2 };
331
+ }).pipe(Effect.provide(PresenceManager.layer))
332
+ );
333
+
334
+ expect(Object.keys(result.snapshot1.presences).length).toBe(1);
335
+ expect(result.snapshot1.presences["conn-1"]).toEqual({ data: { x: 10 } });
336
+ expect(result.snapshot1.presences["conn-2"]).toBeUndefined();
337
+
338
+ expect(Object.keys(result.snapshot2.presences).length).toBe(1);
339
+ expect(result.snapshot2.presences["conn-2"]).toEqual({ data: { x: 20 } });
340
+ expect(result.snapshot2.presences["conn-1"]).toBeUndefined();
341
+ });
342
+
343
+ it("should isolate events between documents", async () => {
344
+ const result = await Effect.runPromise(
345
+ Effect.scoped(
346
+ Effect.gen(function* () {
347
+ const pm = yield* PresenceManager.PresenceManagerTag;
348
+
349
+ // Subscribe to doc-1 only
350
+ const eventStream = yield* pm.subscribe("doc-1");
351
+
352
+ const eventsFiber = yield* Effect.fork(
353
+ Stream.runCollect(
354
+ Stream.take(eventStream, 1).pipe(Stream.timeout("100 millis"))
355
+ )
356
+ );
357
+
358
+ yield* Effect.sleep("10 millis");
359
+
360
+ // Set presence on doc-2 (should NOT trigger doc-1 event)
361
+ yield* pm.set("doc-2", "conn-1", { data: { x: 10 } });
362
+
363
+ // Set presence on doc-1 (should trigger event)
364
+ yield* pm.set("doc-1", "conn-2", { data: { x: 20 } });
365
+
366
+ const events = yield* Fiber.join(eventsFiber);
367
+ return Chunk.toArray(events);
368
+ })
369
+ ).pipe(Effect.provide(PresenceManager.layer))
370
+ );
371
+
372
+ expect(result.length).toBe(1);
373
+ if (result[0]!.type === "presence_update") {
374
+ expect(result[0]!.id).toBe("conn-2");
375
+ expect(result[0]!.data).toEqual({ x: 20 });
376
+ }
377
+ });
378
+ });
379
+
380
+ describe("multiple connections per document", () => {
381
+ it("should handle multiple connections setting presence independently", async () => {
382
+ const result = await Effect.runPromise(
383
+ Effect.gen(function* () {
384
+ const pm = yield* PresenceManager.PresenceManagerTag;
385
+
386
+ yield* pm.set("doc-1", "conn-1", { data: { x: 10 }, userId: "user-1" });
387
+ yield* pm.set("doc-1", "conn-2", { data: { x: 20 }, userId: "user-2" });
388
+ yield* pm.set("doc-1", "conn-3", { data: { x: 30 }, userId: "user-3" });
389
+
390
+ // Update one connection
391
+ yield* pm.set("doc-1", "conn-2", { data: { x: 200 }, userId: "user-2" });
392
+
393
+ // Remove one connection
394
+ yield* pm.remove("doc-1", "conn-1");
395
+
396
+ return yield* pm.getSnapshot("doc-1");
397
+ }).pipe(Effect.provide(PresenceManager.layer))
398
+ );
399
+
400
+ expect(Object.keys(result.presences).length).toBe(2);
401
+ expect(result.presences["conn-1"]).toBeUndefined();
402
+ expect(result.presences["conn-2"]).toEqual({
403
+ data: { x: 200 },
404
+ userId: "user-2",
405
+ });
406
+ expect(result.presences["conn-3"]).toEqual({
407
+ data: { x: 30 },
408
+ userId: "user-3",
409
+ });
410
+ });
411
+ });
412
+
413
+ describe("PresenceManagerTag", () => {
414
+ it("should have correct tag identifier", () => {
415
+ expect(PresenceManager.PresenceManagerTag.key).toBe(
416
+ "@voidhash/mimic-server-effect/PresenceManager"
417
+ );
418
+ });
419
+ });
420
+ });
421
+