cojson 0.13.18 → 0.13.21

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 (63) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/PeerState.d.ts +1 -1
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +7 -36
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/coValueCore/coValueCore.d.ts +2 -2
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +15 -10
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/localNode.d.ts +2 -3
  12. package/dist/localNode.d.ts.map +1 -1
  13. package/dist/localNode.js +55 -35
  14. package/dist/localNode.js.map +1 -1
  15. package/dist/streamUtils.d.ts +5 -5
  16. package/dist/streamUtils.d.ts.map +1 -1
  17. package/dist/streamUtils.js +5 -20
  18. package/dist/streamUtils.js.map +1 -1
  19. package/dist/sync.d.ts +6 -4
  20. package/dist/sync.d.ts.map +1 -1
  21. package/dist/sync.js +35 -19
  22. package/dist/sync.js.map +1 -1
  23. package/dist/tests/PeerState.test.js +0 -31
  24. package/dist/tests/PeerState.test.js.map +1 -1
  25. package/dist/tests/SyncStateManager.test.js +41 -6
  26. package/dist/tests/SyncStateManager.test.js.map +1 -1
  27. package/dist/tests/account.test.js +16 -0
  28. package/dist/tests/account.test.js.map +1 -1
  29. package/dist/tests/group.test.js.map +1 -1
  30. package/dist/tests/permissions.test.js +5 -3
  31. package/dist/tests/permissions.test.js.map +1 -1
  32. package/dist/tests/sync.auth.test.js +64 -15
  33. package/dist/tests/sync.auth.test.js.map +1 -1
  34. package/dist/tests/sync.invite.test.d.ts +2 -0
  35. package/dist/tests/sync.invite.test.d.ts.map +1 -0
  36. package/dist/tests/sync.invite.test.js +123 -0
  37. package/dist/tests/sync.invite.test.js.map +1 -0
  38. package/dist/tests/sync.load.test.js +2 -2
  39. package/dist/tests/sync.load.test.js.map +1 -1
  40. package/dist/tests/testUtils.d.ts +11 -2
  41. package/dist/tests/testUtils.d.ts.map +1 -1
  42. package/dist/tests/testUtils.js +27 -30
  43. package/dist/tests/testUtils.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/PeerState.ts +8 -40
  46. package/src/coValueCore/coValueCore.ts +18 -22
  47. package/src/localNode.ts +79 -49
  48. package/src/streamUtils.ts +7 -34
  49. package/src/sync.ts +51 -22
  50. package/src/tests/PeerState.test.ts +0 -37
  51. package/src/tests/SyncStateManager.test.ts +56 -6
  52. package/src/tests/account.test.ts +24 -0
  53. package/src/tests/group.test.ts +0 -1
  54. package/src/tests/permissions.test.ts +15 -10
  55. package/src/tests/sync.auth.test.ts +79 -21
  56. package/src/tests/sync.invite.test.ts +163 -0
  57. package/src/tests/sync.load.test.ts +3 -2
  58. package/src/tests/testUtils.ts +35 -34
  59. package/dist/utils.d.ts +0 -5
  60. package/dist/utils.d.ts.map +0 -1
  61. package/dist/utils.js +0 -7
  62. package/dist/utils.js.map +0 -1
  63. package/src/utils.ts +0 -9
package/src/localNode.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Result, ResultAsync, err, ok, okAsync } from "neverthrow";
1
+ import { Result, err, ok } from "neverthrow";
2
2
  import { CoID } from "./coValue.js";
3
3
  import { RawCoValue } from "./coValue.js";
