cojson 0.17.10 → 0.17.12

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 (78) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/coValueCore/SessionMap.d.ts +3 -2
  4. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  5. package/dist/coValueCore/SessionMap.js.map +1 -1
  6. package/dist/coValueCore/coValueCore.d.ts +9 -4
  7. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  8. package/dist/coValueCore/coValueCore.js +16 -8
  9. package/dist/coValueCore/coValueCore.js.map +1 -1
  10. package/dist/coValueCore/verifiedState.d.ts +2 -2
  11. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  12. package/dist/coValueCore/verifiedState.js +1 -1
  13. package/dist/coValueCore/verifiedState.js.map +1 -1
  14. package/dist/coValues/group.d.ts.map +1 -1
  15. package/dist/coValues/group.js +6 -2
  16. package/dist/coValues/group.js.map +1 -1
  17. package/dist/crypto/PureJSCrypto.d.ts +2 -2
  18. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  19. package/dist/crypto/PureJSCrypto.js +3 -0
  20. package/dist/crypto/PureJSCrypto.js.map +1 -1
  21. package/dist/crypto/WasmCrypto.d.ts +1 -1
  22. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  23. package/dist/crypto/WasmCrypto.js.map +1 -1
  24. package/dist/crypto/crypto.d.ts +1 -1
  25. package/dist/crypto/crypto.d.ts.map +1 -1
  26. package/dist/localNode.js +1 -1
  27. package/dist/localNode.js.map +1 -1
  28. package/dist/permissions.d.ts +17 -1
  29. package/dist/permissions.d.ts.map +1 -1
  30. package/dist/permissions.js.map +1 -1
  31. package/dist/queue/LocalTransactionsSyncQueue.d.ts +15 -0
  32. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -1
  33. package/dist/queue/LocalTransactionsSyncQueue.js +25 -0
  34. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -1
  35. package/dist/sync.d.ts +8 -5
  36. package/dist/sync.d.ts.map +1 -1
  37. package/dist/sync.js +78 -66
  38. package/dist/sync.js.map +1 -1
  39. package/dist/tests/PureJSCrypto.test.js +15 -1
  40. package/dist/tests/PureJSCrypto.test.js.map +1 -1
  41. package/dist/tests/WasmCrypto.test.js +1 -1
  42. package/dist/tests/WasmCrypto.test.js.map +1 -1
  43. package/dist/tests/coValueCore.test.js +2 -2
  44. package/dist/tests/coValueCore.test.js.map +1 -1
  45. package/dist/tests/group.addMember.test.js +6 -11
  46. package/dist/tests/group.addMember.test.js.map +1 -1
  47. package/dist/tests/sync.known.test.d.ts +2 -0
  48. package/dist/tests/sync.known.test.d.ts.map +1 -0
  49. package/dist/tests/sync.known.test.js +78 -0
  50. package/dist/tests/sync.known.test.js.map +1 -0
  51. package/dist/tests/sync.load.test.js +40 -1
  52. package/dist/tests/sync.load.test.js.map +1 -1
  53. package/dist/tests/sync.sharding.test.d.ts +2 -0
  54. package/dist/tests/sync.sharding.test.d.ts.map +1 -0
  55. package/dist/tests/sync.sharding.test.js +51 -0
  56. package/dist/tests/sync.sharding.test.js.map +1 -0
  57. package/dist/tests/sync.test.js +30 -1
  58. package/dist/tests/sync.test.js.map +1 -1
  59. package/package.json +2 -2
  60. package/src/coValueCore/SessionMap.ts +4 -5
  61. package/src/coValueCore/coValueCore.ts +42 -32
  62. package/src/coValueCore/verifiedState.ts +1 -3
  63. package/src/coValues/group.ts +10 -2
  64. package/src/crypto/PureJSCrypto.ts +6 -2
  65. package/src/crypto/WasmCrypto.ts +1 -1
  66. package/src/crypto/crypto.ts +1 -1
  67. package/src/localNode.ts +1 -1
  68. package/src/permissions.ts +17 -1
  69. package/src/queue/LocalTransactionsSyncQueue.ts +32 -1
  70. package/src/sync.ts +103 -80
  71. package/src/tests/PureJSCrypto.test.ts +25 -2
  72. package/src/tests/WasmCrypto.test.ts +0 -2
  73. package/src/tests/coValueCore.test.ts +0 -4
  74. package/src/tests/group.addMember.test.ts +69 -63
  75. package/src/tests/sync.known.test.ts +109 -0
  76. package/src/tests/sync.load.test.ts +52 -0
  77. package/src/tests/sync.sharding.test.ts +76 -0
  78. package/src/tests/sync.test.ts +43 -2
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, test } from "vitest";
2
2
  import { expectMap } from "../coValue.js";
