cojson 0.8.37 → 0.8.39

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 (50) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/native/PeerState.js +11 -2
  3. package/dist/native/PeerState.js.map +1 -1
  4. package/dist/native/PriorityBasedMessageQueue.js +20 -11
  5. package/dist/native/PriorityBasedMessageQueue.js.map +1 -1
  6. package/dist/native/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
  7. package/dist/native/SyncStateManager.js.map +1 -0
  8. package/dist/native/coValueCore.js +3 -0
  9. package/dist/native/coValueCore.js.map +1 -1
  10. package/dist/native/coValues/coMap.js +2 -3
  11. package/dist/native/coValues/coMap.js.map +1 -1
  12. package/dist/native/coValues/group.js +9 -3
  13. package/dist/native/coValues/group.js.map +1 -1
  14. package/dist/native/exports.js.map +1 -1
  15. package/dist/native/sync.js +34 -10
  16. package/dist/native/sync.js.map +1 -1
  17. package/dist/web/PeerState.js +11 -2
  18. package/dist/web/PeerState.js.map +1 -1
  19. package/dist/web/PriorityBasedMessageQueue.js +20 -11
  20. package/dist/web/PriorityBasedMessageQueue.js.map +1 -1
  21. package/dist/web/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
  22. package/dist/web/SyncStateManager.js.map +1 -0
  23. package/dist/web/coValueCore.js +3 -0
  24. package/dist/web/coValueCore.js.map +1 -1
  25. package/dist/web/coValues/coMap.js +2 -3
  26. package/dist/web/coValues/coMap.js.map +1 -1
  27. package/dist/web/coValues/group.js +9 -3
  28. package/dist/web/coValues/group.js.map +1 -1
  29. package/dist/web/exports.js.map +1 -1
  30. package/dist/web/sync.js +34 -10
  31. package/dist/web/sync.js.map +1 -1
  32. package/package.json +5 -1
  33. package/src/PeerState.ts +12 -2
  34. package/src/PriorityBasedMessageQueue.ts +24 -12
  35. package/src/{SyncStateSubscriptionManager.ts → SyncStateManager.ts} +48 -35
  36. package/src/coValueCore.ts +6 -0
  37. package/src/coValues/coMap.ts +2 -3
  38. package/src/coValues/group.ts +11 -3
  39. package/src/exports.ts +2 -1
  40. package/src/sync.ts +57 -23
  41. package/src/tests/PeerState.test.ts +49 -0
  42. package/src/tests/PriorityBasedMessageQueue.test.ts +43 -4
  43. package/src/tests/{SyncStateSubscriptionManager.test.ts → SyncStateManager.test.ts} +109 -25
  44. package/src/tests/coMap.test.ts +4 -13
  45. package/src/tests/group.test.ts +6 -11
  46. package/src/tests/permissions.test.ts +45 -15
  47. package/src/tests/sync.test.ts +112 -71
  48. package/src/tests/testUtils.ts +128 -11
  49. package/dist/native/SyncStateSubscriptionManager.js.map +0 -1
  50. package/dist/web/SyncStateSubscriptionManager.js.map +0 -1
@@ -188,4 +188,53 @@ describe("PeerState", () => {
188
188
  expect(knownStatesSpy).toHaveBeenCalledWith(action);
189
189
  expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
190
190
  });
191
+
192
+ test("should use same reference for knownStates and optimisticKnownStates for storage peers", () => {
193
+ const mockStoragePeer: Peer = {
194
+ id: "test-storage-peer",
195
+ role: "storage",
196
+ priority: 1,
197
+ crashOnClose: false,
198
+ incoming: (async function* () {})(),
199
+ outgoing: {
200
+ push: vi.fn().mockResolvedValue(undefined),
201
+ close: vi.fn(),
202
+ },
203
+ };
204
+ const peerState = new PeerState(mockStoragePeer, undefined);
205
+
206
+ // Verify they are the same reference
207
+ expect(peerState.knownStates).toBe(peerState.optimisticKnownStates);
208
+
209
+ // Verify that dispatching only updates one state
210
+ const knownStatesSpy = vi.spyOn(peerState.knownStates, "dispatch");
211
+ const optimisticKnownStatesSpy = vi.spyOn(
212
+ peerState.optimisticKnownStates,
213
+ "dispatch",
214
+ );
215
+
216
+ const action: PeerKnownStateActions = {
217
+ type: "SET",
218
+ id: "co_z1",
219
+ value: {
220
+ id: "co_z1",
221
+ header: false,
222
+ sessions: {},
223
+ },
224
+ };
225
+ peerState.dispatchToKnownStates(action);
226
+
227
+ // Only one dispatch should happen since they're the same reference
228
+ expect(knownStatesSpy).toHaveBeenCalledTimes(1);
229
+ expect(knownStatesSpy).toHaveBeenCalledWith(action);
230
+ expect(optimisticKnownStatesSpy).toHaveBeenCalledTimes(1);
231
+ expect(optimisticKnownStatesSpy).toHaveBeenCalledWith(action);
232
+ });
233
+
234
+ test("should use separate references for knownStates and optimisticKnownStates for non-storage peers", () => {
235
+ const { peerState } = setup(); // Uses a regular peer
236
+
237
+ // Verify they are different references
238
+ expect(peerState.knownStates).not.toBe(peerState.optimisticKnownStates);
239
+ });
191
240
  });
