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.
Files changed (76) hide show
  1. package/.turbo/turbo-test.log +321 -253
  2. package/CHANGELOG.md +3 -2
  3. package/dist/PeerState.js +58 -0
  4. package/dist/PeerState.js.map +1 -0
  5. package/dist/PriorityBasedMessageQueue.js +51 -0
  6. package/dist/PriorityBasedMessageQueue.js.map +1 -0
  7. package/dist/base64url.js.map +1 -1
  8. package/dist/coValue.js.map +1 -1
  9. package/dist/coValueCore.js +3 -0
  10. package/dist/coValueCore.js.map +1 -1
  11. package/dist/coValues/account.js +2 -2
  12. package/dist/coValues/account.js.map +1 -1
  13. package/dist/coValues/coList.js.map +1 -1
  14. package/dist/coValues/coMap.js.map +1 -1
  15. package/dist/coValues/coStream.js +14 -15
  16. package/dist/coValues/coStream.js.map +1 -1
  17. package/dist/coValues/group.js +8 -8
  18. package/dist/coValues/group.js.map +1 -1
  19. package/dist/coreToCoValue.js.map +1 -1
  20. package/dist/crypto/PureJSCrypto.js.map +1 -1
  21. package/dist/crypto/WasmCrypto.js.map +1 -1
  22. package/dist/crypto/crypto.js.map +1 -1
  23. package/dist/index.js +2 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/jsonStringify.js.map +1 -1
  26. package/dist/localNode.js +2 -2
  27. package/dist/localNode.js.map +1 -1
  28. package/dist/permissions.js.map +1 -1
  29. package/dist/priority.js +31 -0
  30. package/dist/priority.js.map +1 -0
  31. package/dist/storage/FileSystem.js.map +1 -1
  32. package/dist/storage/chunksAndKnownStates.js +2 -0
  33. package/dist/storage/chunksAndKnownStates.js.map +1 -1
  34. package/dist/storage/index.js.map +1 -1
  35. package/dist/streamUtils.js.map +1 -1
  36. package/dist/sync.js +7 -18
  37. package/dist/sync.js.map +1 -1
  38. package/dist/tests/PeerState.test.js +80 -0
  39. package/dist/tests/PeerState.test.js.map +1 -0
  40. package/dist/tests/PriorityBasedMessageQueue.test.js +97 -0
  41. package/dist/tests/PriorityBasedMessageQueue.test.js.map +1 -0
  42. package/dist/tests/coMap.test.js.map +1 -1
  43. package/dist/tests/coStream.test.js +34 -1
  44. package/dist/tests/coStream.test.js.map +1 -1
  45. package/dist/tests/permissions.test.js.map +1 -1
  46. package/dist/tests/priority.test.js +61 -0
  47. package/dist/tests/priority.test.js.map +1 -0
  48. package/dist/tests/sync.test.js +323 -12
  49. package/dist/tests/sync.test.js.map +1 -1
  50. package/dist/tests/testUtils.js.map +1 -1
  51. package/dist/typeUtils/accountOrAgentIDfromSessionID.js.map +1 -1
  52. package/dist/typeUtils/expectGroup.js.map +1 -1
  53. package/dist/typeUtils/isAccountID.js.map +1 -1
  54. package/package.json +3 -3
  55. package/src/PeerState.ts +74 -0
  56. package/src/PriorityBasedMessageQueue.ts +77 -0
  57. package/src/coValueCore.ts +10 -7
  58. package/src/coValues/account.ts +5 -5
  59. package/src/coValues/coList.ts +4 -4
  60. package/src/coValues/coMap.ts +3 -3
  61. package/src/coValues/coStream.ts +29 -26
  62. package/src/coValues/group.ts +11 -15
  63. package/src/ids.ts +2 -2
  64. package/src/index.ts +5 -5
  65. package/src/localNode.ts +11 -12
  66. package/src/permissions.ts +5 -5
  67. package/src/priority.ts +39 -0
  68. package/src/storage/chunksAndKnownStates.ts +2 -0
  69. package/src/sync.ts +19 -34
  70. package/src/tests/PeerState.test.ts +92 -0
  71. package/src/tests/PriorityBasedMessageQueue.test.ts +111 -0
  72. package/src/tests/coStream.test.ts +58 -1
  73. package/src/tests/priority.test.ts +75 -0
  74. package/src/tests/sync.test.ts +487 -25
  75. package/src/typeUtils/accountOrAgentIDfromSessionID.ts +3 -3
  76. package/src/typeUtils/isAccountID.ts +2 -2
