cojson 0.19.4 → 0.19.6

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.
@@ -29,7 +29,11 @@ import {
29
29
  } from "./crypto.js";
30
30
  import { ControlledAccountOrAgent } from "../coValues/account.js";
31
31
 
32
- type Blake3State = ReturnType<typeof blake3.create>;
32
+ export type Blake3State = {
33
+ update: (buf: Uint8Array) => Blake3State;
34
+ digest: () => Uint8Array;
35
+ clone: () => Blake3State;
36
+ };
33
37
 
34
38
  const x25519SharedSecretCache = new Map<string, Uint8Array>();
35
39
 
@@ -67,6 +71,10 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
67
71
  return new PureJSCrypto();
68
72
  }
69
73
 
74
+ createStreamingHash(): Blake3State {
75
+ return blake3.create({});
76
+ }
77
+
70
78
  blake3HashOnce(data: Uint8Array) {
71
79
  return blake3(data);
72
80
  }
@@ -75,7 +83,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
75
83
  data: Uint8Array,
76
84
  { context }: { context: Uint8Array },
77
85
  ) {
78
- return blake3.create({}).update(context).update(data).digest();
86
+ return this.createStreamingHash().update(context).update(data).digest();
79
87
  }
80
88
 
81
89
  generateNonce(input: Uint8Array): Uint8Array {
@@ -224,7 +232,7 @@ export class PureJSSessionLog implements SessionLogImpl {
224
232
  private readonly signerID: SignerID | undefined,
225
233
  private readonly crypto: PureJSCrypto,
226
234
  ) {
227
- this.streamingHash = blake3.create({});
235
+ this.streamingHash = crypto.createStreamingHash();
228
236
  }
229
237
 
230
238
  clone(): SessionLogImpl {
@@ -271,7 +279,7 @@ export class PureJSSessionLog implements SessionLogImpl {
271
279
 
272
280
  if (!this.crypto.verify(newSignature, newHashEncoded, this.signerID)) {
273
281
  // Rebuild the streaming hash to the original state
274
- this.streamingHash = blake3.create({});
282
+ this.streamingHash = this.crypto.createStreamingHash();
275
283
  for (const tx of this.transactions) {
276
284
  this.streamingHash.update(textEncoder.encode(tx));
277
285
  }
@@ -1,12 +1,17 @@
1
- import { expect, test } from "vitest";
1
+ import { beforeEach, expect, test } from "vitest";
2
2
  import { expectAccount } from "../coValues/account.js";
3
3
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
4
4
  import { LocalNode } from "../localNode.js";
5
5
  import { connectedPeers } from "../streamUtils.js";
6
6
  import { createAsyncStorage } from "./testStorage.js";
7
+ import { setupTestAccount, setupTestNode } from "./testUtils.js";
7
8
 
8
9
  const Crypto = await WasmCrypto.create();
9
10
 
11
+ beforeEach(async () => {
12
+ await setupTestNode({ isSyncServer: true });
13
+ });
14
+
10
15
  test("Can create a node while creating a new account with profile", async () => {
11
16
  const { node, accountID, accountSecret, sessionID } =
12
17
  await LocalNode.withNewlyCreatedAccount({
@@ -75,66 +80,42 @@ test("Can create account with one node, and then load it on another", async () =
75
80
  });
76
81
 
77
82
  test("Should migrate the root from private to trusting", async () => {
78
- const { node, accountID, accountSecret } =
79
- await LocalNode.withNewlyCreatedAccount({
80
- creationProps: { name: "Hermes Puggington" },
81
- crypto: Crypto,
82
- });
83
+ const session1 = await setupTestAccount({ connected: true });
83
84
 
84
- const group = await node.createGroup();
85
+ const group = session1.node.createGroup();
85
86
  expect(group).not.toBeNull();
86
87
 
87
88
  const map = group.createMap();
88
89
  map.set("foo", "bar", "private");
89
90
  expect(map.get("foo")).toEqual("bar");
90
91
 
91
- const peers1 = connectedPeers("node1", "node2", {
92
- peer1role: "server",
93
- peer2role: "client",
94
- });
95
-
96
- const account = await node.load(accountID);
92
+ const account = await session1.node.load(session1.accountID);
97
93
  if (account === "unavailable") throw new Error("Account unavailable");
98
94
 
99
95
  account.set("root", map.id, "private");
100
96
 
101
- node.syncManager.addPeer(peers1[1]);
97
+ // Waiting to ensure that the migration is always applied on a different timestamp
98
+ // to make the test more stable
99
+ await new Promise((resolve) => setTimeout(resolve, 4));
102
100
 
103
- const node2 = await LocalNode.withLoadedAccount({
104
- accountID,
105
- accountSecret,
106
- sessionID: Crypto.newRandomSessionID(accountID),
107
- peers: [peers1[0]],
108
- crypto: Crypto,
109
- });
101
+ const session2 = await session1.spawnNewSession();
110
102
 
111
- const account2 = await node2.load(accountID);
112
- if (account2 === "unavailable") throw new Error("Account unavailable");
103
+ const accountInNewSession = await session2.node.load(session1.accountID);
104
+ if (accountInNewSession === "unavailable")
105
+ throw new Error("Account unavailable");
113
106
 
114
- expect(account2.getRaw("root")?.trusting).toEqual(true);
115
-
116
- node2.gracefulShutdown(); // Stop getting updates from node1
117
-
118
- const peers2 = connectedPeers("node2", "node3", {
119
- peer1role: "server",
120
- peer2role: "client",
121
- });
107
+ expect(accountInNewSession.getRaw("root")?.trusting).toEqual(true);
122
108
 
123
- node.syncManager.addPeer(peers2[1]);
109
+ await accountInNewSession.core.waitForSync();
124
110
 
125
- const node3 = await LocalNode.withLoadedAccount({
126
- accountID,
127
- accountSecret,
128
- sessionID: Crypto.newRandomSessionID(accountID),
129
- peers: [peers2[0]],
130
- crypto: Crypto,
131
- });
111
+ const session3 = await session2.spawnNewSession();
132
112
 
133
- const account3 = await node3.load(accountID);
134
- if (account3 === "unavailable") throw new Error("Account unavailable");
113
+ const accountInNewSession2 = await session3.node.load(session1.accountID);
114
+ if (accountInNewSession2 === "unavailable")
115
+ throw new Error("Account unavailable");
135
116
 
136
- expect(account3.getRaw("root")?.trusting).toEqual(true);
137
- expect(account3.ops).toEqual(account2.ops); // No new transactions were made
117
+ expect(accountInNewSession2.getRaw("root")?.trusting).toEqual(true);
118
+ expect(accountInNewSession2.ops).toEqual(accountInNewSession.ops); // No new transactions were made
138
119
  });
139
120
 
140
121
  test("myRole returns 'admin' for the current account", async () => {
@@ -0,0 +1,293 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import {
3
+ setupTestAccount,
4
+ setupTestNode,
5
+ hotSleep,
6
+ loadCoValueOrFail,
7
+ } from "./testUtils";
8
+
9
+ let jazzCloud: ReturnType<typeof setupTestNode>;
10
+
11
+ beforeEach(async () => {
12
+ jazzCloud = setupTestNode({ isSyncServer: true });
13
+ });
14
+
15
+ describe("Parent Group Cache", () => {
16
+ describe("Property 1: Parent group cache update correctness", () => {
17
+ test("cache contains correct entries after processing parent group reference transactions", async () => {
18
+ const account = await setupTestAccount({
19
+ connected: true,
20
+ });
21
+ const parentGroup = account.node.createGroup();
22
+ const childGroup = account.node.createGroup();
23
+
24
+ // Initially no parent groups
25
+ expect(childGroup.getParentGroups()).toEqual([]);
26
+
27
+ // Extend parent group
28
+ childGroup.extend(parentGroup);
29
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
30
+ });
31
+
32
+ test("cache handles multiple updates to same parent group", async () => {
33
+ const account = await setupTestAccount({
34
+ connected: true,
35
+ });
36
+ const parentGroup = account.node.createGroup();
37
+ const childGroup = account.node.createGroup();
38
+
39
+ childGroup.extend(parentGroup);
40
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
41
+
42
+ // Revoke and re-extend
43
+ childGroup.revokeExtend(parentGroup);
44
+ expect(childGroup.getParentGroups()).toEqual([]);
45
+
46
+ childGroup.extend(parentGroup, "admin");
47
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
48
+ });
49
+
50
+ test("cache correctly handles empty state (no parent groups)", async () => {
51
+ const account = await setupTestAccount({
52
+ connected: true,
53
+ });
54
+ const group = account.node.createGroup();
55
+
56
+ expect(group.getParentGroups()).toEqual([]);
57
+ });
58
+
59
+ test("cache only contains parent groups that have reference transactions", async () => {
60
+ const account = await setupTestAccount({
61
+ connected: true,
62
+ });
63
+ const parentGroup1 = account.node.createGroup();
64
+ const parentGroup2 = account.node.createGroup();
65
+ const childGroup = account.node.createGroup();
66
+
67
+ // Only extend one parent group
68
+ childGroup.extend(parentGroup1);
69
+
70
+ const parentGroups = childGroup.getParentGroups();
71
+ expect(parentGroups).toHaveLength(1);
72
+ expect(parentGroups[0]?.id).toEqual(parentGroup1.id);
73
+ });
74
+
75
+ test("cache handles multiple parent groups", async () => {
76
+ const account = await setupTestAccount({
77
+ connected: true,
78
+ });
79
+ const parentGroup1 = account.node.createGroup();
80
+ const parentGroup2 = account.node.createGroup();
81
+ const parentGroup3 = account.node.createGroup();
82
+ const childGroup = account.node.createGroup();
83
+
84
+ childGroup.extend(parentGroup1);
85
+ childGroup.extend(parentGroup2);
86
+ childGroup.extend(parentGroup3);
87
+
88
+ const parentGroups = childGroup.getParentGroups();
89
+ expect(parentGroups).toHaveLength(3);
90
+ expect(parentGroups.map((g) => g.id).sort()).toEqual(
91
+ [parentGroup1.id, parentGroup2.id, parentGroup3.id].sort(),
92
+ );
93
+ });
94
+ });
95
+
96
+ describe("Property 2: Parent group cache chronological ordering", () => {
97
+ test("multiple parent group changes are stored in chronological order", async () => {
98
+ const account = await setupTestAccount({
99
+ connected: true,
100
+ });
101
+ const parentGroup = account.node.createGroup();
102
+ const childGroup = account.node.createGroup();
103
+
104
+ const t1 = hotSleep(10);
105
+ childGroup.extend(parentGroup, "reader");
106
+ const t2 = hotSleep(10);
107
+ childGroup.extend(parentGroup, "writer");
108
+ const t3 = hotSleep(10);
109
+ childGroup.extend(parentGroup, "admin");
110
+
111
+ // Check time travel queries return correct historical states
112
+ expect(childGroup.atTime(t1).getParentGroups()).toEqual([]);
113
+ expect(childGroup.atTime(t2).getParentGroups()).toEqual([
114
+ parentGroup.atTime(t2),
115
+ ]);
116
+ expect(childGroup.atTime(t3).getParentGroups()).toEqual([
117
+ parentGroup.atTime(t3),
118
+ ]);
119
+
120
+ // Current state should have admin role
121
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
122
+ });
123
+
124
+ test("out-of-order transaction processing maintains chronological order", async () => {
125
+ const account = await setupTestAccount({
126
+ connected: true,
127
+ });
128
+ const parentGroup = account.node.createGroup();
129
+ const childGroup = account.node.createGroup();
130
+
131
+ // Create transactions with different timestamps
132
+ const t1 = hotSleep(10);
133
+ const t2 = hotSleep(10);
134
+
135
+ childGroup.core.makeTransaction(
136
+ [
137
+ {
138
+ op: "set",
139
+ key: `parent_${parentGroup.id}`,
140
+ value: "revoked",
141
+ },
142
+ ],
143
+ "private",
144
+ undefined,
145
+ t2,
146
+ );
147
+
148
+ childGroup.core.makeTransaction(
149
+ [
150
+ {
151
+ op: "set",
152
+ key: `parent_${parentGroup.id}`,
153
+ value: "extend",
154
+ },
155
+ ],
156
+ "private",
157
+ undefined,
158
+ t1,
159
+ );
160
+ // Verify chronological ordering through time travel
161
+ const groupAtT1 = childGroup.atTime(t1);
162
+ const groupAtT2 = childGroup.atTime(t2);
163
+
164
+ expect(groupAtT1.getParentGroups()).toEqual([parentGroup.atTime(t1)]);
165
+ expect(groupAtT2.getParentGroups()).toEqual([]);
166
+ });
167
+
168
+ test("chronological ordering after rebuild", async () => {
169
+ const account = await setupTestAccount({
170
+ connected: true,
171
+ });
172
+ const parentGroup = account.node.createGroup();
173
+ const childGroup = account.node.createGroup();
174
+
175
+ const t1 = hotSleep(10);
176
+ childGroup.extend(parentGroup, "reader");
177
+ const t2 = hotSleep(10);
178
+ childGroup.extend(parentGroup, "writer");
179
+
180
+ // Rebuild should maintain chronological order
181
+ childGroup.rebuildFromCore();
182
+
183
+ const groupAtT1 = childGroup.atTime(t1);
184
+ const groupAtT2 = childGroup.atTime(t2);
185
+
186
+ expect(groupAtT1.getParentGroups()).toEqual([]);
187
+ expect(groupAtT2.getParentGroups()).toEqual([parentGroup.atTime(t2)]);
188
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
189
+ });
190
+ });
191
+
192
+ describe("Property 3: Rebuild round-trip", () => {
193
+ test("cache is cleared after rebuildFromCore", async () => {
194
+ const account = await setupTestAccount({
195
+ connected: true,
196
+ });
197
+ const parentGroup = account.node.createGroup();
198
+ const childGroup = account.node.createGroup();
199
+
200
+ childGroup.extend(parentGroup);
201
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
202
+
203
+ // Rebuild should clear cache and rebuild it
204
+ childGroup.rebuildFromCore();
205
+
206
+ // Cache should be rebuilt and still work
207
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
208
+ });
209
+
210
+ test("cache is rebuilt correctly after clear", async () => {
211
+ const account = await setupTestAccount({
212
+ connected: true,
213
+ });
214
+ const parentGroup1 = account.node.createGroup();
215
+ const parentGroup2 = account.node.createGroup();
216
+ const childGroup = account.node.createGroup();
217
+
218
+ childGroup.extend(parentGroup1);
219
+ childGroup.extend(parentGroup2);
220
+
221
+ expect(childGroup.getParentGroups()).toHaveLength(2);
222
+
223
+ // Rebuild
224
+ childGroup.rebuildFromCore();
225
+
226
+ // Cache should be rebuilt with all parent groups
227
+ const parentGroups = childGroup.getParentGroups();
228
+ expect(parentGroups).toHaveLength(2);
229
+ expect(parentGroups.map((g) => g.id).sort()).toEqual(
230
+ [parentGroup1.id, parentGroup2.id].sort(),
231
+ );
232
+ });
233
+
234
+ test("parent group lookups work correctly after rebuild", async () => {
235
+ const alice = await setupTestAccount({
236
+ connected: true,
237
+ });
238
+ const bob = await setupTestAccount({
239
+ connected: true,
240
+ });
241
+ const parentGroup = alice.node.createGroup();
242
+ const childGroup = alice.node.createGroup();
243
+
244
+ parentGroup.addMember(
245
+ await loadCoValueOrFail(alice.node, bob.accountID),
246
+ "writer",
247
+ );
248
+
249
+ childGroup.extend(parentGroup);
250
+
251
+ // Check role inheritance before rebuild
252
+ expect(childGroup.roleOf(bob.accountID)).toEqual("writer");
253
+
254
+ // Rebuild
255
+ childGroup.rebuildFromCore();
256
+
257
+ // Role inheritance should still work after rebuild
258
+ expect(childGroup.roleOf(bob.accountID)).toEqual("writer");
259
+ expect(childGroup.getParentGroups()).toEqual([parentGroup]);
260
+ });
261
+
262
+ test("rebuild maintains chronological ordering", async () => {
263
+ const alice = await setupTestAccount({
264
+ connected: true,
265
+ });
266
+ const bob = await setupTestAccount({
267
+ connected: true,
268
+ });
269
+ const parentGroup = alice.node.createGroup();
270
+ const childGroup = alice.node.createGroup();
271
+
272
+ parentGroup.addMember(
273
+ await loadCoValueOrFail(alice.node, bob.accountID),
274
+ "writer",
275
+ );
276
+
277
+ const t1 = hotSleep(10);
278
+ childGroup.extend(parentGroup, "reader");
279
+ const t2 = hotSleep(10);
280
+ childGroup.extend(parentGroup, "writer");
281
+
282
+ // Rebuild
283
+ childGroup.rebuildFromCore();
284
+
285
+ // Chronological ordering should be maintained
286
+ expect(childGroup.atTime(t1).roleOf(bob.accountID)).toEqual(undefined);
287
+ expect(childGroup.atTime(t2).roleOf(bob.accountID)).toEqual("reader");
288
+ expect(childGroup.atTime(Date.now()).roleOf(bob.accountID)).toEqual(
289
+ "writer",
290
+ );
291
+ });
292
+ });
293
+ });