3
3
  import {
4
4
  SyncMessagesLog,
5
- TEST_NODE_CONFIG,
6
5
  loadCoValueOrFail,
7
6
  setupTestAccount,
8
7
  setupTestNode,
@@ -220,68 +219,75 @@ describe("Group.addMember", () => {
220
219
  expect(personOnReaderNode.get("name")).toEqual(undefined);
221
220
  });
222
221
 
223
- test("an admin should not be able downgrade an admin", async () => {
224
- const admin = await setupTestAccount({
225
- connected: true,
226
- });
227
-
228
- const otherAdmin = await setupTestAccount({
229
- connected: true,
230
- });
231
-
232
- const group = admin.node.createGroup();
233
- const person = group.createMap({
234
- name: "John Doe",
235
- });
236
-
237
- const otherAdminOnAdminNode = await loadCoValueOrFail(
238
- admin.node,
239
- otherAdmin.accountID,
240
- );
241
- group.addMember(otherAdminOnAdminNode, "admin");
242
-
243
- // Try to downgrade other admin
244
- try {
245
- group.addMember(otherAdminOnAdminNode, "writer");
246
- } catch (e) {
247
- expect(e).toBeDefined();
248
- }
249
-
250
- expect(group.roleOf(otherAdmin.accountID)).toEqual("admin");
251
-
252
- // Verify other admin still has admin access by adding a new member
253
- const reader = await setupTestAccount({
254
- connected: true,
255
- });
256
-
257
- const readerOnOtherAdminNode = await loadCoValueOrFail(
258
- otherAdmin.node,
259
- reader.accountID,
260
- );
261
- group.addMember(readerOnOtherAdminNode, "reader");
262
-
263
- const personOnReaderNode = await loadCoValueOrFail(reader.node, person.id);
264
-
265
- await waitFor(() => {
266
- expect(
267
- expectMap(personOnReaderNode.core.getCurrentContent()).get("name"),
268
- ).toEqual("John Doe");
269
- });
270
- });
271
-
272
- test("an admin should be able downgrade themselves", async () => {
273
- const admin = await setupTestAccount({
274
- connected: true,
275
- });
276
-
277
- const group = admin.node.createGroup();
278
-
279
- const account = await loadCoValueOrFail(admin.node, admin.accountID);
280
-
281
- // Downgrade self to writer
282
- group.addMember(account, "writer");
283
- expect(group.roleOf(admin.accountID)).toEqual("writer");
284
- });
222
+ test.each(["writer", "reader", "writeOnly"] as const)(
223
+ "an admin should not be able to downgrade an admin to %s",
224
+ async (targetRole) => {
225
+ const admin = await setupTestAccount({
226
+ connected: true,
227
+ });
228
+
229
+ const otherAdmin = await setupTestAccount({
230
+ connected: true,
231
+ });
232
+
233
+ const group = admin.node.createGroup();
234
+ const person = group.createMap({
235
+ name: "John Doe",
236
+ });
237
+
238
+ const otherAdminOnAdminNode = await loadCoValueOrFail(
239
+ admin.node,
240
+ otherAdmin.accountID,
241
+ );
242
+ group.addMember(otherAdminOnAdminNode, "admin");
243
+
244
+ // Try to downgrade other admin
245
+ expect(() => group.addMember(otherAdminOnAdminNode, targetRole)).toThrow(
246
+ "Administrators cannot demote other administrators in a group",
247
+ );
248
+
249
+ expect(group.roleOf(otherAdmin.accountID)).toEqual("admin");
250
+
251
+ // Verify other admin still has admin access by adding a new member
252
+ const reader = await setupTestAccount({
253
+ connected: true,
254
+ });
255
+
256
+ const readerOnOtherAdminNode = await loadCoValueOrFail(
257
+ otherAdmin.node,
258
+ reader.accountID,
259
+ );
260
+ group.addMember(readerOnOtherAdminNode, "reader");
261
+
262
+ const personOnReaderNode = await loadCoValueOrFail(
263
+ reader.node,
264
+ person.id,
265
+ );
266
+
267
+ await waitFor(() => {
268
+ expect(
269
+ expectMap(personOnReaderNode.core.getCurrentContent()).get("name"),
270
+ ).toEqual("John Doe");
271
+ });
272
+ },
273
+ );
274
+
275
+ test.each(["writer", "reader", "writeOnly"] as const)(
276
+ "an admin should be able downgrade themselves to %s",
277
+ async (targetRole) => {
278
+ const admin = await setupTestAccount({
279
+ connected: true,
280
+ });
281
+
282
+ const group = admin.node.createGroup();
283
+
284
+ const account = await loadCoValueOrFail(admin.node, admin.accountID);
285
+
286
+ // Downgrade self to target role
287
+ group.addMember(account, targetRole);
288
+ expect(group.roleOf(admin.accountID)).toEqual(targetRole);
289
+ },
290
+ );
285
291
 
286
292
  test("an admin should be able downgrade a writeOnly to reader", async () => {
287
293
  const admin = await setupTestAccount({
@@ -0,0 +1,109 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import { setCoValueLoadingRetryDelay } from "../config";
3
+ import {
4
+ SyncMessagesLog,
5
+ TEST_NODE_CONFIG,
6
+ blockMessageTypeOnOutgoingPeer,
7
+ getSyncServerConnectedPeer,
8
+ loadCoValueOrFail,
9
+ setupTestAccount,
10
+ setupTestNode,
11
+ } from "./testUtils";
12
+
13
+ let jazzCloud: ReturnType<typeof setupTestNode>;
14
+
15
+ // Set a short timeout to make the tests on unavailable complete faster
16
+ setCoValueLoadingRetryDelay(100);
17
+
18
+ beforeEach(async () => {
19
+ // We want to simulate a real world communication that happens asynchronously
20
+ TEST_NODE_CONFIG.withAsyncPeers = true;
21
+
22
+ SyncMessagesLog.clear();
23
+ jazzCloud = setupTestNode({ isSyncServer: true });
24
+ });
25
+
26
+ describe("sending known coValues", () => {
27
+ test("dependencies are included when responding to a client", async () => {
28
+ const group = jazzCloud.node.createGroup();
29
+ const map = group.createMap();
30
+ map.set("hello", "world", "trusting");
31
+
32
+ const { node: client } = setupTestNode({
33
+ connected: true,
34
+ });
35
+
36
+ map.set("hello2", "world2", "trusting");
37
+ await map.core.waitForSync();
38
+
39
+ const mapOnClient = await loadCoValueOrFail(client, map.id);
40
+ expect(mapOnClient.get("hello")).toEqual("world");
41
+ expect(mapOnClient.get("hello2")).toEqual("world2");
42
+
43
+ expect(
44
+ SyncMessagesLog.getMessages({
45
+ Group: group.core,
46
+ Map: map.core,
47
+ }),
48
+ ).toMatchInlineSnapshot(`
49
+ [
50
+ "client -> server | LOAD Map sessions: empty",
51
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
52
+ "server -> client | CONTENT Map header: true new: After: 0 New: 2",
53
+ "client -> server | KNOWN Group sessions: header/3",
54
+ "client -> server | KNOWN Map sessions: header/2",
55
+ ]
56
+ `);
57
+ });
58
+
59
+ test("dependencies are excluded when responding to a server", async () => {
60
+ // Create a disconnected client
61
+ const { node: client, accountID } = await setupTestAccount({
62
+ connected: false,
63
+ });
64
+ const account = client.expectCurrentAccount(accountID);
65
+
66
+ // Prepare a group -- this will be a non-account dependency of a forthcoming map.
67
+ const group = client.createGroup();
68
+ group.addMember("everyone", "writer");
69
+
70
+ // Let the queue drain
71
+ await new Promise((resolve) => setTimeout(resolve, 1));
72
+
73
+ // Disable transaction verification on the server so it doesn't ask for dependencies.
74
+ jazzCloud.node.syncManager.disableTransactionVerification();
75
+
76
+ // Connect the client, but don't setup syncing just yet...
77
+ const { peer } = getSyncServerConnectedPeer({
78
+ peerId: client.getCurrentAgent().id,
79
+ syncServer: jazzCloud.node,
80
+ });
81
+
82
+ // Disable reconciliation while we setup syncing because we don't want the
83
+ // server to know about our forthcoming map's dependencies (group + account).
84
+ const blocker = blockMessageTypeOnOutgoingPeer(peer, "load", {});
85
+ client.syncManager.addPeer(peer);
86
+ blocker.unblock();
87
+
88
+ // Create a map and set a value on it, this will trigger:
89
+ // - CONTENT from client to server
90
+ // - KNOWN from server to client
91
+ //
92
+ // We don't expect any more messages to be sent from client to server in this
93
+ // case because clients shouldn't greedily send dependencies to a server.
94
+ const map = group.createMap();
95
+ await map.core.waitForSync();
96
+
97
+ const syncMessages = SyncMessagesLog.getMessages({
98
+ Account: account.core,
99
+ Group: group.core,
100
+ Map: map.core,
101
+ });
102
+ expect(
103
+ syncMessages.some(
104
+ (msg) =>
105
+ msg.includes("CONTENT Account") || msg.includes("CONTENT Group"),
106
+ ),
107
+ ).toBe(false);
108
+ });
109
+ });
@@ -9,6 +9,7 @@ import {
9
9
  SyncMessagesLog,
10
10
  TEST_NODE_CONFIG,
11
11
  blockMessageTypeOnOutgoingPeer,
12
+ getSyncServerConnectedPeer,
12
13
  loadCoValueOrFail,
13
14
  setupTestAccount,
14
15
  setupTestNode,
@@ -1034,4 +1035,55 @@ describe("loading coValues from server", () => {
1034
1035
 
1035
1036
  vi.useRealTimers();
1036
1037
  });
1038
+
1039
+ test("should not request dependencies if transaction verification is disabled", async () => {
1040
+ // Create a disconnected client
1041
+ const { node: client, accountID } = await setupTestAccount({
1042
+ connected: false,
1043
+ });
1044
+ const account = client.expectCurrentAccount(accountID);
1045
+
1046
+ // Prepare a group -- this will be a non-account dependency of a forthcoming map.
1047
+ const group = client.createGroup();
1048
+ group.addMember("everyone", "writer");
1049
+
1050
+ // Create a sync server and disable transaction verification
1051
+ const syncServer = await setupTestAccount({ isSyncServer: true });
1052
+ syncServer.node.syncManager.disableTransactionVerification();
1053
+
1054
+ // Connect the client, but don't setup syncing just yet...
1055
+ const { peer } = getSyncServerConnectedPeer({
1056
+ peerId: client.getCurrentAgent().id,
1057
+ syncServer: syncServer.node,
1058
+ });
1059
+
1060
+ // Disable reconciliation while we setup syncing because we don't want the
1061
+ // server to know about our forthcoming map's dependencies (group + account).
1062
+ const blocker = blockMessageTypeOnOutgoingPeer(peer, "load", {});
1063
+ client.syncManager.addPeer(peer);
1064
+ blocker.unblock();
1065
+
1066
+ // Create a map and set a value on it.
1067
+ // If transaction verification were enabled, this would trigger LOAD messages
1068
+ // from the server to the client asking for the group and account. However, we
1069
+ // don't expect to see those messages since we disabled transaction verification.
1070
+ const map = group.createMap();
1071
+ map.set("hello", "world");
1072
+ await map.core.waitForSync();
1073
+
1074
+ const syncMessages = SyncMessagesLog.getMessages({
1075
+ Account: account.core,
1076
+ Group: group.core,
1077
+ Map: map.core,
1078
+ });
1079
+ expect(
1080
+ syncMessages.some(
1081
+ (msg) => msg.includes("LOAD Account") || msg.includes("LOAD Group"),
1082
+ ),
1083
+ ).toBe(false);
1084
+
1085
+ // Verify the map is available on the server (transaction was accepted)
1086
+ const mapOnServerCore = await syncServer.node.loadCoValueCore(map.core.id);
1087
+ expect(mapOnServerCore.isAvailable()).toBe(true);
1088
+ });
1037
1089
  });
@@ -0,0 +1,76 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import { setCoValueLoadingRetryDelay } from "../config";
3
+ import {
4
+ SyncMessagesLog,
5
+ TEST_NODE_CONFIG,
6
+ blockMessageTypeOnOutgoingPeer,
7
+ getSyncServerConnectedPeer,
8
+ loadCoValueOrFail,
9
+ setupTestAccount,
10
+ setupTestNode,
11
+ } from "./testUtils";
12
+ import { Peer } from "../exports";
13
+ import { ServerPeerSelector } from "../sync";
14
+
15
+ beforeEach(async () => {
16
+ // We want to simulate a real world communication that happens asynchronously
17
+ TEST_NODE_CONFIG.withAsyncPeers = true;
18
+
19
+ SyncMessagesLog.clear();
20
+ });
21
+
22
+ describe("sharding", () => {
23
+ test("server peers are not filtered by default", async () => {
24
+ const { node: client } = setupTestNode({
25
+ connected: false,
26
+ });
27
+ const group = client.createGroup();
28
+
29
+ const allPeers: Peer[] = [];
30
+ for (let i = 0; i < 5; i++) {
31
+ const syncServer = await setupTestAccount({ isSyncServer: true });
32
+
33
+ const { peer } = getSyncServerConnectedPeer({
34
+ peerId: client.getCurrentAgent().id,
35
+ syncServer: syncServer.node,
36
+ });
37
+
38
+ client.syncManager.addPeer(peer);
39
+ allPeers.push(peer);
40
+ }
41
+
42
+ const serverPeers = client.syncManager.getServerPeers(group.id);
43
+ expect(serverPeers.map((p) => p.id)).toEqual(allPeers.map((p) => p.id));
44
+ });
45
+
46
+ test("server peers are filtered when a serverPeerSelector is set", async () => {
47
+ const firstAlphabetical: ServerPeerSelector = (id, serverPeers) => {
48
+ return serverPeers.sort((a, b) => a.id.localeCompare(b.id)).slice(0, 1);
49
+ };
50
+
51
+ const { node: client } = setupTestNode({
52
+ connected: false,
53
+ });
54
+ client.syncManager.serverPeerSelector = firstAlphabetical;
55
+ const group = client.createGroup();
56
+
57
+ const allPeers: Peer[] = [];
58
+ for (let i = 0; i < 5; i++) {
59
+ const syncServer = await setupTestAccount({ isSyncServer: true });
60
+
61
+ const { peer } = getSyncServerConnectedPeer({
62
+ peerId: client.getCurrentAgent().id,
63
+ syncServer: syncServer.node,
64
+ });
65
+
66
+ client.syncManager.addPeer(peer);
67
+ allPeers.push(peer);
68
+ }
69
+
70
+ const serverPeers = client.syncManager.getServerPeers(group.id);
71
+ expect(serverPeers.length).toBe(1);
72
+ expect(serverPeers[0]!.id).toEqual(
73
+ allPeers.sort((a, b) => a.id.localeCompare(b.id))[0]!.id,
74
+ );
75
+ });
76
+ });
@@ -180,9 +180,7 @@ test("should not verify transactions when SyncManager has verification disabled"
180
180
  madeAt: Date.now(),
181
181
  },
182
182
  ],
183
- undefined,
184
183
  Crypto.sign(agent.currentSignerSecret(), "hash_z12345678"),
185
- "immediate",
186
184
  true,
187
185
  );
188
186
 
@@ -1117,3 +1115,46 @@ describe("SyncManager.handleSyncMessage", () => {
1117
1115
  expect(peerState.knownStates.has(group.id)).toBe(true);
1118
1116
  });
1119
1117
  });
