@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.
- package/README.md +0 -0
- package/package.json +40 -0
- package/src/DocumentManager.ts +252 -0
- package/src/DocumentProtocol.ts +112 -0
- package/src/MimicAuthService.ts +103 -0
- package/src/MimicConfig.ts +131 -0
- package/src/MimicDataStorage.ts +157 -0
- package/src/MimicServer.ts +363 -0
- package/src/PresenceManager.ts +297 -0
- package/src/WebSocketHandler.ts +735 -0
- package/src/auth/NoAuth.ts +46 -0
- package/src/errors.ts +113 -0
- package/src/index.ts +48 -0
- package/src/storage/InMemoryDataStorage.ts +66 -0
- package/tests/DocumentManager.test.ts +340 -0
- package/tests/DocumentProtocol.test.ts +113 -0
- package/tests/InMemoryDataStorage.test.ts +190 -0
- package/tests/MimicAuthService.test.ts +185 -0
- package/tests/MimicConfig.test.ts +175 -0
- package/tests/MimicDataStorage.test.ts +190 -0
- package/tests/MimicServer.test.ts +385 -0
- package/tests/NoAuth.test.ts +94 -0
- package/tests/PresenceManager.test.ts +421 -0
- package/tests/WebSocketHandler.test.ts +321 -0
- package/tests/errors.test.ts +77 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
|
@@ -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
|
+
|