cojson 0.8.32 → 0.8.35

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 (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/native/coValueCore.js +73 -37
  3. package/dist/native/coValueCore.js.map +1 -1
  4. package/dist/native/coValues/coMap.js +2 -2
  5. package/dist/native/coValues/coMap.js.map +1 -1
  6. package/dist/native/coValues/group.js +132 -5
  7. package/dist/native/coValues/group.js.map +1 -1
  8. package/dist/native/exports.js +5 -2
  9. package/dist/native/exports.js.map +1 -1
  10. package/dist/native/ids.js +33 -0
  11. package/dist/native/ids.js.map +1 -1
  12. package/dist/native/permissions.js +206 -145
  13. package/dist/native/permissions.js.map +1 -1
  14. package/dist/native/storage/index.js +8 -4
  15. package/dist/native/storage/index.js.map +1 -1
  16. package/dist/native/sync.js +41 -31
  17. package/dist/native/sync.js.map +1 -1
  18. package/dist/web/coValueCore.js +73 -37
  19. package/dist/web/coValueCore.js.map +1 -1
  20. package/dist/web/coValues/coMap.js +2 -2
  21. package/dist/web/coValues/coMap.js.map +1 -1
  22. package/dist/web/coValues/group.js +132 -5
  23. package/dist/web/coValues/group.js.map +1 -1
  24. package/dist/web/exports.js +5 -2
  25. package/dist/web/exports.js.map +1 -1
  26. package/dist/web/ids.js +33 -0
  27. package/dist/web/ids.js.map +1 -1
  28. package/dist/web/permissions.js +206 -145
  29. package/dist/web/permissions.js.map +1 -1
  30. package/dist/web/storage/index.js +8 -4
  31. package/dist/web/storage/index.js.map +1 -1
  32. package/dist/web/sync.js +41 -31
  33. package/dist/web/sync.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/coValueCore.ts +119 -46
  36. package/src/coValues/coMap.ts +3 -6
  37. package/src/coValues/group.ts +219 -6
  38. package/src/exports.ts +18 -3
  39. package/src/ids.ts +48 -0
  40. package/src/permissions.ts +297 -204
  41. package/src/storage/index.ts +12 -4
  42. package/src/sync.ts +43 -34
  43. package/src/tests/group.test.ts +152 -1
  44. package/src/tests/permissions.test.ts +785 -2
  45. package/src/tests/sync.test.ts +29 -0
  46. package/src/tests/testUtils.ts +102 -1
@@ -2,9 +2,16 @@ import { CoID } from "./coValue.js";
2
2
  import { CoValueCore, Transaction } from "./coValueCore.js";
3
3
  import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
4
4
  import { MapOpPayload } from "./coValues/coMap.js";
5
- import { EVERYONE, Everyone } from "./coValues/group.js";
5
+ import { EVERYONE, Everyone, RawGroup } from "./coValues/group.js";
6
6
  import { KeyID } from "./crypto/crypto.js";
7
- import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
7
+ import {
8
+ AgentID,
9
+ ParentGroupReference,
10
+ RawCoID,
11
+ SessionID,
12
+ TransactionID,
13
+ getParentGroupId,
14
+ } from "./ids.js";
8
15
  import { parseJSON } from "./jsonStringify.js";
9
16
  import { JsonValue } from "./jsonValue.js";
10
17
  import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
@@ -24,202 +31,20 @@ export type Role =
24
31
  | "writerInvite"
25
32
  | "readerInvite";
26
33
 
34
+ type ValidTransactionsResult = { txID: TransactionID; tx: Transaction };
35
+ type MemberState = { [agent: RawAccountID | AgentID]: Role; [EVERYONE]?: Role };
36
+
27
37
  export function determineValidTransactions(
28
38
  coValue: CoValueCore,
29
39
  ): { txID: TransactionID; tx: Transaction }[] {
30
40
  if (coValue.header.ruleset.type === "group") {
31
- const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
32
- ([sessionID, sessionLog]) => {
33
- return sessionLog.transactions.map((tx, txIndex) => ({
34
- sessionID,
35
- txIndex,
36
- tx,
37
- })) as {
38
- sessionID: SessionID;
39
- txIndex: number;
40
- tx: Transaction;
41
- }[];
42
- },
43
- );
44
-
45
- allTransactionsSorted.sort((a, b) => {
46
- return a.tx.madeAt - b.tx.madeAt;
47
- });
48
-
49
41
  const initialAdmin = coValue.header.ruleset.initialAdmin;
50
-
51
42
  if (!initialAdmin) {
52
43
  throw new Error("Group must have initialAdmin");
53
44
  }
54
45
 
55
- const memberState: {
56
- [agent: RawAccountID | AgentID]: Role;
57
- [EVERYONE]?: Role;
58
- } = {};
59
-
60
- const validTransactions: { txID: TransactionID; tx: Transaction }[] = [];
61
-
62
- for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
63
- // console.log("before", { memberState, validTransactions });
64
- const transactor = accountOrAgentIDfromSessionID(sessionID);
65
-
66
- if (tx.privacy === "private") {
67
- if (memberState[transactor] === "admin") {
68
- validTransactions.push({
69
- txID: { sessionID, txIndex },
70
- tx,
71
- });
72
- continue;
73
- } else {
74
- console.warn("Only admins can make private transactions in groups");
75
- continue;
76
- }
77
- }
78
-
79
- let changes;
80
-
81
- try {
82
- changes = parseJSON(tx.changes);
83
- } catch (e) {
84
- console.warn(
85
- coValue.id,
86
- "Invalid JSON in transaction",
87
- e,
88
- tx,
89
- JSON.stringify(tx.changes, (k, v) =>
90
- k === "changes" || k === "encryptedChanges"
91
- ? v.slice(0, 20) + "..."
92
- : v,
93
- ),
94
- );
95
- continue;
96
- }
97
-
98
- const change = changes[0] as
99
- | MapOpPayload<RawAccountID | AgentID | Everyone, Role>
100
- | MapOpPayload<"readKey", JsonValue>
101
- | MapOpPayload<"profile", CoID<RawProfile>>;
102
- if (changes.length !== 1) {
103
- console.warn("Group transaction must have exactly one change");
104
- continue;
105
- }
106
-
107
- if (change.op !== "set") {
108
- console.warn("Group transaction must set a role or readKey");
109
- continue;
110
- }
111
-
112
- if (change.key === "readKey") {
113
- if (memberState[transactor] !== "admin") {
114
- console.warn("Only admins can set readKeys");
115
- continue;
116
- }
117
-
118
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
119
- continue;
120
- } else if (change.key === "profile") {
121
- if (memberState[transactor] !== "admin") {
122
- console.warn("Only admins can set profile");
123
- continue;
124
- }
125
-
126
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
127
- continue;
128
- } else if (
129
- isKeyForKeyField(change.key) ||
130
- isKeyForAccountField(change.key)
131
- ) {
132
- if (
133
- memberState[transactor] !== "admin" &&
134
- memberState[transactor] !== "adminInvite" &&
135
- memberState[transactor] !== "writerInvite" &&
136
- memberState[transactor] !== "readerInvite"
137
- ) {
138
- console.warn("Only admins can reveal keys");
139
- continue;
140
- }
141
-
142
- // TODO: check validity of agents who the key is revealed to?
143
-
144
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
145
- continue;
146
- }
147
-
148
- const affectedMember = change.key;
149
- const assignedRole = change.value;
150
-
151
- if (
152
- change.value !== "admin" &&
153
- change.value !== "writer" &&
154
- change.value !== "reader" &&
155
- change.value !== "revoked" &&
156
- change.value !== "adminInvite" &&
157
- change.value !== "writerInvite" &&
158
- change.value !== "readerInvite"
159
- ) {
160
- console.warn("Group transaction must set a valid role");
161
- continue;
162
- }
163
-
164
- if (
165
- affectedMember === EVERYONE &&
166
- !(
167
- change.value === "reader" ||
168
- change.value === "writer" ||
169
- change.value === "revoked"
170
- )
171
- ) {
172
- console.warn("Everyone can only be set to reader, writer or revoked");
173
- continue;
174
- }
175
-
176
- const isFirstSelfAppointment =
177
- !memberState[transactor] &&
178
- transactor === initialAdmin &&
179
- change.op === "set" &&
180
- change.key === transactor &&
181
- change.value === "admin";
182
-
183
- if (!isFirstSelfAppointment) {
184
- if (memberState[transactor] === "admin") {
185
- if (
186
- memberState[affectedMember] === "admin" &&
187
- affectedMember !== transactor &&
188
- assignedRole !== "admin"
189
- ) {
190
- console.warn("Admins can only demote themselves.");
191
- continue;
192
- }
193
- } else if (memberState[transactor] === "adminInvite") {
194
- if (change.value !== "admin") {
195
- console.warn("AdminInvites can only create admins.");
196
- continue;
197
- }
198
- } else if (memberState[transactor] === "writerInvite") {
199
- if (change.value !== "writer") {
200
- console.warn("WriterInvites can only create writers.");
201
- continue;
202
- }
203
- } else if (memberState[transactor] === "readerInvite") {
204
- if (change.value !== "reader") {
205
- console.warn("ReaderInvites can only create reader.");
206
- continue;
207
- }
208
- } else {
209
- console.warn(
210
- "Group transaction must be made by current admin or invite",
211
- );
212
- continue;
213
- }
214
- }
215
-
216
- memberState[affectedMember] = change.value;
217
- validTransactions.push({ txID: { sessionID, txIndex }, tx });
218
-
219
- // console.log("after", { memberState, validTransactions });
220
- }
221
-
222
- return validTransactions;
46
+ return determineValidTransactionsForGroup(coValue, initialAdmin)
47
+ .validTransactions;
223
48
  } else if (coValue.header.ruleset.type === "ownedByGroup") {
224
49
  const groupContent = expectGroup(
225
50
  coValue.node
@@ -241,27 +66,18 @@ export function determineValidTransactions(
241
66
  return sessionLog.transactions
242
67
  .filter((tx) => {
243
68
  const groupAtTime = groupContent.atTime(tx.madeAt);
244
- const effectiveTransactor =
245
- transactor === groupContent.id &&
246
- groupAtTime instanceof RawAccount
247
- ? groupAtTime.currentAgentID().match(
248
- (agentID) => agentID,
249
- (e) => {
250
- console.error(
251
- "Error while determining current agent ID in valid transactions",
252
- e,
253
- );
254
- return undefined;
255
- },
256
- )
257
- : transactor;
69
+ const effectiveTransactor = agentInAccountOrMemberInGroup(
70
+ transactor,
71
+ groupAtTime,
72
+ );
258
73
 
259
74
  if (!effectiveTransactor) {
260
75
  return false;
261
76
  }
262
77
 
263
78
  const transactorRoleAtTxTime =
264
- groupAtTime.get(effectiveTransactor) || groupAtTime.get(EVERYONE);
79
+ groupAtTime.roleOfInternal(effectiveTransactor)?.role ||
80
+ groupAtTime.roleOfInternal(EVERYONE)?.role;
265
81
 
266
82
  return (
267
83
  transactorRoleAtTxTime === "admin" ||
@@ -291,6 +107,275 @@ export function determineValidTransactions(
291
107
  }
292
108
  }
293
109
 
110
+ function isHigherRole(a: Role, b: Role | undefined) {
111
+ if (a === undefined) return false;
112
+ if (b === undefined) return true;
113
+ if (b === "admin") return false;
114
+ if (a === "admin") return true;
115
+
116
+ return a === "writer" && b === "reader";
117
+ }
118
+
119
+ function resolveMemberStateFromParentReference(
120
+ coValue: CoValueCore,
121
+ memberState: MemberState,
122
+ parentReference: ParentGroupReference,
123
+ ) {
124
+ const parentGroup = coValue.node.expectCoValueLoaded(
125
+ getParentGroupId(parentReference),
126
+ "Expected parent group to be loaded",
127
+ );
128
+
129
+ if (parentGroup.header.ruleset.type !== "group") {
130
+ return;
131
+ }
132
+
133
+ const initialAdmin = parentGroup.header.ruleset.initialAdmin;
134
+
135
+ if (!initialAdmin) {
136
+ throw new Error("Group must have initialAdmin");
137
+ }
138
+
139
+ const { memberState: parentGroupMemberState } =
140
+ determineValidTransactionsForGroup(parentGroup, initialAdmin);
141
+
142
+ for (const agent of Object.keys(parentGroupMemberState) as Array<
143
+ keyof MemberState
144
+ >) {
145
+ const parentRole = parentGroupMemberState[agent];
146
+ const currentRole = memberState[agent];
147
+
148
+ if (parentRole && isHigherRole(parentRole, currentRole)) {
149
+ memberState[agent] = parentRole;
150
+ }
151
+ }
152
+ }
153
+
154
+ function determineValidTransactionsForGroup(
155
+ coValue: CoValueCore,
156
+ initialAdmin: RawAccountID | AgentID,
157
+ ): { validTransactions: ValidTransactionsResult[]; memberState: MemberState } {
158
+ const allTransactionsSorted = [...coValue.sessionLogs.entries()].flatMap(
159
+ ([sessionID, sessionLog]) => {
160
+ return sessionLog.transactions.map((tx, txIndex) => ({
161
+ sessionID,
162
+ txIndex,
163
+ tx,
164
+ })) as {
165
+ sessionID: SessionID;
166
+ txIndex: number;
167
+ tx: Transaction;
168
+ }[];
169
+ },
170
+ );
171
+
172
+ allTransactionsSorted.sort((a, b) => {
173
+ return a.tx.madeAt - b.tx.madeAt;
174
+ });
175
+
176
+ const memberState: MemberState = {};
177
+ const validTransactions: ValidTransactionsResult[] = [];
178
+
179
+ for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
180
+ // console.log("before", { memberState, validTransactions });
181
+ const transactor = accountOrAgentIDfromSessionID(sessionID);
182
+
183
+ if (tx.privacy === "private") {
184
+ if (memberState[transactor] === "admin") {
185
+ validTransactions.push({
186
+ txID: { sessionID, txIndex },
187
+ tx,
188
+ });
189
+ continue;
190
+ } else {
191
+ console.warn("Only admins can make private transactions in groups");
192
+ continue;
193
+ }
194
+ }
195
+
196
+ let changes;
197
+
198
+ try {
199
+ changes = parseJSON(tx.changes);
200
+ } catch (e) {
201
+ console.warn(
202
+ coValue.id,
203
+ "Invalid JSON in transaction",
204
+ e,
205
+ tx,
206
+ JSON.stringify(tx.changes, (k, v) =>
207
+ k === "changes" || k === "encryptedChanges"
208
+ ? v.slice(0, 20) + "..."
209
+ : v,
210
+ ),
211
+ );
212
+ continue;
213
+ }
214
+
215
+ const change = changes[0] as
216
+ | MapOpPayload<RawAccountID | AgentID | Everyone, Role>
217
+ | MapOpPayload<"readKey", JsonValue>
218
+ | MapOpPayload<"profile", CoID<RawProfile>>
219
+ | MapOpPayload<`parent_${CoID<RawGroup>}`, CoID<RawGroup>>
220
+ | MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
221
+
222
+ if (changes.length !== 1) {
223
+ console.warn("Group transaction must have exactly one change");
224
+ continue;
225
+ }
226
+
227
+ if (change.op !== "set") {
228
+ console.warn("Group transaction must set a role or readKey");
229
+ continue;
230
+ }
231
+
232
+ if (change.key === "readKey") {
233
+ if (memberState[transactor] !== "admin") {
234
+ console.warn("Only admins can set readKeys");
235
+ continue;
236
+ }
237
+
238
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
239
+ continue;
240
+ } else if (change.key === "profile") {
241
+ if (memberState[transactor] !== "admin") {
242
+ console.warn("Only admins can set profile");
243
+ continue;
244
+ }
245
+
246
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
247
+ continue;
248
+ } else if (
249
+ isKeyForKeyField(change.key) ||
250
+ isKeyForAccountField(change.key)
251
+ ) {
252
+ if (
253
+ memberState[transactor] !== "admin" &&
254
+ memberState[transactor] !== "adminInvite" &&
255
+ memberState[transactor] !== "writerInvite" &&
256
+ memberState[transactor] !== "readerInvite"
257
+ ) {
258
+ console.warn("Only admins can reveal keys");
259
+ continue;
260
+ }
261
+
262
+ // TODO: check validity of agents who the key is revealed to?
263
+
264
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
265
+ continue;
266
+ } else if (isParentExtension(change.key)) {
267
+ if (memberState[transactor] !== "admin") {
268
+ console.warn("Only admins can set parent extensions");
269
+ continue;
270
+ }
271
+ resolveMemberStateFromParentReference(coValue, memberState, change.key);
272
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
273
+ continue;
274
+ } else if (isChildExtension(change.key)) {
275
+ if (memberState[transactor] !== "admin") {
276
+ console.warn("Only admins can set child extensions");
277
+ continue;
278
+ }
279
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
280
+ continue;
281
+ }
282
+
283
+ const affectedMember = change.key;
284
+ const assignedRole = change.value;
285
+
286
+ if (
287
+ change.value !== "admin" &&
288
+ change.value !== "writer" &&
289
+ change.value !== "reader" &&
290
+ change.value !== "revoked" &&
291
+ change.value !== "adminInvite" &&
292
+ change.value !== "writerInvite" &&
293
+ change.value !== "readerInvite"
294
+ ) {
295
+ console.warn("Group transaction must set a valid role");
296
+ continue;
297
+ }
298
+
299
+ if (
300
+ affectedMember === EVERYONE &&
301
+ !(
302
+ change.value === "reader" ||
303
+ change.value === "writer" ||
304
+ change.value === "revoked"
305
+ )
306
+ ) {
307
+ console.warn("Everyone can only be set to reader, writer or revoked");
308
+ continue;
309
+ }
310
+
311
+ const isFirstSelfAppointment =
312
+ !memberState[transactor] &&
313
+ transactor === initialAdmin &&
314
+ change.op === "set" &&
315
+ change.key === transactor &&
316
+ change.value === "admin";
317
+
318
+ if (!isFirstSelfAppointment) {
319
+ if (memberState[transactor] === "admin") {
320
+ if (
321
+ memberState[affectedMember] === "admin" &&
322
+ affectedMember !== transactor &&
323
+ assignedRole !== "admin"
324
+ ) {
325
+ console.warn("Admins can only demote themselves.");
326
+ continue;
327
+ }
328
+ } else if (memberState[transactor] === "adminInvite") {
329
+ if (change.value !== "admin") {
330
+ console.warn("AdminInvites can only create admins.");
331
+ continue;
332
+ }
333
+ } else if (memberState[transactor] === "writerInvite") {
334
+ if (change.value !== "writer") {
335
+ console.warn("WriterInvites can only create writers.");
336
+ continue;
337
+ }
338
+ } else if (memberState[transactor] === "readerInvite") {
339
+ if (change.value !== "reader") {
340
+ console.warn("ReaderInvites can only create reader.");
341
+ continue;
342
+ }
343
+ } else {
344
+ console.warn(
345
+ "Group transaction must be made by current admin or invite",
346
+ );
347
+ continue;
348
+ }
349
+ }
350
+
351
+ memberState[affectedMember] = change.value;
352
+ validTransactions.push({ txID: { sessionID, txIndex }, tx });
353
+
354
+ // console.log("after", { memberState, validTransactions });
355
+ }
356
+
357
+ return { validTransactions, memberState };
358
+ }
359
+
360
+ function agentInAccountOrMemberInGroup(
361
+ transactor: RawAccountID | AgentID,
362
+ groupAtTime: RawGroup,
363
+ ): RawAccountID | AgentID | undefined {
364
+ if (transactor === groupAtTime.id && groupAtTime instanceof RawAccount) {
365
+ return groupAtTime.currentAgentID().match(
366
+ (agentID) => agentID,
367
+ (e) => {
368
+ console.error(
369
+ "Error while determining current agent ID in valid transactions",
370
+ e,
371
+ );
372
+ return undefined;
373
+ },
374
+ );
375
+ }
376
+ return transactor;
377
+ }
378
+
294
379
  export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
295
380
  return co.startsWith("key_") && co.includes("_for_key");
296
381
  }
@@ -304,3 +389,11 @@ export function isKeyForAccountField(
304
389
  co.includes("_for_everyone")
305
390
  );
306
391
  }
392
+
393
+ function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
394
+ return key.startsWith("parent_");
395
+ }
396
+
397
+ function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
398
+ return key.startsWith("child_");
399
+ }
@@ -146,7 +146,7 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
146
146
  asDependencyOf || id,