4
4
  import {
@@ -223,10 +223,19 @@ export class LocalNode {
223
223
  account.set("profile", profile.id, "trusting");
224
224
  }
225
225
 
226
- if (!account.get("profile")) {
226
+ const profileId = account.get("profile");
227
+
228
+ if (!profileId) {
227
229
  throw new Error("Must set account profile in initial migration");
228
230
  }
229
231
 
232
+ if (node.syncManager.hasStoragePeers()) {
233
+ await Promise.all([
234
+ node.syncManager.waitForStorageSync(account.id),
235
+ node.syncManager.waitForStorageSync(profileId),
236
+ ]);
237
+ }
238
+
230
239
  return {
231
240
  node,
232
241
  accountID: account.id,
@@ -272,11 +281,9 @@ export class LocalNode {
272
281
  if (!profileID) {
273
282
  throw new Error("Account has no profile");
274
283
  }
275
- const profile = await node.load(profileID);
276
284
 
277
- if (profile === "unavailable") {
278
- throw new Error("Profile unavailable from all peers");
279
- }
285
+ // Preload the profile
286
+ await node.load(profileID);
280
287
 
281
288
  if (migration) {
282
289
  await migration(account, node);
@@ -426,32 +433,44 @@ export class LocalNode {
426
433
  groupOrOwnedValueID: CoID<T>,
427
434
  inviteSecret: InviteSecret,
428
435
  ): Promise<void> {
429
- const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
436
+ const value = await this.load(groupOrOwnedValueID);
430
437
 
431
- if (groupOrOwnedValue === "unavailable") {
438
+ if (value === "unavailable") {
432
439
  throw new Error(
433
440
  "Trying to accept invite: Group/owned value unavailable from all peers",
434
441
  );
435
442
  }
436
443
 
437
- if (
438
- groupOrOwnedValue.core.verified.header.ruleset.type === "ownedByGroup"
439
- ) {
440
- return this.acceptInvite(
441
- groupOrOwnedValue.core.verified.header.ruleset.group as CoID<RawGroup>,
442
- inviteSecret,
443
- );
444
- } else if (
445
- groupOrOwnedValue.core.verified.header.ruleset.type !== "group"
446
- ) {
447
- 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");
450
+ }
451
+
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);
448
464
  }
449
465
 
450
- const group = expectGroup(groupOrOwnedValue);
466
+ if (group.core.verified.header.meta?.type === "account") {
467
+ throw new Error("Can't accept invites to values owned by accounts");
468
+ }
451
469
 
452
470
  const inviteAgentSecret = this.crypto.agentSecretFromSecretSeed(
453
471
  secretSeedFromInviteSecret(inviteSecret),
454
472
  );
473
+
455
474
  const inviteAgentID = this.crypto.getAgentID(inviteAgentSecret);
456
475
 
457
476
  const inviteRole = await new Promise((resolve, reject) => {
@@ -486,9 +505,10 @@ export class LocalNode {
486
505
  }
487
506
 
488
507
  const groupAsInvite = expectGroup(
489
- group.core.contentInClonedNodeWithDifferentAccount(
490
- new ControlledAgent(inviteAgentSecret, this.crypto),
491
- ),
508
+ this.loadCoValueAsDifferentAgent(
509
+ group.id,
510
+ inviteAgentSecret,
511
+ ).getCurrentContent(),
492
512
  );
493
513
 
494
514
  groupAsInvite.addMemberInternal(
@@ -517,7 +537,7 @@ export class LocalNode {
517
537
 
518
538
  if (!coValue.isAvailable()) {
519
539
  throw new Error(
520
- `${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded. Current state: ${JSON.stringify(coValue)}`,
540
+ `${expectation ? expectation + ": " : ""}CoValue ${id} not yet loaded.`,
521
541
  );
522
542
  }
523
543
  return coValue;
@@ -614,46 +634,56 @@ export class LocalNode {
614
634
  return group;
615
635
  }
616
636
 
617
- /** @internal */
618
- cloneWithDifferentAccount(
619
- controlledAccountOrAgent: ControlledAccountOrAgent,
620
- ): LocalNode {
637
+ loadCoValueAsDifferentAgent(
638
+ id: RawCoID,
639
+ secret: AgentSecret,
640
+ accountId?: RawAccountID | AgentID,
641
+ ) {
642
+ const agent = new ControlledAgent(secret, this.crypto);
643
+
621
644
  const newNode = new LocalNode(
622
- controlledAccountOrAgent.agentSecret,
623
- this.crypto.newRandomSessionID(controlledAccountOrAgent.id),
645
+ secret,
646
+ this.crypto.newRandomSessionID(accountId || agent.id),
624
647
  this.crypto,
625
648
  );
626
649
 
627
- newNode.cloneVerifiedStateFrom(this);
650
+ newNode.cloneVerifiedStateFrom(this, id);
628
651
 
629
- return newNode;
652
+ return newNode.expectCoValueLoaded(id);
630
653
  }
631
654
 
632
655
  /** @internal */
633
- cloneVerifiedStateFrom(otherNode: LocalNode) {
634
- const coValuesToCopy = Array.from(otherNode.coValues.entries());
656
+ cloneVerifiedStateFrom(otherNode: LocalNode, id: RawCoID) {
657
+ const coValuesIdsToCopy = [id];
635
658
 
636
- while (coValuesToCopy.length > 0) {
637
- 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);
638
663
 
639
664
  if (!coValue.isAvailable()) {
640
- coValuesToCopy.pop();
641
665
  continue;
642
- } else {
643
- const allDepsCopied = coValue
644
- .getDependedOnCoValues()
645
- .every((dep) => this.coValues.get(dep)?.isAvailable());
646
-
647
- if (!allDepsCopied) {
648
- // move to end of queue
649
- coValuesToCopy.unshift(coValuesToCopy.pop()!);
650
- continue;
651
- }
666
+ }
652
667
 
653
- this.putCoValue(coValueID, coValue.verified);
668
+ for (const dep of coValue.getDependedOnCoValues()) {
669
+ coValuesIdsToCopy.push(dep);
670
+ }
671
+ }
672
+
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;
680
+ }
654
681
 
655
- coValuesToCopy.pop();
682
+ if (this.coValues.get(coValueID)?.isAvailable()) {
683
+ continue;
656
684
  }
685
+
686
+ this.putCoValue(coValueID, coValue.verified);
657
687
  }
658
688
  }
659
689
 
@@ -6,23 +6,17 @@ export function connectedPeers(
6
6
  peer1id: PeerID,
7
7
  peer2id: PeerID,
8
8
  {
9
- trace = false,
10
9
  peer1role = "client",
11
10
  peer2role = "client",
12
11
  crashOnClose = false,
13
12
  }: {
14
- trace?: boolean;
15
13
  peer1role?: Peer["role"];
16
14
  peer2role?: Peer["role"];
17
15
  crashOnClose?: boolean;
18
16
  } = {},
19
17
  ): [Peer, Peer] {
20
- const [from1to2Rx, from1to2Tx] = newQueuePair(
21
- trace ? { traceAs: `${peer1id} -> ${peer2id}` } : undefined,
22
- );
23
- const [from2to1Rx, from2to1Tx] = newQueuePair(
24
- trace ? { traceAs: `${peer2id} -> ${peer1id}` } : undefined,
25
- );
18
+ const [from1to2Rx, from1to2Tx] = newQueuePair();
19
+ const [from2to1Rx, from2to1Tx] = newQueuePair();
26
20
 
27
21
  const peer2AsPeer: Peer = {
28
22
  id: peer2id,
@@ -43,32 +37,11 @@ export function connectedPeers(
43
37
  return [peer1AsPeer, peer2AsPeer];
44
38
  }
45
39
 
46
- export function newQueuePair(
47
- options: { traceAs?: string } = {},
48
- ): [AsyncIterable<SyncMessage>, Channel<SyncMessage>] {
40
+ export function newQueuePair(): [
41
+ AsyncIterable<SyncMessage>,
42
+ Channel<SyncMessage>,
43
+ ] {
49
44
  const channel = new Channel<SyncMessage>();
50
45
 
51
- if (options.traceAs) {
52
- return [
53
- (async function* () {
54
- for await (const msg of channel) {
55
- console.debug(
56
- options.traceAs,
57
- JSON.stringify(
58
- msg,
59
- (k, v) =>
60
- k === "changes" || k === "encryptedChanges"
61
- ? v.slice(0, 20) + "..."
62
- : v,
63
- 2,
64
- ),
65
- );
66
- yield msg;
67
- }
68
- })(),
69
- channel,
70
- ];
71
- } else {
72
- return [channel.wrap(), channel];
73
- }
46
+ return [channel.wrap(), channel];
74
47
  }
package/src/sync.ts CHANGED
@@ -159,6 +159,12 @@ export class SyncManager {
159
159
  );
160
160
  }
161
161
 
162
+ hasStoragePeers(): boolean {
163
+ return this.getPeers().some(
164
+ (peer) => peer.role === "storage" && !peer.closed,
165
+ );
166
+ }
167
+
162
168
  handleSyncMessage(msg: SyncMessage, peer: PeerState) {
163
169
  if (this.local.getCoValue(msg.id).isErroredInPeer(peer.id)) {
164
170
  logger.warn(
@@ -393,10 +399,12 @@ export class SyncManager {
393
399
 
394
400
  return;
395
401
  } else {
396
- // Should move the state to loading
397
- this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
398
- logger.error("Error loading coValue in handleLoad", { err: e });
399
- });
402
+ // Syncronously updates the state loading is possible
403
+ coValue
404
+ .loadFromPeers(this.getServerAndStoragePeers(peer.id))
405
+ .catch((e) => {
406
+ logger.error("Error loading coValue in handleLoad", { err: e });
407
+ });
400
408
  }
401
409
  }
402
410
 
@@ -622,28 +630,24 @@ export class SyncManager {
622
630
 
623
631
  handleUnsubscribe(_msg: DoneMessage) {}
624
632
 
625
- requestedSyncs = new Map<RawCoID, Promise<void>>();
626
-
627
- async requestCoValueSync(coValue: CoValueCore) {
628
- const promise = this.requestedSyncs.get(coValue.id);
633
+ requestedSyncs = new Set<RawCoID>();
634
+ requestCoValueSync(coValue: CoValueCore) {
635
+ if (this.requestedSyncs.has(coValue.id)) {
636
+ return;
637
+ }
629
638
 
630
- if (promise) {
631
- return promise;
632
- } else {
633
- const promise = new Promise<void>((resolve) => {
634
- queueMicrotask(() => {
635
- this.requestedSyncs.delete(coValue.id);
636
- this.syncCoValue(coValue);
637
- resolve();
638
- });
639
- });
639
+ queueMicrotask(() => {
640
+ if (this.requestedSyncs.has(coValue.id)) {
641
+ this.syncCoValue(coValue);
642
+ }
643
+ });
640
644
 
641
- this.requestedSyncs.set(coValue.id, promise);
642
- return promise;
643
- }
645
+ this.requestedSyncs.add(coValue.id);
644
646
  }
645
647
 
646
648
  async syncCoValue(coValue: CoValueCore) {
649
+ this.requestedSyncs.delete(coValue.id);
650
+
647
651
  for (const peer of this.peersInPriorityOrder()) {
648
652
  if (peer.closed) continue;
649
653
  if (coValue.isErroredInPeer(peer.id)) continue;
@@ -674,6 +678,21 @@ export class SyncManager {
674
678
  return true;
675
679
  }
676
680
 
681
+ const peerState = this.peers[peerId];
682
+
683
+ // The peer has been closed, so it isn't possible to sync
684
+ if (!peerState || peerState.closed) {
685
+ return true;
686
+ }
687
+
688
+ // The client isn't subscribed to the coValue, so we won't sync it
689
+ if (
690
+ peerState.role === "client" &&
691
+ !peerState.optimisticKnownStates.has(id)
692
+ ) {
693
+ return true;
694
+ }
695
+
677
696
  return new Promise((resolve, reject) => {
678
697
  const unsubscribe = this.syncState.subscribeToPeerUpdates(
679
698
  peerId,
@@ -693,10 +712,20 @@ export class SyncManager {
693
712
  });
694
713
  }
695
714
 
715
+ async waitForStorageSync(id: RawCoID, timeout = 30_000) {
716
+ const peers = this.getPeers();
717
+
718
+ await Promise.all(
719
+ peers
720
+ .filter((peer) => peer.role === "storage")
721
+ .map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
722
+ );
723
+ }
724
+
696
725
  async waitForSync(id: RawCoID, timeout = 30_000) {
697
726
  const peers = this.getPeers();
698
727
 
699
- return Promise.all(
728
+ await Promise.all(
700
729
  peers.map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
701
730
  );
702
731
  }
@@ -174,9 +174,6 @@ describe("PeerState", () => {
174
174
  test("should dispatch to both states", () => {
175
175
  const { peerState } = setup();
176
176
  const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
177
- if (peerState._optimisticKnownStates === "assumeInfallible") {
178
- throw new Error("Expected normal optimisticKnownStates");
179
- }
180
177
 
181
178
  const optimisticKnownStatesSpy = vi.spyOn(
182
179
  peerState._optimisticKnownStates,
@@ -195,40 +192,6 @@ describe("PeerState", () => {
195
192
  expect(optimisticKnownStatesSpy).toHaveBeenCalledWith("co_z1", state);
196
193
  });
197
194
 
198
- test("should use same reference for knownStates and optimisticKnownStates for storage peers", () => {
199
- const mockStoragePeer: Peer = {
200
- id: "test-storage-peer",
201
- role: "storage",
202
- priority: 1,
203
- crashOnClose: false,
204
- incoming: (async function* () {})(),
205
- outgoing: {
206
- push: vi.fn().mockResolvedValue(undefined),
207
- close: vi.fn(),
208
- },
209
- };
210
- const peerState = new PeerState(mockStoragePeer, undefined);
211
-
212
- // Verify they are the same reference
213
- expect(peerState.knownStates).toBe(peerState.optimisticKnownStates);
214
-
215
- // Verify that dispatching only updates one state
216
- const knownStatesSpy = vi.spyOn(peerState._knownStates, "set");
217
- expect(peerState._optimisticKnownStates).toBe("assumeInfallible");
218
-
219
- const state: CoValueKnownState = {
220
- id: "co_z1",
221
- header: false,
222
- sessions: {},
223
- };
224
-
225
- peerState.setKnownState("co_z1", state);
226
-
227
- // Only one dispatch should happen since they're the same reference
228
- expect(knownStatesSpy).toHaveBeenCalledTimes(1);
229
- expect(knownStatesSpy).toHaveBeenCalledWith("co_z1", state);
230
- });
231
-
232
195
  test("should use separate references for knownStates and optimisticKnownStates for non-storage peers", () => {
233
196
  const { peerState } = setup(); // Uses a regular peer
234
197
 
@@ -70,12 +70,12 @@ describe("SyncStateManager", () => {
70
70
  const map = group.createMap();
71
71
  map.set("key1", "value1", "trusting");
72
72
 
73
- const [clientStoragePeer] = connectedPeers("clientStorage", "unusedPeer", {
74
- peer1role: "client",
75
- peer2role: "server",
73
+ const [serverPeer] = connectedPeers("serverPeer", "unusedPeer", {
74
+ peer1role: "server",
75
+ peer2role: "client",
76
76
  });
77
77
 
78
- client.node.syncManager.addPeer(clientStoragePeer);
78
+ client.node.syncManager.addPeer(serverPeer);
79
79
 
80
80
  const subscriptionManager = client.node.syncManager.syncState;
81
81
 
@@ -86,7 +86,7 @@ describe("SyncStateManager", () => {
86
86
  updateToJazzCloudSpy,
87
87
  );
88
88
  const unsubscribe2 = subscriptionManager.subscribeToPeerUpdates(
89
- clientStoragePeer.id,
89
+ serverPeer.id,
90
90
  updateToStorageSpy,
91
91
  );
92
92
 
@@ -115,7 +115,7 @@ describe("SyncStateManager", () => {
115
115
  );
116
116
 
117
117
  expect(updateToStorageSpy).toHaveBeenLastCalledWith(
118
- emptyKnownState(map.core.id),
118
+ emptyKnownState(group.core.id),
119
119
  { uploaded: false },
120
120
  );
121
121
  });
@@ -247,4 +247,54 @@ describe("SyncStateManager", () => {
247
247
  ),
248
248
  ).toEqual({ uploaded: true });
249
249
  });
250
+
251
+ test("should skip closed peers", async () => {
252
+ const client = setupTestNode();
253
+ const { peerState } = client.connectToSyncServer();
254
+
255
+ peerState.gracefulShutdown();
256
+
257
+ const group = client.node.createGroup();
258
+ const map = group.createMap();
259
+
260
+ await expect(map.core.waitForSync()).resolves.toBeUndefined();
261
+ });
262
+
263
+ test("should skip client peers that are not subscribed to the coValue", async () => {
264
+ const server = setupTestNode({ isSyncServer: true });
265
+ const client = setupTestNode();
266
+
267
+ client.connectToSyncServer({
268
+ syncServer: server.node,
269
+ });
270
+
271
+ const group = server.node.createGroup();
272
+ const map = group.createMap();
273
+
274
+ await map.core.waitForSync();
275
+
276
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(false);
277
+ });
278
+
279
+ test("should wait for client peers that are subscribed to the coValue", async () => {
280
+ const server = setupTestNode({ isSyncServer: true });
281
+ const client = setupTestNode();
282
+
283
+ const { peerStateOnServer } = client.connectToSyncServer();
284
+
285
+ const group = server.node.createGroup();
286
+ const map = group.createMap();
287
+ map.set("key1", "value1", "trusting");
288
+
289
+ // Simulate the subscription to the coValue
290
+ peerStateOnServer.setKnownState(map.core.id, {
291
+ id: map.core.id,
292
+ header: true,
293
+ sessions: {},
294
+ });
295
+
296
+ await map.core.waitForSync();
297
+
298
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
299
+ });
250
300
  });
@@ -1,7 +1,9 @@
1
1
  import { expect, test } from "vitest";
2
+ import { expectAccount } from "../coValues/account.js";
2
3
  import { WasmCrypto } from "../crypto/WasmCrypto.js";
3
4
  import { LocalNode } from "../localNode.js";
4
5
  import { connectedPeers } from "../streamUtils.js";
6
+ import { createMockStoragePeer } from "./testUtils.js";
5
7
 
6
8
  const Crypto = await WasmCrypto.create();
7
9
 
@@ -85,3 +87,25 @@ test("throws an error if the user tried to create an invite from an account", as
85
87
  "Cannot create invite from an account",
86
88
  );
87
89
  });
90
+
91
+ test("wait for storage sync before resolving withNewlyCreatedAccount", async () => {
92
+ const { storage, peer } = createMockStoragePeer({
93
+ peerId: "account-node",
94
+ });
95
+
96
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
97
+ creationProps: { name: "Hermes Puggington" },
98
+ crypto: Crypto,
99
+ peersToLoadFrom: [peer],
100
+ });
101
+
102
+ const account = storage.getCoValue(accountID);
103
+
104
+ expect(account.isAvailable()).toBe(true);
105
+
106
+ const profile = storage.getCoValue(
107
+ expectAccount(account.getCurrentContent()).get("profile")!,
108
+ );
109
+
110
+ expect(profile.isAvailable()).toBe(true);
111
+ });
@@ -750,7 +750,6 @@ describe("extend with role mapping", () => {
750
750
  const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
751
751
 
752
752
  expect(mapOnNode2.get("test")).toEqual("Written from the admin");
753
-
754
753
  mapOnNode2.set("test", "Written from the inherited role");
755
754
  expect(mapOnNode2.get("test")).toEqual("Written from the inherited role");
756
755
 
@@ -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