cojson 0.8.19 → 0.8.23

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 (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/native/CoValuesStore.js +31 -0
  3. package/dist/native/CoValuesStore.js.map +1 -0
  4. package/dist/native/PeerState.js +7 -0
  5. package/dist/native/PeerState.js.map +1 -1
  6. package/dist/native/SyncStateSubscriptionManager.js +2 -2
  7. package/dist/native/SyncStateSubscriptionManager.js.map +1 -1
  8. package/dist/native/coValueState.js +175 -27
  9. package/dist/native/coValueState.js.map +1 -1
  10. package/dist/native/localNode.js +20 -41
  11. package/dist/native/localNode.js.map +1 -1
  12. package/dist/native/sync.js +49 -93
  13. package/dist/native/sync.js.map +1 -1
  14. package/dist/web/CoValuesStore.js +31 -0
  15. package/dist/web/CoValuesStore.js.map +1 -0
  16. package/dist/web/PeerState.js +7 -0
  17. package/dist/web/PeerState.js.map +1 -1
  18. package/dist/web/SyncStateSubscriptionManager.js +2 -2
  19. package/dist/web/SyncStateSubscriptionManager.js.map +1 -1
  20. package/dist/web/coValueState.js +175 -27
  21. package/dist/web/coValueState.js.map +1 -1
  22. package/dist/web/localNode.js +20 -41
  23. package/dist/web/localNode.js.map +1 -1
  24. package/dist/web/sync.js +49 -93
  25. package/dist/web/sync.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/CoValuesStore.ts +38 -0
  28. package/src/PeerKnownStates.ts +1 -1
  29. package/src/PeerState.ts +10 -1
  30. package/src/SyncStateSubscriptionManager.ts +2 -2
  31. package/src/coValueState.ts +253 -42
  32. package/src/localNode.ts +28 -56
  33. package/src/sync.ts +64 -116
  34. package/src/tests/PeerState.test.ts +44 -1
  35. package/src/tests/coValueState.test.ts +362 -0
  36. package/src/tests/group.test.ts +1 -1
  37. package/src/tests/sync.test.ts +140 -19
@@ -0,0 +1,362 @@
1
+ import { describe, expect, test, vi } from "vitest";
2
+ import { PeerState } from "../PeerState";
3
+ import { CoValueCore } from "../coValueCore";
4
+ import { CO_VALUE_LOADING_MAX_RETRIES, CoValueState } from "../coValueState";
5
+ import { RawCoID } from "../ids";
6
+ import { Peer } from "../sync";
7
+
8
+ describe("CoValueState", () => {
9
+ const mockCoValueId = "co_test123" as RawCoID;
10
+
11
+ test("should create unknown state", () => {
12
+ const state = CoValueState.Unknown(mockCoValueId);
13
+
14
+ expect(state.id).toBe(mockCoValueId);
15
+ expect(state.state.type).toBe("unknown");
16
+ });
17
+
18
+ test("should create loading state", () => {
19
+ const peerIds = ["peer1", "peer2"];
20
+ const state = CoValueState.Loading(mockCoValueId, peerIds);
21
+
22
+ expect(state.id).toBe(mockCoValueId);
23
+ expect(state.state.type).toBe("loading");
24
+ });
25
+
26
+ test("should create available state", async () => {
27
+ const mockCoValue = { id: mockCoValueId } as CoValueCore;
28
+ const state = CoValueState.Available(mockCoValue);
29
+
30
+ expect(state.id).toBe(mockCoValueId);
31
+ expect(state.state.type).toBe("available");
32
+ expect((state.state as any).coValue).toBe(mockCoValue);
33
+ await expect(state.getCoValue()).resolves.toEqual(mockCoValue);
34
+ });
35
+
36
+ test("should handle found action", async () => {
37
+ const mockCoValue = { id: mockCoValueId } as CoValueCore;
38
+ const state = CoValueState.Loading(mockCoValueId, ["peer1", "peer2"]);
39
+
40
+ const stateValuePromise = state.getCoValue();
41
+
42
+ state.dispatch({
43
+ type: "available",
44
+ coValue: mockCoValue,
45
+ });
46
+
47
+ const result = await state.getCoValue();
48
+ expect(result).toBe(mockCoValue);
49
+ await expect(stateValuePromise).resolves.toBe(mockCoValue);
50
+ });
51
+
52
+ test("should ignore actions when not in loading state", () => {
53
+ const state = CoValueState.Unknown(mockCoValueId);
54
+
55
+ state.dispatch({
56
+ type: "not-found-in-peer",
57
+ peerId: "peer1",
58
+ });
59
+
60
+ expect(state.state.type).toBe("unknown");
61
+ });
62
+
63
+ test("should retry loading from peers when unsuccessful", async () => {
64
+ vi.useFakeTimers();
65
+
66
+ const peer1 = createMockPeerState(
67
+ {
68
+ id: "peer1",
69
+ role: "server",
70
+ },
71
+ async () => {
72
+ state.dispatch({
73
+ type: "not-found-in-peer",
74
+ peerId: "peer1",
75
+ });
76
+ },
77
+ );
78
+ const peer2 = createMockPeerState(
79
+ {
80
+ id: "peer2",
81
+ role: "server",
82
+ },
83
+ async () => {
84
+ state.dispatch({
85
+ type: "not-found-in-peer",
86
+ peerId: "peer2",
87
+ });
88
+ },
89
+ );
90
+ const mockPeers = [peer1, peer2] as unknown as PeerState[];
91
+
92
+ const state = CoValueState.Unknown(mockCoValueId);
93
+ const loadPromise = state.loadFromPeers(mockPeers);
94
+
95
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
96
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
97
+ await vi.runAllTimersAsync();
98
+ }
99
+
100
+ await loadPromise;
101
+
102
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(
103
+ CO_VALUE_LOADING_MAX_RETRIES,
104
+ );
105
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
106
+ CO_VALUE_LOADING_MAX_RETRIES,
107
+ );
108
+ expect(state.state.type).toBe("unavailable");
109
+ await expect(state.getCoValue()).resolves.toBe("unavailable");
110
+
111
+ vi.useRealTimers();
112
+ });
113
+
114
+ test("should skip errored coValues when loading from peers", async () => {
115
+ vi.useFakeTimers();
116
+
117
+ const peer1 = createMockPeerState(
118
+ {
119
+ id: "peer1",
120
+ role: "server",
121
+ },
122
+ async () => {
123
+ peer1.erroredCoValues.set(mockCoValueId, new Error("test") as any);
124
+ state.dispatch({
125
+ type: "not-found-in-peer",
126
+ peerId: "peer1",
127
+ });
128
+ },
129
+ );
130
+ const peer2 = createMockPeerState(
131
+ {
132
+ id: "peer2",
133
+ role: "server",
134
+ },
135
+ async () => {
136
+ state.dispatch({
137
+ type: "not-found-in-peer",
138
+ peerId: "peer2",
139
+ });
140
+ },
141
+ );
142
+
143
+ const mockPeers = [peer1, peer2] as unknown as PeerState[];
144
+
145
+ const state = CoValueState.Unknown(mockCoValueId);
146
+ const loadPromise = state.loadFromPeers(mockPeers);
147
+
148
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
149
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
150
+ await vi.runAllTimersAsync();
151
+ }
152
+
153
+ await loadPromise;
154
+
155
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(1);
156
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
157
+ CO_VALUE_LOADING_MAX_RETRIES,
158
+ );
159
+ expect(state.state.type).toBe("unavailable");
160
+ await expect(state.getCoValue()).resolves.toBe("unavailable");
161
+
162
+ vi.useRealTimers();
163
+ });
164
+
165
+ test("should retry only on server peers", async () => {
166
+ vi.useFakeTimers();
167
+
168
+ const peer1 = createMockPeerState(
169
+ {
170
+ id: "peer1",
171
+ role: "storage",
172
+ },
173
+ async () => {
174
+ state.dispatch({
175
+ type: "not-found-in-peer",
176
+ peerId: "peer1",
177
+ });
178
+ },
179
+ );
180
+ const peer2 = createMockPeerState(
181
+ {
182
+ id: "peer2",
183
+ role: "server",
184
+ },
185
+ async () => {
186
+ state.dispatch({
187
+ type: "not-found-in-peer",
188
+ peerId: "peer2",
189
+ });
190
+ },
191
+ );
192
+ const mockPeers = [peer1, peer2] as unknown as PeerState[];
193
+
194
+ const state = CoValueState.Unknown(mockCoValueId);
195
+ const loadPromise = state.loadFromPeers(mockPeers);
196
+
197
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
198
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
199
+ await vi.runAllTimersAsync();
200
+ }
201
+
202
+ await loadPromise;
203
+
204
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(1);
205
+ expect(peer2.pushOutgoingMessage).toHaveBeenCalledTimes(
206
+ CO_VALUE_LOADING_MAX_RETRIES,
207
+ );
208
+ expect(state.state.type).toBe("unavailable");
209
+ await expect(state.getCoValue()).resolves.toEqual("unavailable");
210
+
211
+ vi.useRealTimers();
212
+ });
213
+
214
+ test("should handle the coValues that become available in between of the retries", async () => {
215
+ vi.useFakeTimers();
216
+
217
+ let retries = 0;
218
+
219
+ const peer1 = createMockPeerState(
220
+ {
221
+ id: "peer1",
222
+ role: "server",
223
+ },
224
+ async () => {
225
+ retries++;
226
+ state.dispatch({
227
+ type: "not-found-in-peer",
228
+ peerId: "peer1",
229
+ });
230
+
231
+ if (retries === 2) {
232
+ setTimeout(() => {
233
+ state.dispatch({
234
+ type: "available",
235
+ coValue: { id: mockCoValueId } as CoValueCore,
236
+ });
237
+ }, 100);
238
+ }
239
+ },
240
+ );
241
+
242
+ const mockPeers = [peer1] as unknown as PeerState[];
243
+
244
+ const state = CoValueState.Unknown(mockCoValueId);
245
+ const loadPromise = state.loadFromPeers(mockPeers);
246
+
247
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
248
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES + 1; i++) {
249
+ await vi.runAllTimersAsync();
250
+ }
251
+
252
+ await loadPromise;
253
+
254
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(2);
255
+ expect(state.state.type).toBe("available");
256
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
257
+ vi.useRealTimers();
258
+ });
259
+
260
+ test("should have a coValue as value property when becomes available after that have been marked as unavailable", async () => {
261
+ vi.useFakeTimers();
262
+
263
+ const peer1 = createMockPeerState(
264
+ {
265
+ id: "peer1",
266
+ role: "server",
267
+ },
268
+ async () => {
269
+ state.dispatch({
270
+ type: "not-found-in-peer",
271
+ peerId: "peer1",
272
+ });
273
+ },
274
+ );
275
+
276
+ const mockPeers = [peer1] as unknown as PeerState[];
277
+
278
+ const state = CoValueState.Unknown(mockCoValueId);
279
+ const loadPromise = state.loadFromPeers(mockPeers);
280
+
281
+ // Should attempt CO_VALUE_LOADING_MAX_RETRIES retries
282
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
283
+ await vi.runAllTimersAsync();
284
+ }
285
+
286
+ state.dispatch({
287
+ type: "available",
288
+ coValue: { id: mockCoValueId } as CoValueCore,
289
+ });
290
+
291
+ await loadPromise;
292
+
293
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(5);
294
+ expect(state.state.type).toBe("available");
295
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
296
+
297
+ vi.useRealTimers();
298
+ });
299
+
300
+ test("should stop retrying when value becomes available", async () => {
301
+ vi.useFakeTimers();
302
+
303
+ let run = 1;
304
+
305
+ const peer1 = createMockPeerState(
306
+ {
307
+ id: "peer1",
308
+ role: "server",
309
+ },
310
+ async () => {
311
+ if (run > 2) {
312
+ state.dispatch({
313
+ type: "available",
314
+ coValue: { id: mockCoValueId } as CoValueCore,
315
+ });
316
+ }
317
+ state.dispatch({
318
+ type: "not-found-in-peer",
319
+ peerId: "peer1",
320
+ });
321
+ run++;
322
+ },
323
+ );
324
+
325
+ const mockPeers = [peer1] as unknown as PeerState[];
326
+
327
+ const state = CoValueState.Unknown(mockCoValueId);
328
+ const loadPromise = state.loadFromPeers(mockPeers);
329
+
330
+ for (let i = 0; i < CO_VALUE_LOADING_MAX_RETRIES; i++) {
331
+ await vi.runAllTimersAsync();
332
+ }
333
+ await loadPromise;
334
+
335
+ expect(peer1.pushOutgoingMessage).toHaveBeenCalledTimes(3);
336
+ expect(state.state.type).toBe("available");
337
+ await expect(state.getCoValue()).resolves.toEqual({ id: mockCoValueId });
338
+
339
+ vi.useRealTimers();
340
+ });
341
+ });
342
+
343
+ function createMockPeerState(
344
+ peer: Partial<Peer>,
345
+ pushFn = () => Promise.resolve(),
346
+ ) {
347
+ const peerState = new PeerState(
348
+ {
349
+ id: "peer",
350
+ role: "server",
351
+ outgoing: {
352
+ push: pushFn,
353
+ },
354
+ ...peer,
355
+ } as Peer,
356
+ undefined,
357
+ );
358
+
359
+ vi.spyOn(peerState, "pushOutgoingMessage").mockImplementation(pushFn);
360
+
361
+ return peerState;
362
+ }
@@ -42,7 +42,7 @@ test("Can create a CoStream in a group", () => {
42
42
  expect(stream instanceof RawCoStream).toEqual(true);
43
43
  });
