cojson 0.20.9 → 0.20.10

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.
Files changed (169) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/PeerState.d.ts +2 -2
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +3 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/StorageReconciliationAckTracker.d.ts +14 -0
  8. package/dist/StorageReconciliationAckTracker.d.ts.map +1 -0
  9. package/dist/StorageReconciliationAckTracker.js +72 -0
  10. package/dist/StorageReconciliationAckTracker.js.map +1 -0
  11. package/dist/SyncStateManager.js +2 -2
  12. package/dist/SyncStateManager.js.map +1 -1
  13. package/dist/coValueCore/coValueCore.d.ts +2 -1
  14. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  15. package/dist/coValueCore/coValueCore.js +43 -10
  16. package/dist/coValueCore/coValueCore.js.map +1 -1
  17. package/dist/coValues/coList.d.ts +2 -0
  18. package/dist/coValues/coList.d.ts.map +1 -1
  19. package/dist/coValues/coList.js +28 -0
  20. package/dist/coValues/coList.js.map +1 -1
  21. package/dist/coValues/group.d.ts +4 -1
  22. package/dist/coValues/group.d.ts.map +1 -1
  23. package/dist/coValues/group.js +15 -1
  24. package/dist/coValues/group.js.map +1 -1
  25. package/dist/config.d.ts +8 -0
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +14 -0
  28. package/dist/config.js.map +1 -1
  29. package/dist/exports.d.ts +9 -1
  30. package/dist/exports.d.ts.map +1 -1
  31. package/dist/exports.js +5 -1
  32. package/dist/exports.js.map +1 -1
  33. package/dist/localNode.d.ts +7 -3
  34. package/dist/localNode.d.ts.map +1 -1
  35. package/dist/localNode.js +13 -5
  36. package/dist/localNode.js.map +1 -1
  37. package/dist/permissions.d.ts +1 -0
  38. package/dist/permissions.d.ts.map +1 -1
  39. package/dist/queue/LinkedList.d.ts +2 -0
  40. package/dist/queue/LinkedList.d.ts.map +1 -1
  41. package/dist/queue/LinkedList.js +7 -0
  42. package/dist/queue/LinkedList.js.map +1 -1
  43. package/dist/queue/OutgoingLoadQueue.d.ts +4 -1
  44. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -1
  45. package/dist/queue/OutgoingLoadQueue.js +41 -13
  46. package/dist/queue/OutgoingLoadQueue.js.map +1 -1
  47. package/dist/queue/PriorityBasedMessageQueue.d.ts +1 -0
  48. package/dist/queue/PriorityBasedMessageQueue.d.ts.map +1 -1
  49. package/dist/queue/PriorityBasedMessageQueue.js +11 -1
  50. package/dist/queue/PriorityBasedMessageQueue.js.map +1 -1
  51. package/dist/storage/knownState.d.ts +2 -0
  52. package/dist/storage/knownState.d.ts.map +1 -1
  53. package/dist/storage/knownState.js +11 -0
  54. package/dist/storage/knownState.js.map +1 -1
  55. package/dist/storage/sqlite/client.d.ts +10 -1
  56. package/dist/storage/sqlite/client.d.ts.map +1 -1
  57. package/dist/storage/sqlite/client.js +84 -0
  58. package/dist/storage/sqlite/client.js.map +1 -1
  59. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  60. package/dist/storage/sqlite/sqliteMigrations.js +11 -0
  61. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  62. package/dist/storage/sqliteAsync/client.d.ts +10 -1
  63. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  64. package/dist/storage/sqliteAsync/client.js +86 -0
  65. package/dist/storage/sqliteAsync/client.js.map +1 -1
  66. package/dist/storage/storageAsync.d.ts +9 -2
  67. package/dist/storage/storageAsync.d.ts.map +1 -1
  68. package/dist/storage/storageAsync.js +19 -0
  69. package/dist/storage/storageAsync.js.map +1 -1
  70. package/dist/storage/storageSync.d.ts +9 -2
  71. package/dist/storage/storageSync.d.ts.map +1 -1
  72. package/dist/storage/storageSync.js +20 -13
  73. package/dist/storage/storageSync.js.map +1 -1
  74. package/dist/storage/types.d.ts +64 -0
  75. package/dist/storage/types.d.ts.map +1 -1
  76. package/dist/storage/types.js.map +1 -1
  77. package/dist/sync.d.ts +44 -2
  78. package/dist/sync.d.ts.map +1 -1
  79. package/dist/sync.js +268 -44
  80. package/dist/sync.js.map +1 -1
  81. package/dist/tests/OutgoingLoadQueue.test.js +137 -39
  82. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -1
  83. package/dist/tests/SQLiteClientAsync.test.js +1 -1
  84. package/dist/tests/SQLiteClientAsync.test.js.map +1 -1
  85. package/dist/tests/StorageApiAsync.test.js +138 -0
  86. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  87. package/dist/tests/StorageApiSync.test.js +154 -0
  88. package/dist/tests/StorageApiSync.test.js.map +1 -1
  89. package/dist/tests/StorageReconciliationAckTracker.test.d.ts +2 -0
  90. package/dist/tests/StorageReconciliationAckTracker.test.d.ts.map +1 -0
  91. package/dist/tests/StorageReconciliationAckTracker.test.js +74 -0
  92. package/dist/tests/StorageReconciliationAckTracker.test.js.map +1 -0
  93. package/dist/tests/SyncStateManager.test.js +18 -0
  94. package/dist/tests/SyncStateManager.test.js.map +1 -1
  95. package/dist/tests/coList.test.js +112 -1
  96. package/dist/tests/coList.test.js.map +1 -1
  97. package/dist/tests/coValueCore.loadFromStorage.test.js +36 -0
  98. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  99. package/dist/tests/group.test.js +44 -0
  100. package/dist/tests/group.test.js.map +1 -1
  101. package/dist/tests/knownState.lazyLoading.test.js +6 -0
  102. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  103. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  104. package/dist/tests/messagesTestUtils.js +4 -0
  105. package/dist/tests/messagesTestUtils.js.map +1 -1
  106. package/dist/tests/sync.concurrentLoad.test.js +333 -1
  107. package/dist/tests/sync.concurrentLoad.test.js.map +1 -1
  108. package/dist/tests/sync.garbageCollection.test.js +4 -0
  109. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  110. package/dist/tests/sync.load.test.js +19 -0
  111. package/dist/tests/sync.load.test.js.map +1 -1
  112. package/dist/tests/sync.mesh.test.js +1 -0
  113. package/dist/tests/sync.mesh.test.js.map +1 -1
  114. package/dist/tests/sync.multipleServers.test.js +41 -3
  115. package/dist/tests/sync.multipleServers.test.js.map +1 -1
  116. package/dist/tests/sync.storage.test.js +2 -0
  117. package/dist/tests/sync.storage.test.js.map +1 -1
  118. package/dist/tests/sync.storageAsync.test.js +1 -0
  119. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  120. package/dist/tests/sync.storageReconciliation.test.d.ts +2 -0
  121. package/dist/tests/sync.storageReconciliation.test.d.ts.map +1 -0
  122. package/dist/tests/sync.storageReconciliation.test.js +501 -0
  123. package/dist/tests/sync.storageReconciliation.test.js.map +1 -0
  124. package/dist/tests/testUtils.d.ts +1 -0
  125. package/dist/tests/testUtils.d.ts.map +1 -1
  126. package/dist/tests/testUtils.js +3 -2
  127. package/dist/tests/testUtils.js.map +1 -1
  128. package/package.json +4 -4
  129. package/src/PeerState.ts +10 -3
  130. package/src/StorageReconciliationAckTracker.ts +83 -0
  131. package/src/SyncStateManager.ts +3 -3
  132. package/src/coValueCore/coValueCore.ts +47 -16
  133. package/src/coValues/coList.ts +23 -0
  134. package/src/coValues/group.ts +18 -0
  135. package/src/config.ts +18 -0
  136. package/src/exports.ts +8 -0
  137. package/src/localNode.ts +18 -0
  138. package/src/permissions.ts +1 -1
  139. package/src/queue/LinkedList.ts +10 -0
  140. package/src/queue/OutgoingLoadQueue.ts +57 -15
  141. package/src/queue/PriorityBasedMessageQueue.ts +15 -1
  142. package/src/storage/knownState.ts +14 -0
  143. package/src/storage/sqlite/client.ts +128 -0
  144. package/src/storage/sqlite/sqliteMigrations.ts +11 -0
  145. package/src/storage/sqliteAsync/client.ts +139 -0
  146. package/src/storage/storageAsync.ts +37 -0
  147. package/src/storage/storageSync.ts +41 -16
  148. package/src/storage/types.ts +110 -0
  149. package/src/sync.ts +311 -14
  150. package/src/tests/OutgoingLoadQueue.test.ts +226 -59
  151. package/src/tests/SQLiteClientAsync.test.ts +1 -1
  152. package/src/tests/StorageApiAsync.test.ts +161 -1
  153. package/src/tests/StorageApiSync.test.ts +176 -0
  154. package/src/tests/StorageReconciliationAckTracker.test.ts +99 -0
  155. package/src/tests/SyncStateManager.test.ts +25 -0
  156. package/src/tests/coList.test.ts +138 -0
  157. package/src/tests/coValueCore.loadFromStorage.test.ts +72 -1
  158. package/src/tests/group.test.ts +87 -0
  159. package/src/tests/knownState.lazyLoading.test.ts +36 -1
  160. package/src/tests/messagesTestUtils.ts +4 -0
  161. package/src/tests/sync.concurrentLoad.test.ts +491 -0
  162. package/src/tests/sync.garbageCollection.test.ts +4 -0
  163. package/src/tests/sync.load.test.ts +26 -0
  164. package/src/tests/sync.mesh.test.ts +1 -0
  165. package/src/tests/sync.multipleServers.test.ts +60 -2
  166. package/src/tests/sync.storage.test.ts +2 -0
  167. package/src/tests/sync.storageAsync.test.ts +1 -0
  168. package/src/tests/sync.storageReconciliation.test.ts +697 -0
  169. package/src/tests/testUtils.ts +10 -1