@@ -1,14 +1,23 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { afterEach, describe, expect, test } from "vitest";
2
2
  import { PriorityBasedMessageQueue } from "../PriorityBasedMessageQueue.js";
3
3
  import { CO_VALUE_PRIORITY } from "../priority.js";
4
- import { SyncMessage } from "../sync.js";
4
+ import type { SyncMessage } from "../sync.js";
5
+ import {
6
+ createTestMetricReader,
7
+ tearDownTestMetricReader,
8
+ } from "./testUtils.js";
5
9
 
6
10
  function setup() {
11
+ const metricReader = createTestMetricReader();
7
12
  const queue = new PriorityBasedMessageQueue(CO_VALUE_PRIORITY.MEDIUM);
8
- return { queue };
13
+ return { queue, metricReader };
9
14
  }
10
15
 
11
16
  describe("PriorityBasedMessageQueue", () => {
17
+ afterEach(() => {
18
+ tearDownTestMetricReader();
19
+ });
20
+
12
21
  test("should initialize with correct properties", () => {
13
22
  const { queue } = setup();
14
23
  expect(queue["defaultPriority"]).toBe(CO_VALUE_PRIORITY.MEDIUM);
@@ -43,7 +52,7 @@ describe("PriorityBasedMessageQueue", () => {
43
52
  });
44
53
 
45
54
  test("should pull messages in priority order", async () => {
46
- const { queue } = setup();
55
+ const { queue, metricReader } = setup();
47
56
  const lowPriorityMsg: SyncMessage = {
48
57
  action: "content",
49
58
  id: "co_zlow",
@@ -64,12 +73,42 @@ describe("PriorityBasedMessageQueue", () => {
64
73
  };
65
74
 
66
75
  void queue.push(lowPriorityMsg);
76
+ expect(
77
+ await metricReader.getMetricValue("jazz.messagequeue.size", {
78
+ priority: lowPriorityMsg.priority,
79
+ }),
80
+ ).toBe(1);
67
81
  void queue.push(mediumPriorityMsg);
82
+ expect(
83
+ await metricReader.getMetricValue("jazz.messagequeue.size", {
84
+ priority: mediumPriorityMsg.priority,
85
+ }),
86
+ ).toBe(1);
68
87
  void queue.push(highPriorityMsg);
88
+ expect(
89
+ await metricReader.getMetricValue("jazz.messagequeue.size", {
90
+ priority: highPriorityMsg.priority,
91
+ }),
92
+ ).toBe(1);
69
93
 
70
94
  expect(queue.pull()?.msg).toEqual(highPriorityMsg);
95
+ expect(
96
+ await metricReader.getMetricValue("jazz.messagequeue.size", {
97
+ priority: highPriorityMsg.priority,
98
+ }),
99
+ ).toBe(0);
71
100
  expect(queue.pull()?.msg).toEqual(mediumPriorityMsg);
101
+ expect(
102
+ await metricReader.getMetricValue("jazz.messagequeue.size", {
103
+ priority: mediumPriorityMsg.priority,
104
+ }),
105
+ ).toBe(0);
72
106
  expect(queue.pull()?.msg).toEqual(lowPriorityMsg);
107
+ expect(
108
+ await metricReader.getMetricValue("jazz.messagequeue.size", {
109
+ priority: lowPriorityMsg.priority,
110
+ }),
111
+ ).toBe(0);
73
112
  });
74
113
 
75
114
  test("should return undefined when pulling from empty queue", () => {
@@ -2,12 +2,19 @@ import { describe, expect, onTestFinished, test, vi } from "vitest";
2
2
  import {
3
3
  GlobalSyncStateListenerCallback,
4
4
  PeerSyncStateListenerCallback,
5
- } from "../SyncStateSubscriptionManager.js";
5
+ } from "../SyncStateManager.js";
6
+ import { accountHeaderForInitialAgentSecret } from "../coValues/account.js";
6
7
  import { connectedPeers } from "../streamUtils.js";
7
8
  import { emptyKnownState } from "../sync.js";
8
- import { createTestNode, waitFor } from "./testUtils.js";
9
-
10
- describe("SyncStateSubscriptionManager", () => {
9
+ import {
10
+ blockMessageTypeOnOutgoingPeer,
11
+ createTestNode,
12
+ createTwoConnectedNodes,
13
+ loadCoValueOrFail,
14
+ waitFor,
15
+ } from "./testUtils.js";
16
+
17
+ describe("SyncStateManager", () => {
11
18
  test("subscribeToUpdates receives updates when peer state changes", async () => {
12
19
  // Setup nodes
13
20
  const client = createTestNode();
@@ -31,7 +38,7 @@ describe("SyncStateSubscriptionManager", () => {
31
38
  client.syncManager.addPeer(jazzCloudAsPeer);
32
39
  jazzCloud.syncManager.addPeer(clientAsPeer);
33
40
 
34
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
41
+ const subscriptionManager = client.syncManager.syncState;
35
42
 
36
43
  const updateSpy: GlobalSyncStateListenerCallback = vi.fn();
37
44
  const unsubscribe = subscriptionManager.subscribeToUpdates(updateSpy);
@@ -41,14 +48,14 @@ describe("SyncStateSubscriptionManager", () => {
41
48
  expect(updateSpy).toHaveBeenCalledWith(
42
49
  "jazzCloudConnection",
43
50
  emptyKnownState(map.core.id),
44
- { isUploaded: false },
51
+ { uploaded: false },
45
52
  );
46
53
 
47
54
  await waitFor(() => {
48
- return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
55
+ return subscriptionManager.getCurrentSyncState(
49
56
  "jazzCloudConnection",
50
57
  map.core.id,
51
- );
58
+ ).uploaded;
52
59
  });
53
60
 
54
61
  expect(updateSpy).toHaveBeenCalledWith(
@@ -56,7 +63,7 @@ describe("SyncStateSubscriptionManager", () => {
56
63
  client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
57
64
  map.core.id,
58
65
  )!,
59
- { isUploaded: true },
66
+ { uploaded: true },
60
67
  );
61
68
 
62
69
  // Cleanup
@@ -92,7 +99,7 @@ describe("SyncStateSubscriptionManager", () => {
92
99
  client.syncManager.addPeer(clientStoragePeer);
93
100
  jazzCloud.syncManager.addPeer(clientAsPeer);
94
101
 
95
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
102
+ const subscriptionManager = client.syncManager.syncState;
96
103
 
97
104
  const updateToJazzCloudSpy: PeerSyncStateListenerCallback = vi.fn();
98
105
  const updateToStorageSpy: PeerSyncStateListenerCallback = vi.fn();
@@ -114,26 +121,26 @@ describe("SyncStateSubscriptionManager", () => {
114
121
 
115
122
  expect(updateToJazzCloudSpy).toHaveBeenCalledWith(
116
123
  emptyKnownState(map.core.id),
117
- { isUploaded: false },
124
+ { uploaded: false },
118
125
  );
119
126
 
120
127
  await waitFor(() => {
121
- return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
128
+ return subscriptionManager.getCurrentSyncState(
122
129
  "jazzCloudConnection",
123
130
  map.core.id,
124
- );
131
+ ).uploaded;
125
132
  });
126
133
 
127
134
  expect(updateToJazzCloudSpy).toHaveBeenLastCalledWith(
128
135
  client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
129
136
  map.core.id,
130
137
  )!,
131
- { isUploaded: true },
138
+ { uploaded: true },
132
139
  );
133
140
 
134
141
  expect(updateToStorageSpy).toHaveBeenLastCalledWith(
135
142
  emptyKnownState(map.core.id),
136
- { isUploaded: false },
143
+ { uploaded: false },
137
144
  );
138
145
  });
139
146
 
@@ -162,27 +169,27 @@ describe("SyncStateSubscriptionManager", () => {
162
169
 
163
170
  await client.syncManager.actuallySyncCoValue(map.core);
164
171
 
165
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
172
+ const subscriptionManager = client.syncManager.syncState;
166
173
 
167
174
  expect(
168
- subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
175
+ subscriptionManager.getCurrentSyncState(
169
176
  "jazzCloudConnection",
170
177
  map.core.id,
171
- ),
178
+ ).uploaded,
172
179
  ).toBe(false);
173
180
 
174
181
  await waitFor(() => {
175
- return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
182
+ return subscriptionManager.getCurrentSyncState(
176
183
  "jazzCloudConnection",
177
184
  map.core.id,
178
- );
185
+ ).uploaded;
179
186
  });
180
187
 
181
188
  expect(
182
- subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
189
+ subscriptionManager.getCurrentSyncState(
183
190
  "jazzCloudConnection",
184
191
  map.core.id,
185
- ),
192
+ ).uploaded,
186
193
  ).toBe(true);
187
194
  });
188
195
 
@@ -209,7 +216,7 @@ describe("SyncStateSubscriptionManager", () => {
209
216
  client.syncManager.addPeer(jazzCloudAsPeer);
210
217
  jazzCloud.syncManager.addPeer(clientAsPeer);
211
218
 
212
- const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
219
+ const subscriptionManager = client.syncManager.syncState;
213
220
  const anyUpdateSpy = vi.fn();
214
221
  const unsubscribe1 = subscriptionManager.subscribeToUpdates(anyUpdateSpy);
215
222
  const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
@@ -225,12 +232,89 @@ describe("SyncStateSubscriptionManager", () => {
225
232
  anyUpdateSpy.mockClear();
226
233
 
227
234
  await waitFor(() => {
228
- return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
235
+ return subscriptionManager.getCurrentSyncState(
229
236
  "jazzCloudConnection",
230
237
  map.core.id,
231
- );
238
+ ).uploaded;
232
239
  });
233
240
 
234
241
  expect(anyUpdateSpy).not.toHaveBeenCalled();
235
242
  });
243
+
244
+ test("getCurrentSyncState should return the correct state", async () => {
245
+ // Setup nodes
246
+ const {
247
+ node1: clientNode,
248
+ node2: serverNode,
249
+ node1ToNode2Peer: clientToServerPeer,
250
+ node2ToNode1Peer: serverToClientPeer,
251
+ } = await createTwoConnectedNodes("client", "server");
252
+
253
+ // Create test data
254
+ const group = clientNode.node.createGroup();
255
+ const map = group.createMap();
256
+ map.set("key1", "value1", "trusting");
257
+ group.addMember("everyone", "writer");
258
+
259
+ // Initially should not be synced
260
+ expect(
261
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
262
+ clientToServerPeer.id,
263
+ map.core.id,
264
+ ),
265
+ ).toEqual({ uploaded: false });
266
+
267
+ // Wait for full sync
268
+ await map.core.waitForSync();
269
+
270
+ expect(
271
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
272
+ clientToServerPeer.id,
273
+ map.core.id,
274
+ ),
275
+ ).toEqual({ uploaded: true });
276
+
277
+ const mapOnServer = await loadCoValueOrFail(serverNode.node, map.id);
278
+
279
+ // Block the content messages so the client won't fully sync immediately
280
+ const outgoing = blockMessageTypeOnOutgoingPeer(
281
+ serverToClientPeer,
282
+ "content",
283
+ );
284
+
285
+ mapOnServer.set("key2", "value2", "trusting");
286
+
287
+ expect(
288
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
289
+ clientToServerPeer.id,
290
+ map.core.id,
291
+ ),
292
+ ).toEqual({ uploaded: true });
293
+
294
+ expect(
295
+ serverNode.node.syncManager.syncState.getCurrentSyncState(
296
+ serverToClientPeer.id,
297
+ map.core.id,
298
+ ),
299
+ ).toEqual({ uploaded: false });
300
+
301
+ await outgoing.sendBlockedMessages();
302
+ outgoing.unblock();
303
+
304
+ await mapOnServer.core.waitForSync();
305
+
306
+ expect(
307
+ clientNode.node.syncManager.syncState.getCurrentSyncState(
308
+ clientToServerPeer.id,
309
+ map.core.id,
310
+ ),
311
+ ).toEqual({ uploaded: true });
312
+
313
+ expect(
314
+ serverNode.node.syncManager.syncState.getCurrentSyncState(
315
+ serverToClientPeer.id,
316
+ map.core.id,
317
+ ),
318
+ ).toEqual({ uploaded: true });
319
+ });
236
320
  });
@@ -4,7 +4,7 @@ import { operationToEditEntry } from "../coValues/coMap.js";
4
4
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
5
5
  import { LocalNode } from "../localNode.js";
6
6
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
7
- import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
7
+ import { hotSleep, randomAnonymousAccountAndSessionID } from "./testUtils.js";
8
8
 
9
9
  const Crypto = await WasmCrypto.create();
10
10
 
@@ -63,20 +63,11 @@ test("Can get CoMap entry values at different points in time", () => {
63
63
 
64
64
  expect(content.type).toEqual("comap");
65
65
 
66
- const beforeA = Date.now();
67
- while (Date.now() < beforeA + 10) {
68
- /* hot sleep */
69
- }
66
+ const beforeA = hotSleep(10);
70
67
  content.set("hello", "A", "trusting");
71
- const beforeB = Date.now();
72
- while (Date.now() < beforeB + 10) {
73
- /* hot sleep */
74
- }
68
+ const beforeB = hotSleep(10);
75
69
  content.set("hello", "B", "trusting");
76
- const beforeC = Date.now();
77
- while (Date.now() < beforeC + 10) {
78
- /* hot sleep */
79
- }
70
+ const beforeC = hotSleep(10);
80
71
  content.set("hello", "C", "trusting");
81
72
  expect(content.get("hello")).toEqual("C");
82
73
  expect(content.atTime(Date.now()).get("hello")).toEqual("C");
@@ -7,10 +7,8 @@ import { WasmCrypto } from "../crypto/WasmCrypto.js";
7
7
  import { LocalNode } from "../localNode.js";
8
8
  import {
9
9
  createThreeConnectedNodes,
10
- createTwoConnectedNodes,
11
10
  loadCoValueOrFail,
12
11
  randomAnonymousAccountAndSessionID,
13
- waitFor,
14
12
  } from "./testUtils.js";
15
13
 
16
14
  const Crypto = await WasmCrypto.create();
@@ -89,12 +87,12 @@ test("Remove a member from a group where the admin role is inherited", async ()
89
87
  // The reader should be automatically kicked out of the child group
90
88
  await group.removeMember(node3.account);
91
89
 
92
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
90
+ await group.core.waitForSync();
93
91
 
94
92
  // Update the map to check that node3 can't read updates anymore
95
93
  map.set("test", "Hidden to node3");
96
94
 
97
- await node2.syncManager.waitForUploadIntoPeer(node2ToNode3Peer.id, map.id);
95
+ await map.core.waitForSync();
98
96
 
99
97
  // Check that the value has not been updated on node3
100
98
  expect(mapOnNode3.get("test")).toEqual("Available to everyone");
@@ -121,16 +119,13 @@ test("An admin should be able to rotate the readKey on child groups and keep acc
121
119
  const childGroup = node2.createGroup();
122
120
  childGroup.extend(groupOnNode2);
123
121
 
124
- await node2.syncManager.waitForUploadIntoPeer(
125
- node2ToNode1Peer.id,
126
- childGroup.id,
127
- );
122
+ await childGroup.core.waitForSync();
128
123
 
129
124
  // The node1 account removes the reader from the group
130
125
  // In this case we want to ensure that node1 is still able to read new coValues
131
126
  // Even if some childs are not available when the readKey is rotated
132
127
  await group.removeMember(node3.account);
133
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
128
+ await group.core.waitForSync();
134
129
 
135
130
  const map = childGroup.createMap();
136
131
  map.set("test", "Available to node1");
@@ -160,7 +155,7 @@ test("An admin should be able to rotate the readKey on child groups even if it w
160
155
  // In this case we want to ensure that node1 is still able to read new coValues
161
156
  // Even if some childs are not available when the readKey is rotated
162
157
  await group.removeMember(node3.account);
163
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
158
+ await group.core.waitForSync();
164
159
 
165
160
  const map = childGroup.createMap();
166
161
  map.set("test", "Available to node1");
@@ -195,7 +190,7 @@ test("An admin should be able to rotate the readKey on child groups even if it w
195
190
  // In this case we want to ensure that node1 is still able to read new coValues
196
191
  // Even if some childs are not available when the readKey is rotated
197
192
  await group.removeMember(node3.account);
198
- await node1.syncManager.waitForUploadIntoPeer(node1ToNode2Peer.id, group.id);
193
+ await group.core.waitForSync();
199
194
 
200
195
  const map = childGroup.createMap();
201
196
  map.set("test", "Available to node1");
@@ -7,6 +7,8 @@ import {
7
7
  createTwoConnectedNodes,
8
8
  groupWithTwoAdmins,
9
9
  groupWithTwoAdminsHighLevel,
10
+ hotSleep,
11
+ loadCoValueOrFail,
10
12
  newGroup,
11
13
  newGroupHighLevel,
12
14
  } from "./testUtils.js";
@@ -1802,29 +1804,25 @@ test("Admins can set child extensions", () => {
1802
1804
  });
1803
1805
 
1804
1806
  test("Admins can set child extensions when the admin role is inherited", async () => {
1805
- const { node1, node2 } = createTwoConnectedNodes("server", "server");
1807
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
1806
1808
 
1807
- const node2Account = node2.account;
1808
- const group = node1.createGroup();
1809
+ const node2AccountOnNode1 = await loadCoValueOrFail(
1810
+ node1.node,
1811
+ node2.accountID,
1812
+ );
1809
1813
 
1810
- group.addMember(node2Account, "admin");
1814
+ const group = node1.node.createGroup();
1811
1815
 
1812
- const groupOnNode2 = await node2.load(group.id);
1816
+ group.addMember(node2AccountOnNode1, "admin");
1813
1817
 
1814
- if (groupOnNode2 === "unavailable") {
1815
- throw new Error("Group not found on node2");
1816
- }
1818
+ const groupOnNode2 = await loadCoValueOrFail(node2.node, group.id);
1817
1819
 
1818
- const childGroup = node2.createGroup();
1820
+ const childGroup = node2.node.createGroup();
1819
1821
  childGroup.extend(groupOnNode2);
1820
1822
 
1821
- const childGroupOnNode1 = await node1.load(childGroup.id);
1823
+ const childGroupOnNode1 = await loadCoValueOrFail(node1.node, childGroup.id);
1822
1824
 
1823
- if (childGroupOnNode1 === "unavailable") {
1824
- throw new Error("Child group not found on node1");
1825
- }
1826
-
1827
- const grandChildGroup = node2.createGroup();
1825
+ const grandChildGroup = node2.node.createGroup();
1828
1826
  grandChildGroup.extend(childGroupOnNode1);
1829
1827
 
1830
1828
  expect(childGroupOnNode1.get(`child_${grandChildGroup.id}`)).toEqual(
@@ -2315,6 +2313,38 @@ test("When rotating the key of a parent group, the keys of all child groups are
2315
2313
  expect(newChildReadKeyID).not.toEqual(currentChildReadKeyID);
2316
2314
  });
2317
2315
 
2316
+ test("When rotating the key of a parent group, the old transactions should still be valid", async () => {
2317
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
2318
+
2319
+ const group = node1.node.createGroup();
2320
+ const parentGroup = node1.node.createGroup();
2321
+
2322
+ group.extend(parentGroup);
2323
+
2324
+ const node2AccountOnNode1 = await loadCoValueOrFail(
2325
+ node1.node,
2326
+ node2.accountID,
2327
+ );
2328
+
2329
+ parentGroup.addMember(node2AccountOnNode1, "writer");
2330
+
2331
+ const map = group.createMap();
2332
+ map.set("from", "node1", "private");
2333
+
2334
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
2335
+ mapOnNode2.set("from", "node2", "private");
2336
+
2337
+ await new Promise((resolve) => setTimeout(resolve, 10));
2338
+
2339
+ parentGroup.removeMember(node2AccountOnNode1);
2340
+
2341
+ await new Promise((resolve) => setTimeout(resolve, 10));
2342
+
2343
+ const mapOnNode1 = await loadCoValueOrFail(node1.node, map.id);
2344
+
2345
+ expect(mapOnNode1.get("from")).toEqual("node2");
2346
+ });
2347
+
2318
2348
  test("When rotating the key of a grand-parent group, the keys of all child and grand-child groups are also rotated", () => {
2319
2349
  const { group, node } = newGroupHighLevel();
2320
2350
  const grandParentGroup = node.createGroup();