cojson 0.8.16 → 0.8.17

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.
@@ -0,0 +1,232 @@
1
+ import { describe, expect, onTestFinished, test, vi } from "vitest";
2
+ import { connectedPeers } from "../streamUtils.js";
3
+ import { emptyKnownState } from "../sync.js";
4
+ import { createTestNode, waitFor } from "./testUtils.js";
5
+
6
+ describe("SyncStateSubscriptionManager", () => {
7
+ test("subscribeToUpdates receives updates when peer state changes", async () => {
8
+ // Setup nodes
9
+ const client = createTestNode();
10
+ const jazzCloud = createTestNode();
11
+
12
+ // Create test data
13
+ const group = client.createGroup();
14
+ const map = group.createMap();
15
+ map.set("key1", "value1", "trusting");
16
+
17
+ // Connect nodes
18
+ const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
19
+ "clientConnection",
20
+ "jazzCloudConnection",
21
+ {
22
+ peer1role: "client",
23
+ peer2role: "server",
24
+ },
25
+ );
26
+
27
+ client.syncManager.addPeer(jazzCloudAsPeer);
28
+ jazzCloud.syncManager.addPeer(clientAsPeer);
29
+
30
+ const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
31
+
32
+ const updateSpy = vi.fn();
33
+ const unsubscribe = subscriptionManager.subscribeToUpdates(updateSpy);
34
+
35
+ await client.syncManager.actuallySyncCoValue(map.core);
36
+
37
+ expect(updateSpy).toHaveBeenCalledWith(
38
+ "jazzCloudConnection",
39
+ emptyKnownState(map.core.id),
40
+ false,
41
+ );
42
+
43
+ await waitFor(() => {
44
+ return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
45
+ "jazzCloudConnection",
46
+ map.core.id,
47
+ );
48
+ });
49
+
50
+ expect(updateSpy).toHaveBeenCalledWith(
51
+ "jazzCloudConnection",
52
+ client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
53
+ map.core.id,
54
+ )!,
55
+ true,
56
+ );
57
+
58
+ // Cleanup
59
+ unsubscribe();
60
+ });
61
+
62
+ test("subscribeToPeerUpdates receives updates only for specific peer", async () => {
63
+ // Setup nodes
64
+ const client = createTestNode();
65
+ const jazzCloud = createTestNode();
66
+
67
+ // Create test data
68
+ const group = client.createGroup();
69
+ const map = group.createMap();
70
+ map.set("key1", "value1", "trusting");
71
+
72
+ // Connect nodes
73
+ const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
74
+ "clientConnection",
75
+ "jazzCloudConnection",
76
+ {
77
+ peer1role: "client",
78
+ peer2role: "server",
79
+ },
80
+ );
81
+
82
+ const [clientStoragePeer] = connectedPeers("clientStorage", "unusedPeer", {
83
+ peer1role: "client",
84
+ peer2role: "server",
85
+ });
86
+
87
+ client.syncManager.addPeer(jazzCloudAsPeer);
88
+ client.syncManager.addPeer(clientStoragePeer);
89
+ jazzCloud.syncManager.addPeer(clientAsPeer);
90
+
91
+ const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
92
+
93
+ const updateToJazzCloudSpy = vi.fn();
94
+ const updateToStorageSpy = vi.fn();
95
+ const unsubscribe1 = subscriptionManager.subscribeToPeerUpdates(
96
+ "jazzCloudConnection",
97
+ updateToJazzCloudSpy,
98
+ );
99
+ const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
100
+ "clientStorage",
101
+ updateToStorageSpy,
102
+ );
103
+
104
+ onTestFinished(() => {
105
+ unsubscribe1();
106
+ unsubscribe2();
107
+ });
108
+
109
+ await client.syncManager.actuallySyncCoValue(map.core);
110
+
111
+ expect(updateToJazzCloudSpy).toHaveBeenCalledWith(
112
+ emptyKnownState(map.core.id),
113
+ false,
114
+ );
115
+
116
+ await waitFor(() => {
117
+ return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
118
+ "jazzCloudConnection",
119
+ map.core.id,
120
+ );
121
+ });
122
+
123
+ expect(updateToJazzCloudSpy).toHaveBeenLastCalledWith(
124
+ client.syncManager.peers["jazzCloudConnection"]!.knownStates.get(
125
+ map.core.id,
126
+ )!,
127
+ true,
128
+ );
129
+
130
+ expect(updateToStorageSpy).toHaveBeenLastCalledWith(
131
+ emptyKnownState(map.core.id),
132
+ false,
133
+ );
134
+ });
135
+
136
+ test("getIsCoValueFullyUploadedIntoPeer returns correct status", async () => {
137
+ // Setup nodes
138
+ const client = createTestNode();
139
+ const jazzCloud = createTestNode();
140
+
141
+ // Create test data
142
+ const group = client.createGroup();
143
+ const map = group.createMap();
144
+ map.set("key1", "value1", "trusting");
145
+
146
+ // Connect nodes
147
+ const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
148
+ "clientConnection",
149
+ "jazzCloudConnection",
150
+ {
151
+ peer1role: "client",
152
+ peer2role: "server",
153
+ },
154
+ );
155
+
156
+ client.syncManager.addPeer(jazzCloudAsPeer);
157
+ jazzCloud.syncManager.addPeer(clientAsPeer);
158
+
159
+ await client.syncManager.actuallySyncCoValue(map.core);
160
+
161
+ const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
162
+
163
+ expect(
164
+ subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
165
+ "jazzCloudConnection",
166
+ map.core.id,
167
+ ),
168
+ ).toBe(false);
169
+
170
+ await waitFor(() => {
171
+ return subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
172
+ "jazzCloudConnection",
173
+ map.core.id,
174
+ );
175
+ });
176
+
177
+ expect(
178
+ subscriptionManager.getIsCoValueFullyUploadedIntoPeer(
179
+ "jazzCloudConnection",
180
+ map.core.id,
181
+ ),
182
+ ).toBe(true);
183
+ });
184
+
185
+ test("unsubscribe stops receiving updates", async () => {
186
+ // Setup nodes
187
+ const client = createTestNode();
188
+ const jazzCloud = createTestNode();
189
+
190
+ // Create test data
191
+ const group = client.createGroup();
192
+ const map = group.createMap();
193
+ map.set("key1", "value1", "trusting");
194
+
195
+ // Connect nodes
196
+ const [clientAsPeer, jazzCloudAsPeer] = connectedPeers(
197
+ "clientConnection",
198
+ "jazzCloudConnection",
199
+ {
200
+ peer1role: "client",
201
+ peer2role: "server",
202
+ },
203
+ );
204
+
205
+ client.syncManager.addPeer(jazzCloudAsPeer);
206
+ jazzCloud.syncManager.addPeer(clientAsPeer);
207
+
208
+ const subscriptionManager = client.syncManager.syncStateSubscriptionManager;
209
+ const anyUpdateSpy = vi.fn();
210
+ const unsubscribe1 = subscriptionManager.subscribeToUpdates(anyUpdateSpy);
211
+ const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
212
+ "jazzCloudConnection",
213
+ anyUpdateSpy,
214
+ );
215
+
216
+ unsubscribe1();
217
+ unsubscribe2();
218
+
219
+ await client.syncManager.actuallySyncCoValue(map.core);
220
+
221
+ anyUpdateSpy.mockClear();
222
+
223
+ await waitFor(() => {
224
+ return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
225
+ "jazzCloudConnection",
226
+ map.core.id,
227
+ );
228
+ });
229
+
230
+ expect(anyUpdateSpy).not.toHaveBeenCalled();
231
+ });
232
+ });
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test, vi } from "vitest";
2
2
  import { expectMap } from "../coValue.js";
