@sylphx/lens-server 1.3.1 → 1.5.0

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.
@@ -3,11 +3,7 @@
3
3
  */
4
4
 
5
5
  import { beforeEach, describe, expect, it, mock } from "bun:test";
6
- import {
7
- GraphStateManager,
8
- type StateClient,
9
- type StateUpdateMessage,
10
- } from "./graph-state-manager";
6
+ import { GraphStateManager, type StateClient, type StateUpdateMessage } from "./graph-state-manager";
11
7
 
12
8
  describe("GraphStateManager", () => {
13
9
  let manager: GraphStateManager;
@@ -332,8 +328,568 @@ describe("GraphStateManager", () => {
332
328
  });
333
329
  });
334
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
+
335
890
  describe("array operations", () => {
336
- interface User {
891
+ // Interface kept for documentation - shows expected array shape
892
+ interface _User {
337
893
  id: string;
338
894
  name: string;
339
895
  }
@@ -529,10 +1085,10 @@ describe("GraphStateManager", () => {
529
1085
  });
530
1086
 
531
1087
  expect(mockClient.messages.length).toBe(2);
532
- expect(mockClient.messages[1].updates._items.data).toEqual([
533
- { id: "1", name: "Alice" },
534
- { id: "2", name: "Bob" },
535
- ]);
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" } }]);
536
1092
  });
537
1093
 
538
1094
  it("does not send update if array unchanged", () => {
@@ -11,13 +11,14 @@
11
11
 
12
12
  import {
13
13
  type ArrayOperation,
14
+ applyUpdate,
15
+ computeArrayDiff,
16
+ createUpdate,
14
17
  type EmitCommand,
15
18
  type EntityKey,
16
19
  type InternalFieldUpdate,
17
- type Update,
18
- applyUpdate,
19
- createUpdate,
20
20
  makeEntityKey,
21
+ type Update,
21
22
  } from "@sylphx/lens-core";
22
23
 
23
24
  // Re-export for convenience
@@ -518,16 +519,40 @@ export class GraphStateManager {
518
519
  return;
519
520
  }
520
521
 
521
- // For now, send full array replacement
522
- // TODO: Compute array diff for optimal transfer
523
- client.send({
524
- type: "update",
525
- entity,
526
- id,
527
- updates: {
528
- _items: { strategy: "value", data: newArray },
529
- },
530
- });
522
+ // Compute optimal array diff
523
+ const diff = computeArrayDiff(lastState, newArray);
524
+
525
+ if (diff === null || diff.length === 0) {
526
+ // Full replace is more efficient
527
+ client.send({
528
+ type: "update",
529
+ entity,
530
+ id,
531
+ updates: {
532
+ _items: { strategy: "value", data: newArray },
533
+ },
534
+ });
535
+ } else if (diff.length === 1 && diff[0].op === "replace") {
536
+ // Single replace op - send as value
537
+ client.send({
538
+ type: "update",
539
+ entity,
540
+ id,
541
+ updates: {
542
+ _items: { strategy: "value", data: newArray },
543
+ },
544
+ });
545
+ } else {
546
+ // Send incremental diff operations
547
+ client.send({
548
+ type: "update",
549
+ entity,
550
+ id,
551
+ updates: {
552
+ _items: { strategy: "array", data: diff },
553
+ },
554
+ });
555
+ }
531
556
 
532
557
  // Update client's last known state
533
558
  clientArrayState.lastState = [...newArray];
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  export {
8
- GraphStateManager,
9
8
  createGraphStateManager,
10
9
  type EntityKey,
10
+ GraphStateManager,
11
+ type GraphStateManagerConfig,
11
12
  type StateClient,
12
- type StateUpdateMessage,
13
13
  type StateFullMessage,
14
+ type StateUpdateMessage,
14
15
  type Subscription,
15
- type GraphStateManagerConfig,
16
16
  } from "./graph-state-manager";