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
@@ -0,0 +1,99 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { PeerState } from "../PeerState.js";
3
+ import { StorageReconciliationAckTracker } from "../StorageReconciliationAckTracker.js";
4
+ import { ConnectedPeerChannel } from "../streamUtils.js";
5
+ import { Peer } from "../sync.js";
6
+
7
+ function createPeerState(id = "peer-1"): PeerState {
8
+ const peer: Peer = {
9
+ id,
10
+ role: "server",
11
+ persistent: true,
12
+ incoming: new ConnectedPeerChannel(),
13
+ outgoing: new ConnectedPeerChannel(),
14
+ };
15
+
16
+ return new PeerState(peer, undefined);
17
+ }
18
+
19
+ describe("StorageReconciliationAckTracker", () => {
20
+ test("tracks pending acks and returns next offset on ack", () => {
21
+ const tracker = new StorageReconciliationAckTracker();
22
+
23
+ tracker.trackBatch("batch-1", "peer-1", 100);
24
+
25
+ expect(tracker.pendingReconciliationAck.get("batch-1#peer-1")).toBe(100);
26
+
27
+ const nextOffset = tracker.handleAck("batch-1", "peer-1");
28
+
29
+ expect(nextOffset).toBe(100);
30
+ expect(tracker.pendingReconciliationAck.has("batch-1#peer-1")).toBe(false);
31
+ });
32
+
33
+ test("invokes registered callback when ack is received", () => {
34
+ const tracker = new StorageReconciliationAckTracker();
35
+ const peer = createPeerState("peer-1");
36
+ const onAck = vi.fn();
37
+
38
+ tracker.trackBatch("batch-1", peer.id, 50);
39
+ tracker.waitForAck("batch-1", peer, onAck);
40
+
41
+ expect(onAck).not.toHaveBeenCalled();
42
+
43
+ tracker.handleAck("batch-1", peer.id);
44
+
45
+ expect(onAck).toHaveBeenCalledTimes(1);
46
+ });
47
+
48
+ test("invokes callback only once even if peer closes after ack", () => {
49
+ const tracker = new StorageReconciliationAckTracker();
50
+ const peer = createPeerState("peer-1");
51
+ const onAck = vi.fn();
52
+
53
+ tracker.trackBatch("batch-1", peer.id, 50);
54
+ tracker.waitForAck("batch-1", peer, onAck);
55
+ tracker.handleAck("batch-1", peer.id);
56
+ peer.gracefulShutdown();
57
+
58
+ expect(onAck).toHaveBeenCalledTimes(1);
59
+ });
60
+
61
+ test("invokes all listeners registered for a batch", () => {
62
+ const tracker = new StorageReconciliationAckTracker();
63
+ const peer = createPeerState("peer-1");
64
+ const first = vi.fn();
65
+ const second = vi.fn();
66
+
67
+ tracker.trackBatch("batch-1", peer.id, 50);
68
+ tracker.waitForAck("batch-1", peer, first);
69
+ tracker.waitForAck("batch-1", peer, second);
70
+ tracker.handleAck("batch-1", peer.id);
71
+
72
+ expect(first).toHaveBeenCalledTimes(1);
73
+ expect(second).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ test("calls callback immediately when batch is not pending", () => {
77
+ const tracker = new StorageReconciliationAckTracker();
78
+ const peer = createPeerState("peer-1");
79
+ const onAck = vi.fn();
80
+
81
+ tracker.waitForAck("missing-batch", peer, onAck);
82
+
83
+ expect(onAck).toHaveBeenCalledTimes(1);
84
+ });
85
+
86
+ test("aborts wait on peer close and clears pending ack", () => {
87
+ const tracker = new StorageReconciliationAckTracker();
88
+ const peer = createPeerState("peer-1");
89
+ const onAck = vi.fn();
90
+
91
+ tracker.trackBatch("batch-1", peer.id, 50);
92
+ tracker.waitForAck("batch-1", peer, onAck);
93
+
94
+ peer.gracefulShutdown();
95
+
96
+ expect(onAck).not.toHaveBeenCalled();
97
+ expect(tracker.pendingReconciliationAck.has("batch-1#peer-1")).toBe(false);
98
+ });
99
+ });
@@ -128,6 +128,31 @@ describe("SyncStateManager", () => {
128
128
  expect(subscriptionManager.isSynced(peerState, map.core.id)).toBe(true);
129
129
  });
130
130
 
131
+ test("isSynced should stay true for garbageCollected CoValues with matching knownState", async () => {
132
+ const client = setupTestNode({ connected: false });
133
+ client.addStorage({ ourName: "client" });
134
+ const { peerState } = client.connectToSyncServer();
135
+
136
+ const group = client.node.createGroup();
137
+ const map = group.createMap();
138
+ map.set("key1", "value1", "trusting");
139
+
140
+ const syncState = client.node.syncManager.syncState;
141
+
142
+ await waitFor(() => syncState.isSynced(peerState, map.core.id));
143
+ expect(syncState.isSynced(peerState, map.core.id)).toBe(true);
144
+
145
+ const mapCore = client.node.getCoValue(map.id);
146
+ const mapKnownState = mapCore.knownState();
147
+ client.node.internalDeleteCoValue(map.id);
148
+
149
+ const garbageCollectedMap = client.node.getCoValue(map.id);
150
+ garbageCollectedMap.setGarbageCollectedState(mapKnownState);
151
+ expect(garbageCollectedMap.loadingState).toBe("garbageCollected");
152
+
153
+ expect(syncState.isSynced(peerState, map.core.id)).toBe(true);
154
+ });
155
+
131
156
  test("unsubscribe stops receiving updates", async () => {
132
157
  // Setup nodes
133
158
  const client = setupTestNode({ connected: true });
@@ -5,6 +5,7 @@ import {
5
5
  hotSleep,
6
6
  loadCoValueOrFail,
7
7
  nodeWithRandomAgentAndSessionID,
8
+ setupTestAccount,
8
9
  setupTestNode,
9
10
  waitFor,
10
11
  } from "./testUtils.js";
@@ -1024,6 +1025,143 @@ test("the list should rebuild when the group permissions change", async () => {
1024
1025
  expect(listOnBob.totalValidTransactions).toEqual(1);
1025
1026
  });
1026
1027
 
1028
+ describe("CoList restricted deletion", () => {
1029
+ test("default ownedByGroup behavior is unchanged (writers can delete)", async () => {
1030
+ const alice = await setupTestAccount({ connected: true });
1031
+ const bob = await setupTestAccount({ connected: true });
1032
+
1033
+ const group = alice.node.createGroup();
1034
+ group.addMember(bob.account, "writer");
1035
+
1036
+ const listCore = alice.node.createCoValue({
1037
+ type: "colist",
1038
+ ruleset: { type: "ownedByGroup", group: group.id },
1039
+ meta: null,
1040
+ ...Crypto.createdNowUnique(),
1041
+ });
1042
+ const list = expectList(listCore.getCurrentContent());
1043
+ list.append("item");
1044
+
1045
+ const listOnBob = await loadCoValueOrFail(bob.node, list.id);
1046
+ listOnBob.delete(0);
1047
+
1048
+ await waitFor(() => {
1049
+ expect(list.toJSON()).toEqual([]);
1050
+ });
1051
+ });
1052
+
1053
+ test("with restrictDeletion enabled, writers can append but cannot delete or replace", async () => {
1054
+ const alice = await setupTestAccount({ connected: true });
1055
+ const bob = await setupTestAccount({ connected: true });
1056
+
1057
+ const group = alice.node.createGroup();
1058
+ group.addMember(bob.account, "writer");
1059
+
1060
+ const listCore = alice.node.createCoValue({
1061
+ type: "colist",
1062
+ ruleset: {
1063
+ type: "ownedByGroup",
1064
+ group: group.id,
1065
+ restrictDeletion: true,
1066
+ },
1067
+ meta: null,
1068
+ ...Crypto.createdNowUnique(),
1069
+ });
1070
+ const list = expectList(listCore.getCurrentContent());
1071
+ list.append("seed");
1072
+
1073
+ const listOnBob = await loadCoValueOrFail(bob.node, list.id);
1074
+
1075
+ listOnBob.append("writer-append");
1076
+ await waitFor(() => {
1077
+ expect(list.toJSON()).toEqual(["seed", "writer-append"]);
1078
+ });
1079
+
1080
+ listOnBob.delete(0);
1081
+ await waitFor(() => {
1082
+ expect(list.toJSON()).toEqual(["seed", "writer-append"]);
1083
+ });
1084
+
1085
+ listOnBob.replace(1, "writer-replace-attempt");
1086
+ await waitFor(() => {
1087
+ expect(list.toJSON()).toEqual(["seed", "writer-append"]);
1088
+ });
1089
+ });
1090
+
1091
+ test("with restrictDeletion enabled, managers and admins can delete", async () => {
1092
+ const alice = await setupTestAccount({ connected: true });
1093
+ const bob = await setupTestAccount({ connected: true });
1094
+
1095
+ const group = alice.node.createGroup();
1096
+ group.addMember(bob.account, "manager");
1097
+
1098
+ const listCore = alice.node.createCoValue({
1099
+ type: "colist",
1100
+ ruleset: {
1101
+ type: "ownedByGroup",
1102
+ group: group.id,
1103
+ restrictDeletion: true,
1104
+ },
1105
+ meta: null,
1106
+ ...Crypto.createdNowUnique(),
1107
+ });
1108
+ const list = expectList(listCore.getCurrentContent());
1109
+ list.appendItems(["first", "second", "third"]);
1110
+
1111
+ const listOnBob = await loadCoValueOrFail(bob.node, list.id);
1112
+ listOnBob.delete(1);
1113
+
1114
+ await waitFor(() => {
1115
+ expect(list.toJSON()).toEqual(["first", "third"]);
1116
+ });
1117
+
1118
+ list.delete(0);
1119
+ await waitFor(() => {
1120
+ expect(list.toJSON()).toEqual(["third"]);
1121
+ });
1122
+ });
1123
+
1124
+ test("deletions made while writer stay blocked after promotion", async () => {
1125
+ const alice = await setupTestAccount({ connected: true });
1126
+ const bob = await setupTestAccount({ connected: true });
1127
+
1128
+ const group = alice.node.createGroup();
1129
+ group.addMember(bob.account, "writer");
1130
+
1131
+ const listCore = alice.node.createCoValue({
1132
+ type: "colist",
1133
+ ruleset: {
1134
+ type: "ownedByGroup",
1135
+ group: group.id,
1136
+ restrictDeletion: true,
1137
+ },
1138
+ meta: null,
1139
+ ...Crypto.createdNowUnique(),
1140
+ });
1141
+ const list = expectList(listCore.getCurrentContent());
1142
+ list.append("item");
1143
+
1144
+ const listOnBob = await loadCoValueOrFail(bob.node, list.id);
1145
+ listOnBob.delete(0);
1146
+
1147
+ await waitFor(() => {
1148
+ expect(list.toJSON()).toEqual(["item"]);
1149
+ });
1150
+
1151
+ group.addMember(bob.account, "manager");
1152
+
1153
+ await waitFor(() => {
1154
+ expect(list.toJSON()).toEqual(["item"]);
1155
+ expect(listOnBob.toJSON()).toEqual(["item"]);
1156
+ });
1157
+
1158
+ listOnBob.delete(0);
1159
+ await waitFor(() => {
1160
+ expect(list.toJSON()).toEqual([]);
1161
+ });
1162
+ });
1163
+ });
1164
+
1027
1165
  test("items appended after a losing init transaction are preserved", async () => {
1028
1166
  const alice = setupTestNode({ connected: true });
1029
1167
  const bob = setupTestNode({ connected: true });
@@ -1,7 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
2
  import { RawCoID } from "../ids";
3
3
  import { PeerID } from "../sync";
4
- import { StorageAPI } from "../storage/types";
4
+ import type {
5
+ StorageAPI,
6
+ StorageReconciliationAcquireResult,
7
+ } from "../storage/types";
5
8
  import {
6
9
  createTestMetricReader,
7
10
  createTestNode,
@@ -29,6 +32,12 @@ function setup() {
29
32
 
30
33
  function createMockStorage(
31
34
  opts: {
35
+ getCoValueIDs?: (
36
+ limit: number,
37
+ offset: number,
38
+ callback: (batch: { id: RawCoID }[]) => void,
39
+ ) => void;
40
+ getCoValueCount?: (callback: (count: number) => void) => void;
32
41
  load?: (
33
42
  id: RawCoID,
34
43
  callback: (data: any) => void,
@@ -50,9 +59,26 @@ function createMockStorage(
50
59
  markDeleteAsValid?: (id: RawCoID) => void;
51
60
  enableDeletedCoValuesErasure?: () => void;
52
61
  eraseAllDeletedCoValues?: () => Promise<void>;
62
+ tryAcquireStorageReconciliationLock?: (
63
+ sessionId: string,
64
+ peerId: string,
65
+ callback: (result: StorageReconciliationAcquireResult) => void,
66
+ ) => void;
67
+ renewStorageReconciliationLock?: (
68
+ sessionId: string,
69
+ peerId: string,
70
+ offset: number,
71
+ ) => void;
72
+ releaseStorageReconciliationLock?: (
73
+ sessionId: string,
74
+ peerId: string,
75
+ callback?: () => void,
76
+ ) => void;
53
77
  } = {},
54
78
  ): StorageAPI {
55
79
  return {
80
+ getCoValueIDs: opts.getCoValueIDs || vi.fn(),
81
+ getCoValueCount: opts.getCoValueCount || vi.fn(),
56
82
  markDeleteAsValid: opts.markDeleteAsValid || vi.fn(),
57
83
  enableDeletedCoValuesErasure: opts.enableDeletedCoValuesErasure || vi.fn(),
58
84
  eraseAllDeletedCoValues: opts.eraseAllDeletedCoValues || vi.fn(),
@@ -67,6 +93,15 @@ function createMockStorage(
67
93
  stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
68
94
  onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
69
95
  close: opts.close || vi.fn().mockResolvedValue(undefined),
96
+ tryAcquireStorageReconciliationLock:
97
+ opts.tryAcquireStorageReconciliationLock ||
98
+ vi.fn((_sessionId, _peerId, callback) =>
99
+ callback({ acquired: false as const, reason: "not_due" as const }),
100
+ ),
101
+ renewStorageReconciliationLock:
102
+ opts.renewStorageReconciliationLock || vi.fn(),
103
+ releaseStorageReconciliationLock:
104
+ opts.releaseStorageReconciliationLock || vi.fn(),
70
105
  };
71
106
  }
72
107
 
@@ -518,6 +553,22 @@ describe("CoValueCore.loadFromStorage", () => {
518
553
 
519
554
  expect(loadSpy).toHaveBeenCalledTimes(1);
520
555
  });
556
+
557
+ test("should keep garbageCollected loadingState even when a peer is pending", () => {
558
+ const { state, node, id } = setup();
559
+ const storage = createMockStorage();
560
+ node.setStorage(storage);
561
+
562
+ state.setGarbageCollectedState({
563
+ id,
564
+ header: true,
565
+ sessions: {},
566
+ });
567
+ state.markPending("peer1");
568
+
569
+ expect(state.getLoadingStateForPeer("peer1")).toBe("pending");
570
+ expect(state.loadingState).toBe("garbageCollected");
571
+ });
521
572
  });
522
573
 
523
574
  describe("when state is onlyKnownState", () => {
@@ -576,6 +627,26 @@ describe("CoValueCore.loadFromStorage", () => {
576
627
 
577
628
  expect(loadSpy).toHaveBeenCalledTimes(1);
578
629
  });
630
+
631
+ test("should keep onlyKnownState loadingState even when a peer is pending", () => {
632
+ const { state, node, id } = setup();
633
+ const storage = createMockStorage({
634
+ loadKnownState: (id, callback) => {
635
+ callback({
636
+ id,
637
+ header: true,
638
+ sessions: { session1: 1 },
639
+ });
640
+ },
641
+ });
642
+ node.setStorage(storage);
643
+
644
+ state.getKnownStateFromStorage(() => {});
645
+ state.markPending("peer1");
646
+
647
+ expect(state.getLoadingStateForPeer("peer1")).toBe("pending");
648
+ expect(state.loadingState).toBe("onlyKnownState");
649
+ });
579
650
  });
580
651
 
581
652
  describe("edge cases and integration", () => {
@@ -435,6 +435,93 @@ test("Should heal the missing key_for_everyone", async () => {
435
435
  );
436
436
  });
437
437
 
438
+ describe("reader cannot create content", () => {
439
+ test("createMap throws for reader", async () => {
440
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
441
+
442
+ const group = node1.node.createGroup();
443
+ group.addMember(
444
+ await loadCoValueOrFail(node1.node, node2.accountID),
445
+ "reader",
446
+ );
447
+
448
+ await group.core.waitForSync();
449
+
450
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
451
+ expect(groupOnNode2.myRole()).toBe("reader");
452
+ expect(() => groupOnNode2.createMap()).toThrow(
453
+ "does not have write permissions",
454
+ );
455
+ });
456
+
457
+ test("createList throws for reader", async () => {
458
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
459
+
460
+ const group = node1.node.createGroup();
461
+ group.addMember(
462
+ await loadCoValueOrFail(node1.node, node2.accountID),
463
+ "reader",
464
+ );
465
+
466
+ await group.core.waitForSync();
467
+
468
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
469
+ expect(() => groupOnNode2.createList()).toThrow(
470
+ "does not have write permissions",
471
+ );
472
+ });
473
+
474
+ test("createStream throws for reader", async () => {
475
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
476
+
477
+ const group = node1.node.createGroup();
478
+ group.addMember(
479
+ await loadCoValueOrFail(node1.node, node2.accountID),
480
+ "reader",
481
+ );
482
+
483
+ await group.core.waitForSync();
484
+
485
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
486
+ expect(() => groupOnNode2.createStream()).toThrow(
487
+ "does not have write permissions",
488
+ );
489
+ });
490
+
491
+ test("createBinaryStream throws for reader", async () => {
492
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
493
+
494
+ const group = node1.node.createGroup();
495
+ group.addMember(
496
+ await loadCoValueOrFail(node1.node, node2.accountID),
497
+ "reader",
498
+ );
499
+
500
+ await group.core.waitForSync();
501
+
502
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
503
+ expect(() => groupOnNode2.createBinaryStream()).toThrow(
504
+ "does not have write permissions",
505
+ );
506
+ });
507
+
508
+ test("writer can still create content", async () => {
509
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
510
+
511
+ const group = node1.node.createGroup();
512
+ group.addMember(
513
+ await loadCoValueOrFail(node1.node, node2.accountID),
514
+ "writer",
515
+ );
516
+
517
+ await group.core.waitForSync();
518
+
519
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
520
+ const map = groupOnNode2.createMap();
521
+ expect(map.type).toBe("comap");
522
+ });
523
+ });
524
+
438
525
  describe("writeOnly", () => {
439
526
  test("Admins can invite writeOnly members", async () => {
440
527
  const { node1, node2 } = await createTwoConnectedNodes("server", "server");
@@ -1,12 +1,21 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
2
  import { RawCoID, SessionID } from "../ids";
3
3
  import { PeerID } from "../sync";
4
- import { StorageAPI } from "../storage/types";
4
+ import type {
5
+ StorageAPI,
6
+ StorageReconciliationAcquireResult,
7
+ } from "../storage/types";
5
8
  import { CoValueKnownState, peerHasAllContent } from "../knownState";
6
9
  import { createTestNode, createUnloadedCoValue } from "./testUtils";
7
10
 
8
11
  function createMockStorage(
9
12
  opts: {
13
+ getCoValueIDs?: (
14
+ limit: number,
15
+ offset: number,
16
+ callback: (batch: { id: RawCoID }[]) => void,
17
+ ) => void;
18
+ getCoValueCount?: (callback: (count: number) => void) => void;
10
19
  load?: (
11
20
  id: RawCoID,
12
21
  callback: (data: any) => void,
@@ -28,9 +37,26 @@ function createMockStorage(
28
37
  markDeleteAsValid?: (id: RawCoID) => void;
29
38
  enableDeletedCoValuesErasure?: () => void;
30
39
  eraseAllDeletedCoValues?: () => Promise<void>;
40
+ tryAcquireStorageReconciliationLock?: (
41
+ sessionId: string,
42
+ peerId: string,
43
+ callback: (result: StorageReconciliationAcquireResult) => void,
44
+ ) => void;
45
+ renewStorageReconciliationLock?: (
46
+ sessionId: string,
47
+ peerId: string,
48
+ offset: number,
49
+ ) => void;
50
+ releaseStorageReconciliationLock?: (
51
+ sessionId: string,
52
+ peerId: string,
53
+ callback?: () => void,
54
+ ) => void;
31
55
  } = {},
32
56
  ): StorageAPI {
33
57
  return {
58
+ getCoValueIDs: opts.getCoValueIDs || vi.fn(),
59
+ getCoValueCount: opts.getCoValueCount || vi.fn(),
34
60
  markDeleteAsValid: opts.markDeleteAsValid || vi.fn(),
35
61
  enableDeletedCoValuesErasure: opts.enableDeletedCoValuesErasure || vi.fn(),
36
62
  eraseAllDeletedCoValues: opts.eraseAllDeletedCoValues || vi.fn(),
@@ -45,6 +71,15 @@ function createMockStorage(
45
71
  stopTrackingSyncState: opts.stopTrackingSyncState || vi.fn(),
46
72
  onCoValueUnmounted: opts.onCoValueUnmounted || vi.fn(),
47
73
  close: opts.close || vi.fn().mockResolvedValue(undefined),
74
+ tryAcquireStorageReconciliationLock:
75
+ opts.tryAcquireStorageReconciliationLock ||
76
+ vi.fn((_sessionId, _peerId, callback) =>
77
+ callback({ acquired: false as const, reason: "not_due" as const }),
78
+ ),
79
+ renewStorageReconciliationLock:
80
+ opts.renewStorageReconciliationLock || vi.fn(),
81
+ releaseStorageReconciliationLock:
82
+ opts.releaseStorageReconciliationLock || vi.fn(),
48
83
  };
49
84
  }
50
85
 
@@ -60,6 +60,10 @@ export function toSimplifiedMessages(
60
60
  return `${from} -> ${to} | GET_KNOWN_STATE ${getCoValue(msg.id)}`;
61
61
  case "lazyLoadResult":
62
62
  return `${from} -> ${to} | GET_KNOWN_STATE_RESULT ${getCoValue(msg.id)} sessions: ${simplifySessions(msg)}`;
63
+ case "reconcile":
64
+ return `${from} -> ${to} | RECONCILE`;
65
+ case "reconcile-ack":
66
+ return `${from} -> ${to} | RECONCILE_ACK`;
63
67
  }
64
68
  }
65
69