cojson 0.7.35-unique.2 → 0.7.35
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.
- package/.turbo/turbo-test.log +321 -253
- package/CHANGELOG.md +3 -2
- package/dist/PeerState.js +58 -0
- package/dist/PeerState.js.map +1 -0
- package/dist/PriorityBasedMessageQueue.js +51 -0
- package/dist/PriorityBasedMessageQueue.js.map +1 -0
- package/dist/base64url.js.map +1 -1
- package/dist/coValue.js.map +1 -1
- package/dist/coValueCore.js +3 -0
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/account.js +2 -2
- package/dist/coValues/account.js.map +1 -1
- package/dist/coValues/coList.js.map +1 -1
- package/dist/coValues/coMap.js.map +1 -1
- package/dist/coValues/coStream.js +14 -15
- package/dist/coValues/coStream.js.map +1 -1
- package/dist/coValues/group.js +8 -8
- package/dist/coValues/group.js.map +1 -1
- package/dist/coreToCoValue.js.map +1 -1
- package/dist/crypto/PureJSCrypto.js.map +1 -1
- package/dist/crypto/WasmCrypto.js.map +1 -1
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonStringify.js.map +1 -1
- package/dist/localNode.js +2 -2
- package/dist/localNode.js.map +1 -1
- package/dist/permissions.js.map +1 -1
- package/dist/priority.js +31 -0
- package/dist/priority.js.map +1 -0
- package/dist/storage/FileSystem.js.map +1 -1
- package/dist/storage/chunksAndKnownStates.js +2 -0
- package/dist/storage/chunksAndKnownStates.js.map +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/streamUtils.js.map +1 -1
- package/dist/sync.js +7 -18
- package/dist/sync.js.map +1 -1
- package/dist/tests/PeerState.test.js +80 -0
- package/dist/tests/PeerState.test.js.map +1 -0
- package/dist/tests/PriorityBasedMessageQueue.test.js +97 -0
- package/dist/tests/PriorityBasedMessageQueue.test.js.map +1 -0
- package/dist/tests/coMap.test.js.map +1 -1
- package/dist/tests/coStream.test.js +34 -1
- package/dist/tests/coStream.test.js.map +1 -1
- package/dist/tests/permissions.test.js.map +1 -1
- package/dist/tests/priority.test.js +61 -0
- package/dist/tests/priority.test.js.map +1 -0
- package/dist/tests/sync.test.js +323 -12
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/testUtils.js.map +1 -1
- package/dist/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
- package/dist/typeUtils/expectGroup.js.map +1 -1
- package/dist/typeUtils/isAccountID.js.map +1 -1
- package/package.json +3 -3
- package/src/PeerState.ts +74 -0
- package/src/PriorityBasedMessageQueue.ts +77 -0
- package/src/coValueCore.ts +10 -7
- package/src/coValues/account.ts +5 -5
- package/src/coValues/coList.ts +4 -4
- package/src/coValues/coMap.ts +3 -3
- package/src/coValues/coStream.ts +29 -26
- package/src/coValues/group.ts +11 -15
- package/src/ids.ts +2 -2
- package/src/index.ts +5 -5
- package/src/localNode.ts +11 -12
- package/src/permissions.ts +5 -5
- package/src/priority.ts +39 -0
- package/src/storage/chunksAndKnownStates.ts +2 -0
- package/src/sync.ts +19 -34
- package/src/tests/PeerState.test.ts +92 -0
- package/src/tests/PriorityBasedMessageQueue.test.ts +111 -0
- package/src/tests/coStream.test.ts +58 -1
- package/src/tests/priority.test.ts +75 -0
- package/src/tests/sync.test.ts +487 -25
- package/src/typeUtils/accountOrAgentIDfromSessionID.ts +3 -3
- package/src/typeUtils/isAccountID.ts +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { expect, test, describe } from "vitest";
|
|
2
|
+
import { WasmCrypto } from "../index.js";
|
|
3
|
+
import { LocalNode } from "../localNode.js";
|
|
4
|
+
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
|
5
|
+
import { getPriorityFromHeader, CO_VALUE_PRIORITY } from "../priority.js";
|
|
6
|
+
const Crypto = await WasmCrypto.create();
|
|
7
|
+
describe("getPriorityFromHeader", () => {
|
|
8
|
+
test("returns MEDIUM priority for boolean or undefined headers", () => {
|
|
9
|
+
expect(getPriorityFromHeader(true)).toEqual(CO_VALUE_PRIORITY.MEDIUM);
|
|
10
|
+
expect(getPriorityFromHeader(false)).toEqual(CO_VALUE_PRIORITY.MEDIUM);
|
|
11
|
+
expect(getPriorityFromHeader(undefined)).toEqual(CO_VALUE_PRIORITY.MEDIUM);
|
|
12
|
+
});
|
|
13
|
+
test("returns MEDIUM priority for costream type", () => {
|
|
14
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
15
|
+
const costream = node.createCoValue({
|
|
16
|
+
type: "costream",
|
|
17
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
18
|
+
meta: null,
|
|
19
|
+
...Crypto.createdNowUnique(),
|
|
20
|
+
});
|
|
21
|
+
expect(getPriorityFromHeader(costream.header)).toEqual(CO_VALUE_PRIORITY.MEDIUM);
|
|
22
|
+
});
|
|
23
|
+
test("returns LOW priority for binary costream type", () => {
|
|
24
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
25
|
+
const costream = node.createCoValue({
|
|
26
|
+
type: "costream",
|
|
27
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
28
|
+
meta: { type: "binary" },
|
|
29
|
+
...Crypto.createdNowUnique(),
|
|
30
|
+
});
|
|
31
|
+
expect(getPriorityFromHeader(costream.header)).toEqual(CO_VALUE_PRIORITY.LOW);
|
|
32
|
+
});
|
|
33
|
+
test("returns HIGH priority for account type", async () => {
|
|
34
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
35
|
+
const account = node.createAccount(node.crypto.newRandomAgentSecret());
|
|
36
|
+
expect(getPriorityFromHeader(account.core.header)).toEqual(CO_VALUE_PRIORITY.HIGH);
|
|
37
|
+
});
|
|
38
|
+
test("returns HIGH priority for group type", () => {
|
|
39
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
40
|
+
const group = node.createGroup();
|
|
41
|
+
expect(getPriorityFromHeader(group.core.header)).toEqual(CO_VALUE_PRIORITY.HIGH);
|
|
42
|
+
});
|
|
43
|
+
test("returns MEDIUM priority for other types", () => {
|
|
44
|
+
const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
|
|
45
|
+
const comap = node.createCoValue({
|
|
46
|
+
type: "comap",
|
|
47
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
48
|
+
meta: null,
|
|
49
|
+
...Crypto.createdNowUnique(),
|
|
50
|
+
});
|
|
51
|
+
const colist = node.createCoValue({
|
|
52
|
+
type: "colist",
|
|
53
|
+
ruleset: { type: "unsafeAllowAll" },
|
|
54
|
+
meta: null,
|
|
55
|
+
...Crypto.createdNowUnique(),
|
|
56
|
+
});
|
|
57
|
+
expect(getPriorityFromHeader(comap.header)).toEqual(CO_VALUE_PRIORITY.MEDIUM);
|
|
58
|
+
expect(getPriorityFromHeader(colist.header)).toEqual(CO_VALUE_PRIORITY.MEDIUM);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=priority.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"priority.test.js","sourceRoot":"","sources":["../../src/tests/priority.test.ts"],"names":[],"mappings":"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"}
|
package/dist/tests/sync.test.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
1
|
+
import { expect, test, describe } from "vitest";
|
|
2
2
|
import { LocalNode } from "../localNode.js";
|
|
3
|
+
import { RawCoMap } from "../coValues/coMap.js";
|
|
3
4
|
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
|
4
5
|
import { connectedPeers, newQueuePair } from "../streamUtils.js";
|
|
5
6
|
import { stableStringify } from "../jsonStringify.js";
|
|
6
7
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
7
8
|
import { expectMap } from "../coValue.js";
|
|
8
9
|
import { newRandomSessionID } from "../coValueCore.js";
|
|
10
|
+
import { getPriorityFromHeader } from "../priority.js";
|
|
9
11
|
const Crypto = await WasmCrypto.create();
|
|
10
12
|
test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
11
13
|
const [admin, session] = randomAnonymousAccountAndSessionID();
|
|
@@ -39,16 +41,17 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
|
39
41
|
// expect((await outRxQ.next()).value).toMatchObject(admContEx(admin.id));
|
|
40
42
|
expect((await outRxQ.next()).value).toMatchObject(groupContentEx(group));
|
|
41
43
|
const newContentMsg = (await outRxQ.next()).value;
|
|
44
|
+
const expectedHeader = {
|
|
45
|
+
type: "comap",
|
|
46
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
47
|
+
meta: null,
|
|
48
|
+
createdAt: map.core.header.createdAt,
|
|
49
|
+
uniqueness: map.core.header.uniqueness,
|
|
50
|
+
};
|
|
42
51
|
expect(newContentMsg).toEqual({
|
|
43
52
|
action: "content",
|
|
44
53
|
id: map.core.id,
|
|
45
|
-
header:
|
|
46
|
-
type: "comap",
|
|
47
|
-
ruleset: { type: "ownedByGroup", group: group.id },
|
|
48
|
-
meta: null,
|
|
49
|
-
createdAt: map.core.header.createdAt,
|
|
50
|
-
uniqueness: map.core.header.uniqueness,
|
|
51
|
-
},
|
|
54
|
+
header: expectedHeader,
|
|
52
55
|
new: {
|
|
53
56
|
[node.currentSessionID]: {
|
|
54
57
|
after: 0,
|
|
@@ -70,6 +73,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
|
70
73
|
.lastSignature,
|
|
71
74
|
},
|
|
72
75
|
},
|
|
76
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
73
77
|
});
|
|
74
78
|
});
|
|
75
79
|
test("Node replies with only new tx to subscribe with some known state", async () => {
|
|
@@ -132,6 +136,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
|
|
132
136
|
.lastSignature,
|
|
133
137
|
},
|
|
134
138
|
},
|
|
139
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
135
140
|
});
|
|
136
141
|
});
|
|
137
142
|
test.todo("TODO: node only replies with new tx to subscribe with some known state, even in the depended on coValues");
|
|
@@ -173,6 +178,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
173
178
|
id: map.core.id,
|
|
174
179
|
header: map.core.header,
|
|
175
180
|
new: {},
|
|
181
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
176
182
|
});
|
|
177
183
|
map.set("hello", "world", "trusting");
|
|
178
184
|
const mapEditMsg1 = (await outRxQ.next()).value;
|
|
@@ -200,6 +206,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
200
206
|
.lastSignature,
|
|
201
207
|
},
|
|
202
208
|
},
|
|
209
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
203
210
|
});
|
|
204
211
|
map.set("goodbye", "world", "trusting");
|
|
205
212
|
const mapEditMsg2 = (await outRxQ.next()).value;
|
|
@@ -227,6 +234,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
227
234
|
.lastSignature,
|
|
228
235
|
},
|
|
229
236
|
},
|
|
237
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
230
238
|
});
|
|
231
239
|
});
|
|
232
240
|
test("Client replies with known new content to tellKnownState from server", async () => {
|
|
@@ -289,6 +297,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
|
|
289
297
|
.lastSignature,
|
|
290
298
|
},
|
|
291
299
|
},
|
|
300
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
292
301
|
});
|
|
293
302
|
});
|
|
294
303
|
test("No matter the optimistic known state, node respects invalid known state messages and resyncs", async () => {
|
|
@@ -329,6 +338,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|
|
329
338
|
id: map.core.id,
|
|
330
339
|
header: map.core.header,
|
|
331
340
|
new: {},
|
|
341
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
332
342
|
});
|
|
333
343
|
map.set("hello", "world", "trusting");
|
|
334
344
|
map.set("goodbye", "world", "trusting");
|
|
@@ -369,6 +379,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|
|
369
379
|
.lastSignature,
|
|
370
380
|
},
|
|
371
381
|
},
|
|
382
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
372
383
|
});
|
|
373
384
|
});
|
|
374
385
|
test("If we add a peer, but it never subscribes to a coValue, it won't get any messages", async () => {
|
|
@@ -451,6 +462,7 @@ test.todo("If we add a server peer, all updates to all coValues are sent to it,
|
|
|
451
462
|
lastSignature: map.core.sessionLogs.get(node.currentSessionID).lastSignature,
|
|
452
463
|
},
|
|
453
464
|
},
|
|
465
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
454
466
|
});
|
|
455
467
|
});
|
|
456
468
|
test.skip("If we add a server peer, newly created coValues are auto-subscribed to", async () => {
|
|
@@ -489,6 +501,7 @@ test.skip("If we add a server peer, newly created coValues are auto-subscribed t
|
|
|
489
501
|
id: map.core.id,
|
|
490
502
|
header: map.core.header,
|
|
491
503
|
new: {},
|
|
504
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
492
505
|
});
|
|
493
506
|
});
|
|
494
507
|
test.todo("TODO: when receiving a subscribe response that is behind our optimistic state (due to already sent content), we ignore it");
|
|
@@ -613,6 +626,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
|
|
|
613
626
|
id: map.core.id,
|
|
614
627
|
header: map.core.header,
|
|
615
628
|
new: {},
|
|
629
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
616
630
|
});
|
|
617
631
|
await inTx2.push(mapSubscriptionMsg);
|
|
618
632
|
const mapTellKnownStateMsg = (await outRxQ2.next()).value;
|
|
@@ -666,7 +680,7 @@ test("Can sync a coValue through a server to another client", async () => {
|
|
|
666
680
|
map.set("hello", "world", "trusting");
|
|
667
681
|
const [serverUser, serverSession] = randomAnonymousAccountAndSessionID();
|
|
668
682
|
const server = new LocalNode(serverUser, serverSession, Crypto);
|
|
669
|
-
const [serverAsPeerForClient1, client1AsPeer] =
|
|
683
|
+
const [serverAsPeerForClient1, client1AsPeer] = connectedPeers("serverFor1", "client1", {
|
|
670
684
|
peer1role: "server",
|
|
671
685
|
peer2role: "client",
|
|
672
686
|
trace: true,
|
|
@@ -695,7 +709,7 @@ test("Can sync a coValue with private transactions through a server to another c
|
|
|
695
709
|
map.set("hello", "world", "private");
|
|
696
710
|
const [serverUser, serverSession] = randomAnonymousAccountAndSessionID();
|
|
697
711
|
const server = new LocalNode(serverUser, serverSession, Crypto);
|
|
698
|
-
const [serverAsPeer, client1AsPeer] =
|
|
712
|
+
const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
|
|
699
713
|
trace: true,
|
|
700
714
|
peer1role: "server",
|
|
701
715
|
peer2role: "client",
|
|
@@ -703,7 +717,7 @@ test("Can sync a coValue with private transactions through a server to another c
|
|
|
703
717
|
client1.syncManager.addPeer(serverAsPeer);
|
|
704
718
|
server.syncManager.addPeer(client1AsPeer);
|
|
705
719
|
const client2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
706
|
-
const [serverAsOtherPeer, client2AsPeer] =
|
|
720
|
+
const [serverAsOtherPeer, client2AsPeer] = connectedPeers("server", "client2", {
|
|
707
721
|
trace: true,
|
|
708
722
|
peer1role: "server",
|
|
709
723
|
peer2role: "client",
|
|
@@ -834,7 +848,7 @@ test("If we start loading a coValue before connecting to a peer that has it, it
|
|
|
834
848
|
const map = group.createMap();
|
|
835
849
|
map.set("hello", "world", "trusting");
|
|
836
850
|
const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
837
|
-
const [node1asPeer, node2asPeer] =
|
|
851
|
+
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
|
|
838
852
|
peer1role: "server",
|
|
839
853
|
peer2role: "client",
|
|
840
854
|
trace: true,
|
|
@@ -849,6 +863,303 @@ test("If we start loading a coValue before connecting to a peer that has it, it
|
|
|
849
863
|
}
|
|
850
864
|
expect(expectMap(mapOnNode2.getCurrentContent()).get("hello")).toEqual("world");
|
|
851
865
|
});
|
|
866
|
+
describe("sync - extra tests", () => {
|
|
867
|
+
test("Node handles disconnection and reconnection of a peer gracefully", async () => {
|
|
868
|
+
// Create two nodes
|
|
869
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
870
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
871
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
872
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
873
|
+
// Create a group and a map on node1
|
|
874
|
+
const group = node1.createGroup();
|
|
875
|
+
group.addMember("everyone", "writer");
|
|
876
|
+
const map = group.createMap();
|
|
877
|
+
map.set("key1", "value1", "trusting");
|
|
878
|
+
// Connect the nodes
|
|
879
|
+
const [node1AsPeer, node2AsPeer] = connectedPeers("node1", "node2", {
|
|
880
|
+
peer1role: "server",
|
|
881
|
+
peer2role: "client",
|
|
882
|
+
});
|
|
883
|
+
node1.syncManager.addPeer(node2AsPeer);
|
|
884
|
+
node2.syncManager.addPeer(node1AsPeer);
|
|
885
|
+
// Wait for initial sync
|
|
886
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
887
|
+
// Verify that node2 has received the map
|
|
888
|
+
const mapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
889
|
+
if (mapOnNode2 === "unavailable") {
|
|
890
|
+
throw new Error("Map is unavailable on node2");
|
|
891
|
+
}
|
|
892
|
+
expect(expectMap(mapOnNode2.getCurrentContent()).get("key1")).toEqual("value1");
|
|
893
|
+
// Simulate disconnection
|
|
894
|
+
node1.syncManager.gracefulShutdown();
|
|
895
|
+
node2.syncManager.gracefulShutdown();
|
|
896
|
+
// Make changes on node1 while disconnected
|
|
897
|
+
map.set("key2", "value2", "trusting");
|
|
898
|
+
// Simulate reconnection
|
|
899
|
+
const [newNode1AsPeer, newNode2AsPeer] = connectedPeers("node11", "node22", {
|
|
900
|
+
peer1role: "server",
|
|
901
|
+
peer2role: "client",
|
|
902
|
+
// trace: true,
|
|
903
|
+
});
|
|
904
|
+
node1.syncManager.addPeer(newNode2AsPeer);
|
|
905
|
+
node2.syncManager.addPeer(newNode1AsPeer);
|
|
906
|
+
// Wait for re-sync
|
|
907
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
908
|
+
// Verify that node2 has received the changes made during disconnection
|
|
909
|
+
const updatedMapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
910
|
+
if (updatedMapOnNode2 === "unavailable") {
|
|
911
|
+
throw new Error("Updated map is unavailable on node2");
|
|
912
|
+
}
|
|
913
|
+
expect(expectMap(updatedMapOnNode2.getCurrentContent()).get("key2")).toEqual("value2");
|
|
914
|
+
// Make a new change on node2 to verify two-way sync
|
|
915
|
+
const mapOnNode2ForEdit = await node2.loadCoValueCore(map.core.id);
|
|
916
|
+
if (mapOnNode2ForEdit === "unavailable") {
|
|
917
|
+
throw new Error("Updated map is unavailable on node2");
|
|
918
|
+
}
|
|
919
|
+
const success = mapOnNode2ForEdit.makeTransaction([
|
|
920
|
+
{
|
|
921
|
+
op: "set",
|
|
922
|
+
key: "key3",
|
|
923
|
+
value: "value3",
|
|
924
|
+
},
|
|
925
|
+
], "trusting");
|
|
926
|
+
if (!success) {
|
|
927
|
+
throw new Error("Failed to make transaction");
|
|
928
|
+
}
|
|
929
|
+
// Wait for sync back to node1
|
|
930
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
931
|
+
const mapOnNode1 = await node1.loadCoValueCore(map.core.id);
|
|
932
|
+
if (mapOnNode1 === "unavailable") {
|
|
933
|
+
throw new Error("Updated map is unavailable on node1");
|
|
934
|
+
}
|
|
935
|
+
// Verify that node1 has received the change from node2
|
|
936
|
+
expect(expectMap(mapOnNode1.getCurrentContent()).get("key3")).toEqual("value3");
|
|
937
|
+
});
|
|
938
|
+
test("Concurrent modifications on multiple nodes are resolved correctly", async () => {
|
|
939
|
+
// Create three nodes
|
|
940
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
941
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
942
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
943
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
944
|
+
const [admin3, session3] = randomAnonymousAccountAndSessionID();
|
|
945
|
+
const node3 = new LocalNode(admin3, session3, Crypto);
|
|
946
|
+
// Create a group and a map on node1
|
|
947
|
+
const group = node1.createGroup();
|
|
948
|
+
group.addMember("everyone", "writer");
|
|
949
|
+
const map = group.createMap();
|
|
950
|
+
// Connect the nodes in a triangle topology
|
|
951
|
+
const [node1AsPeerFor2, node2AsPeerFor1] = connectedPeers("node1", "node2", {
|
|
952
|
+
peer1role: "server",
|
|
953
|
+
peer2role: "client",
|
|
954
|
+
// trace: true,
|
|
955
|
+
});
|
|
956
|
+
const [node2AsPeerFor3, node3AsPeerFor2] = connectedPeers("node2", "node3", {
|
|
957
|
+
peer1role: "server",
|
|
958
|
+
peer2role: "client",
|
|
959
|
+
// trace: true,
|
|
960
|
+
});
|
|
961
|
+
const [node3AsPeerFor1, node1AsPeerFor3] = connectedPeers("node3", "node1", {
|
|
962
|
+
peer1role: "server",
|
|
963
|
+
peer2role: "client",
|
|
964
|
+
// trace: true,
|
|
965
|
+
});
|
|
966
|
+
node1.syncManager.addPeer(node2AsPeerFor1);
|
|
967
|
+
node1.syncManager.addPeer(node3AsPeerFor1);
|
|
968
|
+
node2.syncManager.addPeer(node1AsPeerFor2);
|
|
969
|
+
node2.syncManager.addPeer(node3AsPeerFor2);
|
|
970
|
+
node3.syncManager.addPeer(node1AsPeerFor3);
|
|
971
|
+
node3.syncManager.addPeer(node2AsPeerFor3);
|
|
972
|
+
// Wait for initial sync
|
|
973
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
974
|
+
// Verify that all nodes have the map
|
|
975
|
+
const mapOnNode1 = await node1.loadCoValueCore(map.core.id);
|
|
976
|
+
const mapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
977
|
+
const mapOnNode3 = await node3.loadCoValueCore(map.core.id);
|
|
978
|
+
if (mapOnNode1 === "unavailable" ||
|
|
979
|
+
mapOnNode2 === "unavailable" ||
|
|
980
|
+
mapOnNode3 === "unavailable") {
|
|
981
|
+
throw new Error("Map is unavailable on node2 or node3");
|
|
982
|
+
}
|
|
983
|
+
// Perform concurrent modifications
|
|
984
|
+
map.set("key1", "value1", "trusting");
|
|
985
|
+
new RawCoMap(mapOnNode2).set("key2", "value2", "trusting");
|
|
986
|
+
new RawCoMap(mapOnNode3).set("key3", "value3", "trusting");
|
|
987
|
+
// Wait for sync to complete
|
|
988
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
989
|
+
// Verify that all nodes have the same final state
|
|
990
|
+
const finalStateNode1 = expectMap(mapOnNode1.getCurrentContent());
|
|
991
|
+
const finalStateNode2 = expectMap(mapOnNode2.getCurrentContent());
|
|
992
|
+
const finalStateNode3 = expectMap(mapOnNode3.getCurrentContent());
|
|
993
|
+
const expectedState = {
|
|
994
|
+
key1: "value1",
|
|
995
|
+
key2: "value2",
|
|
996
|
+
key3: "value3",
|
|
997
|
+
};
|
|
998
|
+
expect(finalStateNode1.toJSON()).toEqual(expectedState);
|
|
999
|
+
expect(finalStateNode2.toJSON()).toEqual(expectedState);
|
|
1000
|
+
expect(finalStateNode3.toJSON()).toEqual(expectedState);
|
|
1001
|
+
});
|
|
1002
|
+
test.skip("Large coValues are synced efficiently in chunks", async () => {
|
|
1003
|
+
// Create two nodes
|
|
1004
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
1005
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
1006
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
1007
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
1008
|
+
// Create a group and a large map on node1
|
|
1009
|
+
const group = node1.createGroup();
|
|
1010
|
+
group.addMember("everyone", "writer");
|
|
1011
|
+
const largeMap = group.createMap();
|
|
1012
|
+
// Generate a large amount of data (about 10MB)
|
|
1013
|
+
const dataSize = 1 * 1024 * 1024;
|
|
1014
|
+
const chunkSize = 1024; // 1KB chunks
|
|
1015
|
+
const chunks = dataSize / chunkSize;
|
|
1016
|
+
for (let i = 0; i < chunks; i++) {
|
|
1017
|
+
const key = `key${i}`;
|
|
1018
|
+
const value = Buffer.alloc(chunkSize, `value${i}`).toString("base64");
|
|
1019
|
+
largeMap.set(key, value, "trusting");
|
|
1020
|
+
}
|
|
1021
|
+
// Connect the nodes
|
|
1022
|
+
const [node1AsPeer, node2AsPeer] = connectedPeers("node1", "node2", {
|
|
1023
|
+
peer1role: "server",
|
|
1024
|
+
peer2role: "client",
|
|
1025
|
+
});
|
|
1026
|
+
node1.syncManager.addPeer(node2AsPeer);
|
|
1027
|
+
node2.syncManager.addPeer(node1AsPeer);
|
|
1028
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
1029
|
+
// Measure sync time
|
|
1030
|
+
const startSync = performance.now();
|
|
1031
|
+
// Load the large map on node2
|
|
1032
|
+
const largeMapOnNode2 = await node2.loadCoValueCore(largeMap.core.id);
|
|
1033
|
+
if (largeMapOnNode2 === "unavailable") {
|
|
1034
|
+
throw new Error("Large map is unavailable on node2");
|
|
1035
|
+
}
|
|
1036
|
+
const endSync = performance.now();
|
|
1037
|
+
const syncTime = endSync - startSync;
|
|
1038
|
+
// Verify that all data was synced correctly
|
|
1039
|
+
const syncedMap = new RawCoMap(largeMapOnNode2);
|
|
1040
|
+
expect(Object.keys(largeMapOnNode2.getCurrentContent().toJSON() || {})
|
|
1041
|
+
.length).toBe(chunks);
|
|
1042
|
+
for (let i = 0; i < chunks; i++) {
|
|
1043
|
+
const key = `key${i}`;
|
|
1044
|
+
const expectedValue = Buffer.alloc(chunkSize, `value${i}`).toString("base64");
|
|
1045
|
+
expect(syncedMap.get(key)).toBe(expectedValue);
|
|
1046
|
+
}
|
|
1047
|
+
// Check that sync time is reasonable (this threshold may need adjustment)
|
|
1048
|
+
const reasonableSyncTime = 10; // 30 seconds
|
|
1049
|
+
expect(syncTime).toBeLessThan(reasonableSyncTime);
|
|
1050
|
+
// Check memory usage (this threshold may need adjustment)
|
|
1051
|
+
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024; // in MB
|
|
1052
|
+
const reasonableMemoryUsage = 1; // 500 MB
|
|
1053
|
+
expect(memoryUsage).toBeLessThan(reasonableMemoryUsage);
|
|
1054
|
+
});
|
|
1055
|
+
test("Node correctly handles and recovers from network partitions", async () => {
|
|
1056
|
+
// Create three nodes
|
|
1057
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
1058
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
1059
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
1060
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
1061
|
+
const [admin3, session3] = randomAnonymousAccountAndSessionID();
|
|
1062
|
+
const node3 = new LocalNode(admin3, session3, Crypto);
|
|
1063
|
+
// Create a group and a map on node1
|
|
1064
|
+
const group = node1.createGroup();
|
|
1065
|
+
group.addMember("everyone", "writer");
|
|
1066
|
+
const map = group.createMap();
|
|
1067
|
+
map.set("initial", "value", "trusting");
|
|
1068
|
+
// Connect all nodes
|
|
1069
|
+
const [node1AsPeerFor2, node2AsPeerFor1] = connectedPeers("node1", "node2", {
|
|
1070
|
+
peer1role: "server",
|
|
1071
|
+
peer2role: "client",
|
|
1072
|
+
// trace: true,
|
|
1073
|
+
});
|
|
1074
|
+
const [node2AsPeerFor3, node3AsPeerFor2] = connectedPeers("node2", "node3", {
|
|
1075
|
+
peer1role: "server",
|
|
1076
|
+
peer2role: "client",
|
|
1077
|
+
// trace: true,
|
|
1078
|
+
});
|
|
1079
|
+
const [node3AsPeerFor1, node1AsPeerFor3] = connectedPeers("node3", "node1", {
|
|
1080
|
+
peer1role: "server",
|
|
1081
|
+
peer2role: "client",
|
|
1082
|
+
// trace: true,
|
|
1083
|
+
});
|
|
1084
|
+
node1.syncManager.addPeer(node2AsPeerFor1);
|
|
1085
|
+
node1.syncManager.addPeer(node3AsPeerFor1);
|
|
1086
|
+
node2.syncManager.addPeer(node1AsPeerFor2);
|
|
1087
|
+
node2.syncManager.addPeer(node3AsPeerFor2);
|
|
1088
|
+
node3.syncManager.addPeer(node1AsPeerFor3);
|
|
1089
|
+
node3.syncManager.addPeer(node2AsPeerFor3);
|
|
1090
|
+
// Wait for initial sync
|
|
1091
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1092
|
+
// Verify initial state
|
|
1093
|
+
const mapOnNode1Core = await node1.loadCoValueCore(map.core.id);
|
|
1094
|
+
const mapOnNode2Core = await node2.loadCoValueCore(map.core.id);
|
|
1095
|
+
const mapOnNode3Core = await node3.loadCoValueCore(map.core.id);
|
|
1096
|
+
if (mapOnNode1Core === "unavailable" ||
|
|
1097
|
+
mapOnNode2Core === "unavailable" ||
|
|
1098
|
+
mapOnNode3Core === "unavailable") {
|
|
1099
|
+
throw new Error("Map is unavailable on node2 or node3");
|
|
1100
|
+
}
|
|
1101
|
+
// const mapOnNode1 = new RawCoMap(mapOnNode1Core);
|
|
1102
|
+
const mapOnNode2 = new RawCoMap(mapOnNode2Core);
|
|
1103
|
+
const mapOnNode3 = new RawCoMap(mapOnNode3Core);
|
|
1104
|
+
expect(mapOnNode2.get("initial")).toBe("value");
|
|
1105
|
+
expect(mapOnNode3.get("initial")).toBe("value");
|
|
1106
|
+
// Simulate network partition: disconnect node3 from node1 and node2
|
|
1107
|
+
node1.syncManager.peers["node3"]?.gracefulShutdown();
|
|
1108
|
+
delete node1.syncManager.peers["node3"];
|
|
1109
|
+
node2.syncManager.peers["node3"]?.gracefulShutdown();
|
|
1110
|
+
delete node2.syncManager.peers["node3"];
|
|
1111
|
+
node3.syncManager.peers["node1"]?.gracefulShutdown();
|
|
1112
|
+
delete node3.syncManager.peers["node1"];
|
|
1113
|
+
node3.syncManager.peers["node2"]?.gracefulShutdown();
|
|
1114
|
+
delete node3.syncManager.peers["node2"];
|
|
1115
|
+
// Make changes on both sides of the partition
|
|
1116
|
+
map.set("node1", "partition", "trusting");
|
|
1117
|
+
mapOnNode2.set("node2", "partition", "trusting");
|
|
1118
|
+
mapOnNode3.set("node3", "partition", "trusting");
|
|
1119
|
+
// Wait for sync between node1 and node2
|
|
1120
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1121
|
+
// Verify that node1 and node2 are in sync, but node3 is not
|
|
1122
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).get("node1")).toBe("partition");
|
|
1123
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).get("node2")).toBe("partition");
|
|
1124
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).toJSON()?.node3).toBe(undefined);
|
|
1125
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).get("node1")).toBe("partition");
|
|
1126
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).get("node2")).toBe("partition");
|
|
1127
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).toJSON()?.node3).toBe(undefined);
|
|
1128
|
+
expect(expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node1).toBe(undefined);
|
|
1129
|
+
expect(expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node2).toBe(undefined);
|
|
1130
|
+
expect(expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node3).toBe("partition");
|
|
1131
|
+
// Restore connectivity
|
|
1132
|
+
const [newNode3AsPeerFor1, newNode1AsPeerFor3] = connectedPeers("node3", "node1", {
|
|
1133
|
+
peer1role: "server",
|
|
1134
|
+
peer2role: "client",
|
|
1135
|
+
trace: true,
|
|
1136
|
+
});
|
|
1137
|
+
const [newNode3AsPeerFor2, newNode2AsPeerFor3] = connectedPeers("node3", "node2", {
|
|
1138
|
+
peer1role: "server",
|
|
1139
|
+
peer2role: "client",
|
|
1140
|
+
trace: true,
|
|
1141
|
+
});
|
|
1142
|
+
node1.syncManager.addPeer(newNode3AsPeerFor1);
|
|
1143
|
+
node2.syncManager.addPeer(newNode3AsPeerFor2);
|
|
1144
|
+
node3.syncManager.addPeer(newNode1AsPeerFor3);
|
|
1145
|
+
node3.syncManager.addPeer(newNode2AsPeerFor3);
|
|
1146
|
+
// Wait for re-sync
|
|
1147
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1148
|
+
// Verify final state: all nodes should have all changes
|
|
1149
|
+
const finalStateNode1 = expectMap(mapOnNode1Core.getCurrentContent()).toJSON();
|
|
1150
|
+
const finalStateNode2 = expectMap(mapOnNode2Core.getCurrentContent()).toJSON();
|
|
1151
|
+
const finalStateNode3 = expectMap(mapOnNode3Core.getCurrentContent()).toJSON();
|
|
1152
|
+
const expectedFinalState = {
|
|
1153
|
+
initial: "value",
|
|
1154
|
+
node1: "partition",
|
|
1155
|
+
node2: "partition",
|
|
1156
|
+
node3: "partition",
|
|
1157
|
+
};
|
|
1158
|
+
expect(finalStateNode1).toEqual(expectedFinalState);
|
|
1159
|
+
expect(finalStateNode2).toEqual(expectedFinalState);
|
|
1160
|
+
expect(finalStateNode3).toEqual(expectedFinalState);
|
|
1161
|
+
});
|
|
1162
|
+
});
|
|
852
1163
|
function groupContentEx(group) {
|
|
853
1164
|
return {
|
|
854
1165
|
action: "content",
|