3
3
  import { CoValueHeader } from "../coValueCore.js";
4
4
  import { RawAccountID } from "../coValues/account.js";
@@ -10,7 +10,11 @@ import { LocalNode } from "../localNode.js";
10
10
  import { getPriorityFromHeader } from "../priority.js";
11
11
  import { connectedPeers, newQueuePair } from "../streamUtils.js";
12
12
  import { SyncMessage } from "../sync.js";
13
- import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
13
+ import {
14
+ createTestNode,
15
+ randomAnonymousAccountAndSessionID,
16
+ waitFor,
17
+ } from "./testUtils.js";
14
18
 
15
19
  const Crypto = await WasmCrypto.create();
16
20
 
@@ -1561,6 +1565,303 @@ describe("sync - extra tests", () => {
1561
1565
  });
1562
1566
  });
1563
1567
 
1568
+ function createTwoConnectedNodes() {
1569
+ // Setup nodes
1570
+ const client = createTestNode();
1571
+ const jazzCloud = createTestNode();
1572
+
1573
+ // Connect nodes initially
1574
+ const [connectionWithClientAsPeer, jazzCloudConnectionAsPeer] =
1575
+ connectedPeers("connectionWithClient", "jazzCloudConnection", {
1576
+ peer1role: "client",
1577
+ peer2role: "server",
1578
+ });
1579
+
1580
+ client.syncManager.addPeer(jazzCloudConnectionAsPeer);
1581
+ jazzCloud.syncManager.addPeer(connectionWithClientAsPeer);
1582
+
1583
+ return {
1584
+ client,
1585
+ jazzCloud,
1586
+ connectionWithClientAsPeer,
1587
+ jazzCloudConnectionAsPeer,
1588
+ };
1589
+ }
1590
+
1591
+ describe("SyncManager - knownStates vs optimisticKnownStates", () => {
1592
+ test("knownStates and optimisticKnownStates are the same when the coValue is fully synced", async () => {
1593
+ const { client } = createTwoConnectedNodes();
1594
+
1595
+ // Create test data
1596
+ const group = client.createGroup();
1597
+ const map = group.createMap();
1598
+ map.set("key1", "value1", "trusting");
1599
+
1600
+ await client.syncManager.actuallySyncCoValue(map.core);
1601
+
1602
+ // Wait for the full sync to complete
1603
+ await waitFor(() => {
1604
+ return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
1605
+ "jazzCloudConnection",
1606
+ map.core.id,
1607
+ );
1608
+ });
1609
+
1610
+ const peerState = client.syncManager.peers["jazzCloudConnection"]!;
1611
+
1612
+ // 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
+ );
1616
+ });
1617
+
1618
+ test("optimisticKnownStates is updated as new transactions are received, while knownStates only when the coValue is fully synced", async () => {
1619
+ const { client, jazzCloudConnectionAsPeer } = createTwoConnectedNodes();
1620
+
1621
+ // Create test data and sync the first change
1622
+ // We want that both the nodes know about the coValue so we can test
1623
+ // the content acknowledgement flow.
1624
+ const group = client.createGroup();
1625
+ const map = group.createMap();
1626
+ map.set("key1", "value1", "trusting");
1627
+
1628
+ await client.syncManager.actuallySyncCoValue(map.core);
1629
+ await waitFor(() => {
1630
+ return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
1631
+ "jazzCloudConnection",
1632
+ map.core.id,
1633
+ );
1634
+ });
1635
+
1636
+ map.set("key2", "value2", "trusting");
1637
+
1638
+ await client.syncManager.actuallySyncCoValue(map.core);
1639
+
1640
+ // Block the content messages
1641
+ // The main difference between optimisticKnownStates and knownStates is that
1642
+ // optimisticKnownStates is updated when the content messages are sent,
1643
+ // while knownStates is only updated when we receive the "known" messages
1644
+ // that are acknowledging the receipt of the content messages
1645
+ const push = jazzCloudConnectionAsPeer.outgoing.push;
1646
+ const pushSpy = vi.spyOn(jazzCloudConnectionAsPeer.outgoing, "push");
1647
+
1648
+ const blockedMessages: SyncMessage[] = [];
1649
+
1650
+ pushSpy.mockImplementation(async (msg) => {
1651
+ if (msg.action === "content") {
1652
+ blockedMessages.push(msg);
1653
+ return Promise.resolve();
1654
+ }
1655
+
1656
+ return push.call(jazzCloudConnectionAsPeer.outgoing, msg);
1657
+ });
1658
+
1659
+ const peerState = client.syncManager.peers["jazzCloudConnection"]!;
1660
+
1661
+ expect(peerState.optimisticKnownStates.get(map.core.id)).not.toEqual(
1662
+ peerState.knownStates.get(map.core.id),
1663
+ );
1664
+
1665
+ // Restore the implementation of push and send the blocked messages
1666
+ // After this the full sync can be completed and the other node will
1667
+ // respond with a "known" message acknowledging the receipt of the content messages
1668
+ pushSpy.mockRestore();
1669
+
1670
+ for (const msg of blockedMessages) {
1671
+ await jazzCloudConnectionAsPeer.outgoing.push(msg);
1672
+ }
1673
+
1674
+ await waitFor(() => {
1675
+ return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
1676
+ "jazzCloudConnection",
1677
+ map.core.id,
1678
+ );
1679
+ });
1680
+
1681
+ expect(peerState.optimisticKnownStates.get(map.core.id)).toEqual(
1682
+ peerState.knownStates.get(map.core.id),
1683
+ );
1684
+ });
1685
+ });
1686
+
1687
+ describe("SyncManager.addPeer", () => {
1688
+ test("new peer gets a copy of previous peer's knownStates when replacing it", async () => {
1689
+ const { client } = createTwoConnectedNodes();
1690
+
1691
+ // Create test data
1692
+ const group = client.createGroup();
1693
+ const map = group.createMap();
1694
+ map.set("key1", "value1", "trusting");
1695
+
1696
+ await client.syncManager.actuallySyncCoValue(map.core);
1697
+
1698
+ // Wait for initial sync
1699
+ await waitFor(() => {
1700
+ return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
1701
+ "jazzCloudConnection",
1702
+ map.core.id,
1703
+ );
1704
+ });
1705
+
1706
+ // Store the initial known states
1707
+ const initialKnownStates =
1708
+ client.syncManager.peers["jazzCloudConnection"]!.knownStates;
1709
+
1710
+ // Create new connection with same ID
1711
+ const [jazzCloudConnectionAsPeer2] = connectedPeers(
1712
+ "jazzCloudConnection",
1713
+ "unusedPeer",
1714
+ {
1715
+ peer1role: "server",
1716
+ peer2role: "client",
1717
+ },
1718
+ );
1719
+
1720
+ // Add new peer with same ID
1721
+ client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
1722
+
1723
+ // Verify that the new peer has a copy of the previous known states
1724
+ const newPeerKnownStates =
1725
+ client.syncManager.peers["jazzCloudConnection"]!.knownStates;
1726
+
1727
+ expect(newPeerKnownStates).not.toBe(initialKnownStates); // Should be a different instance
1728
+ expect(newPeerKnownStates.get(map.core.id)).toEqual(
1729
+ initialKnownStates.get(map.core.id),
1730
+ );
1731
+ });
1732
+
1733
+ test("new peer with new ID starts with empty knownStates", async () => {
1734
+ const { client } = createTwoConnectedNodes();
1735
+
1736
+ // Create test data
1737
+ const group = client.createGroup();
1738
+ const map = group.createMap();
1739
+ map.set("key1", "value1", "trusting");
1740
+
1741
+ await client.syncManager.actuallySyncCoValue(map.core);
1742
+
1743
+ // Wait for initial sync
1744
+ await waitFor(() => {
1745
+ return client.syncManager.syncStateSubscriptionManager.getIsCoValueFullyUploadedIntoPeer(
1746
+ "jazzCloudConnection",
1747
+ map.core.id,
1748
+ );
1749
+ });
1750
+
1751
+ // Connect second peer with different ID
1752
+ const [brandNewPeer] = connectedPeers("brandNewPeer", "unusedPeer", {
1753
+ peer1role: "client",
1754
+ peer2role: "server",
1755
+ });
1756
+
1757
+ // Add new peer with different ID
1758
+ client.syncManager.addPeer(brandNewPeer);
1759
+
1760
+ // Verify that the new peer starts with empty known states
1761
+ const newPeerKnownStates =
1762
+ client.syncManager.peers["brandNewPeer"]!.knownStates;
1763
+ expect(newPeerKnownStates.get(map.core.id)).toBe(undefined);
1764
+ });
1765
+
1766
+ test("when adding a peer with the same ID as a previous peer, the previous peer is closed", async () => {
1767
+ const { client } = createTwoConnectedNodes();
1768
+
1769
+ // Store reference to first peer
1770
+ const firstPeer = client.syncManager.peers["jazzCloudConnection"]!;
1771
+ const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
1772
+
1773
+ // Create and add replacement peer
1774
+ const [jazzCloudConnectionAsPeer2] = connectedPeers(
1775
+ "jazzCloudConnection",
1776
+ "unusedPeer",
1777
+ {
1778
+ peer1role: "server",
1779
+ peer2role: "client",
1780
+ },
1781
+ );
1782
+
1783
+ client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
1784
+
1785
+ // Verify thet the first peer had ben closed correctly
1786
+ expect(closeSpy).toHaveBeenCalled();
1787
+ expect(firstPeer.closed).toBe(true);
1788
+ });
1789
+
1790
+ test("when adding a peer with the same ID as a previous peer and the previous peer is closed, do not attempt to close it again", async () => {
1791
+ const { client } = createTwoConnectedNodes();
1792
+
1793
+ // Store reference to first peer
1794
+ const firstPeer = client.syncManager.peers["jazzCloudConnection"]!;
1795
+
1796
+ firstPeer.gracefulShutdown();
1797
+ const closeSpy = vi.spyOn(firstPeer, "gracefulShutdown");
1798
+
1799
+ // Create and add replacement peer
1800
+ const [jazzCloudConnectionAsPeer2] = connectedPeers(
1801
+ "jazzCloudConnection",
1802
+ "unusedPeer",
1803
+ {
1804
+ peer1role: "server",
1805
+ peer2role: "client",
1806
+ },
1807
+ );
1808
+
1809
+ client.syncManager.addPeer(jazzCloudConnectionAsPeer2);
1810
+
1811
+ // Verify thet the first peer had not been closed again
1812
+ expect(closeSpy).not.toHaveBeenCalled();
1813
+ expect(firstPeer.closed).toBe(true);
1814
+ });
1815
+ });
1816
+
1817
+ describe("waitForUploadIntoPeer", () => {
1818
+ test("should resolve when the coValue is fully uploaded into the peer", async () => {
1819
+ const { client, jazzCloudConnectionAsPeer: peer } =
1820
+ createTwoConnectedNodes();
1821
+
1822
+ // Create test data
1823
+ const group = client.createGroup();
1824
+ const map = group.createMap();
1825
+ map.set("key1", "value1", "trusting");
1826
+
1827
+ await client.syncManager.actuallySyncCoValue(map.core);
1828
+
1829
+ await expect(
1830
+ Promise.race([
1831
+ client.syncManager.waitForUploadIntoPeer(peer.id, map.core.id),
1832
+ new Promise((_, reject) =>
1833
+ setTimeout(() => reject(new Error("Timeout")), 100),
1834
+ ),
1835
+ ]),
1836
+ ).resolves.toBe(true);
1837
+ });
1838
+
1839
+ test("should not resolve when the coValue is not synced", async () => {
1840
+ const { client, jazzCloudConnectionAsPeer: peer } =
1841
+ createTwoConnectedNodes();
1842
+
1843
+ // Create test data
1844
+ const group = client.createGroup();
1845
+ const map = group.createMap();
1846
+ map.set("key1", "value1", "trusting");
1847
+
1848
+ vi.spyOn(peer.outgoing, "push").mockImplementation(async () => {
1849
+ return Promise.resolve();
1850
+ });
1851
+
1852
+ await client.syncManager.actuallySyncCoValue(map.core);
1853
+
1854
+ await expect(
1855
+ Promise.race([
1856
+ client.syncManager.waitForUploadIntoPeer(peer.id, map.core.id),
1857
+ new Promise((_, reject) =>
1858
+ setTimeout(() => reject(new Error("Timeout")), 100),
1859
+ ),
1860
+ ]),
1861
+ ).rejects.toThrow("Timeout");
1862
+ });
1863
+ });
1864
+
1564
1865
  function groupContentEx(group: RawGroup) {
1565
1866
  return {
1566
1867
  action: "content",
@@ -18,6 +18,11 @@ export function randomAnonymousAccountAndSessionID(): [
18
18
  return [new ControlledAgent(agentSecret, Crypto), sessionID];
19
19
  }
20
20
 
21
+ export function createTestNode() {
22
+ const [admin, session] = randomAnonymousAccountAndSessionID();
23
+ return new LocalNode(admin, session, Crypto);
24
+ }
25
+
21
26
  export function newGroup() {
22
27
  const [admin, sessionID] = randomAnonymousAccountAndSessionID();
23
28
 
@@ -93,3 +98,31 @@ export function shouldNotResolve<T>(
93
98
  setTimeout(resolve, ops.timeout);
94
99
  });
95
100
  }
101
+
102
+ export function waitFor(callback: () => boolean | void) {
103
+ return new Promise<void>((resolve, reject) => {
104
+ const checkPassed = () => {
105
+ try {
106
+ return { ok: callback(), error: null };
107
+ } catch (error) {
108
+ return { ok: false, error };
109
+ }
110
+ };
111
+
112
+ let retries = 0;
113
+
114
+ const interval = setInterval(() => {
115
+ const { ok, error } = checkPassed();
116
+
117
+ if (ok !== false) {
118
+ clearInterval(interval);
119
+ resolve();
120
+ }
121
+
122
+ if (++retries > 10) {
123
+ clearInterval(interval);
124
+ reject(error);
125
+ }
126
+ }, 100);
127
+ });
128
+ }