@@ -1,15 +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 { RawAccountID } from "../coValues/account.js";
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 { newRandomSessionID } from "../coValueCore.js";
12
+ import { CoValueHeader, newRandomSessionID } from "../coValueCore.js";
13
+ import { getPriorityFromHeader } from "../priority.js";
13
14
 
14
15
  const Crypto = await WasmCrypto.create();
15
16
 
@@ -56,16 +57,18 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
56
57
 
57
58
  const newContentMsg = (await outRxQ.next()).value;
58
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
+
59
68
  expect(newContentMsg).toEqual({
60
69
  action: "content",
61
70
  id: map.core.id,
62
- header: {
63
- type: "comap",
64
- ruleset: { type: "ownedByGroup", group: group.id },
65
- meta: null,
66
- createdAt: map.core.header.createdAt,
67
- uniqueness: map.core.header.uniqueness,
68
- },
71
+ header: expectedHeader,
69
72
  new: {
70
73
  [node.currentSessionID]: {
71
74
  after: 0,
@@ -87,6 +90,7 @@ test("Node replies with initial tx and header to empty subscribe", async () => {
87
90
  .lastSignature!,
88
91
  },
89
92
  },
93
+ priority: getPriorityFromHeader(map.core.header),
90
94
  } satisfies SyncMessage);
91
95
  });
92
96
 
@@ -161,6 +165,7 @@ test("Node replies with only new tx to subscribe with some known state", async (
161
165
  .lastSignature!,
162
166
  },
163
167
  },
168
+ priority: getPriorityFromHeader(map.core.header),
164
169
  } satisfies SyncMessage);
165
170
  });
166
171
  test.todo(
@@ -215,6 +220,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
215
220
  id: map.core.id,
216
221
  header: map.core.header,
217
222
  new: {},
223
+ priority: getPriorityFromHeader(map.core.header),
218
224
  } satisfies SyncMessage);
219
225
 
220
226
  map.set("hello", "world", "trusting");
@@ -245,6 +251,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
245
251
  .lastSignature!,
246
252
  },
247
253
  },
254
+ priority: getPriorityFromHeader(map.core.header),
248
255
  } satisfies SyncMessage);
249
256
 
250
257
  map.set("goodbye", "world", "trusting");
@@ -275,6 +282,7 @@ test("After subscribing, node sends own known state and new txs to peer", async
275
282
  .lastSignature!,
276
283
  },
277
284
  },
285
+ priority: getPriorityFromHeader(map.core.header),
278
286
  } satisfies SyncMessage);
279
287
  });
280
288
 
@@ -350,6 +358,7 @@ test("Client replies with known new content to tellKnownState from server", asyn
350
358
  .lastSignature!,
351
359
  },
352
360
  },
361
+ priority: getPriorityFromHeader(map.core.header),
353
362
  } satisfies SyncMessage);
354
363
  });
355
364
 
@@ -401,6 +410,7 @@ test("No matter the optimistic known state, node respects invalid known state me
401
410
  id: map.core.id,
402
411
  header: map.core.header,
403
412
  new: {},
413
+ priority: getPriorityFromHeader(map.core.header),
404
414
  } satisfies SyncMessage);
405
415
 
406
416
  map.set("hello", "world", "trusting");
@@ -448,6 +458,7 @@ test("No matter the optimistic known state, node respects invalid known state me
448
458
  .lastSignature!,
449
459
  },
450
460
  },
461
+ priority: getPriorityFromHeader(map.core.header),
451
462
  } satisfies SyncMessage);
452
463
  });
453
464
 
@@ -561,6 +572,7 @@ test.todo(
561
572
  )!.lastSignature!,
562
573
  },
563
574
  },
575
+ priority: getPriorityFromHeader(map.core.header),
564
576
  } satisfies SyncMessage);
565
577
  },
566
578
  );
@@ -611,6 +623,7 @@ test.skip("If we add a server peer, newly created coValues are auto-subscribed t
611
623
  id: map.core.id,
612
624
  header: map.core.header,
613
625
  new: {},
626
+ priority: getPriorityFromHeader(map.core.header),
614
627
  } satisfies SyncMessage);
615
628
  });
616
629
 
@@ -777,6 +790,7 @@ test.skip("When replaying creation and transactions of a coValue as new content,
777
790
  id: map.core.id,
778
791
  header: map.core.header,
779
792
  new: {},
793
+ priority: getPriorityFromHeader(map.core.header),
780
794
  } satisfies SyncMessage);
