cojson 0.13.20 → 0.13.23

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.
@@ -311,14 +311,10 @@ export class CoValueCore {
311
311
  }
312
312
  }
313
313
 
314
- contentInClonedNodeWithDifferentAccount(
315
- controlledAccountOrAgent: ControlledAccountOrAgent,
316
- ): RawCoValue {
317
- const newNode = this.node.cloneWithDifferentAccount(
318
- controlledAccountOrAgent,
319
- );
320
-
321
- return newNode.expectCoValueLoaded(this.id).getCurrentContent();
314
+ contentInClonedNodeWithDifferentAccount(account: ControlledAccountOrAgent) {
315
+ return this.node
316
+ .loadCoValueAsDifferentAgent(this.id, account.agentSecret, account.id)
317
+ .getCurrentContent();
322
318
  }
323
319
 
324
320
  knownState(): CoValueKnownState {
@@ -319,11 +319,7 @@ export class RawBinaryCoStreamView<
319
319
  return lastItem?.type === "end";
320
320
  }
321
321
 
322
- getBinaryChunks(
323
- allowUnfinished?: boolean,
324
- ):
325
- | (BinaryStreamInfo & { chunks: Uint8Array[]; finished: boolean })
326
- | undefined {
322
+ getBinaryStreamInfo(): BinaryStreamInfo | undefined {
327
323
  const items = this.getSingleStream();
328
324
 
329
325
  // No active streams
@@ -336,6 +332,27 @@ export class RawBinaryCoStreamView<
336
332
  return;
337
333
  }
338
334
 
335
+ return {
336
+ mimeType: start.mimeType,
337
+ fileName: start.fileName,
338
+ totalSizeBytes: start.totalSizeBytes,
339
+ };
340
+ }
341
+
342
+ getBinaryChunks(
343
+ allowUnfinished?: boolean,
344
+ ):
345
+ | (BinaryStreamInfo & { chunks: Uint8Array[]; finished: boolean })
346
+ | undefined {
347
+ const items = this.getSingleStream();
348
+
349
+ // No active streams
350
+ if (!items) return;
351
+
352
+ const info = this.getBinaryStreamInfo();
353
+
354
+ if (!info) return;
355
+
339
356
  const end = items[items.length - 1];
340
357
 
341
358
  if (end?.type !== "end" && !allowUnfinished) return;
@@ -360,9 +377,7 @@ export class RawBinaryCoStreamView<
360
377
  }
361
378
 
362
379
  return {
363
- mimeType: start.mimeType,
364
- fileName: start.fileName,
365
- totalSizeBytes: start.totalSizeBytes,
380
+ ...info,
366
381
  chunks,
367
382
  finished,
368
383
  };
package/src/localNode.ts CHANGED
@@ -433,32 +433,44 @@ export class LocalNode {
433
433
  groupOrOwnedValueID: CoID<T>,
434
434
  inviteSecret: InviteSecret,
435
435
  ): Promise<void> {
436
- const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
436
+ const value = await this.load(groupOrOwnedValueID);
437
437
 
438
- if (groupOrOwnedValue === "unavailable") {
438
+ if (value === "unavailable") {
439
439
  throw new Error(
440
440
  "Trying to accept invite: Group/owned value unavailable from all peers",
441
441
  );
442
442
  }
443
443
 
444
- if (
445
- groupOrOwnedValue.core.verified.header.ruleset.type === "ownedByGroup"
446
- ) {
447
- return this.acceptInvite(
448
- groupOrOwnedValue.core.verified.header.ruleset.group as CoID<RawGroup>,
449
- inviteSecret,
450
- );
451
- } else if (
452
- groupOrOwnedValue.core.verified.header.ruleset.type !== "group"
453
- ) {
454
- throw new Error("Can only accept invites to groups");
444
+ const ruleset = value.core.verified.header.ruleset;
445
+
446
+ let group: RawGroup;
447
+
448
+ if (ruleset.type === "unsafeAllowAll") {
449
+ throw new Error("Can only accept invites to values owned by groups");
455
450
  }
456
451
 
457
- const group = expectGroup(groupOrOwnedValue);
452
+ if (ruleset.type === "ownedByGroup") {
453
+ const owner = await this.load(ruleset.group as CoID<RawGroup>);
454
+
455
+ if (owner === "unavailable") {
456
+ throw new Error(
457
+ "Trying to accept invite: CoValue owner unavailable from all peers",
458
+ );
459
+ }
460
+
461
+ group = expectGroup(owner);
462
+ } else {
463
+ group = expectGroup(value);
464
+ }
465
+
466
+ if (group.core.verified.header.meta?.type === "account") {
467
+ throw new Error("Can't accept invites to values owned by accounts");
468
+ }
458
469
 
459
470
  const inviteAgentSecret = this.crypto.agentSecretFromSecretSeed(
460
471
  secretSeedFromInviteSecret(inviteSecret),
461
472
  );
473
+
462
474
  const inviteAgentID = this.crypto.getAgentID(inviteAgentSecret);
463
475
 
464
476
  const inviteRole = await new Promise((resolve, reject) => {
@@ -493,9 +505,10 @@ export class LocalNode {
493
505
  }
494
506
 
495
507
  const groupAsInvite = expectGroup(
496
- group.core.contentInClonedNodeWithDifferentAccount(
497
- new ControlledAgent(inviteAgentSecret, this.crypto),
498
- ),
508
+ this.loadCoValueAsDifferentAgent(
509
+ group.id,
510
+ inviteAgentSecret,
511
+ ).getCurrentContent(),
499
512
  );
500
513
 
501
514
  groupAsInvite.addMemberInternal(
@@ -524,7 +537,7 @@ export class LocalNode {
524
537
 
525
538
  if (!coValue.isAvailable()) {
526
539
  throw new Error(
527
- `${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded. Current state: ${JSON.stringify(coValue)}`,
540
+ `${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded.`,
528
541
  );
529
542
  }
530
543
  return coValue;
@@ -621,46 +634,56 @@ export class LocalNode {
621
634
  return group;
622
635
  }
623
636
 
624
- /** @internal */
625
- cloneWithDifferentAccount(
626
- controlledAccountOrAgent: ControlledAccountOrAgent,
627
- ): LocalNode {
637
+ loadCoValueAsDifferentAgent(
638
+ id: RawCoID,
639
+ secret: AgentSecret,
640
+ accountId?: RawAccountID | AgentID,
641
+ ) {
642
+ const agent = new ControlledAgent(secret, this.crypto);
643
+
628
644
  const newNode = new LocalNode(
629
- controlledAccountOrAgent.agentSecret,
630
- this.crypto.newRandomSessionID(controlledAccountOrAgent.id),
645
+ secret,
646
+ this.crypto.newRandomSessionID(accountId || agent.id),
631
647
  this.crypto,
632
648
  );
633
649
 
634
- newNode.cloneVerifiedStateFrom(this);
650
+ newNode.cloneVerifiedStateFrom(this, id);
635
651
 
636
- return newNode;
652
+ return newNode.expectCoValueLoaded(id);
637
653
  }
638
654
 
639
655
  /** @internal */
640
- cloneVerifiedStateFrom(otherNode: LocalNode) {
641
- const coValuesToCopy = Array.from(otherNode.coValues.entries());
656
+ cloneVerifiedStateFrom(otherNode: LocalNode, id: RawCoID) {
657
+ const coValuesIdsToCopy = [id];
642
658
 
643
- while (coValuesToCopy.length > 0) {
644
- const [coValueID, coValue] = coValuesToCopy[coValuesToCopy.length - 1]!;
659
+ // Scan all the dependencies and add them to the list
660
+ for (let i = 0; i < coValuesIdsToCopy.length; i++) {
661
+ const coValueID = coValuesIdsToCopy[i]!;
662
+ const coValue = otherNode.getCoValue(coValueID);
645
663
 
646
664
  if (!coValue.isAvailable()) {
647
- coValuesToCopy.pop();
648
665
  continue;
649
- } else {
650
- const allDepsCopied = coValue
651
- .getDependedOnCoValues()
652
- .every((dep) => this.coValues.get(dep)?.isAvailable());
653
-
654
- if (!allDepsCopied) {
655
- // move to end of queue
656
- coValuesToCopy.unshift(coValuesToCopy.pop()!);
657
- continue;
658
- }
666
+ }
659
667
 
660
- this.putCoValue(coValueID, coValue.verified);
668
+ for (const dep of coValue.getDependedOnCoValues()) {
669
+ coValuesIdsToCopy.push(dep);
670
+ }
671
+ }
661
672
 
662
- coValuesToCopy.pop();
673
+ // Copy the coValue all the dependencies by following the dependency order
674
+ while (coValuesIdsToCopy.length > 0) {
675
+ const coValueID = coValuesIdsToCopy.pop()!;
676
+ const coValue = otherNode.getCoValue(coValueID);
677
+
678
+ if (!coValue.isAvailable()) {
679
+ continue;
663
680
  }
681
+
682
+ if (this.coValues.get(coValueID)?.isAvailable()) {
683
+ continue;
684
+ }
685
+
686
+ this.putCoValue(coValueID, coValue.verified);
664
687
  }
665
688
  }
666
689
 
@@ -512,7 +512,6 @@ test("Admins can set group read key and then use it to create private transactio
512
512
  const { node, groupCore, admin } = newGroup();
513
513
 
514
514
  const reader1 = createAccountInNode(node);
515
-
516
515
  const reader2 = createAccountInNode(node);
517
516
 
518
517
  const { secret: readKey, id: readKeyID } = Crypto.newRandomKeySecret();
@@ -583,6 +582,9 @@ test("Admins can set group read key and then use it to create private transactio
583
582
  childObject.contentInClonedNodeWithDifferentAccount(reader2),
584
583
  );
585
584
 
585
+ // Need to copy the account coValue to the new node to be able to read the readKey
586
+ childObjectAsReader2.core.node.cloneVerifiedStateFrom(node, reader2.id);
587
+
586
588
  expect(childObjectAsReader2.core.getCurrentReadKey().secret).toEqual(readKey);
587
589
  expect(childObjectAsReader2.get("foo")).toEqual("bar");
588
590
  });
@@ -1113,9 +1115,10 @@ test("Admins can create an adminInvite, which can add an admin (high-level)", as
1113
1115
  const invitedAdminSecret = Crypto.newRandomAgentSecret();
1114
1116
  const invitedAdminID = Crypto.getAgentID(invitedAdminSecret);
1115
1117
 
1116
- const nodeAsInvitedAdmin = node.cloneWithDifferentAccount(
1117
- new ControlledAgent(invitedAdminSecret, Crypto),
1118
- );
1118
+ const nodeAsInvitedAdmin = node.loadCoValueAsDifferentAgent(
1119
+ group.id,
1120
+ invitedAdminSecret,
1121
+ ).node;
1119
1122
 
1120
1123
  await nodeAsInvitedAdmin.acceptInvite(group.id, inviteSecret);
1121
1124
 
@@ -1219,9 +1222,10 @@ test("Admins can create a writerInvite, which can add a writer (high-level)", as
1219
1222
  const invitedWriterSecret = Crypto.newRandomAgentSecret();
1220
1223
  const invitedWriterID = Crypto.getAgentID(invitedWriterSecret);
1221
1224
 
1222
- const nodeAsInvitedWriter = node.cloneWithDifferentAccount(
1223
- new ControlledAgent(invitedWriterSecret, Crypto),
1224
- );
1225
+ const nodeAsInvitedWriter = node.loadCoValueAsDifferentAgent(
1226
+ group.id,
1227
+ invitedWriterSecret,
1228
+ ).node;
1225
1229
 
1226
1230
  await nodeAsInvitedWriter.acceptInvite(group.id, inviteSecret);
1227
1231
 
@@ -1308,9 +1312,10 @@ test("Admins can create a readerInvite, which can add a reader (high-level)", as
1308
1312
  const invitedReaderSecret = Crypto.newRandomAgentSecret();
1309
1313
  const invitedReaderID = Crypto.getAgentID(invitedReaderSecret);
1310
1314
 
1311
- const nodeAsInvitedReader = node.cloneWithDifferentAccount(
1312
- new ControlledAgent(invitedReaderSecret, Crypto),
1313
- );
1315
+ const nodeAsInvitedReader = node.loadCoValueAsDifferentAgent(
1316
+ group.id,
1317
+ invitedReaderSecret,
1318
+ ).node;
1314
1319
 
1315
1320
  await nodeAsInvitedReader.acceptInvite(group.id, inviteSecret);
1316
1321
 
@@ -0,0 +1,163 @@
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+
3
+ import { expectList, expectMap } from "../coValue";
4
+ import { WasmCrypto } from "../crypto/WasmCrypto";
5
+ import {
6
+ SyncMessagesLog,
7
+ loadCoValueOrFail,
8
+ setupTestNode,
9
+ waitFor,
10
+ } from "./testUtils";
11
+
12
+ const Crypto = await WasmCrypto.create();
13
+ let jazzCloud = setupTestNode({ isSyncServer: true });
14
+
15
+ beforeEach(async () => {
16
+ SyncMessagesLog.clear();
17
+ jazzCloud = setupTestNode({ isSyncServer: true });
18
+ });
19
+
20
+ describe("invitations sync", () => {
21
+ test("invite a client to a group", async () => {
22
+ const client = setupTestNode();
23
+ client.connectToSyncServer({
24
+ ourName: "invite-provider",
25
+ });
26
+ const client2 = setupTestNode();
27
+ client2.connectToSyncServer({
28
+ ourName: "invite-consumer",
29
+ });
30
+
31
+ const group = client.node.createGroup();
32
+ const map = group.createMap();
33
+ map.set("hello", "world");
34
+
35
+ await map.core.waitForSync();
36
+
37
+ const invite = group.createInvite("reader");
38
+
39
+ await group.core.waitForSync();
40
+ SyncMessagesLog.clear();
41
+
42
+ await client2.node.acceptInvite(map.id, invite);
43
+
44
+ const mapOnClient2 = await loadCoValueOrFail(client2.node, map.id);
45
+ expect(mapOnClient2.get("hello")).toEqual("world");
46
+
47
+ expect(
48
+ SyncMessagesLog.getMessages({
49
+ Group: group.core,
50
+ Map: map.core,
51
+ }),
52
+ ).toMatchInlineSnapshot(`
53
+ [
54
+ "invite-consumer -> server | LOAD Map sessions: empty",
55
+ "server -> invite-consumer | CONTENT Group header: true new: After: 0 New: 5",
56
+ "invite-consumer -> server | KNOWN Group sessions: header/5",
57
+ "server -> invite-consumer | CONTENT Map header: true new: After: 0 New: 1",
58
+ "invite-consumer -> server | KNOWN Map sessions: header/1",
59
+ ]
60
+ `);
61
+ });
62
+
63
+ test("invite a client to a group with parents", async () => {
64
+ const client = setupTestNode();
65
+ client.connectToSyncServer({
66
+ ourName: "invite-provider",
67
+ });
68
+ const client2 = setupTestNode();
69
+ client2.connectToSyncServer({
70
+ ourName: "invite-consumer",
71
+ });
72
+
73
+ const parentGroup = client.node.createGroup();
74
+ const group = client.node.createGroup();
75
+
76
+ group.extend(parentGroup);
77
+ const map = group.createMap();
78
+ map.set("hello", "world");
79
+
80
+ await map.core.waitForSync();
81
+
82
+ const invite = group.createInvite("reader");
83
+
84
+ await group.core.waitForSync();
85
+ SyncMessagesLog.clear();
86
+
87
+ await client2.node.acceptInvite(map.id, invite);
88
+
89
+ const mapOnClient2 = await loadCoValueOrFail(client2.node, map.id);
90
+ expect(mapOnClient2.get("hello")).toEqual("world");
91
+
92
+ expect(
93
+ SyncMessagesLog.getMessages({
94
+ Group: group.core,
95
+ ParentGroup: parentGroup.core,
96
+ Map: map.core,
97
+ }),
98
+ ).toMatchInlineSnapshot(`
99
+ [
100
+ "invite-consumer -> server | LOAD Map sessions: empty",
101
+ "server -> invite-consumer | CONTENT ParentGroup header: true new: After: 0 New: 4",
102
+ "invite-consumer -> server | KNOWN ParentGroup sessions: header/4",
103
+ "server -> invite-consumer | CONTENT Group header: true new: After: 0 New: 7",
104
+ "invite-consumer -> server | KNOWN Group sessions: header/7",
105
+ "server -> invite-consumer | CONTENT Map header: true new: After: 0 New: 1",
106
+ "invite-consumer -> server | KNOWN Map sessions: header/1",
107
+ ]
108
+ `);
109
+ });
110
+
111
+ test("invite a client to a parent group", async () => {
112
+ const client = setupTestNode();
113
+ client.connectToSyncServer({
114
+ ourName: "invite-provider",
115
+ });
116
+ const client2 = setupTestNode();
117
+ client2.connectToSyncServer({
118
+ ourName: "invite-consumer",
119
+ });
120
+
121
+ const parentGroup = client.node.createGroup();
122
+ const group = client.node.createGroup();
123
+
124
+ group.extend(parentGroup);
125
+ const map = group.createMap();
126
+ map.set("hello", "world");
127
+
128
+ await map.core.waitForSync();
129
+
130
+ const invite = parentGroup.createInvite("reader");
131
+
132
+ await parentGroup.core.waitForSync();
133
+ SyncMessagesLog.clear();
134
+
135
+ await client2.node.acceptInvite(parentGroup.id, invite);
136
+
137
+ const mapOnClient2 = await loadCoValueOrFail(client2.node, map.id);
138
+ expect(mapOnClient2.get("hello")).toEqual("world");
139
+
140
+ expect(
141
+ SyncMessagesLog.getMessages({
142
+ Group: group.core,
143
+ ParentGroup: parentGroup.core,
144
+ Map: map.core,
145
+ }),
146
+ ).toMatchInlineSnapshot(`
147
+ [
148
+ "invite-consumer -> server | LOAD ParentGroup sessions: empty",
149
+ "server -> invite-consumer | CONTENT ParentGroup header: true new: After: 0 New: 6",
150
+ "invite-consumer -> server | KNOWN ParentGroup sessions: header/6",
151
+ "invite-consumer -> server | LOAD Map sessions: empty",
152
+ "server -> invite-consumer | CONTENT Group header: true new: After: 0 New: 5",
153
+ "invite-consumer -> server | KNOWN Group sessions: header/5",
154
+ "server -> invite-consumer | CONTENT Map header: true new: After: 0 New: 1",
155
+ "invite-consumer -> server | CONTENT ParentGroup header: false new: After: 0 New: 2",
156
+ "server -> invite-consumer | KNOWN ParentGroup sessions: header/8",
157
+ "server -> invite-provider | CONTENT ParentGroup header: false new: After: 0 New: 2",
158
+ "invite-consumer -> server | KNOWN Map sessions: header/1",
159
+ "invite-provider -> server | KNOWN ParentGroup sessions: header/8",
160
+ ]
161
+ `);
162
+ });
163
+ });
package/dist/utils.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export declare function parseError(e: unknown): {
2
- message: string | null;
3
- stack: string | null;
4
- };
5
- //# sourceMappingURL=utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG;IACtC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAKA"}
package/dist/utils.js DELETED
@@ -1,7 +0,0 @@
1
- export function parseError(e) {
2
- return {
3
- message: e instanceof Error ? e.message : null,
4
- stack: e instanceof Error ? (e.stack ?? null) : null,
5
- };
6
- }
7
- //# sourceMappingURL=utils.js.map
package/dist/utils.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,UAAU,CAAC,CAAU;IAInC,OAAO;QACL,OAAO,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;QAC9C,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;KACrD,CAAC;AACJ,CAAC"}
package/src/utils.ts DELETED
@@ -1,9 +0,0 @@
1
- export function parseError(e: unknown): {
2
- message: string | null;
3
- stack: string | null;
4
- } {
5
- return {
6
- message: e instanceof Error ? e.message : null,
7
- stack: e instanceof Error ? (e.stack ?? null) : null,
8
- };
9
- }