cojson 0.0.21 → 0.0.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.
Files changed (56) hide show
  1. package/dist/account.d.ts +1 -1
  2. package/dist/account.js +1 -1
  3. package/dist/account.js.map +1 -1
  4. package/dist/coValue.d.ts +10 -4
  5. package/dist/coValue.js +32 -15
  6. package/dist/coValue.js.map +1 -1
  7. package/dist/contentTypes/coMap.d.ts +2 -2
  8. package/dist/contentTypes/coMap.js +6 -6
  9. package/dist/contentTypes/coMap.js.map +1 -1
  10. package/dist/index.d.ts +5 -2
  11. package/dist/index.js +3 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/node.d.ts +2 -1
  14. package/dist/node.js +68 -11
  15. package/dist/node.js.map +1 -1
  16. package/dist/permissions.d.ts +4 -32
  17. package/dist/permissions.js +42 -106
  18. package/dist/permissions.js.map +1 -1
  19. package/dist/team.d.ts +37 -0
  20. package/dist/team.js +116 -0
  21. package/dist/team.js.map +1 -0
  22. package/dist/testUtils.d.ts +2 -2
  23. package/dist/testUtils.js +1 -1
  24. package/dist/testUtils.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/account.ts +1 -1
  27. package/src/coValue.test.ts +58 -0
  28. package/src/coValue.ts +41 -24
  29. package/src/contentTypes/coMap.ts +8 -8
  30. package/src/crypto.test.ts +10 -9
  31. package/src/index.ts +5 -0
  32. package/src/node.ts +151 -26
  33. package/src/permissions.test.ts +503 -3
  34. package/src/permissions.ts +70 -206
  35. package/src/sync.test.ts +8 -8
  36. package/src/team.ts +233 -0
  37. package/src/testUtils.ts +1 -1
  38. package/tsconfig.json +1 -0
  39. package/dist/account.test.d.ts +0 -1
  40. package/dist/account.test.js +0 -40
  41. package/dist/account.test.js.map +0 -1
  42. package/dist/coValue.test.d.ts +0 -1
  43. package/dist/coValue.test.js +0 -78
  44. package/dist/coValue.test.js.map +0 -1
  45. package/dist/contentType.test.d.ts +0 -1
  46. package/dist/contentType.test.js +0 -145
  47. package/dist/contentType.test.js.map +0 -1
  48. package/dist/crypto.test.d.ts +0 -1
  49. package/dist/crypto.test.js +0 -111
  50. package/dist/crypto.test.js.map +0 -1
  51. package/dist/permissions.test.d.ts +0 -1
  52. package/dist/permissions.test.js +0 -711
  53. package/dist/permissions.test.js.map +0 -1
  54. package/dist/sync.test.d.ts +0 -1
  55. package/dist/sync.test.js +0 -827
  56. package/dist/sync.test.js.map +0 -1
package/src/coValue.ts CHANGED
@@ -24,11 +24,10 @@ import { JsonObject, JsonValue } from "./jsonValue.js";
24
24
  import { base58 } from "@scure/base";
25
25
  import {
26
26
  PermissionsDef as RulesetDef,
27
- Team,
28
27
  determineValidTransactions,
29
- expectTeamContent,
30
28
  isKeyForKeyField,
31
29
  } from "./permissions.js";
30
+ import { Team, expectTeamContent } from "./team.js";
32
31
  import { LocalNode } from "./node.js";
33
32
  import { CoValueKnownState, NewContentMessage } from "./sync.js";
34
33
  import { RawCoID, SessionID, TransactionID } from "./ids.js";