781
795
 
782
796
  await inTx2.push(mapSubscriptionMsg);
@@ -851,7 +865,7 @@ test("Can sync a coValue through a server to another client", async () => {
851
865
 
852
866
  const server = new LocalNode(serverUser, serverSession, Crypto);
853
867
 
854
- const [serverAsPeerForClient1, client1AsPeer] = await connectedPeers(
868
+ const [serverAsPeerForClient1, client1AsPeer] = connectedPeers(
855
869
  "serverFor1",
856
870
  "client1",
857
871
  {
@@ -903,22 +917,18 @@ test("Can sync a coValue with private transactions through a server to another c
903
917
 
904
918
  const server = new LocalNode(serverUser, serverSession, Crypto);
905
919
 
906
- const [serverAsPeer, client1AsPeer] = await connectedPeers(
907
- "server",
908
- "client1",
909
- {
910
- trace: true,
911
- peer1role: "server",
912
- peer2role: "client",
913
- },
914
- );
920
+ const [serverAsPeer, client1AsPeer] = connectedPeers("server", "client1", {
921
+ trace: true,
922
+ peer1role: "server",
923
+ peer2role: "client",
924
+ });
915
925
 
916
926
  client1.syncManager.addPeer(serverAsPeer);
917
927
  server.syncManager.addPeer(client1AsPeer);
918
928
 
919
929
  const client2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
920
930
 
921
- const [serverAsOtherPeer, client2AsPeer] = await connectedPeers(
931
+ const [serverAsOtherPeer, client2AsPeer] = connectedPeers(
922
932
  "server",
923
933
  "client2",
924
934
  {
@@ -1066,7 +1076,7 @@ test("If we start loading a coValue before connecting to a peer that has it, it
1066
1076
 
1067
1077
  const node2 = new LocalNode(admin, newRandomSessionID(admin.id), Crypto);
1068
1078
 
1069
- const [node1asPeer, node2asPeer] = await connectedPeers("peer1", "peer2", {
1079
+ const [node1asPeer, node2asPeer] = connectedPeers("peer1", "peer2", {
1070
1080
  peer1role: "server",
1071
1081
  peer2role: "client",
1072
1082
  trace: true,
@@ -1090,6 +1100,458 @@ test("If we start loading a coValue before connecting to a peer that has it, it
1090
1100
  );
1091
1101
  });
1092
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
+
1093
1555
  function groupContentEx(group: RawGroup) {
1094
1556
  return {
1095
1557
  action: "content",
@@ -1097,7 +1559,7 @@ function groupContentEx(group: RawGroup) {
1097
1559
  };
1098
1560
  }
1099
1561
 
1100
- function _admContEx(adminID: RawAccountID) {
1562
+ function _admContEx(adminID: AccountID) {
1101
1563
  return {
1102
1564
  action: "content",
1103
1565
  id: adminID,
@@ -1111,7 +1573,7 @@ function groupStateEx(group: RawGroup) {
1111
1573
  };
1112
1574
  }
1113
1575
 
1114
- function _admStateEx(adminID: RawAccountID) {
1576
+ function _admStateEx(adminID: AccountID) {
1115
1577
  return {
1116
1578
  action: "known",
1117
1579
  id: adminID,
@@ -1,9 +1,9 @@
1
1
  import { AgentID, SessionID } from "../ids.js";
2
- import { RawAccountID } from "../coValues/account.js";
2
+ import { AccountID } from "../coValues/account.js";
3
3
 
4
4
  export function accountOrAgentIDfromSessionID(
5
5
  sessionID: SessionID,
6
- ): RawAccountID | AgentID {
6
+ ): AccountID | AgentID {
7
7
  const until = sessionID.indexOf("_session");
8
- return sessionID.slice(0, until) as RawAccountID | AgentID;
8
+ return sessionID.slice(0, until) as AccountID | AgentID;
9
9
  }
@@ -1,6 +1,6 @@
1
- import type { RawAccountID } from "../coValues/account.js";
1
+ import type { AccountID } from "../coValues/account.js";
2
2
  import type { AgentID } from "../ids.js";
3
3
 
4
- export function isAccountID(id: RawAccountID | AgentID): id is RawAccountID {
4
+ export function isAccountID(id: AccountID | AgentID): id is AccountID {
5
5
  return id.startsWith("co_");
6
6
  }