147
147
  );
148
148
  } else if (!known?.header && coValue.header?.ruleset.type === "group") {
149
- const dependedOnAccounts = new Set();
149
+ const dependedOnAccountsAndGroups = new Set();
150
150
  for (const session of Object.values(coValue.sessionEntries)) {
151
151
  for (const entry of session) {
152
152
  for (const tx of entry.transactions) {
@@ -154,16 +154,24 @@ export class LSMStorage<WH, RH, FS extends FileSystem<WH, RH>> {
154
154
  const parsedChanges = JSON.parse(tx.changes);
155
155
  for (const change of parsedChanges) {
156
156
  if (change.op === "set" && change.key.startsWith("co_")) {
157
- dependedOnAccounts.add(change.key);
157
+ dependedOnAccountsAndGroups.add(change.key);
158
+ }
159
+ if (
160
+ change.op === "set" &&
161
+ change.key.startsWith("parent_co_")
162
+ ) {
163
+ dependedOnAccountsAndGroups.add(
164
+ change.key.replace("parent_", ""),
165
+ );
158
166
  }
159
167
  }
160
168
  }
161
169
  }
162
170
  }
163
171
  }
164
- for (const account of dependedOnAccounts) {
172
+ for (const accountOrGroup of dependedOnAccountsAndGroups) {
165
173
  await this.sendNewContent(
166
- account as CoID<RawCoValue>,
174
+ accountOrGroup as CoID<RawCoValue>,
167
175
  undefined,
168
176
  asDependencyOf || id,
169
177
  );
package/src/sync.ts CHANGED
@@ -304,14 +304,19 @@ export class SyncManager {
304
304
 
305
305
  if (peerState.isServerOrStoragePeer()) {
306
306
  const initialSync = async () => {
307
- for (const id of this.local.coValuesStore.getKeys()) {
308
- // console.log("subscribing to after peer added", id, peer.id)
309
- await this.subscribeToIncludingDependencies(id, peerState);
307
+ for (const entry of this.local.coValuesStore.getValues()) {
308
+ await this.subscribeToIncludingDependencies(entry.id, peerState);
310
309
 
311
- peerState.optimisticKnownStates.dispatch({
312
- type: "SET_AS_EMPTY",
313
- id,
314
- });
310
+ if (entry.state.type === "available") {
311
+ await this.sendNewContentIncludingDependencies(entry.id, peerState);
312
+ }
313
+
314
+ if (!peerState.optimisticKnownStates.has(entry.id)) {
315
+ peerState.optimisticKnownStates.dispatch({
316
+ type: "SET_AS_EMPTY",
317
+ id: entry.id,
318
+ });
319
+ }
315
320
  }
316
321
  };
317
322
  void initialSync();
@@ -403,27 +408,39 @@ export class SyncManager {
403
408
  }
404
409
 
405
410
  if (entry.state.type === "loading") {
406
- const value = await entry.getCoValue();
411
+ // We need to return from handleLoad immediately and wait for the CoValue to be loaded
412
+ // in a new task, otherwise we might block further incoming content messages that would
413
+ // resolve the CoValue as available. This can happen when we receive fresh
414
+ // content from a client, but we are a server with our own upstream server(s)
415
+ entry
416
+ .getCoValue()
417
+ .then(async (value) => {
418
+ if (value === "unavailable") {
419
+ peer.dispatchToKnownStates({
420
+ type: "SET",
421
+ id: msg.id,
422
+ value: knownStateIn(msg),
423
+ });
424
+ peer.toldKnownState.add(msg.id);
425
+
426
+ this.trySendToPeer(peer, {
427
+ action: "known",
428
+ id: msg.id,
429
+ header: false,
430
+ sessions: {},
431
+ }).catch((e) => {
432
+ console.error("Error sending known state back", e);
433
+ });
407
434
 
408
- if (value === "unavailable") {
409
- peer.dispatchToKnownStates({
410
- type: "SET",
411
- id: msg.id,
412
- value: knownStateIn(msg),
413
- });
414
- peer.toldKnownState.add(msg.id);
415
-
416
- this.trySendToPeer(peer, {
417
- action: "known",
418
- id: msg.id,
419
- header: false,
420
- sessions: {},
421
- }).catch((e) => {
422
- console.error("Error sending known state back", e);
423
- });
435
+ return;
436
+ }
424
437
 
425
- return;
426
- }
438
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
439
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
440
+ })
441
+ .catch((e) => {
442
+ console.error("Error loading coValue in handleLoad loading state", e);
443
+ });
427
444
  }
428
445
 
429
446
  if (entry.state.type === "available") {
@@ -462,15 +479,7 @@ export class SyncManager {
462
479
  e,
463
480
  );
464
481
  });
465
- } else {
466
- throw new Error(
467
- "Expected coValue dependency entry to be created, missing subscribe?",
468
- );
469
482
  }
470
- } else {
471
- throw new Error(
472
- `Expected coValue entry for ${msg.id} to be created on known state, missing subscribe?`,
473
- );
474
483
  }
475
484
  }
476
485