@@ -97,15 +96,31 @@ export class CoValue {
97
96
  id: RawCoID;
98
97
  node: LocalNode;
99
98
  header: CoValueHeader;
100
- sessions: { [key: SessionID]: SessionLog };
101
- content?: ContentType;
99
+ _sessions: { [key: SessionID]: SessionLog };
100
+ _cachedContent?: ContentType;
102
101
  listeners: Set<(content?: ContentType) => void> = new Set();
103
102
 
104
- constructor(header: CoValueHeader, node: LocalNode) {
103
+ constructor(header: CoValueHeader, node: LocalNode, internalInitSessions: { [key: SessionID]: SessionLog } = {}) {
105
104
  this.id = idforHeader(header);
106
105
  this.header = header;
107
- this.sessions = {};
106
+ this._sessions = internalInitSessions;
108
107
  this.node = node;
108
+
109
+ if (header.ruleset.type == "ownedByTeam") {
110
+ this.node
111
+ .expectCoValueLoaded(header.ruleset.team)
112
+ .subscribe((_teamUpdate) => {
113
+ this._cachedContent = undefined;
114
+ const newContent = this.getCurrentContent();
115
+ for (const listener of this.listeners) {
116
+ listener(newContent);
117
+ }
118
+ });
119
+ }
120
+ }
121
+
122
+ get sessions(): Readonly<{ [key: SessionID]: SessionLog }> {
123
+ return this._sessions;
109
124
  }
110
125
 
111
126
  testWithDifferentAccount(
@@ -172,7 +187,10 @@ export class CoValue {
172
187
  );
173
188
 
174
189
  if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
175
- console.warn("Invalid hash", { expectedNewHash, givenExpectedNewHash });
190
+ console.warn("Invalid hash", {
191
+ expectedNewHash,
192
+ givenExpectedNewHash,
193
+ });
176
194
  return false;
177
195
  }
178
196
 
@@ -190,14 +208,14 @@ export class CoValue {
190
208
 
191
209
  transactions.push(...newTransactions);
192
210
 
193
- this.sessions[sessionID] = {
211
+ this._sessions[sessionID] = {
194
212
  transactions,
195
213
  lastHash: expectedNewHash,
196
214
  streamingHash: newStreamingHash,
197
215
  lastSignature: newSignature,
198
216
  };
199
217
 
200
- this.content = undefined;
218
+ this._cachedContent = undefined;
201
219
 
202
220
  const content = this.getCurrentContent();
203
221
 
@@ -296,23 +314,23 @@ export class CoValue {
296
314
  }
297
315
 
298
316
  getCurrentContent(): ContentType {
299
- if (this.content) {
300
- return this.content;
317
+ if (this._cachedContent) {
318
+ return this._cachedContent;
301
319
  }
302
320
 
303
321
  if (this.header.type === "comap") {
304
- this.content = new CoMap(this);
322
+ this._cachedContent = new CoMap(this);
305
323
  } else if (this.header.type === "colist") {
306
- this.content = new CoList(this);
324
+ this._cachedContent = new CoList(this);
307
325
  } else if (this.header.type === "costream") {
308
- this.content = new CoStream(this);
326
+ this._cachedContent = new CoStream(this);
309
327
  } else if (this.header.type === "static") {
310
- this.content = new Static(this);
328
+ this._cachedContent = new Static(this);
311
329
  } else {
312
330
  throw new Error(`Unknown coValue type ${this.header.type}`);
313
331
  }
314
332
 
315
- return this.content;
333
+ return this._cachedContent;
316
334
  }
317
335
 
318
336
  getValidSortedTransactions(): DecryptedTransaction[] {
@@ -399,7 +417,9 @@ export class CoValue {
399
417
 
400
418
  // Try to find key revelation for us
401
419
 
402
- const readKeyEntry = content.getLastEntry(`${keyID}_for_${this.node.account.id}`);
420
+ const readKeyEntry = content.getLastEntry(
421
+ `${keyID}_for_${this.node.account.id}`
422
+ );
403
423
 
404
424
  if (readKeyEntry) {
405
425
  const revealer = accountOrAgentIDfromSessionID(
@@ -428,7 +448,8 @@ export class CoValue {
428
448
  for (const field of content.keys()) {
429
449
  if (isKeyForKeyField(field) && field.startsWith(keyID)) {
430
450
  const encryptingKeyID = field.split("_for_")[1] as KeyID;
431
- const encryptingKeySecret = this.getReadKey(encryptingKeyID);
451
+ const encryptingKeySecret =
452
+ this.getReadKey(encryptingKeyID);
432
453
 
433
454
  if (!encryptingKeySecret) {
434
455
  continue;
@@ -453,7 +474,6 @@ export class CoValue {
453
474
  );
454
475
  }
455
476
  }
456
-
457
477
  }
458
478
 
459
479
  return undefined;
@@ -525,10 +545,7 @@ export class CoValue {
525
545
  ),
526
546
  };
527
547
 
528
- if (
529
- !newContent.header &&
530
- Object.keys(newContent.new).length === 0
531
- ) {
548
+ if (!newContent.header && Object.keys(newContent.new).length === 0) {
532
549
  return undefined;
533
550
  }
534
551
 
@@ -544,4 +561,4 @@ export class CoValue {
544
561
  ? [this.header.ruleset.team]
545
562
  : [];
546
563
  }
547
- }
564
+ }
@@ -12,12 +12,12 @@ type MapOp<K extends string, V extends JsonValue> = {
12
12
  // TODO: add after TransactionID[] for conflicts/ordering
13
13
 
14
14
  export type MapOpPayload<K extends string, V extends JsonValue> = {
15
- op: "insert";
15
+ op: "set";
16
16
  key: K;
17
17
  value: V;
18
18
  } |
19
19
  {
20
- op: "delete";
20
+ op: "del";
21
21
  key: K;
22
22
  };
23
23
 
@@ -81,7 +81,7 @@ export class CoMap<
81
81
 
82
82
  const lastEntry = ops[ops.length - 1]!;
83
83
 
84
- if (lastEntry.op === "delete") {
84
+ if (lastEntry.op === "del") {
85
85
  return undefined;
86
86
  } else {
87
87
  return lastEntry.value;
@@ -100,7 +100,7 @@ export class CoMap<
100
100
  return undefined;
101
101
  }
102
102
 
103
- if (lastOpBeforeOrAtTime.op === "delete") {
103
+ if (lastOpBeforeOrAtTime.op === "del") {
104
104
  return undefined;
105
105
  } else {
106
106
  return lastOpBeforeOrAtTime.value;
@@ -139,7 +139,7 @@ export class CoMap<
139
139
 
140
140
  const lastEntry = ops[ops.length - 1]!;
141
141
 
142
- if (lastEntry.op === "delete") {
142
+ if (lastEntry.op === "del") {
143
143
  return undefined;
144
144
  } else {
145
145
  return { at: lastEntry.madeAt, txID: lastEntry.txID, value: lastEntry.value };
@@ -155,7 +155,7 @@ export class CoMap<
155
155
  const history: { at: number; txID: TransactionID; value: M[K] | undefined; }[] = [];
156
156
 
157
157
  for (const op of ops) {
158
- if (op.op === "delete") {
158
+ if (op.op === "del") {
159
159
  history.push({ at: op.madeAt, txID: op.txID, value: undefined });
160
160
  } else {
161
161
  history.push({ at: op.madeAt, txID: op.txID, value: op.value });
@@ -199,7 +199,7 @@ export class WriteableCoMap<
199
199
  set<K extends MapK<M>>(key: K, value: M[K], privacy: "private" | "trusting" = "private"): void {
200
200
  this.coValue.makeTransaction([
201
201
  {
202
- op: "insert",
202
+ op: "set",
203
203
  key,
204
204
  value,
205
205
  },
@@ -211,7 +211,7 @@ export class WriteableCoMap<
211
211
  delete(key: MapK<M>, privacy: "private" | "trusting" = "private"): void {
212
212
  this.coValue.makeTransaction([
213
213
  {
214
- op: "delete",
214
+ op: "del",
215
215
  key,
216
216
  },
217
217
  ], privacy);
@@ -20,6 +20,7 @@ import { x25519 } from "@noble/curves/ed25519";
20
20
  import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
21
21
  import { blake3 } from "@noble/hashes/blake3";
22
22
  import stableStringify from "fast-json-stable-stringify";
23
+ import { SessionID } from './ids.js';
23
24
 
24
25
  test("Signatures round-trip and use stable stringify", () => {
25
26
  const data = { b: "world", a: "hello" };
@@ -49,7 +50,7 @@ test("encrypting round-trips, but invalid receiver can't unseal", () => {
49
50
 
50
51
  const nOnceMaterial = {
51
52
  in: "co_zTEST",
52
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
53
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
53
54
  } as const;
54
55
 
55
56
  const sealed = seal(
@@ -101,22 +102,22 @@ test("Encryption for transactions round-trips", () => {
101
102
 
102
103
  const encrypted1 = encryptForTransaction({ a: "hello" }, secret, {
103
104
  in: "co_zTEST",
104
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
105
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
105
106
  });
106
107
 
107
108
  const encrypted2 = encryptForTransaction({ b: "world" }, secret, {
108
109
  in: "co_zTEST",
109
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
110
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
110
111
  });
111
112
 
112
113
  const decrypted1 = decryptForTransaction(encrypted1, secret, {
113
114
  in: "co_zTEST",
114
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
115
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
115
116
  });
116
117
 
117
118
  const decrypted2 = decryptForTransaction(encrypted2, secret, {
118
119
  in: "co_zTEST",
119
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
120
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
120
121
  });
121
122
 
122
123
  expect([decrypted1, decrypted2]).toEqual([{ a: "hello" }, { b: "world" }]);
@@ -128,22 +129,22 @@ test("Encryption for transactions doesn't decrypt with a wrong key", () => {
128
129
 
129
130
  const encrypted1 = encryptForTransaction({ a: "hello" }, secret, {
130
131
  in: "co_zTEST",
131
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
132
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
132
133
  });
133
134
 
134
135
  const encrypted2 = encryptForTransaction({ b: "world" }, secret, {
135
136
  in: "co_zTEST",
136
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
137
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
137
138
  });
138
139
 
139
140
  const decrypted1 = decryptForTransaction(encrypted1, secret2, {
140
141
  in: "co_zTEST",
141
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 0 },
142
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 0 },
142
143
  });
143
144
 
144
145
  const decrypted2 = decryptForTransaction(encrypted2, secret2, {
145
146
  in: "co_zTEST",
146
- tx: { sessionID: "co_zTEST_session_zTEST", txIndex: 1 },
147
+ tx: { sessionID: "co_zTEST_session_zTEST" as SessionID, txIndex: 1 },
147
148
  });
148
149
 
149
150
  expect([decrypted1, decrypted2]).toEqual([undefined, undefined]);
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  import { connectedPeers } from "./streamUtils.js";
15
15
  import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
16
16
  import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
17
+ import { Team, expectTeamContent } from "./team.js"
17
18
 
18
19
  import type { SessionID, AgentID } from "./ids.js";
19
20
  import type { CoID, ContentType } from "./contentType.js";
@@ -26,6 +27,7 @@ import type {
26
27
  ProfileContent,
27
28
  Profile,
28
29
  } from "./account.js";
30
+ import type { InviteSecret } from "./team.js";
29
31
 
30
32
  type Value = JsonValue | ContentType;
31
33
 
@@ -42,6 +44,7 @@ export const cojsonInternals = {
42
44
  agentSecretFromSecretSeed,
43
45
  secretSeedLength,
44
46
  shortHashLength,
47
+ expectTeamContent
45
48
  };
46
49
 
47
50
  export {
@@ -50,6 +53,7 @@ export {
50
53
  CoMap,
51
54
  AnonymousControlledAccount,
52
55
  ControlledAccount,
56
+ Team
53
57
  };
54
58
 
55
59
  export type {
@@ -66,6 +70,7 @@ export type {
66
70
  AccountContent,
67
71
  Profile,
68
72
  ProfileContent,
73
+ InviteSecret
69
74
  };
70
75
 
71
76
  // eslint-disable-next-line @typescript-eslint/no-namespace
package/src/node.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  AgentSecret,
3
+ agentSecretFromSecretSeed,
3
4
  createdNowUnique,
4
5
  getAgentID,
5
6
  getAgentSealerID,
@@ -9,7 +10,13 @@ import {
9
10
  seal,
10
11
  } from "./crypto.js";
11
12
  import { CoValue, CoValueHeader, newRandomSessionID } from "./coValue.js";
12
- import { Team, TeamContent, expectTeamContent } from "./permissions.js";
13
+ import {
14
+ InviteSecret,
15
+ Team,
16
+ TeamContent,
17
+ expectTeamContent,
18
+ secretSeedFromInviteSecret,
19
+ } from "./team.js";
13
20
  import { Peer, SyncManager } from "./sync.js";
14
21
  import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
15
22
  import { CoID, ContentType } from "./contentType.js";
@@ -43,7 +50,10 @@ export class LocalNode {
43
50
  this.ownSessionID = ownSessionID;
44
51
  }
45
52
 
46
- static withNewlyCreatedAccount(name: string, initialAgentSecret = newRandomAgentSecret()): {
53
+ static withNewlyCreatedAccount(
54
+ name: string,
55
+ initialAgentSecret = newRandomAgentSecret()
56
+ ): {
47
57
  node: LocalNode;
48
58
  accountID: AccountID;
49
59
  accountSecret: AgentSecret;
@@ -70,8 +80,16 @@ export class LocalNode {
70
80
  };
71
81
  }
72
82
 
73
- static async withLoadedAccount(accountID: AccountID, accountSecret: AgentSecret, sessionID: SessionID, peersToLoadFrom: Peer[]): Promise<LocalNode> {
74
- const loadingNode = new LocalNode(new AnonymousControlledAccount(accountSecret), newRandomSessionID(accountID));
83
+ static async withLoadedAccount(
84
+ accountID: AccountID,
85
+ accountSecret: AgentSecret,
86
+ sessionID: SessionID,
87
+ peersToLoadFrom: Peer[]
88
+ ): Promise<LocalNode> {
89
+ const loadingNode = new LocalNode(
90
+ new AnonymousControlledAccount(accountSecret),
91
+ newRandomSessionID(accountID)
92
+ );
75
93
 
76
94
  const accountPromise = loadingNode.load(accountID);
77
95
 
@@ -82,7 +100,10 @@ export class LocalNode {
82
100
  const account = await accountPromise;
83
101
 
84
102
  // since this is all synchronous, we can just swap out nodes for the SyncManager
85
- const node = loadingNode.testWithDifferentAccount(new ControlledAccount(accountSecret, account, loadingNode), sessionID);
103
+ const node = loadingNode.testWithDifferentAccount(
104
+ new ControlledAccount(accountSecret, account, loadingNode),
105
+ sessionID
106
+ );
86
107
  node.sync = loadingNode.sync;
87
108
  node.sync.local = node;
88
109
 
@@ -124,7 +145,81 @@ export class LocalNode {
124
145
  if (!profileID) {
125
146
  throw new Error(`Account ${id} has no profile`);
126
147
  }
127
- return (await this.loadCoValue(profileID)).getCurrentContent() as Profile;
148
+ return (
149
+ await this.loadCoValue(profileID)
150
+ ).getCurrentContent() as Profile;
151
+ }
152
+
153
+ async acceptInvite<T extends ContentType>(
154
+ teamOrOwnedValueID: CoID<T>,
155
+ inviteSecret: InviteSecret
156
+ ): Promise<void> {
157
+ const teamOrOwnedValue = await this.load(teamOrOwnedValueID);
158
+
159
+ if (teamOrOwnedValue.coValue.header.ruleset.type === "ownedByTeam") {
160
+ return this.acceptInvite(
161
+ teamOrOwnedValue.coValue.header.ruleset.team as CoID<
162
+ CoMap<TeamContent>
163
+ >,
164
+ inviteSecret
165
+ );
166
+ } else if (teamOrOwnedValue.coValue.header.ruleset.type !== "team") {
167
+ throw new Error("Can only accept invites to teams");
168
+ }
169
+
170
+ const team = new Team(expectTeamContent(teamOrOwnedValue), this);
171
+
172
+ const inviteAgentSecret = agentSecretFromSecretSeed(
173
+ secretSeedFromInviteSecret(inviteSecret)
174
+ );
175
+ const inviteAgentID = getAgentID(inviteAgentSecret);
176
+
177
+ const invitationRole = await new Promise((resolve, reject) => {
178
+ team.teamMap.subscribe((teamMap) => {
179
+ const role = teamMap.get(inviteAgentID);
180
+ if (role) {
181
+ resolve(role);
182
+ }
183
+ });
184
+ setTimeout(
185
+ () =>
186
+ reject(
187
+ new Error("Couldn't find invitation before timeout")
188
+ ),
189
+ 1000
190
+ );
191
+ });
192
+
193
+ if (!invitationRole) {
194
+ throw new Error("No invitation found");
195
+ }
196
+
197
+ const existingRole = team.teamMap.get(this.account.id);
198
+
199
+ if (
200
+ existingRole === "admin" ||
201
+ (existingRole === "writer" && invitationRole === "reader")
202
+ ) {
203
+ console.debug("Not accepting invite that would downgrade role");
204
+ return;
205
+ }
206
+
207
+ const teamAsInvite = team.testWithDifferentAccount(
208
+ new AnonymousControlledAccount(inviteAgentSecret),
209
+ newRandomSessionID(inviteAgentID)
210
+ );
211
+
212
+ teamAsInvite.addMember(
213
+ this.account.id,
214
+ invitationRole === "adminInvite"
215
+ ? "admin"
216
+ : invitationRole === "writerInvite"
217
+ ? "writer"
218
+ : "reader"
219
+ );
220
+
221
+ team.teamMap.coValue._sessions = teamAsInvite.teamMap.coValue.sessions;
222
+ team.teamMap.coValue._cachedContent = undefined;
128
223
  }
129
224
 
130
225
  expectCoValueLoaded(id: RawCoID, expectation?: string): CoValue {
@@ -146,7 +241,9 @@ export class LocalNode {
146
241
 
147
242
  expectProfileLoaded(id: AccountID, expectation?: string): Profile {
148
243
  const account = this.expectCoValueLoaded(id, expectation);
149
- const profileID = expectTeamContent(account.getCurrentContent()).get("profile");
244
+ const profileID = expectTeamContent(account.getCurrentContent()).get(
245
+ "profile"
246
+ );
150
247
  if (!profileID) {
151
248
  throw new Error(
152
249
  `${
@@ -154,10 +251,16 @@ export class LocalNode {
154
251
  }Account ${id} has no profile`
155
252
  );
156
253
  }
157
- return this.expectCoValueLoaded(profileID, expectation).getCurrentContent() as Profile;
254
+ return this.expectCoValueLoaded(
255
+ profileID,
256
+ expectation
257
+ ).getCurrentContent() as Profile;
158
258
  }
159
259
 
160
- createAccount(name: string, agentSecret = newRandomAgentSecret()): ControlledAccount {
260
+ createAccount(
261
+ name: string,
262
+ agentSecret = newRandomAgentSecret()
263
+ ): ControlledAccount {
161
264
  const account = this.createCoValue(
162
265
  accountHeaderForInitialAgentSecret(agentSecret)
163
266
  ).testWithDifferentAccount(
@@ -165,7 +268,10 @@ export class LocalNode {
165
268
  newRandomSessionID(getAgentID(agentSecret))
166
269
  );
167
270
 
168
- const accountAsTeam = new Team(expectTeamContent(account.getCurrentContent()), account.node);
271
+ const accountAsTeam = new Team(
272
+ expectTeamContent(account.getCurrentContent()),
273
+ account.node
274
+ );
169
275
 
170
276
  accountAsTeam.teamMap.edit((editable) => {
171
277
  editable.set(getAgentID(agentSecret), "admin", "trusting");
@@ -195,7 +301,9 @@ export class LocalNode {
195
301
  account.node
196
302
  );
197
303
 
198
- const profile = accountAsTeam.createMap<ProfileContent, ProfileMeta>({ type: "profile" });
304
+ const profile = accountAsTeam.createMap<ProfileContent, ProfileMeta>({
305
+ type: "profile",
306
+ });
199
307
 
200
308
  profile.edit((editable) => {
201
309
  editable.set("name", name, "trusting");
@@ -205,6 +313,11 @@ export class LocalNode {
205
313
  editable.set("profile", profile.id, "trusting");
206
314
  });
207
315
 
316
+ const accountOnThisNode = this.expectCoValueLoaded(account.id);
317
+
318
+ accountOnThisNode._sessions = {...accountAsTeam.teamMap.coValue.sessions};
319
+ accountOnThisNode._cachedContent = undefined;
320
+
208
321
  return controlledAccount;
209
322
  }
210
323
 
@@ -276,24 +389,36 @@ export class LocalNode {
276
389
  ): LocalNode {
277
390
  const newNode = new LocalNode(account, ownSessionID);
278
391
 
279
- newNode.coValues = Object.fromEntries(
280
- Object.entries(this.coValues)
281
- .map(([id, entry]) => {
282
- if (entry.state === "loading") {
283
- return undefined;
284
- }
392
+ const coValuesToCopy = Object.entries(this.coValues);
285
393
 
286
- const newCoValue = new CoValue(
287
- entry.coValue.header,
288
- newNode
289
- );
394
+ while (coValuesToCopy.length > 0) {
395
+ const [coValueID, entry] =
396
+ coValuesToCopy[coValuesToCopy.length - 1]!;
290
397
 
291
- newCoValue.sessions = entry.coValue.sessions;
398
+ if (entry.state === "loading") {
399
+ coValuesToCopy.pop();
400
+ continue;
401
+ } else {
402
+ const allDepsCopied = entry.coValue
403
+ .getDependedOnCoValues()
404
+ .every((dep) => newNode.coValues[dep]?.state === "loaded");
292
405
 
293
- return [id, { state: "loaded", coValue: newCoValue }];
294
- })
295
- .filter((x): x is Exclude<typeof x, undefined> => !!x)
296
- );
406
+ if (!allDepsCopied) {
407
+ // move to end of queue
408
+ coValuesToCopy.unshift(coValuesToCopy.pop()!);
409
+ continue;
410
+ }
411
+
412
+ const newCoValue = new CoValue(entry.coValue.header, newNode, {...entry.coValue.sessions});
413
+
414
+ newNode.coValues[coValueID as RawCoID] = {
415
+ state: "loaded",
416
+ coValue: newCoValue,
417
+ };
418
+
419
+ coValuesToCopy.pop();
420
+ }
421
+ }
297
422
 
298
423
  return newNode;
299
424
  }