@@ -1,4 +1,12 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ onTestFinished,
7
+ test,
8
+ vi,
9
+ } from "vitest";
2
10
  import {
3
11
  CO_VALUE_LOADING_CONFIG,
4
12
  setMaxInFlightLoadsPerPeer,
@@ -195,6 +203,26 @@ describe("OutgoingLoadQueue", () => {
195
203
  expect(duplicateCallbackCount).toBe(0);
196
204
  });
197
205
 
206
+ test("should skip duplicate enqueue for same CoValue ID while in-flight", () => {
207
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
208
+ const node = createTestNode();
209
+ const otherNode = createTestNode();
210
+ const group = node.createGroup();
211
+
212
+ const map = group.createMap();
213
+ const sameIdCoValue = otherNode.getCoValue(map.id);
214
+
215
+ let duplicateCallbackCount = 0;
216
+
217
+ queue.enqueue(map.core, () => {});
218
+ queue.enqueue(sameIdCoValue, () => {
219
+ duplicateCallbackCount += 1;
220
+ });
221
+
222
+ expect(queue.inFlightCount).toBe(1);
223
+ expect(duplicateCallbackCount).toBe(0);
224
+ });
225
+
198
226
  test("should skip duplicate enqueue for same CoValue ID", () => {
199
227
  setMaxInFlightLoadsPerPeer(1);
200
228
  const queue = new OutgoingLoadQueue(TEST_PEER_ID);
@@ -417,6 +445,34 @@ describe("OutgoingLoadQueue", () => {
417
445
  expect(queue.inFlightCount).toBe(1);
418
446
  });
419
447
 
448
+ test("should complete in-flight request by CoValue ID even with different instance", () => {
449
+ setMaxInFlightLoadsPerPeer(1);
450
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
451
+ const node = createTestNode();
452
+ const otherNode = createTestNode();
453
+ const group = node.createGroup();
454
+
455
+ const map = group.createMap();
456
+ const nextMap = group.createMap();
457
+ const sameIdCoValue = otherNode.getCoValue(map.id);
458
+
459
+ let nextCallbackCalled = false;
460
+
461
+ queue.enqueue(map.core, () => {});
462
+ queue.enqueue(nextMap.core, () => {
463
+ nextCallbackCalled = true;
464
+ });
465
+
466
+ expect(queue.inFlightCount).toBe(1);
467
+ expect(nextCallbackCalled).toBe(false);
468
+
469
+ // Complete using a different CoValue instance with the same ID.
470
+ queue.trackComplete(sameIdCoValue);
471
+
472
+ expect(nextCallbackCalled).toBe(true);
473
+ expect(queue.inFlightCount).toBe(1);
474
+ });
475
+
420
476
  test("should allow re-enqueue after completion", () => {
421
477
  setMaxInFlightLoadsPerPeer(1);
422
478
  const queue = new OutgoingLoadQueue(TEST_PEER_ID);
@@ -445,7 +501,7 @@ describe("OutgoingLoadQueue", () => {
445
501
  expect(secondCallbackCount).toBe(1);
446
502
  });
447
503
 
448
- test("should not complete if CoValue is streaming", () => {
504
+ test("should not complete if CoValue is streaming for content updates", () => {
449
505
  setMaxInFlightLoadsPerPeer(1);
450
506
  const queue = new OutgoingLoadQueue(TEST_PEER_ID);
451
507
  const node = createTestNode();
@@ -468,7 +524,7 @@ describe("OutgoingLoadQueue", () => {
468
524
  expect(callback2Called).toBe(false);
469
525
 
470
526
  // trackComplete should not complete because map1 is streaming
471
- queue.trackComplete(map1.core);
527
+ queue.trackComplete(map1.core, "content");
472
528
 
473
529
  // Should still be in-flight and pending should not be processed
474
530
  expect(queue.inFlightCount).toBe(1);
@@ -479,7 +535,35 @@ describe("OutgoingLoadQueue", () => {
479
535
  vi.spyOn(map1.core, "isStreaming").mockReturnValue(false);
480
536
 
481
537
  // Now trackComplete should work
482
- queue.trackComplete(map1.core);
538
+ queue.trackComplete(map1.core, "content");
539
+
540
+ expect(queue.inFlightCount).toBe(1); // map2 is now in-flight
541
+ expect(callback2Called).toBe(true);
542
+ expect(queue.pendingCount).toBe(0);
543
+ });
544
+
545
+ test("should complete if CoValue is streaming for known-state updates", () => {
546
+ setMaxInFlightLoadsPerPeer(1);
547
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
548
+ const node = createTestNode();
549
+ const group = node.createGroup();
550
+
551
+ const map1 = group.createMap();
552
+ const map2 = group.createMap();
553
+
554
+ vi.spyOn(map1.core, "isStreaming").mockReturnValue(true);
555
+
556
+ let callback2Called = false;
557
+
558
+ queue.enqueue(map1.core, () => {});
559
+ queue.enqueue(map2.core, () => {
560
+ callback2Called = true;
561
+ });
562
+
563
+ expect(queue.inFlightCount).toBe(1);
564
+ expect(callback2Called).toBe(false);
565
+
566
+ queue.trackComplete(map1.core, "known");
483
567
 
484
568
  expect(queue.inFlightCount).toBe(1); // map2 is now in-flight
485
569
  expect(callback2Called).toBe(true);
@@ -731,62 +815,145 @@ describe("OutgoingLoadQueue", () => {
731
815
 
732
816
  test("should balance push/pull metrics when clearing pending items", async () => {
733
817
  const metricReader = createTestMetricReader();
818
+ onTestFinished(tearDownTestMetricReader);
734
819
 
735
- try {
736
- setMaxInFlightLoadsPerPeer(1);
737
- const queue = new OutgoingLoadQueue(TEST_PEER_ID);
738
- const node = createTestNode();
739
- const group = node.createGroup();
740
-
741
- // Block the queue first (this item is pushed then immediately shifted to go in-flight)
742
- const blockerMap = group.createMap();
743
- queue.enqueue(blockerMap.core, () => {});
744
-
745
- // Enqueue multiple items that will be pending
746
- const map1 = group.createMap();
747
- const map2 = group.createMap();
748
- const map3 = group.createMap();
749
-
750
- queue.enqueue(map1.core, () => {});
751
- queue.enqueue(map2.core, () => {});
752
- queue.enqueue(map3.core, () => {});
753
-
754
- expect(queue.pendingCount).toBe(3);
755
-
756
- // Get metrics before clear
757
- // 4 items pushed (blocker + 3 pending), 1 pulled (blocker was shifted to go in-flight)
758
- const pushedBefore = await metricReader.getMetricValue(
759
- "jazz.messagequeue.load-requests-queue.pushed",
760
- { priority: "high" },
761
- );
762
- const pulledBefore = await metricReader.getMetricValue(
763
- "jazz.messagequeue.load-requests-queue.pulled",
764
- { priority: "high" },
765
- );
766
-
767
- expect(pushedBefore).toBe(4);
768
- expect(pulledBefore).toBe(1);
769
-
770
- // Clear the queue - this should drain pending items, incrementing pulled counter
771
- queue.clear();
772
-
773
- // Get metrics after clear - pushed and pulled should now be equal
774
- const pushedAfter = await metricReader.getMetricValue(
775
- "jazz.messagequeue.load-requests-queue.pushed",
776
- { priority: "high" },
777
- );
778
- const pulledAfter = await metricReader.getMetricValue(
779
- "jazz.messagequeue.load-requests-queue.pulled",
780
- { priority: "high" },
781
- );
782
-
783
- // All 4 pushed items should now be pulled (balanced)
784
- expect(pushedAfter).toBe(4);
785
- expect(pulledAfter).toBe(4);
786
- expect(pushedAfter).toBe(pulledAfter);
787
- } finally {
788
- tearDownTestMetricReader();
789
- }
820
+ setMaxInFlightLoadsPerPeer(1);
821
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
822
+ const node = createTestNode();
823
+ const group = node.createGroup();
824
+
825
+ // Block the queue first (this item is pushed then immediately shifted to go in-flight)
826
+ const blockerMap = group.createMap();
827
+ queue.enqueue(blockerMap.core, () => {});
828
+
829
+ // Enqueue multiple items that will be pending
830
+ const map1 = group.createMap();
831
+ const map2 = group.createMap();
832
+ const map3 = group.createMap();
833
+
834
+ queue.enqueue(map1.core, () => {});
835
+ queue.enqueue(map2.core, () => {});
836
+ queue.enqueue(map3.core, () => {});
837
+
838
+ expect(queue.pendingCount).toBe(3);
839
+
840
+ // Get metrics before clear
841
+ // 4 items pushed (blocker + 3 pending), 1 pulled (blocker was shifted to go in-flight)
842
+ const pushedBefore = await metricReader.getMetricValue(
843
+ "jazz.messagequeue.load-requests-queue.pushed",
844
+ { priority: "high" },
845
+ );
846
+ const pulledBefore = await metricReader.getMetricValue(
847
+ "jazz.messagequeue.load-requests-queue.pulled",
848
+ { priority: "high" },
849
+ );
850
+
851
+ expect(pushedBefore).toBe(4);
852
+ expect(pulledBefore).toBe(1);
853
+
854
+ // Clear the queue - this should drain pending items, incrementing pulled counter
855
+ queue.clear();
856
+
857
+ // Get metrics after clear - pushed and pulled should now be equal
858
+ const pushedAfter = await metricReader.getMetricValue(
859
+ "jazz.messagequeue.load-requests-queue.pushed",
860
+ { priority: "high" },
861
+ );
862
+ const pulledAfter = await metricReader.getMetricValue(
863
+ "jazz.messagequeue.load-requests-queue.pulled",
864
+ { priority: "high" },
865
+ );
866
+
867
+ // All 4 pushed items should now be pulled (balanced)
868
+ expect(pushedAfter).toBe(4);
869
+ expect(pulledAfter).toBe(4);
870
+ expect(pushedAfter).toBe(pulledAfter);
871
+ });
872
+
873
+ test("should track in-flight loads with OpenTelemetry", async () => {
874
+ const metricReader = createTestMetricReader();
875
+ onTestFinished(tearDownTestMetricReader);
876
+
877
+ setMaxInFlightLoadsPerPeer(2);
878
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
879
+ const node = createTestNode();
880
+ const group = node.createGroup();
881
+
882
+ const map1 = group.createMap();
883
+ const map2 = group.createMap();
884
+
885
+ expect(
886
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
887
+ ).toBe(0);
888
+
889
+ queue.enqueue(map1.core, () => {});
890
+ expect(
891
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
892
+ ).toBe(1);
893
+
894
+ queue.enqueue(map2.core, () => {});
895
+ expect(
896
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
897
+ ).toBe(2);
898
+
899
+ queue.trackComplete(map1.core);
900
+ expect(
901
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
902
+ ).toBe(1);
903
+
904
+ queue.clear();
905
+ expect(
906
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
907
+ ).toBe(0);
908
+ });
909
+
910
+ test("should decrement in-flight metric when a load times out", async () => {
911
+ CO_VALUE_LOADING_CONFIG.TIMEOUT = 5;
912
+
913
+ const metricReader = createTestMetricReader();
914
+ onTestFinished(tearDownTestMetricReader);
915
+
916
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
917
+ const node = createTestNode();
918
+ const coValue = node.getCoValue("co_zTestMetricTimeout00001" as any);
919
+
920
+ queue.enqueue(coValue, () => {});
921
+ expect(
922
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
923
+ ).toBe(1);
924
+
925
+ await new Promise((resolve) => setTimeout(resolve, 20));
926
+ expect(
927
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
928
+ ).toBe(0);
929
+ });
930
+
931
+ test("should not change in-flight metric when completing unknown CoValue", async () => {
932
+ const metricReader = createTestMetricReader();
933
+ onTestFinished(tearDownTestMetricReader);
934
+
935
+ const queue = new OutgoingLoadQueue(TEST_PEER_ID);
936
+ const node = createTestNode();
937
+ const group = node.createGroup();
938
+ const knownMap = group.createMap();
939
+ const unknownCoValue = node.getCoValue(
940
+ "co_zTestUnknownMetric0001" as any,
941
+ );
942
+
943
+ queue.enqueue(knownMap.core, () => {});
944
+ expect(
945
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
946
+ ).toBe(1);
947
+
948
+ queue.trackComplete(unknownCoValue);
949
+ expect(
950
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
951
+ ).toBe(1);
952
+
953
+ queue.trackComplete(knownMap.core);
954
+ expect(
955
+ await metricReader.getMetricValue("jazz.loadqueue.outgoing.inflight"),
956
+ ).toBe(0);
790
957
  });
791
958
  });
792
959
 
@@ -36,7 +36,7 @@ describe("SQLiteClientAsync", () => {
36
36
 
37
37
  const signatures = await dbClient.getSignatures(0, 0);
38
38
  expect(signatures.length).toBe(10);
39
- signatures.forEach(async ({ signature }, i) => {
39
+ signatures.forEach(({ signature }, i) => {
40
40
  expect(signature).toBe(`signature_z${i}`);
41
41
  });
42
42
  });
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, test, vi } from "vitest";
2
- import { CoID, RawCoMap, logger } from "../exports.js";
2
+ import { CoID, RawCoID, RawCoMap, logger } from "../exports.js";
3
3
  import { CoValueCore } from "../exports.js";
4
4
  import { NewContentMessage } from "../sync.js";
5
5
  import {
@@ -1413,4 +1413,164 @@ describe("StorageApiAsync", () => {
1413
1413
  expect(dbClientSpy).toHaveBeenCalledTimes(0);
1414
1414
  });
1415
1415
  });
1416
+
1417
+ describe("getCoValueIDs", () => {
1418
+ test("should return empty array when storage has no CoValues", async () => {
1419
+ const client = setupTestNode();
1420
+ const { storage } = await client.addAsyncStorage({
1421
+ ourName: "test",
1422
+ storageName: "test-storage",
1423
+ });
1424
+
1425
+ const ids = await new Promise<{ id: RawCoID }[]>((resolve) => {
1426
+ storage.getCoValueIDs(100, 0, resolve);
1427
+ });
1428
+
1429
+ expect(ids).toEqual([]);
1430
+ });
1431
+
1432
+ test("should return CoValue IDs in batch after storing CoValues", async () => {
1433
+ const dbPath = getDbPath();
1434
+ const fixtures = setupTestNode();
1435
+ await fixtures.addAsyncStorage({
1436
+ ourName: "test",
1437
+ storageName: "test-storage",
1438
+ filename: dbPath,
1439
+ });
1440
+
1441
+ const client = setupTestNode();
1442
+ const { storage } = await client.addAsyncStorage({
1443
+ ourName: "test",
1444
+ storageName: "test-storage",
1445
+ filename: dbPath,
1446
+ });
1447
+
1448
+ // Create CoValues and sync to storage
1449
+ const group = fixtures.node.createGroup();
1450
+ group.addMember("everyone", "reader");
1451
+ const map = group.createMap();
1452
+ map.set("key", "value", "trusting");
1453
+ await map.core.waitForSync();
1454
+
1455
+ const ids = await new Promise<{ id: RawCoID }[]>((resolve) => {
1456
+ storage.getCoValueIDs(100, 0, resolve);
1457
+ });
1458
+
1459
+ expect(ids.map((e) => e.id)).toContain(group.id);
1460
+ expect(ids.map((e) => e.id)).toContain(map.id);
1461
+ expect(ids.length).toEqual(2);
1462
+ });
1463
+
1464
+ test("should paginate when there are more CoValues than the limit and return each ID only once", async () => {
1465
+ const dbPath = getDbPath();
1466
+ const fixtures = setupTestNode();
1467
+ await fixtures.addAsyncStorage({
1468
+ ourName: "test",
1469
+ storageName: "test-storage",
1470
+ filename: dbPath,
1471
+ });
1472
+
1473
+ const client = setupTestNode();
1474
+ const { storage } = await client.addAsyncStorage({
1475
+ ourName: "test",
1476
+ storageName: "test-storage",
1477
+ filename: dbPath,
1478
+ });
1479
+
1480
+ // Create more CoValues than the page size (1 group + 4 maps = 5 CoValues, limit = 2)
1481
+ const group = fixtures.node.createGroup();
1482
+ group.addMember("everyone", "reader");
1483
+ const expectedIds = new Set<RawCoID>([group.id]);
1484
+ const maps: ReturnType<typeof group.createMap>[] = [];
1485
+ for (let i = 0; i < 4; i++) {
1486
+ const map = group.createMap();
1487
+ map.set(`key${i}`, `value${i}`, "trusting");
1488
+ maps.push(map);
1489
+ expectedIds.add(map.id);
1490
+ }
1491
+ await maps[maps.length - 1]!.core.waitForSync();
1492
+
1493
+ const limit = 2;
1494
+ const allIds: RawCoID[] = [];
1495
+ await new Promise<void>((resolve) => {
1496
+ const fetchBatch = (offset: number) => {
1497
+ storage.getCoValueIDs(limit, offset, (batch) => {
1498
+ for (const { id } of batch) {
1499
+ allIds.push(id);
1500
+ }
1501
+ if (batch.length >= limit) {
1502
+ fetchBatch(offset + batch.length);
1503
+ } else {
1504
+ resolve();
1505
+ }
1506
+ });
1507
+ };
1508
+ fetchBatch(0);
1509
+ });
1510
+
1511
+ expect(allIds).toHaveLength(expectedIds.size);
1512
+ const seen = new Set<RawCoID>();
1513
+ for (const id of allIds) {
1514
+ expect(seen.has(id)).toBe(false);
1515
+ seen.add(id);
1516
+ expect(expectedIds.has(id)).toBe(true);
1517
+ }
1518
+ });
1519
+ });
1520
+
1521
+ describe("getCoValueCount", () => {
1522
+ test("should return 0 when storage has no CoValues", async () => {
1523
+ const client = setupTestNode();
1524
+ const { storage } = await client.addAsyncStorage({
1525
+ ourName: "test",
1526
+ storageName: "test-storage",
1527
+ });
1528
+
1529
+ const count = await new Promise<number>((resolve) => {
1530
+ storage.getCoValueCount(resolve);
1531
+ });
1532
+
1533
+ expect(count).toBe(0);
1534
+ });
1535
+
1536
+ test("should return CoValue count after storing CoValues", async () => {
1537
+ const dbPath = getDbPath();
1538
+ const fixtures = setupTestNode();
1539
+ await fixtures.addAsyncStorage({
1540
+ ourName: "test",
1541
+ storageName: "test-storage",
1542
+ filename: dbPath,
1543
+ });
1544
+
1545
+ const client = setupTestNode();
1546
+ const { storage } = await client.addAsyncStorage({
1547
+ ourName: "test",
1548
+ storageName: "test-storage",
1549
+ filename: dbPath,
1550
+ });
1551
+
1552
+ const countEmpty = await new Promise<number>((resolve) => {
1553
+ storage.getCoValueCount(resolve);
1554
+ });
1555
+ expect(countEmpty).toBe(0);
1556
+
1557
+ const group = fixtures.node.createGroup();
1558
+ group.addMember("everyone", "reader");
1559
+ await group.core.waitForSync();
1560
+
1561
+ const countOne = await new Promise<number>((resolve) => {
1562
+ storage.getCoValueCount(resolve);
1563
+ });
1564
+ expect(countOne).toBe(1);
1565
+
1566
+ const map = group.createMap();
1567
+ map.set("key", "value", "trusting");
1568
+ await map.core.waitForSync();
1569
+
1570
+ const countTwo = await new Promise<number>((resolve) => {
1571
+ storage.getCoValueCount(resolve);
1572
+ });
1573
+ expect(countTwo).toBe(2);
1574
+ });
1575
+ });
1416
1576
  });
@@ -1237,4 +1237,180 @@ describe("StorageApiSync", () => {
1237
1237
  expect(result2).toEqual(result1);
1238
1238
  });
1239
1239
  });
1240
+
1241
+ describe("getCoValueIDs", () => {
1242
+ test("should return empty array when storage has no CoValues", async () => {
1243
+ const client = setupTestNode();
1244
+ const { storage } = client.addStorage({
1245
+ storage: createSyncStorage({
1246
+ nodeName: "test",
1247
+ storageName: "test-storage",
1248
+ }),
1249
+ });
1250
+
1251
+ const ids = await new Promise<{ id: RawCoID }[]>((resolve) => {
1252
+ storage.getCoValueIDs(100, 0, resolve);
1253
+ });
1254
+
1255
+ expect(ids).toEqual([]);
1256
+ });
1257
+
1258
+ test("should return CoValue IDs in batch after storing CoValues", async () => {
1259
+ const dbPath = getDbPath();
1260
+ const fixtures = setupTestNode();
1261
+ fixtures.addStorage({
1262
+ storage: createSyncStorage({
1263
+ filename: dbPath,
1264
+ nodeName: "test",
1265
+ storageName: "test-storage",
1266
+ }),
1267
+ });
1268
+
1269
+ const client = setupTestNode();
1270
+ const { storage } = client.addStorage({
1271
+ storage: createSyncStorage({
1272
+ filename: dbPath,
1273
+ nodeName: "test",
1274
+ storageName: "test-storage",
1275
+ }),
1276
+ });
1277
+
1278
+ // Create CoValues and sync to storage
1279
+ const group = fixtures.node.createGroup();
1280
+ group.addMember("everyone", "reader");
1281
+ const map = group.createMap();
1282
+ map.set("key", "value", "trusting");
1283
+ await map.core.waitForSync();
1284
+
1285
+ const ids = await new Promise<{ id: RawCoID }[]>((resolve) => {
1286
+ storage.getCoValueIDs(100, 0, resolve);
1287
+ });
1288
+
1289
+ expect(ids.map((e) => e.id)).toContain(group.id);
1290
+ expect(ids.map((e) => e.id)).toContain(map.id);
1291
+ expect(ids.length).toEqual(2);
1292
+ });
1293
+
1294
+ test("should paginate when there are more CoValues than the limit and return each ID only once", async () => {
1295
+ const dbPath = getDbPath();
1296
+ const fixtures = setupTestNode();
1297
+ fixtures.addStorage({
1298
+ storage: createSyncStorage({
1299
+ filename: dbPath,
1300
+ nodeName: "test",
1301
+ storageName: "test-storage",
1302
+ }),
1303
+ });
1304
+
1305
+ const client = setupTestNode();
1306
+ const { storage } = client.addStorage({
1307
+ storage: createSyncStorage({
1308
+ filename: dbPath,
1309
+ nodeName: "test",
1310
+ storageName: "test-storage",
1311
+ }),
1312
+ });
1313
+
1314
+ // Create more CoValues than the page size (1 group + 4 maps = 5 CoValues, limit = 2)
1315
+ const group = fixtures.node.createGroup();
1316
+ group.addMember("everyone", "reader");
1317
+ const expectedIds = new Set<RawCoID>([group.id]);
1318
+ const maps: ReturnType<typeof group.createMap>[] = [];
1319
+ for (let i = 0; i < 4; i++) {
1320
+ const map = group.createMap();
1321
+ map.set(`key${i}`, `value${i}`, "trusting");
1322
+ maps.push(map);
1323
+ expectedIds.add(map.id);
1324
+ }
1325
+ await maps[maps.length - 1]!.core.waitForSync();
1326
+
1327
+ const limit = 2;
1328
+ const allIds: RawCoID[] = [];
1329
+ await new Promise<void>((resolve) => {
1330
+ const fetchBatch = (offset: number) => {
1331
+ storage.getCoValueIDs(limit, offset, (batch) => {
1332
+ for (const { id } of batch) {
1333
+ allIds.push(id);
1334
+ }
1335
+ if (batch.length >= limit) {
1336
+ fetchBatch(offset + batch.length);
1337
+ } else {
1338
+ resolve();
1339
+ }
1340
+ });
1341
+ };
1342
+ fetchBatch(0);
1343
+ });
1344
+
1345
+ expect(allIds).toHaveLength(expectedIds.size);
1346
+ const seen = new Set<RawCoID>();
1347
+ for (const id of allIds) {
1348
+ expect(seen.has(id)).toBe(false);
1349
+ seen.add(id);
1350
+ expect(expectedIds.has(id)).toBe(true);
1351
+ }
1352
+ });
1353
+ });
1354
+
1355
+ describe("getCoValueCount", () => {
1356
+ test("should return 0 when storage has no CoValues", async () => {
1357
+ const client = setupTestNode();
1358
+ const { storage } = client.addStorage({
1359
+ storage: createSyncStorage({
1360
+ nodeName: "test",
1361
+ storageName: "test-storage",
1362
+ }),
1363
+ });
1364
+
1365
+ const count = await new Promise<number>((resolve) => {
1366
+ storage.getCoValueCount(resolve);
1367
+ });
1368
+
1369
+ expect(count).toBe(0);
1370
+ });
1371
+
1372
+ test("should return CoValue count after storing CoValues", async () => {
1373
+ const dbPath = getDbPath();
1374
+ const fixtures = setupTestNode();
1375
+ fixtures.addStorage({
1376
+ storage: createSyncStorage({
1377
+ filename: dbPath,
1378
+ nodeName: "test",
1379
+ storageName: "test-storage",
1380
+ }),
1381
+ });
1382
+
1383
+ const client = setupTestNode();
1384
+ const { storage } = client.addStorage({
1385
+ storage: createSyncStorage({
1386
+ filename: dbPath,
1387
+ nodeName: "test",
1388
+ storageName: "test-storage",
1389
+ }),
1390
+ });
1391
+
1392
+ const countEmpty = await new Promise<number>((resolve) => {
1393
+ storage.getCoValueCount(resolve);
1394
+ });
1395
+ expect(countEmpty).toBe(0);
1396
+
1397
+ const group = fixtures.node.createGroup();
1398
+ group.addMember("everyone", "reader");
1399
+ await group.core.waitForSync();
1400
+
1401
+ const countOne = await new Promise<number>((resolve) => {
1402
+ storage.getCoValueCount(resolve);
1403
+ });
1404
+ expect(countOne).toBe(1);
1405
+
1406
+ const map = group.createMap();
1407
+ map.set("key", "value", "trusting");
1408
+ await map.core.waitForSync();
1409
+
1410
+ const countTwo = await new Promise<number>((resolve) => {
1411
+ storage.getCoValueCount(resolve);
1412
+ });
1413
+ expect(countTwo).toBe(2);
1414
+ });
1415
+ });
1240
1416
  });