44
44
 
45
- test("Can create a BinaryCoStream in a group", () => {
45
+ test("Can create a FileStream in a group", () => {
46
46
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
47
47
 
48
48
  const group = node.createGroup();
@@ -807,7 +807,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
807
807
  sessions: {},
808
808
  } satisfies SyncMessage);
809
809
 
810
- expect(node2.coValues[map.core.id]?.state).toEqual("loading");
810
+ expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("loading");
811
811
 
812
812
  await inTx2.push(mapNewContentMsg);
813
813
 
@@ -875,7 +875,7 @@ test("Can sync a coValue through a server to another client", async () => {
875
875
  {
876
876
  peer1role: "server",
877
877
  peer2role: "client",
878
- trace: true,
878
+ // trace: true,
879
879
  },
880
880
  );
881
881
 
@@ -894,7 +894,7 @@ test("Can sync a coValue through a server to another client", async () => {
894
894
  {
895
895
  peer1role: "server",
896
896
  peer2role: "client",
897
- trace: true,
897
+ // trace: true,
898
898
  },
899
899
  );
900
900
 
@@ -926,7 +926,7 @@ test("Can sync a coValue with private transactions through a server to another c
926
926
  const server = new LocalNode(serverUser, serverSession, Crypto);
927
927
 
928
928
  const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
929
- trace: true,
929
+ // trace: true,
930
930
  peer1role: "server",
931
931
  peer2role: "client",
932
932
  });
@@ -944,7 +944,7 @@ test("Can sync a coValue with private transactions through a server to another c
944
944
  "server",
945
945
  "client2",
946
946
  {
947
- trace: true,
947
+ // trace: true,
948
948
  peer1role: "server",
949
949
  peer2role: "client",
950
950
  },
@@ -1095,14 +1095,14 @@ test("If we start loading a coValue before connecting to a peer that has it, it
1095
1095
  const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
1096
1096
  peer1role: "server",
1097
1097
  peer2role: "client",
1098
- trace: true,
1098
+ // trace: true,
1099
1099
  });
1100
1100
 
1101
1101
  node1.syncManager.addPeer(node2asPeer);
1102
1102
 
1103
1103
  const mapOnNode2Promise = node2.loadCoValueCore(map.core.id);
1104
1104
 
1105
- expect(node2.coValues[map.core.id]?.state.type).toEqual("unknown");
1105
+ expect(node2.coValuesStore.get(map.core.id).state.type).toEqual("unknown");
1106
1106
 
1107
1107
  node2.syncManager.addPeer(node1asPeer);
1108
1108
 
@@ -1116,6 +1116,59 @@ test("If we start loading a coValue before connecting to a peer that has it, it
1116
1116
  );
1117
1117
  });
1118
1118
 
1119
+ test("should keep the peer state when the peer closes", async () => {
1120
+ const {
1121
+ client,
1122
+ jazzCloud,
1123
+ jazzCloudConnectionAsPeer,
1124
+ connectionWithClientAsPeer,
1125
+ } = createTwoConnectedNodes();
1126
+
1127
+ const group = jazzCloud.createGroup();
1128
+ const map = group.createMap();
1129
+ map.set("hello", "world", "trusting");
1130
+
1131
+ await client.loadCoValueCore(map.core.id);
1132
+
1133
+ const syncManager = client.syncManager;
1134
+ const peerState = syncManager.peers[jazzCloudConnectionAsPeer.id];
1135
+
1136
+ // @ts-expect-error Simulating a peer closing, leveraging the direct connection between the client/server peers
1137
+ await connectionWithClientAsPeer.outgoing.push("Disconnected");
1138
+
1139
+ await waitFor(() => peerState?.closed);
1140
+
1141
+ expect(syncManager.peers[jazzCloudConnectionAsPeer.id]).not.toBeUndefined();
1142
+ });
1143
+
1144
+ test("should delete the peer state when the peer closes if deletePeerStateOnClose is true", async () => {
1145
+ const {
1146
+ client,
1147
+ jazzCloud,
1148
+ jazzCloudConnectionAsPeer,
1149
+ connectionWithClientAsPeer,
1150
+ } = createTwoConnectedNodes();
1151
+
1152
+ jazzCloudConnectionAsPeer.deletePeerStateOnClose = true;
1153
+
1154
+ const group = jazzCloud.createGroup();
1155
+ const map = group.createMap();
1156
+ map.set("hello", "world", "trusting");
1157
+
1158
+ await client.loadCoValueCore(map.core.id);
1159
+
1160
+ const syncManager = client.syncManager;
1161
+
1162
+ const peerState = syncManager.peers[jazzCloudConnectionAsPeer.id];
1163
+
1164
+ // @ts-expect-error Simulating a peer closing, leveraging the direct connection between the client/server peers
1165
+ await connectionWithClientAsPeer.outgoing.push("Disconnected");
1166
+
1167
+ await waitFor(() => peerState?.closed);
1168
+
1169
+ expect(syncManager.peers[jazzCloudConnectionAsPeer.id]).toBeUndefined();
1170
+ });
1171
+
1119
1172
  describe("sync - extra tests", () => {
1120
1173
  test("Node handles disconnection and reconnection of a peer gracefully", async () => {
1121
1174
  // Create two nodes
@@ -1519,7 +1572,7 @@ describe("sync - extra tests", () => {
1519
1572
  {
1520
1573
  peer1role: "server",
1521
1574
  peer2role: "client",
1522
- trace: true,
1575
+ // trace: true,
1523
1576
  },
1524
1577
  );
1525
1578
 
@@ -1529,7 +1582,7 @@ describe("sync - extra tests", () => {
1529
1582
  {
1530
1583
  peer1role: "server",
1531
1584
  peer2role: "client",
1532
- trace: true,
1585
+ // trace: true,
1533
1586
  },
1534
1587
  );
1535
1588
 
@@ -1590,32 +1643,39 @@ function createTwoConnectedNodes() {
1590
1643
 
1591
1644
  describe("SyncManager - knownStates vs optimisticKnownStates", () => {
1592
1645
  test("knownStates and optimisticKnownStates are the same when the coValue is fully synced", async () => {
1593
- const { client } = createTwoConnectedNodes();
1646
+ const { client, jazzCloud } = createTwoConnectedNodes();
1594
1647
 
1595
1648
  // Create test data
1596
1649
  const group = client.createGroup();
1597
- const map = group.createMap();
1598
- map.set("key1", "value1", "trusting");
1650
+ const mapOnClient = group.createMap();
1651
+ mapOnClient.set("key1", "value1", "trusting");
1599
1652
 
1600
- await client.syncManager.actuallySyncCoValue(map.core);
1653
+ await client.syncManager.actuallySyncCoValue(mapOnClient.core);
1601
1654
 
1602
1655
  // Wait for the full sync to complete
1603
1656
  await waitFor(() => {
1604
1657
  return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
1605
1658
  "jazzCloudConnection",
1606
- map.core.id,
1659
+ mapOnClient.core.id,
1607
1660
  );
1608
1661
  });
1609
1662
 
1610
- const peerState = client.syncManager.peers["jazzCloudConnection"]!;
1663
+ const peerStateClient = client.syncManager.peers["jazzCloudConnection"]!;
1664
+ const peerStateJazzCloud =
1665
+ jazzCloud.syncManager.peers["connectionWithClient"]!;
1611
1666
 
1612
1667
  // The optimisticKnownStates should be the same as the knownStates after the full sync is complete
1613
- expect(peerState.optimisticKnownStates.get(map.core.id)).toEqual(
1614
- peerState.knownStates.get(map.core.id),
1615
- );
1668
+ expect(
1669
+ peerStateClient.optimisticKnownStates.get(mapOnClient.core.id),
1670
+ ).toEqual(peerStateClient.knownStates.get(mapOnClient.core.id));
1671
+
1672
+ // On the other node the knownStates should be updated correctly based on the messages we received
1673
+ expect(
1674
+ peerStateJazzCloud.optimisticKnownStates.get(mapOnClient.core.id),
1675
+ ).toEqual(peerStateJazzCloud.knownStates.get(mapOnClient.core.id));
1616
1676
  });
1617
1677
 
1618
- test("optimisticKnownStates is updated as new transactions are received, while knownStates only when the coValue is fully synced", async () => {
1678
+ test("optimisticKnownStates is updated as new transactions are sent, while knownStates only when the updates are acknowledged", async () => {
1619
1679
  const { client, jazzCloudConnectionAsPeer } = createTwoConnectedNodes();
1620
1680
 
1621
1681
  // Create test data and sync the first change
@@ -1814,6 +1874,63 @@ describe("SyncManager.addPeer", () => {
1814
1874
  });
1815
1875
  });
1816
1876
 
1877
+ describe("loadCoValueCore with retry", () => {
1878
+ test("should load the value if available on the server", async () => {
1879
+ const { client, jazzCloud } = createTwoConnectedNodes();
1880
+
1881
+ const anotherClient = createTestNode();
1882
+ const [
1883
+ connectionWithAnotherClientAsPeer,
1884
+ jazzCloudConnectionAsPeerForAnotherClient,
1885
+ ] = connectedPeers("connectionWithAnotherClient", "jazzCloudConnection", {
1886
+ peer1role: "client",
1887
+ peer2role: "server",
1888
+ });
1889
+
1890
+ jazzCloud.syncManager.addPeer(connectionWithAnotherClientAsPeer);
1891
+
1892
+ const group = anotherClient.createGroup();
1893
+ const map = group.createMap();
1894
+ map.set("key1", "value1", "trusting");
1895
+
1896
+ const promise = client.loadCoValueCore(map.id);
1897
+
1898
+ anotherClient.syncManager.addPeer(
1899
+ jazzCloudConnectionAsPeerForAnotherClient,
1900
+ );
1901
+ await expect(promise).resolves.not.toBe("unavailable");
1902
+ });
1903
+
1904
+ test("should handle correctly two subsequent loads", async () => {
1905
+ const { client, jazzCloud } = createTwoConnectedNodes();
1906
+
1907
+ const anotherClient = createTestNode();
1908
+ const [
1909
+ connectionWithAnotherClientAsPeer,
1910
+ jazzCloudConnectionAsPeerForAnotherClient,
1911
+ ] = connectedPeers("connectionWithAnotherClient", "jazzCloudConnection", {
1912
+ peer1role: "client",
1913
+ peer2role: "server",
1914
+ });
1915
+
1916
+ jazzCloud.syncManager.addPeer(connectionWithAnotherClientAsPeer);
1917
+
1918
+ const group = anotherClient.createGroup();
1919
+ const map = group.createMap();
1920
+ map.set("key1", "value1", "trusting");
1921
+
1922
+ const promise1 = client.loadCoValueCore(map.id);
1923
+ const promise2 = client.loadCoValueCore(map.id);
1924
+
1925
+ anotherClient.syncManager.addPeer(
1926
+ jazzCloudConnectionAsPeerForAnotherClient,
1927
+ );
1928
+
1929
+ await expect(promise1).resolves.not.toBe("unavailable");
1930
+ await expect(promise2).resolves.not.toBe("unavailable");
1931
+ });
1932
+ });
1933
+
1817
1934
  describe("waitForUploadIntoPeer", () => {
1818
1935
  test("should resolve when the coValue is fully uploaded into the peer", async () => {
1819
1936
  const { client, jazzCloudConnectionAsPeer: peer } =
@@ -1889,3 +2006,7 @@ function _admStateEx(adminID: RawAccountID) {
1889
2006
  id: adminID,
1890
2007
  };
1891
2008
  }
2009
+
2010
+ function sleep(ms: number) {
2011
+ return new Promise((resolve) => setTimeout(resolve, ms));
2012
+ }