1118
+
1119
+ describe("SyncManager.trackDirtyCoValues", () => {
1120
+ test("should track the dirty coValues", async () => {
1121
+ const node = createTestNode();
1122
+
1123
+ const tracking = node.syncManager.trackDirtyCoValues();
1124
+
1125
+ const group = node.createGroup();
1126
+ const map = group.createMap();
1127
+ map.set("key1", "value1", "trusting");
1128
+
1129
+ const trackedValues = tracking.done();
1130
+ expect(trackedValues.size).toBe(2);
1131
+ expect(trackedValues.has(map.id)).toBe(true);
1132
+ expect(trackedValues.has(group.id)).toBe(true);
1133
+ });
1134
+
1135
+ test("should track the dirty coValues only when active", async () => {
1136
+ const node = createTestNode();
1137
+
1138
+ const group = node.createGroup();
1139
+
1140
+ const tracking1 = node.syncManager.trackDirtyCoValues();
1141
+
1142
+ const map1 = group.createMap();
1143
+ map1.set("key1", "value1", "trusting");
1144
+
1145
+ const tracking2 = node.syncManager.trackDirtyCoValues();
1146
+
1147
+ const map2 = group.createMap();
1148
+ map2.set("key2", "value2", "trusting");
1149
+
1150
+ const tracked1 = tracking1.done();
1151
+
1152
+ const map3 = group.createMap();
1153
+ map3.set("key3", "value3", "trusting");
1154
+
1155
+ const tracked2 = tracking2.done();
1156
+
1157
+ expect(Array.from(tracked1)).toEqual([map1.id, map2.id]);
1158
+ expect(Array.from(tracked2)).toEqual([map2.id, map3.id]);
1159
+ });
1160
+ });