@sylphx/lens-server 1.11.3 → 2.0.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.
@@ -1,1105 +0,0 @@
1
- /**
2
- * Tests for GraphStateManager
3
- */
4
-
5
- import { beforeEach, describe, expect, it, mock } from "bun:test";
6
- import { GraphStateManager, type StateClient, type StateUpdateMessage } from "./graph-state-manager.js";
7
-
8
- describe("GraphStateManager", () => {
9
- let manager: GraphStateManager;
10
- let mockClient: StateClient & { messages: StateUpdateMessage[] };
11
-
12
- beforeEach(() => {
13
- manager = new GraphStateManager();
14
- mockClient = {
15
- id: "client-1",
16
- messages: [],
17
- send: mock((msg: StateUpdateMessage) => {
18
- mockClient.messages.push(msg);
19
- }),
20
- };
21
- manager.addClient(mockClient);
22
- });
23
-
24
- describe("client management", () => {
25
- it("adds and removes clients", () => {
26
- expect(manager.getStats().clients).toBe(1);
27
-
28
- manager.removeClient("client-1");
29
- expect(manager.getStats().clients).toBe(0);
30
- });
31
-
32
- it("handles removing non-existent client", () => {
33
- expect(() => manager.removeClient("non-existent")).not.toThrow();
34
- });
35
- });
36
-
37
- describe("subscription", () => {
38
- it("subscribes client to entity", () => {
39
- manager.subscribe("client-1", "Post", "123", ["title", "content"]);
40
-
41
- expect(manager.hasSubscribers("Post", "123")).toBe(true);
42
- });
43
-
44
- it("unsubscribes client from entity", () => {
45
- manager.subscribe("client-1", "Post", "123");
46
- manager.unsubscribe("client-1", "Post", "123");
47
-
48
- expect(manager.hasSubscribers("Post", "123")).toBe(false);
49
- });
50
-
51
- it("sends initial data when subscribing to existing state", () => {
52
- // Emit data first
53
- manager.emit("Post", "123", { title: "Hello", content: "World" });
54
-
55
- // Then subscribe
56
- manager.subscribe("client-1", "Post", "123", ["title"]);
57
-
58
- // Should receive initial data
59
- expect(mockClient.messages.length).toBe(1);
60
- expect(mockClient.messages[0]).toMatchObject({
61
- type: "update",
62
- entity: "Post",
63
- id: "123",
64
- });
65
- expect(mockClient.messages[0].updates.title).toMatchObject({
66
- strategy: "value",
67
- data: "Hello",
68
- });
69
- });
70
-
71
- it("subscribes to all fields with *", () => {
72
- manager.emit("Post", "123", { title: "Hello", content: "World" });
73
- manager.subscribe("client-1", "Post", "123", "*");
74
-
75
- expect(mockClient.messages.length).toBe(1);
76
- expect(mockClient.messages[0].updates).toHaveProperty("title");
77
- expect(mockClient.messages[0].updates).toHaveProperty("content");
78
- });
79
- });
80
-
81
- describe("emit", () => {
82
- it("updates canonical state", () => {
83
- manager.emit("Post", "123", { title: "Hello" });
84
-
85
- expect(manager.getState("Post", "123")).toEqual({ title: "Hello" });
86
- });
87
-
88
- it("merges partial updates by default", () => {
89
- manager.emit("Post", "123", { title: "Hello" });
90
- manager.emit("Post", "123", { content: "World" });
91
-
92
- expect(manager.getState("Post", "123")).toEqual({
93
- title: "Hello",
94
- content: "World",
95
- });
96
- });
97
-
98
- it("replaces state when replace option is true", () => {
99
- manager.emit("Post", "123", { title: "Hello", content: "World" });
100
- manager.emit("Post", "123", { title: "New" }, { replace: true });
101
-
102
- expect(manager.getState("Post", "123")).toEqual({ title: "New" });
103
- });
104
-
105
- it("pushes updates to subscribed clients", () => {
106
- manager.subscribe("client-1", "Post", "123", "*");
107
- mockClient.messages = []; // Clear initial subscription message
108
-
109
- manager.emit("Post", "123", { title: "Hello" });
110
-
111
- expect(mockClient.messages.length).toBe(1);
112
- expect(mockClient.messages[0]).toMatchObject({
113
- type: "update",
114
- entity: "Post",
115
- id: "123",
116
- });
117
- });
118
-
119
- it("only sends updates for changed fields", () => {
120
- manager.subscribe("client-1", "Post", "123", "*");
121
- manager.emit("Post", "123", { title: "Hello", content: "World" });
122
- mockClient.messages = [];
123
-
124
- // Emit same title, different content
125
- manager.emit("Post", "123", { title: "Hello", content: "Updated" });
126
-
127
- expect(mockClient.messages.length).toBe(1);
128
- expect(mockClient.messages[0].updates).toHaveProperty("content");
129
- expect(mockClient.messages[0].updates).not.toHaveProperty("title");
130
- });
131
-
132
- it("does not send if no fields changed", () => {
133
- manager.subscribe("client-1", "Post", "123", "*");
134
- manager.emit("Post", "123", { title: "Hello" });
135
- mockClient.messages = [];
136
-
137
- // Emit same data
138
- manager.emit("Post", "123", { title: "Hello" });
139
-
140
- expect(mockClient.messages.length).toBe(0);
141
- });
142
-
143
- it("only sends subscribed fields", () => {
144
- manager.subscribe("client-1", "Post", "123", ["title"]);
145
- mockClient.messages = [];
146
-
147
- manager.emit("Post", "123", { title: "Hello", content: "World" });
148
-
149
- expect(mockClient.messages.length).toBe(1);
150
- expect(mockClient.messages[0].updates).toHaveProperty("title");
151
- expect(mockClient.messages[0].updates).not.toHaveProperty("content");
152
- });
153
-
154
- it("does not send to unsubscribed clients", () => {
155
- const otherClient = {
156
- id: "client-2",
157
- messages: [] as StateUpdateMessage[],
158
- send: mock((msg: StateUpdateMessage) => {
159
- otherClient.messages.push(msg);
160
- }),
161
- };
162
- manager.addClient(otherClient);
163
-
164
- manager.subscribe("client-1", "Post", "123", "*");
165
- // client-2 not subscribed
166
-
167
- manager.emit("Post", "123", { title: "Hello" });
168
-
169
- expect(mockClient.messages.length).toBe(1);
170
- expect(otherClient.messages.length).toBe(0);
171
- });
172
- });
173
-
174
- describe("update strategies", () => {
175
- it("uses value strategy for short strings", () => {
176
- manager.subscribe("client-1", "Post", "123", "*");
177
- manager.emit("Post", "123", { title: "Hello" });
178
- mockClient.messages = [];
179
-
180
- manager.emit("Post", "123", { title: "World" });
181
-
182
- expect(mockClient.messages[0].updates.title.strategy).toBe("value");
183
- });
184
-
185
- it("uses delta strategy for long strings with small changes", () => {
186
- const longText = "A".repeat(200);
187
- manager.subscribe("client-1", "Post", "123", "*");
188
- manager.emit("Post", "123", { content: longText });
189
- mockClient.messages = [];
190
-
191
- manager.emit("Post", "123", { content: `${longText} appended` });
192
-
193
- // Should use delta for efficient transfer
194
- const update = mockClient.messages[0].updates.content;
195
- expect(["delta", "value"]).toContain(update.strategy);
196
- });
197
-
198
- it("uses patch strategy for objects", () => {
199
- manager.subscribe("client-1", "Post", "123", "*");
200
- manager.emit("Post", "123", {
201
- metadata: { views: 100, likes: 10, tags: ["a", "b"] },
202
- });
203
- mockClient.messages = [];
204
-
205
- manager.emit("Post", "123", {
206
- metadata: { views: 101, likes: 10, tags: ["a", "b"] },
207
- });
208
-
209
- const update = mockClient.messages[0].updates.metadata;
210
- expect(["patch", "value"]).toContain(update.strategy);
211
- });
212
- });
213
-
214
- describe("multiple clients", () => {
215
- it("sends updates to all subscribed clients", () => {
216
- const client2 = {
217
- id: "client-2",
218
- messages: [] as StateUpdateMessage[],
219
- send: mock((msg: StateUpdateMessage) => {
220
- client2.messages.push(msg);
221
- }),
222
- };
223
- manager.addClient(client2);
224
-
225
- manager.subscribe("client-1", "Post", "123", "*");
226
- manager.subscribe("client-2", "Post", "123", "*");
227
- mockClient.messages = [];
228
- client2.messages = [];
229
-
230
- manager.emit("Post", "123", { title: "Hello" });
231
-
232
- expect(mockClient.messages.length).toBe(1);
233
- expect(client2.messages.length).toBe(1);
234
- });
235
-
236
- it("tracks state independently per client", () => {
237
- const client2 = {
238
- id: "client-2",
239
- messages: [] as StateUpdateMessage[],
240
- send: mock((msg: StateUpdateMessage) => {
241
- client2.messages.push(msg);
242
- }),
243
- };
244
- manager.addClient(client2);
245
-
246
- // Emit initial state
247
- manager.emit("Post", "123", { title: "Hello", content: "World" });
248
-
249
- // Subscribe clients at different times
250
- manager.subscribe("client-1", "Post", "123", "*");
251
- mockClient.messages = [];
252
-
253
- // Emit update
254
- manager.emit("Post", "123", { title: "Updated" });
255
-
256
- // Now subscribe client-2 (should get current state)
257
- manager.subscribe("client-2", "Post", "123", "*");
258
-
259
- // client-1 got incremental update
260
- expect(mockClient.messages.length).toBe(1);
261
- expect(mockClient.messages[0].updates.title.data).toBe("Updated");
262
-
263
- // client-2 got full current state
264
- expect(client2.messages.length).toBe(1);
265
- expect(client2.messages[0].updates.title.data).toBe("Updated");
266
- expect(client2.messages[0].updates.content.data).toBe("World");
267
- });
268
- });
269
-
270
- describe("cleanup", () => {
271
- it("calls onEntityUnsubscribed when last client unsubscribes", () => {
272
- const onUnsubscribe = mock(() => {});
273
- const mgr = new GraphStateManager({
274
- onEntityUnsubscribed: onUnsubscribe,
275
- });
276
-
277
- const client = {
278
- id: "c1",
279
- send: mock(() => {}),
280
- };
281
- mgr.addClient(client);
282
- mgr.subscribe("c1", "Post", "123", "*");
283
- mgr.unsubscribe("c1", "Post", "123");
284
-
285
- expect(onUnsubscribe).toHaveBeenCalledWith("Post", "123");
286
- });
287
-
288
- it("cleans up subscriptions when client is removed", () => {
289
- manager.subscribe("client-1", "Post", "123", "*");
290
- manager.subscribe("client-1", "Post", "456", "*");
291
-
292
- manager.removeClient("client-1");
293
-
294
- expect(manager.hasSubscribers("Post", "123")).toBe(false);
295
- expect(manager.hasSubscribers("Post", "456")).toBe(false);
296
- });
297
-
298
- it("clear() removes all state", () => {
299
- manager.subscribe("client-1", "Post", "123", "*");
300
- manager.emit("Post", "123", { title: "Hello" });
301
-
302
- manager.clear();
303
-
304
- expect(manager.getStats()).toEqual({
305
- clients: 0,
306
- entities: 0,
307
- totalSubscriptions: 0,
308
- });
309
- });
310
- });
311
-
312
- describe("stats", () => {
313
- it("returns correct stats", () => {
314
- const client2 = { id: "client-2", send: mock(() => {}) };
315
- manager.addClient(client2);
316
-
317
- manager.emit("Post", "123", { title: "Hello" });
318
- manager.emit("Post", "456", { title: "World" });
319
-
320
- manager.subscribe("client-1", "Post", "123", "*");
321
- manager.subscribe("client-1", "Post", "456", "*");
322
- manager.subscribe("client-2", "Post", "123", "*");
323
-
324
- const stats = manager.getStats();
325
- expect(stats.clients).toBe(2);
326
- expect(stats.entities).toBe(2);
327
- expect(stats.totalSubscriptions).toBe(3);
328
- });
329
- });
330
-
331
- describe("updateSubscription", () => {
332
- it("updates subscription fields for a client", () => {
333
- manager.subscribe("client-1", "Post", "123", ["title"]);
334
- manager.emit("Post", "123", { title: "Hello", content: "World" });
335
- mockClient.messages = [];
336
-
337
- // Update subscription to include content
338
- manager.updateSubscription("client-1", "Post", "123", ["title", "content"]);
339
-
340
- // Emit update with content change
341
- manager.emit("Post", "123", { content: "Updated" });
342
-
343
- expect(mockClient.messages.length).toBe(1);
344
- expect(mockClient.messages[0].updates).toHaveProperty("content");
345
- });
346
-
347
- it("updates subscription from specific fields to all fields (*)", () => {
348
- manager.subscribe("client-1", "Post", "123", ["title"]);
349
- manager.emit("Post", "123", { title: "Hello", content: "World", author: "Alice" });
350
- mockClient.messages = [];
351
-
352
- // Update subscription to all fields
353
- manager.updateSubscription("client-1", "Post", "123", "*");
354
-
355
- // Emit update
356
- manager.emit("Post", "123", { content: "Updated", author: "Bob" });
357
-
358
- expect(mockClient.messages.length).toBe(1);
359
- expect(mockClient.messages[0].updates).toHaveProperty("content");
360
- expect(mockClient.messages[0].updates).toHaveProperty("author");
361
- });
362
-
363
- it("updates subscription from all fields to specific fields", () => {
364
- manager.subscribe("client-1", "Post", "123", "*");
365
- manager.emit("Post", "123", { title: "Hello", content: "World", author: "Alice" });
366
- mockClient.messages = [];
367
-
368
- // Update subscription to only title
369
- manager.updateSubscription("client-1", "Post", "123", ["title"]);
370
-
371
- // Emit update
372
- manager.emit("Post", "123", { title: "New", content: "Updated" });
373
-
374
- expect(mockClient.messages.length).toBe(1);
375
- expect(mockClient.messages[0].updates).toHaveProperty("title");
376
- expect(mockClient.messages[0].updates).not.toHaveProperty("content");
377
- });
378
-
379
- it("handles updating subscription for non-subscribed entity", () => {
380
- // Try to update subscription without subscribing first
381
- expect(() => manager.updateSubscription("client-1", "Post", "999", ["title"])).not.toThrow();
382
- });
383
- });
384
-
385
- describe("emitField", () => {
386
- it("emits a field-level update with specific strategy", () => {
387
- manager.subscribe("client-1", "Post", "123", "*");
388
- mockClient.messages = [];
389
-
390
- manager.emitField("Post", "123", "title", { strategy: "value", data: "Hello World" });
391
-
392
- expect(mockClient.messages.length).toBe(1);
393
- expect(mockClient.messages[0].updates.title).toEqual({
394
- strategy: "value",
395
- data: "Hello World",
396
- });
397
- });
398
-
399
- it("applies field update to canonical state", () => {
400
- manager.emitField("Post", "123", "title", { strategy: "value", data: "First" });
401
- manager.emitField("Post", "123", "content", { strategy: "value", data: "Second" });
402
-
403
- const state = manager.getState("Post", "123");
404
- expect(state).toEqual({
405
- title: "First",
406
- content: "Second",
407
- });
408
- });
409
-
410
- it("applies patch update to existing field", () => {
411
- manager.emitField("Post", "123", "metadata", {
412
- strategy: "value",
413
- data: { views: 100, likes: 10 },
414
- });
415
-
416
- // Subscribe to see the patch
417
- manager.subscribe("client-1", "Post", "123", "*");
418
- mockClient.messages = [];
419
-
420
- // Apply patch
421
- manager.emitField("Post", "123", "metadata", {
422
- strategy: "patch",
423
- data: [{ op: "replace", path: "/views", value: 101 }],
424
- });
425
-
426
- const state = manager.getState("Post", "123");
427
- expect(state?.metadata).toEqual({ views: 101, likes: 10 });
428
- });
429
-
430
- it("sends field update to subscribed clients only for subscribed fields", () => {
431
- manager.subscribe("client-1", "Post", "123", ["title"]);
432
- mockClient.messages = [];
433
-
434
- manager.emitField("Post", "123", "title", { strategy: "value", data: "Hello" });
435
- expect(mockClient.messages.length).toBe(1);
436
-
437
- mockClient.messages = [];
438
- manager.emitField("Post", "123", "content", { strategy: "value", data: "World" });
439
- expect(mockClient.messages.length).toBe(0);
440
- });
441
-
442
- it("does not send update if field value unchanged", () => {
443
- manager.subscribe("client-1", "Post", "123", "*");
444
- manager.emitField("Post", "123", "title", { strategy: "value", data: "Same" });
445
- mockClient.messages = [];
446
-
447
- manager.emitField("Post", "123", "title", { strategy: "value", data: "Same" });
448
- expect(mockClient.messages.length).toBe(0);
449
- });
450
-
451
- it("does not send update if object field unchanged", () => {
452
- manager.subscribe("client-1", "Post", "123", "*");
453
- manager.emitField("Post", "123", "metadata", {
454
- strategy: "value",
455
- data: { views: 100 },
456
- });
457
- mockClient.messages = [];
458
-
459
- manager.emitField("Post", "123", "metadata", {
460
- strategy: "value",
461
- data: { views: 100 },
462
- });
463
- expect(mockClient.messages.length).toBe(0);
464
- });
465
-
466
- it("handles emitField with no subscribers", () => {
467
- expect(() => manager.emitField("Post", "999", "title", { strategy: "value", data: "Hello" })).not.toThrow();
468
- });
469
- });
470
-
471
- describe("emitBatch", () => {
472
- it("emits multiple field updates in a batch", () => {
473
- manager.subscribe("client-1", "Post", "123", "*");
474
- mockClient.messages = [];
475
-
476
- manager.emitBatch("Post", "123", [
477
- { field: "title", update: { strategy: "value", data: "Hello" } },
478
- { field: "content", update: { strategy: "value", data: "World" } },
479
- { field: "author", update: { strategy: "value", data: "Alice" } },
480
- ]);
481
-
482
- expect(mockClient.messages.length).toBe(1);
483
- expect(mockClient.messages[0].updates.title.data).toBe("Hello");
484
- expect(mockClient.messages[0].updates.content.data).toBe("World");
485
- expect(mockClient.messages[0].updates.author.data).toBe("Alice");
486
- });
487
-
488
- it("applies batch updates to canonical state", () => {
489
- manager.emitBatch("Post", "123", [
490
- { field: "title", update: { strategy: "value", data: "Title" } },
491
- { field: "content", update: { strategy: "value", data: "Content" } },
492
- ]);
493
-
494
- const state = manager.getState("Post", "123");
495
- expect(state).toEqual({
496
- title: "Title",
497
- content: "Content",
498
- });
499
- });
500
-
501
- it("only sends batch updates for subscribed fields", () => {
502
- manager.subscribe("client-1", "Post", "123", ["title", "content"]);
503
- mockClient.messages = [];
504
-
505
- manager.emitBatch("Post", "123", [
506
- { field: "title", update: { strategy: "value", data: "Hello" } },
507
- { field: "content", update: { strategy: "value", data: "World" } },
508
- { field: "author", update: { strategy: "value", data: "Alice" } },
509
- ]);
510
-
511
- expect(mockClient.messages.length).toBe(1);
512
- expect(mockClient.messages[0].updates).toHaveProperty("title");
513
- expect(mockClient.messages[0].updates).toHaveProperty("content");
514
- expect(mockClient.messages[0].updates).not.toHaveProperty("author");
515
- });
516
-
517
- it("skips unchanged fields in batch", () => {
518
- manager.subscribe("client-1", "Post", "123", "*");
519
- manager.emitBatch("Post", "123", [
520
- { field: "title", update: { strategy: "value", data: "Same" } },
521
- { field: "content", update: { strategy: "value", data: "Same" } },
522
- ]);
523
- mockClient.messages = [];
524
-
525
- manager.emitBatch("Post", "123", [
526
- { field: "title", update: { strategy: "value", data: "Same" } },
527
- { field: "content", update: { strategy: "value", data: "Changed" } },
528
- ]);
529
-
530
- expect(mockClient.messages.length).toBe(1);
531
- expect(mockClient.messages[0].updates).not.toHaveProperty("title");
532
- expect(mockClient.messages[0].updates).toHaveProperty("content");
533
- });
534
-
535
- it("skips unchanged object fields in batch", () => {
536
- manager.subscribe("client-1", "Post", "123", "*");
537
- manager.emitBatch("Post", "123", [{ field: "metadata", update: { strategy: "value", data: { views: 100 } } }]);
538
- mockClient.messages = [];
539
-
540
- manager.emitBatch("Post", "123", [{ field: "metadata", update: { strategy: "value", data: { views: 100 } } }]);
541
-
542
- expect(mockClient.messages.length).toBe(0);
543
- });
544
-
545
- it("does not send if no fields changed in batch", () => {
546
- manager.subscribe("client-1", "Post", "123", "*");
547
- manager.emitBatch("Post", "123", [{ field: "title", update: { strategy: "value", data: "Same" } }]);
548
- mockClient.messages = [];
549
-
550
- manager.emitBatch("Post", "123", [{ field: "title", update: { strategy: "value", data: "Same" } }]);
551
-
552
- expect(mockClient.messages.length).toBe(0);
553
- });
554
-
555
- it("handles emitBatch with no subscribers", () => {
556
- expect(() =>
557
- manager.emitBatch("Post", "999", [{ field: "title", update: { strategy: "value", data: "Hello" } }]),
558
- ).not.toThrow();
559
- });
560
-
561
- it("sends batch to multiple subscribed clients", () => {
562
- const client2 = {
563
- id: "client-2",
564
- messages: [] as StateUpdateMessage[],
565
- send: mock((msg: StateUpdateMessage) => {
566
- client2.messages.push(msg);
567
- }),
568
- };
569
- manager.addClient(client2);
570
-
571
- manager.subscribe("client-1", "Post", "123", "*");
572
- manager.subscribe("client-2", "Post", "123", "*");
573
- mockClient.messages = [];
574
-
575
- manager.emitBatch("Post", "123", [
576
- { field: "title", update: { strategy: "value", data: "Hello" } },
577
- { field: "content", update: { strategy: "value", data: "World" } },
578
- ]);
579
-
580
- expect(mockClient.messages.length).toBe(1);
581
- expect(client2.messages.length).toBe(1);
582
- });
583
- });
584
-
585
- describe("processCommand", () => {
586
- it("processes full command", () => {
587
- manager.subscribe("client-1", "Post", "123", "*");
588
- mockClient.messages = [];
589
-
590
- manager.processCommand("Post", "123", {
591
- type: "full",
592
- data: { title: "Hello", content: "World" },
593
- replace: true,
594
- });
595
-
596
- expect(mockClient.messages.length).toBe(1);
597
- expect(manager.getState("Post", "123")).toEqual({
598
- title: "Hello",
599
- content: "World",
600
- });
601
- });
602
-
603
- it("processes full command with replace option", () => {
604
- manager.emit("Post", "123", { title: "Old", content: "Old", author: "Alice" });
605
- manager.subscribe("client-1", "Post", "123", "*");
606
- mockClient.messages = [];
607
-
608
- manager.processCommand("Post", "123", {
609
- type: "full",
610
- data: { title: "New" },
611
- replace: true,
612
- });
613
-
614
- expect(manager.getState("Post", "123")).toEqual({ title: "New" });
615
- });
616
-
617
- it("processes field command", () => {
618
- manager.subscribe("client-1", "Post", "123", "*");
619
- mockClient.messages = [];
620
-
621
- manager.processCommand("Post", "123", {
622
- type: "field",
623
- field: "title",
624
- update: { strategy: "value", data: "Hello" },
625
- });
626
-
627
- expect(mockClient.messages.length).toBe(1);
628
- expect(manager.getState("Post", "123")).toEqual({ title: "Hello" });
629
- });
630
-
631
- it("processes batch command", () => {
632
- manager.subscribe("client-1", "Post", "123", "*");
633
- mockClient.messages = [];
634
-
635
- manager.processCommand("Post", "123", {
636
- type: "batch",
637
- updates: [
638
- { field: "title", update: { strategy: "value", data: "Hello" } },
639
- { field: "content", update: { strategy: "value", data: "World" } },
640
- ],
641
- });
642
-
643
- expect(mockClient.messages.length).toBe(1);
644
- expect(manager.getState("Post", "123")).toEqual({
645
- title: "Hello",
646
- content: "World",
647
- });
648
- });
649
- });
650
-
651
- describe("edge cases", () => {
652
- it("handles multiple clients with different field subscriptions", () => {
653
- const client2 = {
654
- id: "client-2",
655
- messages: [] as StateUpdateMessage[],
656
- send: mock((msg: StateUpdateMessage) => {
657
- client2.messages.push(msg);
658
- }),
659
- };
660
- const client3 = {
661
- id: "client-3",
662
- messages: [] as StateUpdateMessage[],
663
- send: mock((msg: StateUpdateMessage) => {
664
- client3.messages.push(msg);
665
- }),
666
- };
667
- manager.addClient(client2);
668
- manager.addClient(client3);
669
-
670
- manager.subscribe("client-1", "Post", "123", ["title"]);
671
- manager.subscribe("client-2", "Post", "123", ["content"]);
672
- manager.subscribe("client-3", "Post", "123", "*");
673
- mockClient.messages = [];
674
-
675
- manager.emit("Post", "123", { title: "Hello", content: "World", author: "Alice" });
676
-
677
- // client-1 should only get title
678
- expect(mockClient.messages.length).toBe(1);
679
- expect(mockClient.messages[0].updates).toHaveProperty("title");
680
- expect(mockClient.messages[0].updates).not.toHaveProperty("content");
681
- expect(mockClient.messages[0].updates).not.toHaveProperty("author");
682
-
683
- // client-2 should only get content
684
- expect(client2.messages.length).toBe(1);
685
- expect(client2.messages[0].updates).not.toHaveProperty("title");
686
- expect(client2.messages[0].updates).toHaveProperty("content");
687
- expect(client2.messages[0].updates).not.toHaveProperty("author");
688
-
689
- // client-3 should get all fields
690
- expect(client3.messages.length).toBe(1);
691
- expect(client3.messages[0].updates).toHaveProperty("title");
692
- expect(client3.messages[0].updates).toHaveProperty("content");
693
- expect(client3.messages[0].updates).toHaveProperty("author");
694
- });
695
-
696
- it("handles deeply nested entity relationships", () => {
697
- manager.subscribe("client-1", "Post", "123", "*");
698
- mockClient.messages = [];
699
-
700
- manager.emit("Post", "123", {
701
- author: {
702
- id: "1",
703
- name: "Alice",
704
- profile: {
705
- bio: "Developer",
706
- location: {
707
- city: "SF",
708
- country: "USA",
709
- },
710
- },
711
- },
712
- });
713
-
714
- expect(mockClient.messages.length).toBe(1);
715
- expect(mockClient.messages[0].updates.author.data).toEqual({
716
- id: "1",
717
- name: "Alice",
718
- profile: {
719
- bio: "Developer",
720
- location: {
721
- city: "SF",
722
- country: "USA",
723
- },
724
- },
725
- });
726
-
727
- // Update nested object
728
- mockClient.messages = [];
729
- manager.emit("Post", "123", {
730
- author: {
731
- id: "1",
732
- name: "Alice",
733
- profile: {
734
- bio: "Senior Developer",
735
- location: {
736
- city: "SF",
737
- country: "USA",
738
- },
739
- },
740
- },
741
- });
742
-
743
- expect(mockClient.messages.length).toBe(1);
744
- expect(mockClient.messages[0].updates).toHaveProperty("author");
745
- });
746
-
747
- it("handles emitField creating entity from scratch", () => {
748
- manager.subscribe("client-1", "Post", "new-123", "*");
749
- mockClient.messages = [];
750
-
751
- // First field on non-existent entity
752
- manager.emitField("Post", "new-123", "title", { strategy: "value", data: "First" });
753
-
754
- expect(mockClient.messages.length).toBe(1);
755
- expect(manager.getState("Post", "new-123")).toEqual({ title: "First" });
756
-
757
- // Add more fields
758
- mockClient.messages = [];
759
- manager.emitField("Post", "new-123", "content", { strategy: "value", data: "Second" });
760
-
761
- expect(manager.getState("Post", "new-123")).toEqual({
762
- title: "First",
763
- content: "Second",
764
- });
765
- });
766
-
767
- it("handles emitBatch creating entity from scratch", () => {
768
- manager.subscribe("client-1", "Post", "new-456", "*");
769
- mockClient.messages = [];
770
-
771
- // Batch update on non-existent entity
772
- manager.emitBatch("Post", "new-456", [
773
- { field: "title", update: { strategy: "value", data: "Title" } },
774
- { field: "content", update: { strategy: "value", data: "Content" } },
775
- ]);
776
-
777
- expect(mockClient.messages.length).toBe(1);
778
- expect(manager.getState("Post", "new-456")).toEqual({
779
- title: "Title",
780
- content: "Content",
781
- });
782
- });
783
-
784
- it("handles rapid succession of updates", () => {
785
- manager.subscribe("client-1", "Post", "123", "*");
786
- mockClient.messages = [];
787
-
788
- // Rapid updates
789
- for (let i = 0; i < 10; i++) {
790
- manager.emit("Post", "123", { counter: i });
791
- }
792
-
793
- // Should have 10 updates
794
- expect(mockClient.messages.length).toBe(10);
795
- expect(mockClient.messages[9].updates.counter.data).toBe(9);
796
- });
797
-
798
- it("handles large number of subscribers to same entity", () => {
799
- const clients = [];
800
- for (let i = 0; i < 100; i++) {
801
- const client = {
802
- id: `client-${i}`,
803
- messages: [] as StateUpdateMessage[],
804
- send: mock((msg: StateUpdateMessage) => {
805
- client.messages.push(msg);
806
- }),
807
- };
808
- manager.addClient(client);
809
- manager.subscribe(`client-${i}`, "Post", "123", "*");
810
- clients.push(client);
811
- }
812
-
813
- manager.emit("Post", "123", { title: "Broadcast" });
814
-
815
- // All clients should receive the update
816
- for (const client of clients) {
817
- expect(client.messages.length).toBe(1);
818
- expect(client.messages[0].updates.title.data).toBe("Broadcast");
819
- }
820
- });
821
-
822
- it("handles undefined field values", () => {
823
- manager.subscribe("client-1", "Post", "123", "*");
824
- manager.emit("Post", "123", { title: "Hello", content: undefined });
825
-
826
- const state = manager.getState("Post", "123");
827
- expect(state).toEqual({ title: "Hello", content: undefined });
828
- });
829
-
830
- it("handles null field values", () => {
831
- manager.subscribe("client-1", "Post", "123", "*");
832
- manager.emit("Post", "123", { title: "Hello", content: null });
833
-
834
- const state = manager.getState("Post", "123");
835
- expect(state).toEqual({ title: "Hello", content: null });
836
- });
837
-
838
- it("handles array field values", () => {
839
- manager.subscribe("client-1", "Post", "123", "*");
840
- mockClient.messages = [];
841
-
842
- manager.emit("Post", "123", { tags: ["javascript", "typescript"] });
843
-
844
- expect(mockClient.messages.length).toBe(1);
845
- expect(mockClient.messages[0].updates.tags.data).toEqual(["javascript", "typescript"]);
846
-
847
- // Update array
848
- mockClient.messages = [];
849
- manager.emit("Post", "123", { tags: ["javascript", "typescript", "react"] });
850
-
851
- expect(mockClient.messages.length).toBe(1);
852
- expect(mockClient.messages[0].updates.tags.data).toEqual(["javascript", "typescript", "react"]);
853
- });
854
-
855
- it("handles boolean field values", () => {
856
- manager.subscribe("client-1", "Post", "123", "*");
857
- mockClient.messages = [];
858
-
859
- manager.emit("Post", "123", { published: true });
860
-
861
- expect(mockClient.messages.length).toBe(1);
862
- expect(mockClient.messages[0].updates.published.data).toBe(true);
863
-
864
- // Toggle boolean
865
- mockClient.messages = [];
866
- manager.emit("Post", "123", { published: false });
867
-
868
- expect(mockClient.messages.length).toBe(1);
869
- expect(mockClient.messages[0].updates.published.data).toBe(false);
870
- });
871
-
872
- it("handles number field values including 0", () => {
873
- manager.subscribe("client-1", "Post", "123", "*");
874
- mockClient.messages = [];
875
-
876
- manager.emit("Post", "123", { likes: 0 });
877
-
878
- expect(mockClient.messages.length).toBe(1);
879
- expect(mockClient.messages[0].updates.likes.data).toBe(0);
880
-
881
- // Update to positive number
882
- mockClient.messages = [];
883
- manager.emit("Post", "123", { likes: 5 });
884
-
885
- expect(mockClient.messages.length).toBe(1);
886
- expect(mockClient.messages[0].updates.likes.data).toBe(5);
887
- });
888
- });
889
-
890
- describe("array operations", () => {
891
- // Interface kept for documentation - shows expected array shape
892
- interface _User {
893
- id: string;
894
- name: string;
895
- }
896
-
897
- it("emits array data", () => {
898
- manager.subscribe("client-1", "Users", "list", "*");
899
- mockClient.messages = [];
900
-
901
- manager.emitArray("Users", "list", [
902
- { id: "1", name: "Alice" },
903
- { id: "2", name: "Bob" },
904
- ]);
905
-
906
- expect(mockClient.messages.length).toBe(1);
907
- expect(mockClient.messages[0]).toMatchObject({
908
- type: "update",
909
- entity: "Users",
910
- id: "list",
911
- });
912
- expect(mockClient.messages[0].updates._items.data).toEqual([
913
- { id: "1", name: "Alice" },
914
- { id: "2", name: "Bob" },
915
- ]);
916
- });
917
-
918
- it("gets array state", () => {
919
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
920
-
921
- expect(manager.getArrayState("Users", "list")).toEqual([{ id: "1", name: "Alice" }]);
922
- });
923
-
924
- it("applies push operation", () => {
925
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
926
- manager.emitArrayOperation("Users", "list", {
927
- op: "push",
928
- item: { id: "2", name: "Bob" },
929
- });
930
-
931
- expect(manager.getArrayState("Users", "list")).toEqual([
932
- { id: "1", name: "Alice" },
933
- { id: "2", name: "Bob" },
934
- ]);
935
- });
936
-
937
- it("applies unshift operation", () => {
938
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
939
- manager.emitArrayOperation("Users", "list", {
940
- op: "unshift",
941
- item: { id: "0", name: "Zero" },
942
- });
943
-
944
- expect(manager.getArrayState("Users", "list")).toEqual([
945
- { id: "0", name: "Zero" },
946
- { id: "1", name: "Alice" },
947
- ]);
948
- });
949
-
950
- it("applies insert operation", () => {
951
- manager.emitArray("Users", "list", [
952
- { id: "1", name: "Alice" },
953
- { id: "3", name: "Charlie" },
954
- ]);
955
- manager.emitArrayOperation("Users", "list", {
956
- op: "insert",
957
- index: 1,
958
- item: { id: "2", name: "Bob" },
959
- });
960
-
961
- expect(manager.getArrayState("Users", "list")).toEqual([
962
- { id: "1", name: "Alice" },
963
- { id: "2", name: "Bob" },
964
- { id: "3", name: "Charlie" },
965
- ]);
966
- });
967
-
968
- it("applies remove operation", () => {
969
- manager.emitArray("Users", "list", [
970
- { id: "1", name: "Alice" },
971
- { id: "2", name: "Bob" },
972
- ]);
973
- manager.emitArrayOperation("Users", "list", { op: "remove", index: 0 });
974
-
975
- expect(manager.getArrayState("Users", "list")).toEqual([{ id: "2", name: "Bob" }]);
976
- });
977
-
978
- it("applies removeById operation", () => {
979
- manager.emitArray("Users", "list", [
980
- { id: "1", name: "Alice" },
981
- { id: "2", name: "Bob" },
982
- ]);
983
- manager.emitArrayOperation("Users", "list", { op: "removeById", id: "1" });
984
-
985
- expect(manager.getArrayState("Users", "list")).toEqual([{ id: "2", name: "Bob" }]);
986
- });
987
-
988
- it("handles removeById for non-existent id", () => {
989
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
990
- manager.emitArrayOperation("Users", "list", { op: "removeById", id: "999" });
991
-
992
- expect(manager.getArrayState("Users", "list")).toEqual([{ id: "1", name: "Alice" }]);
993
- });
994
-
995
- it("applies update operation", () => {
996
- manager.emitArray("Users", "list", [
997
- { id: "1", name: "Alice" },
998
- { id: "2", name: "Bob" },
999
- ]);
1000
- manager.emitArrayOperation("Users", "list", {
1001
- op: "update",
1002
- index: 1,
1003
- item: { id: "2", name: "Robert" },
1004
- });
1005
-
1006
- expect(manager.getArrayState("Users", "list")).toEqual([
1007
- { id: "1", name: "Alice" },
1008
- { id: "2", name: "Robert" },
1009
- ]);
1010
- });
1011
-
1012
- it("applies updateById operation", () => {
1013
- manager.emitArray("Users", "list", [
1014
- { id: "1", name: "Alice" },
1015
- { id: "2", name: "Bob" },
1016
- ]);
1017
- manager.emitArrayOperation("Users", "list", {
1018
- op: "updateById",
1019
- id: "2",
1020
- item: { id: "2", name: "Robert" },
1021
- });
1022
-
1023
- expect(manager.getArrayState("Users", "list")).toEqual([
1024
- { id: "1", name: "Alice" },
1025
- { id: "2", name: "Robert" },
1026
- ]);
1027
- });
1028
-
1029
- it("applies merge operation", () => {
1030
- manager.emitArray("Users", "list", [
1031
- { id: "1", name: "Alice" },
1032
- { id: "2", name: "Bob" },
1033
- ]);
1034
- manager.emitArrayOperation("Users", "list", {
1035
- op: "merge",
1036
- index: 0,
1037
- partial: { name: "Alicia" },
1038
- });
1039
-
1040
- expect(manager.getArrayState("Users", "list")).toEqual([
1041
- { id: "1", name: "Alicia" },
1042
- { id: "2", name: "Bob" },
1043
- ]);
1044
- });
1045
-
1046
- it("applies mergeById operation", () => {
1047
- manager.emitArray("Users", "list", [
1048
- { id: "1", name: "Alice" },
1049
- { id: "2", name: "Bob" },
1050
- ]);
1051
- manager.emitArrayOperation("Users", "list", {
1052
- op: "mergeById",
1053
- id: "2",
1054
- partial: { name: "Bobby" },
1055
- });
1056
-
1057
- expect(manager.getArrayState("Users", "list")).toEqual([
1058
- { id: "1", name: "Alice" },
1059
- { id: "2", name: "Bobby" },
1060
- ]);
1061
- });
1062
-
1063
- it("processCommand handles array operations", () => {
1064
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
1065
-
1066
- manager.processCommand("Users", "list", {
1067
- type: "array",
1068
- operation: { op: "push", item: { id: "2", name: "Bob" } },
1069
- });
1070
-
1071
- expect(manager.getArrayState("Users", "list")).toEqual([
1072
- { id: "1", name: "Alice" },
1073
- { id: "2", name: "Bob" },
1074
- ]);
1075
- });
1076
-
1077
- it("sends array updates to subscribed clients", () => {
1078
- manager.subscribe("client-1", "Users", "list", "*");
1079
- mockClient.messages = [];
1080
-
1081
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
1082
- manager.emitArrayOperation("Users", "list", {
1083
- op: "push",
1084
- item: { id: "2", name: "Bob" },
1085
- });
1086
-
1087
- expect(mockClient.messages.length).toBe(2);
1088
- // Second message should be incremental diff (push operation)
1089
- const update = mockClient.messages[1].updates._items;
1090
- expect(update.strategy).toBe("array");
1091
- expect(update.data).toEqual([{ op: "push", item: { id: "2", name: "Bob" } }]);
1092
- });
1093
-
1094
- it("does not send update if array unchanged", () => {
1095
- manager.subscribe("client-1", "Users", "list", "*");
1096
- manager.emitArray("Users", "list", [{ id: "1", name: "Alice" }]);
1097
- mockClient.messages = [];
1098
-
1099
- // Remove by non-existent id (no change)
1100
- manager.emitArrayOperation("Users", "list", { op: "removeById", id: "999" });
1101
-
1102
- expect(mockClient.messages.length).toBe(0);
1103
- });
1104
- });
1105
- });