cojson 0.7.35-guest-auth.5 → 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-build.log +2 -2
- package/CHANGELOG.md +3 -8
- 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 +9 -0
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/account.js +3 -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 +0 -3
- package/dist/crypto/crypto.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/jsonStringify.js.map +1 -1
- package/dist/localNode.js +9 -9
- 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/account.test.js +2 -1
- package/dist/tests/account.test.js.map +1 -1
- 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 +42 -41
- 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 +328 -16
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/testUtils.js +2 -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 +18 -7
- package/src/coValues/account.ts +6 -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/crypto/crypto.ts +0 -5
- package/src/ids.ts +2 -2
- package/src/index.ts +7 -5
- package/src/localNode.ts +18 -18
- 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/account.test.ts +2 -1
- package/src/tests/coStream.test.ts +58 -1
- package/src/tests/permissions.test.ts +42 -41
- package/src/tests/priority.test.ts +75 -0
- package/src/tests/sync.test.ts +491 -28
- package/src/tests/testUtils.ts +2 -1
- package/src/typeUtils/accountOrAgentIDfromSessionID.ts +3 -3
- package/src/typeUtils/isAccountID.ts +2 -2
package/src/tests/sync.test.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { expect, test } from "vitest";
|
|
1
|
+
import { expect, test, describe } from "vitest";
|
|
2
2
|
import { LocalNode } from "../localNode.js";
|
|
3
3
|
import { SyncMessage } from "../sync.js";
|
|
4
|
-
import { MapOpPayload } from "../coValues/coMap.js";
|
|
4
|
+
import { MapOpPayload, RawCoMap } from "../coValues/coMap.js";
|
|
5
5
|
import { RawGroup } from "../coValues/group.js";
|
|
6
6
|
import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
|
|
7
7
|
import { connectedPeers, newQueuePair } from "../streamUtils.js";
|
|
8
|
-
import {
|
|
8
|
+
import { AccountID } from "../coValues/account.js";
|
|
9
9
|
import { stableStringify } from "../jsonStringify.js";
|
|
10
10
|
import { WasmCrypto } from "../crypto/WasmCrypto.js";
|
|
11
11
|
import { expectMap } from "../coValue.js";
|
|
12
|
+
import { CoValueHeader, newRandomSessionID } from "../coValueCore.js";
|
|
13
|
+
import { getPriorityFromHeader } from "../priority.js";
|
|
12
14
|
|
|
13
15
|
const Crypto = await WasmCrypto.create();
|
|
14
16
|
|
|
@@ -55,16 +57,18 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
|
55
57
|
|
|
56
58
|
const newContentMsg = (await outRxQ.next()).value;
|
|
57
59
|
|
|
60
|
+
const expectedHeader = {
|
|
61
|
+
type: "comap",
|
|
62
|
+
ruleset: { type: "ownedByGroup", group: group.id },
|
|
63
|
+
meta: null,
|
|
64
|
+
createdAt: map.core.header.createdAt,
|
|
65
|
+
uniqueness: map.core.header.uniqueness,
|
|
66
|
+
} satisfies CoValueHeader;
|
|
67
|
+
|
|
58
68
|
expect(newContentMsg).toEqual({
|
|
59
69
|
action: "content",
|
|
60
70
|
id: map.core.id,
|
|
61
|
-
header:
|
|
62
|
-
type: "comap",
|
|
63
|
-
ruleset: { type: "ownedByGroup", group: group.id },
|
|
64
|
-
meta: null,
|
|
65
|
-
createdAt: map.core.header.createdAt,
|
|
66
|
-
uniqueness: map.core.header.uniqueness,
|
|
67
|
-
},
|
|
71
|
+
header: expectedHeader,
|
|
68
72
|
new: {
|
|
69
73
|
[node.currentSessionID]: {
|
|
70
74
|
after: 0,
|
|
@@ -86,6 +90,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
|
86
90
|
.lastSignature!,
|
|
87
91
|
},
|
|
88
92
|
},
|
|
93
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
89
94
|
} satisfies SyncMessage);
|
|
90
95
|
});
|
|
91
96
|
|
|
@@ -160,6 +165,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
|
|
|
160
165
|
.lastSignature!,
|
|
161
166
|
},
|
|
162
167
|
},
|
|
168
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
163
169
|
} satisfies SyncMessage);
|
|
164
170
|
});
|
|
165
171
|
test.todo(
|
|
@@ -214,6 +220,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
214
220
|
id: map.core.id,
|
|
215
221
|
header: map.core.header,
|
|
216
222
|
new: {},
|
|
223
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
217
224
|
} satisfies SyncMessage);
|
|
218
225
|
|
|
219
226
|
map.set("hello", "world", "trusting");
|
|
@@ -244,6 +251,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
244
251
|
.lastSignature!,
|
|
245
252
|
},
|
|
246
253
|
},
|
|
254
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
247
255
|
} satisfies SyncMessage);
|
|
248
256
|
|
|
249
257
|
map.set("goodbye", "world", "trusting");
|
|
@@ -274,6 +282,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
|
|
|
274
282
|
.lastSignature!,
|
|
275
283
|
},
|
|
276
284
|
},
|
|
285
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
277
286
|
} satisfies SyncMessage);
|
|
278
287
|
});
|
|
279
288
|
|
|
@@ -349,6 +358,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
|
|
|
349
358
|
.lastSignature!,
|
|
350
359
|
},
|
|
351
360
|
},
|
|
361
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
352
362
|
} satisfies SyncMessage);
|
|
353
363
|
});
|
|
354
364
|
|
|
@@ -400,6 +410,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|
|
400
410
|
id: map.core.id,
|
|
401
411
|
header: map.core.header,
|
|
402
412
|
new: {},
|
|
413
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
403
414
|
} satisfies SyncMessage);
|
|
404
415
|
|
|
405
416
|
map.set("hello", "world", "trusting");
|
|
@@ -447,6 +458,7 @@ test("No matter the optimistic known state, node respects invalid known state me
|
|
|
447
458
|
.lastSignature!,
|
|
448
459
|
},
|
|
449
460
|
},
|
|
461
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
450
462
|
} satisfies SyncMessage);
|
|
451
463
|
});
|
|
452
464
|
|
|
@@ -560,6 +572,7 @@ test.todo(
|
|
|
560
572
|
)!.lastSignature!,
|
|
561
573
|
},
|
|
562
574
|
},
|
|
575
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
563
576
|
} satisfies SyncMessage);
|
|
564
577
|
},
|
|
565
578
|
);
|
|
@@ -610,6 +623,7 @@ test.skip("If we add a server peer, newly created coValues are auto-subscribed t
|
|
|
610
623
|
id: map.core.id,
|
|
611
624
|
header: map.core.header,
|
|
612
625
|
new: {},
|
|
626
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
613
627
|
} satisfies SyncMessage);
|
|
614
628
|
});
|
|
615
629
|
|
|
@@ -712,7 +726,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
|
|
|
712
726
|
crashOnClose: true,
|
|
713
727
|
});
|
|
714
728
|
|
|
715
|
-
const node2 = new LocalNode(admin,
|
|
729
|
+
const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
716
730
|
|
|
717
731
|
const [inRx2, inTx2] = newQueuePair();
|
|
718
732
|
const [outRx2, outTx2] = newQueuePair();
|
|
@@ -776,6 +790,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
|
|
|
776
790
|
id: map.core.id,
|
|
777
791
|
header: map.core.header,
|
|
778
792
|
new: {},
|
|
793
|
+
priority: getPriorityFromHeader(map.core.header),
|
|
779
794
|
} satisfies SyncMessage);
|
|
780
795
|
|
|
781
796
|
await inTx2.push(mapSubscriptionMsg);
|
|
@@ -850,7 +865,7 @@ test("Can sync a coValue through a server to another client", async () => {
|
|
|
850
865
|
|
|
851
866
|
const server = new LocalNode(serverUser, serverSession, Crypto);
|
|
852
867
|
|
|
853
|
-
const [serverAsPeerForClient1, client1AsPeer] =
|
|
868
|
+
const [serverAsPeerForClient1, client1AsPeer] = connectedPeers(
|
|
854
869
|
"serverFor1",
|
|
855
870
|
"client1",
|
|
856
871
|
{
|
|
@@ -863,7 +878,7 @@ test("Can sync a coValue through a server to another client", async () => {
|
|
|
863
878
|
client1.syncManager.addPeer(serverAsPeerForClient1);
|
|
864
879
|
server.syncManager.addPeer(client1AsPeer);
|
|
865
880
|
|
|
866
|
-
const client2 = new LocalNode(admin,
|
|
881
|
+
const client2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
867
882
|
|
|
868
883
|
const [serverAsPeerForClient2, client2AsPeer] = connectedPeers(
|
|
869
884
|
"serverFor2",
|
|
@@ -902,22 +917,18 @@ test("Can sync a coValue with private transactions through a server to another c
|
|
|
902
917
|
|
|
903
918
|
const server = new LocalNode(serverUser, serverSession, Crypto);
|
|
904
919
|
|
|
905
|
-
const [serverAsPeer, client1AsPeer] =
|
|
906
|
-
|
|
907
|
-
"
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
peer1role: "server",
|
|
911
|
-
peer2role: "client",
|
|
912
|
-
},
|
|
913
|
-
);
|
|
920
|
+
const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
|
|
921
|
+
trace: true,
|
|
922
|
+
peer1role: "server",
|
|
923
|
+
peer2role: "client",
|
|
924
|
+
});
|
|
914
925
|
|
|
915
926
|
client1.syncManager.addPeer(serverAsPeer);
|
|
916
927
|
server.syncManager.addPeer(client1AsPeer);
|
|
917
928
|
|
|
918
|
-
const client2 = new LocalNode(admin,
|
|
929
|
+
const client2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
919
930
|
|
|
920
|
-
const [serverAsOtherPeer, client2AsPeer] =
|
|
931
|
+
const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
|
|
921
932
|
"server",
|
|
922
933
|
"client2",
|
|
923
934
|
{
|
|
@@ -1063,9 +1074,9 @@ test("If we start loading a coValue before connecting to a peer that has it, it
|
|
|
1063
1074
|
const map = group.createMap();
|
|
1064
1075
|
map.set("hello", "world", "trusting");
|
|
1065
1076
|
|
|
1066
|
-
const node2 = new LocalNode(admin,
|
|
1077
|
+
const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
|
|
1067
1078
|
|
|
1068
|
-
const [node1asPeer, node2asPeer] =
|
|
1079
|
+
const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
|
|
1069
1080
|
peer1role: "server",
|
|
1070
1081
|
peer2role: "client",
|
|
1071
1082
|
trace: true,
|
|
@@ -1089,6 +1100,458 @@ test("If we start loading a coValue before connecting to a peer that has it, it
|
|
|
1089
1100
|
);
|
|
1090
1101
|
});
|
|
1091
1102
|
|
|
1103
|
+
describe("sync - extra tests", () => {
|
|
1104
|
+
test("Node handles disconnection and reconnection of a peer gracefully", async () => {
|
|
1105
|
+
// Create two nodes
|
|
1106
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
1107
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
1108
|
+
|
|
1109
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
1110
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
1111
|
+
|
|
1112
|
+
// Create a group and a map on node1
|
|
1113
|
+
const group = node1.createGroup();
|
|
1114
|
+
group.addMember("everyone", "writer");
|
|
1115
|
+
const map = group.createMap();
|
|
1116
|
+
map.set("key1", "value1", "trusting");
|
|
1117
|
+
|
|
1118
|
+
// Connect the nodes
|
|
1119
|
+
const [node1AsPeer, node2AsPeer] = connectedPeers("node1", "node2", {
|
|
1120
|
+
peer1role: "server",
|
|
1121
|
+
peer2role: "client",
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
node1.syncManager.addPeer(node2AsPeer);
|
|
1125
|
+
node2.syncManager.addPeer(node1AsPeer);
|
|
1126
|
+
|
|
1127
|
+
// Wait for initial sync
|
|
1128
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1129
|
+
|
|
1130
|
+
// Verify that node2 has received the map
|
|
1131
|
+
const mapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
1132
|
+
if (mapOnNode2 === "unavailable") {
|
|
1133
|
+
throw new Error("Map is unavailable on node2");
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
expect(expectMap(mapOnNode2.getCurrentContent()).get("key1")).toEqual(
|
|
1137
|
+
"value1",
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
// Simulate disconnection
|
|
1141
|
+
node1.syncManager.gracefulShutdown();
|
|
1142
|
+
node2.syncManager.gracefulShutdown();
|
|
1143
|
+
|
|
1144
|
+
// Make changes on node1 while disconnected
|
|
1145
|
+
map.set("key2", "value2", "trusting");
|
|
1146
|
+
|
|
1147
|
+
// Simulate reconnection
|
|
1148
|
+
const [newNode1AsPeer, newNode2AsPeer] = connectedPeers(
|
|
1149
|
+
"node11",
|
|
1150
|
+
"node22",
|
|
1151
|
+
{
|
|
1152
|
+
peer1role: "server",
|
|
1153
|
+
peer2role: "client",
|
|
1154
|
+
// trace: true,
|
|
1155
|
+
},
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
node1.syncManager.addPeer(newNode2AsPeer);
|
|
1159
|
+
node2.syncManager.addPeer(newNode1AsPeer);
|
|
1160
|
+
|
|
1161
|
+
// Wait for re-sync
|
|
1162
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1163
|
+
|
|
1164
|
+
// Verify that node2 has received the changes made during disconnection
|
|
1165
|
+
const updatedMapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
1166
|
+
if (updatedMapOnNode2 === "unavailable") {
|
|
1167
|
+
throw new Error("Updated map is unavailable on node2");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
expect(
|
|
1171
|
+
expectMap(updatedMapOnNode2.getCurrentContent()).get("key2"),
|
|
1172
|
+
).toEqual("value2");
|
|
1173
|
+
|
|
1174
|
+
// Make a new change on node2 to verify two-way sync
|
|
1175
|
+
const mapOnNode2ForEdit = await node2.loadCoValueCore(map.core.id);
|
|
1176
|
+
if (mapOnNode2ForEdit === "unavailable") {
|
|
1177
|
+
throw new Error("Updated map is unavailable on node2");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const success = mapOnNode2ForEdit.makeTransaction(
|
|
1181
|
+
[
|
|
1182
|
+
{
|
|
1183
|
+
op: "set",
|
|
1184
|
+
key: "key3",
|
|
1185
|
+
value: "value3",
|
|
1186
|
+
},
|
|
1187
|
+
],
|
|
1188
|
+
"trusting",
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
if (!success) {
|
|
1192
|
+
throw new Error("Failed to make transaction");
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Wait for sync back to node1
|
|
1196
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1197
|
+
|
|
1198
|
+
const mapOnNode1 = await node1.loadCoValueCore(map.core.id);
|
|
1199
|
+
if (mapOnNode1 === "unavailable") {
|
|
1200
|
+
throw new Error("Updated map is unavailable on node1");
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Verify that node1 has received the change from node2
|
|
1204
|
+
expect(expectMap(mapOnNode1.getCurrentContent()).get("key3")).toEqual(
|
|
1205
|
+
"value3",
|
|
1206
|
+
);
|
|
1207
|
+
});
|
|
1208
|
+
test("Concurrent modifications on multiple nodes are resolved correctly", async () => {
|
|
1209
|
+
// Create three nodes
|
|
1210
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
1211
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
1212
|
+
|
|
1213
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
1214
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
1215
|
+
|
|
1216
|
+
const [admin3, session3] = randomAnonymousAccountAndSessionID();
|
|
1217
|
+
const node3 = new LocalNode(admin3, session3, Crypto);
|
|
1218
|
+
|
|
1219
|
+
// Create a group and a map on node1
|
|
1220
|
+
const group = node1.createGroup();
|
|
1221
|
+
group.addMember("everyone", "writer");
|
|
1222
|
+
const map = group.createMap();
|
|
1223
|
+
|
|
1224
|
+
// Connect the nodes in a triangle topology
|
|
1225
|
+
const [node1AsPeerFor2, node2AsPeerFor1] = connectedPeers(
|
|
1226
|
+
"node1",
|
|
1227
|
+
"node2",
|
|
1228
|
+
{
|
|
1229
|
+
peer1role: "server",
|
|
1230
|
+
peer2role: "client",
|
|
1231
|
+
// trace: true,
|
|
1232
|
+
},
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
const [node2AsPeerFor3, node3AsPeerFor2] = connectedPeers(
|
|
1236
|
+
"node2",
|
|
1237
|
+
"node3",
|
|
1238
|
+
{
|
|
1239
|
+
peer1role: "server",
|
|
1240
|
+
peer2role: "client",
|
|
1241
|
+
// trace: true,
|
|
1242
|
+
},
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
const [node3AsPeerFor1, node1AsPeerFor3] = connectedPeers(
|
|
1246
|
+
"node3",
|
|
1247
|
+
"node1",
|
|
1248
|
+
{
|
|
1249
|
+
peer1role: "server",
|
|
1250
|
+
peer2role: "client",
|
|
1251
|
+
// trace: true,
|
|
1252
|
+
},
|
|
1253
|
+
);
|
|
1254
|
+
|
|
1255
|
+
node1.syncManager.addPeer(node2AsPeerFor1);
|
|
1256
|
+
node1.syncManager.addPeer(node3AsPeerFor1);
|
|
1257
|
+
node2.syncManager.addPeer(node1AsPeerFor2);
|
|
1258
|
+
node2.syncManager.addPeer(node3AsPeerFor2);
|
|
1259
|
+
node3.syncManager.addPeer(node1AsPeerFor3);
|
|
1260
|
+
node3.syncManager.addPeer(node2AsPeerFor3);
|
|
1261
|
+
|
|
1262
|
+
// Wait for initial sync
|
|
1263
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1264
|
+
|
|
1265
|
+
// Verify that all nodes have the map
|
|
1266
|
+
const mapOnNode1 = await node1.loadCoValueCore(map.core.id);
|
|
1267
|
+
const mapOnNode2 = await node2.loadCoValueCore(map.core.id);
|
|
1268
|
+
const mapOnNode3 = await node3.loadCoValueCore(map.core.id);
|
|
1269
|
+
|
|
1270
|
+
if (
|
|
1271
|
+
mapOnNode1 === "unavailable" ||
|
|
1272
|
+
mapOnNode2 === "unavailable" ||
|
|
1273
|
+
mapOnNode3 === "unavailable"
|
|
1274
|
+
) {
|
|
1275
|
+
throw new Error("Map is unavailable on node2 or node3");
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Perform concurrent modifications
|
|
1279
|
+
map.set("key1", "value1", "trusting");
|
|
1280
|
+
new RawCoMap(mapOnNode2).set("key2", "value2", "trusting");
|
|
1281
|
+
new RawCoMap(mapOnNode3).set("key3", "value3", "trusting");
|
|
1282
|
+
|
|
1283
|
+
// Wait for sync to complete
|
|
1284
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1285
|
+
|
|
1286
|
+
// Verify that all nodes have the same final state
|
|
1287
|
+
const finalStateNode1 = expectMap(mapOnNode1.getCurrentContent());
|
|
1288
|
+
const finalStateNode2 = expectMap(mapOnNode2.getCurrentContent());
|
|
1289
|
+
const finalStateNode3 = expectMap(mapOnNode3.getCurrentContent());
|
|
1290
|
+
|
|
1291
|
+
const expectedState = {
|
|
1292
|
+
key1: "value1",
|
|
1293
|
+
key2: "value2",
|
|
1294
|
+
key3: "value3",
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
expect(finalStateNode1.toJSON()).toEqual(expectedState);
|
|
1298
|
+
expect(finalStateNode2.toJSON()).toEqual(expectedState);
|
|
1299
|
+
expect(finalStateNode3.toJSON()).toEqual(expectedState);
|
|
1300
|
+
});
|
|
1301
|
+
test.skip("Large coValues are synced efficiently in chunks", async () => {
|
|
1302
|
+
// Create two nodes
|
|
1303
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
1304
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
1305
|
+
|
|
1306
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
1307
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
1308
|
+
|
|
1309
|
+
// Create a group and a large map on node1
|
|
1310
|
+
const group = node1.createGroup();
|
|
1311
|
+
group.addMember("everyone", "writer");
|
|
1312
|
+
const largeMap = group.createMap();
|
|
1313
|
+
|
|
1314
|
+
// Generate a large amount of data (about 10MB)
|
|
1315
|
+
const dataSize = 1 * 1024 * 1024;
|
|
1316
|
+
const chunkSize = 1024; // 1KB chunks
|
|
1317
|
+
const chunks = dataSize / chunkSize;
|
|
1318
|
+
|
|
1319
|
+
for (let i = 0; i < chunks; i++) {
|
|
1320
|
+
const key = `key${i}`;
|
|
1321
|
+
const value = Buffer.alloc(chunkSize, `value${i}`).toString(
|
|
1322
|
+
"base64",
|
|
1323
|
+
);
|
|
1324
|
+
largeMap.set(key, value, "trusting");
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Connect the nodes
|
|
1328
|
+
const [node1AsPeer, node2AsPeer] = connectedPeers("node1", "node2", {
|
|
1329
|
+
peer1role: "server",
|
|
1330
|
+
peer2role: "client",
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
node1.syncManager.addPeer(node2AsPeer);
|
|
1334
|
+
node2.syncManager.addPeer(node1AsPeer);
|
|
1335
|
+
|
|
1336
|
+
await new Promise((resolve) => setTimeout(resolve, 4000));
|
|
1337
|
+
|
|
1338
|
+
// Measure sync time
|
|
1339
|
+
const startSync = performance.now();
|
|
1340
|
+
|
|
1341
|
+
// Load the large map on node2
|
|
1342
|
+
const largeMapOnNode2 = await node2.loadCoValueCore(largeMap.core.id);
|
|
1343
|
+
if (largeMapOnNode2 === "unavailable") {
|
|
1344
|
+
throw new Error("Large map is unavailable on node2");
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const endSync = performance.now();
|
|
1348
|
+
const syncTime = endSync - startSync;
|
|
1349
|
+
|
|
1350
|
+
// Verify that all data was synced correctly
|
|
1351
|
+
const syncedMap = new RawCoMap(largeMapOnNode2);
|
|
1352
|
+
expect(
|
|
1353
|
+
Object.keys(largeMapOnNode2.getCurrentContent().toJSON() || {})
|
|
1354
|
+
.length,
|
|
1355
|
+
).toBe(chunks);
|
|
1356
|
+
|
|
1357
|
+
for (let i = 0; i < chunks; i++) {
|
|
1358
|
+
const key = `key${i}`;
|
|
1359
|
+
const expectedValue = Buffer.alloc(chunkSize, `value${i}`).toString(
|
|
1360
|
+
"base64",
|
|
1361
|
+
);
|
|
1362
|
+
expect(syncedMap.get(key)).toBe(expectedValue);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Check that sync time is reasonable (this threshold may need adjustment)
|
|
1366
|
+
const reasonableSyncTime = 10; // 30 seconds
|
|
1367
|
+
expect(syncTime).toBeLessThan(reasonableSyncTime);
|
|
1368
|
+
|
|
1369
|
+
// Check memory usage (this threshold may need adjustment)
|
|
1370
|
+
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024; // in MB
|
|
1371
|
+
const reasonableMemoryUsage = 1; // 500 MB
|
|
1372
|
+
expect(memoryUsage).toBeLessThan(reasonableMemoryUsage);
|
|
1373
|
+
});
|
|
1374
|
+
test("Node correctly handles and recovers from network partitions", async () => {
|
|
1375
|
+
// Create three nodes
|
|
1376
|
+
const [admin1, session1] = randomAnonymousAccountAndSessionID();
|
|
1377
|
+
const node1 = new LocalNode(admin1, session1, Crypto);
|
|
1378
|
+
|
|
1379
|
+
const [admin2, session2] = randomAnonymousAccountAndSessionID();
|
|
1380
|
+
const node2 = new LocalNode(admin2, session2, Crypto);
|
|
1381
|
+
|
|
1382
|
+
const [admin3, session3] = randomAnonymousAccountAndSessionID();
|
|
1383
|
+
const node3 = new LocalNode(admin3, session3, Crypto);
|
|
1384
|
+
|
|
1385
|
+
// Create a group and a map on node1
|
|
1386
|
+
const group = node1.createGroup();
|
|
1387
|
+
group.addMember("everyone", "writer");
|
|
1388
|
+
const map = group.createMap();
|
|
1389
|
+
map.set("initial", "value", "trusting");
|
|
1390
|
+
|
|
1391
|
+
// Connect all nodes
|
|
1392
|
+
const [node1AsPeerFor2, node2AsPeerFor1] = connectedPeers(
|
|
1393
|
+
"node1",
|
|
1394
|
+
"node2",
|
|
1395
|
+
{
|
|
1396
|
+
peer1role: "server",
|
|
1397
|
+
peer2role: "client",
|
|
1398
|
+
// trace: true,
|
|
1399
|
+
},
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
const [node2AsPeerFor3, node3AsPeerFor2] = connectedPeers(
|
|
1403
|
+
"node2",
|
|
1404
|
+
"node3",
|
|
1405
|
+
{
|
|
1406
|
+
peer1role: "server",
|
|
1407
|
+
peer2role: "client",
|
|
1408
|
+
// trace: true,
|
|
1409
|
+
},
|
|
1410
|
+
);
|
|
1411
|
+
|
|
1412
|
+
const [node3AsPeerFor1, node1AsPeerFor3] = connectedPeers(
|
|
1413
|
+
"node3",
|
|
1414
|
+
"node1",
|
|
1415
|
+
{
|
|
1416
|
+
peer1role: "server",
|
|
1417
|
+
peer2role: "client",
|
|
1418
|
+
// trace: true,
|
|
1419
|
+
},
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
node1.syncManager.addPeer(node2AsPeerFor1);
|
|
1423
|
+
node1.syncManager.addPeer(node3AsPeerFor1);
|
|
1424
|
+
node2.syncManager.addPeer(node1AsPeerFor2);
|
|
1425
|
+
node2.syncManager.addPeer(node3AsPeerFor2);
|
|
1426
|
+
node3.syncManager.addPeer(node1AsPeerFor3);
|
|
1427
|
+
node3.syncManager.addPeer(node2AsPeerFor3);
|
|
1428
|
+
|
|
1429
|
+
// Wait for initial sync
|
|
1430
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1431
|
+
|
|
1432
|
+
// Verify initial state
|
|
1433
|
+
const mapOnNode1Core = await node1.loadCoValueCore(map.core.id);
|
|
1434
|
+
const mapOnNode2Core = await node2.loadCoValueCore(map.core.id);
|
|
1435
|
+
const mapOnNode3Core = await node3.loadCoValueCore(map.core.id);
|
|
1436
|
+
|
|
1437
|
+
if (
|
|
1438
|
+
mapOnNode1Core === "unavailable" ||
|
|
1439
|
+
mapOnNode2Core === "unavailable" ||
|
|
1440
|
+
mapOnNode3Core === "unavailable"
|
|
1441
|
+
) {
|
|
1442
|
+
throw new Error("Map is unavailable on node2 or node3");
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// const mapOnNode1 = new RawCoMap(mapOnNode1Core);
|
|
1446
|
+
const mapOnNode2 = new RawCoMap(mapOnNode2Core);
|
|
1447
|
+
const mapOnNode3 = new RawCoMap(mapOnNode3Core);
|
|
1448
|
+
|
|
1449
|
+
expect(mapOnNode2.get("initial")).toBe("value");
|
|
1450
|
+
expect(mapOnNode3.get("initial")).toBe("value");
|
|
1451
|
+
|
|
1452
|
+
// Simulate network partition: disconnect node3 from node1 and node2
|
|
1453
|
+
node1.syncManager.peers["node3"]?.gracefulShutdown();
|
|
1454
|
+
delete node1.syncManager.peers["node3"];
|
|
1455
|
+
node2.syncManager.peers["node3"]?.gracefulShutdown();
|
|
1456
|
+
delete node2.syncManager.peers["node3"];
|
|
1457
|
+
node3.syncManager.peers["node1"]?.gracefulShutdown();
|
|
1458
|
+
delete node3.syncManager.peers["node1"];
|
|
1459
|
+
node3.syncManager.peers["node2"]?.gracefulShutdown();
|
|
1460
|
+
delete node3.syncManager.peers["node2"];
|
|
1461
|
+
|
|
1462
|
+
// Make changes on both sides of the partition
|
|
1463
|
+
map.set("node1", "partition", "trusting");
|
|
1464
|
+
mapOnNode2.set("node2", "partition", "trusting");
|
|
1465
|
+
mapOnNode3.set("node3", "partition", "trusting");
|
|
1466
|
+
|
|
1467
|
+
// Wait for sync between node1 and node2
|
|
1468
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1469
|
+
|
|
1470
|
+
// Verify that node1 and node2 are in sync, but node3 is not
|
|
1471
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).get("node1")).toBe(
|
|
1472
|
+
"partition",
|
|
1473
|
+
);
|
|
1474
|
+
expect(expectMap(mapOnNode1Core.getCurrentContent()).get("node2")).toBe(
|
|
1475
|
+
"partition",
|
|
1476
|
+
);
|
|
1477
|
+
expect(
|
|
1478
|
+
expectMap(mapOnNode1Core.getCurrentContent()).toJSON()?.node3,
|
|
1479
|
+
).toBe(undefined);
|
|
1480
|
+
|
|
1481
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).get("node1")).toBe(
|
|
1482
|
+
"partition",
|
|
1483
|
+
);
|
|
1484
|
+
expect(expectMap(mapOnNode2Core.getCurrentContent()).get("node2")).toBe(
|
|
1485
|
+
"partition",
|
|
1486
|
+
);
|
|
1487
|
+
expect(
|
|
1488
|
+
expectMap(mapOnNode2Core.getCurrentContent()).toJSON()?.node3,
|
|
1489
|
+
).toBe(undefined);
|
|
1490
|
+
|
|
1491
|
+
expect(
|
|
1492
|
+
expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node1,
|
|
1493
|
+
).toBe(undefined);
|
|
1494
|
+
expect(
|
|
1495
|
+
expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node2,
|
|
1496
|
+
).toBe(undefined);
|
|
1497
|
+
|
|
1498
|
+
expect(
|
|
1499
|
+
expectMap(mapOnNode3Core.getCurrentContent()).toJSON()?.node3,
|
|
1500
|
+
).toBe("partition");
|
|
1501
|
+
|
|
1502
|
+
// Restore connectivity
|
|
1503
|
+
const [newNode3AsPeerFor1, newNode1AsPeerFor3] = connectedPeers(
|
|
1504
|
+
"node3",
|
|
1505
|
+
"node1",
|
|
1506
|
+
{
|
|
1507
|
+
peer1role: "server",
|
|
1508
|
+
peer2role: "client",
|
|
1509
|
+
trace: true,
|
|
1510
|
+
},
|
|
1511
|
+
);
|
|
1512
|
+
|
|
1513
|
+
const [newNode3AsPeerFor2, newNode2AsPeerFor3] = connectedPeers(
|
|
1514
|
+
"node3",
|
|
1515
|
+
"node2",
|
|
1516
|
+
{
|
|
1517
|
+
peer1role: "server",
|
|
1518
|
+
peer2role: "client",
|
|
1519
|
+
trace: true,
|
|
1520
|
+
},
|
|
1521
|
+
);
|
|
1522
|
+
|
|
1523
|
+
node1.syncManager.addPeer(newNode3AsPeerFor1);
|
|
1524
|
+
node2.syncManager.addPeer(newNode3AsPeerFor2);
|
|
1525
|
+
node3.syncManager.addPeer(newNode1AsPeerFor3);
|
|
1526
|
+
node3.syncManager.addPeer(newNode2AsPeerFor3);
|
|
1527
|
+
|
|
1528
|
+
// Wait for re-sync
|
|
1529
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1530
|
+
|
|
1531
|
+
// Verify final state: all nodes should have all changes
|
|
1532
|
+
const finalStateNode1 = expectMap(
|
|
1533
|
+
mapOnNode1Core.getCurrentContent(),
|
|
1534
|
+
).toJSON();
|
|
1535
|
+
const finalStateNode2 = expectMap(
|
|
1536
|
+
mapOnNode2Core.getCurrentContent(),
|
|
1537
|
+
).toJSON();
|
|
1538
|
+
const finalStateNode3 = expectMap(
|
|
1539
|
+
mapOnNode3Core.getCurrentContent(),
|
|
1540
|
+
).toJSON();
|
|
1541
|
+
|
|
1542
|
+
const expectedFinalState = {
|
|
1543
|
+
initial: "value",
|
|
1544
|
+
node1: "partition",
|
|
1545
|
+
node2: "partition",
|
|
1546
|
+
node3: "partition",
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1549
|
+
expect(finalStateNode1).toEqual(expectedFinalState);
|
|
1550
|
+
expect(finalStateNode2).toEqual(expectedFinalState);
|
|
1551
|
+
expect(finalStateNode3).toEqual(expectedFinalState);
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1092
1555
|
function groupContentEx(group: RawGroup) {
|
|
1093
1556
|
return {
|
|
1094
1557
|
action: "content",
|
|
@@ -1096,7 +1559,7 @@ function groupContentEx(group: RawGroup) {
|
|
|
1096
1559
|
};
|
|
1097
1560
|
}
|
|
1098
1561
|
|
|
1099
|
-
function _admContEx(adminID:
|
|
1562
|
+
function _admContEx(adminID: AccountID) {
|
|
1100
1563
|
return {
|
|
1101
1564
|
action: "content",
|
|
1102
1565
|
id: adminID,
|
|
@@ -1110,7 +1573,7 @@ function groupStateEx(group: RawGroup) {
|
|
|
1110
1573
|
};
|
|
1111
1574
|
}
|
|
1112
1575
|
|
|
1113
|
-
function _admStateEx(adminID:
|
|
1576
|
+
function _admStateEx(adminID: AccountID) {
|
|
1114
1577
|
return {
|
|
1115
1578
|
action: "known",
|
|
1116
1579
|
id: adminID,
|
package/src/tests/testUtils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect } from "vitest";
|
|
2
|
+
import { newRandomSessionID } from "../coValueCore.js";
|
|
2
3
|
import { LocalNode } from "../localNode.js";
|
|
3
4
|
import { expectGroup } from "../typeUtils/expectGroup.js";
|
|
4
5
|
import { ControlledAgent } from "../coValues/account.js";
|
|
@@ -13,7 +14,7 @@ export function randomAnonymousAccountAndSessionID(): [
|
|
|
13
14
|
] {
|
|
14
15
|
const agentSecret = Crypto.newRandomAgentSecret();
|
|
15
16
|
|
|
16
|
-
const sessionID =
|
|
17
|
+
const sessionID = newRandomSessionID(Crypto.getAgentID(agentSecret));
|
|
17
18
|
|
|
18
19
|
return [new ControlledAgent(agentSecret, Crypto), sessionID];
|
|
19
20
|
}
|