cojson 0.13.17 → 0.13.20

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 (180) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/PeerState.d.ts +4 -1
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +16 -36
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/SyncStateManager.d.ts.map +1 -1
  8. package/dist/SyncStateManager.js +2 -3
  9. package/dist/SyncStateManager.js.map +1 -1
  10. package/dist/coValue.d.ts +4 -4
  11. package/dist/coValue.d.ts.map +1 -1
  12. package/dist/coValue.js +4 -4
  13. package/dist/coValue.js.map +1 -1
  14. package/dist/coValueCore/coValueCore.d.ts +143 -0
  15. package/dist/coValueCore/coValueCore.d.ts.map +1 -0
  16. package/dist/{coValueCore.js → coValueCore/coValueCore.js} +325 -253
  17. package/dist/coValueCore/coValueCore.js.map +1 -0
  18. package/dist/coValueCore/verifiedState.d.ts +65 -0
  19. package/dist/coValueCore/verifiedState.d.ts.map +1 -0
  20. package/dist/coValueCore/verifiedState.js +210 -0
  21. package/dist/coValueCore/verifiedState.js.map +1 -0
  22. package/dist/coValues/account.d.ts +8 -10
  23. package/dist/coValues/account.d.ts.map +1 -1
  24. package/dist/coValues/account.js +12 -13
  25. package/dist/coValues/account.js.map +1 -1
  26. package/dist/coValues/coList.d.ts +3 -3
  27. package/dist/coValues/coList.d.ts.map +1 -1
  28. package/dist/coValues/coList.js +6 -3
  29. package/dist/coValues/coList.js.map +1 -1
  30. package/dist/coValues/coMap.d.ts +3 -3
  31. package/dist/coValues/coMap.d.ts.map +1 -1
  32. package/dist/coValues/coMap.js +3 -3
  33. package/dist/coValues/coMap.js.map +1 -1
  34. package/dist/coValues/coPlainText.d.ts +2 -2
  35. package/dist/coValues/coPlainText.d.ts.map +1 -1
  36. package/dist/coValues/coPlainText.js +4 -4
  37. package/dist/coValues/coPlainText.js.map +1 -1
  38. package/dist/coValues/coStream.d.ts +3 -3
  39. package/dist/coValues/coStream.d.ts.map +1 -1
  40. package/dist/coValues/coStream.js +3 -3
  41. package/dist/coValues/coStream.js.map +1 -1
  42. package/dist/coValues/group.d.ts +7 -2
  43. package/dist/coValues/group.d.ts.map +1 -1
  44. package/dist/coValues/group.js +29 -26
  45. package/dist/coValues/group.js.map +1 -1
  46. package/dist/coreToCoValue.d.ts +2 -2
  47. package/dist/coreToCoValue.d.ts.map +1 -1
  48. package/dist/coreToCoValue.js +10 -14
  49. package/dist/coreToCoValue.js.map +1 -1
  50. package/dist/exports.d.ts +6 -5
  51. package/dist/exports.d.ts.map +1 -1
  52. package/dist/exports.js +3 -4
  53. package/dist/exports.js.map +1 -1
  54. package/dist/localNode.d.ts +30 -24
  55. package/dist/localNode.d.ts.map +1 -1
  56. package/dist/localNode.js +147 -173
  57. package/dist/localNode.js.map +1 -1
  58. package/dist/permissions.d.ts +2 -1
  59. package/dist/permissions.d.ts.map +1 -1
  60. package/dist/permissions.js +15 -11
  61. package/dist/permissions.js.map +1 -1
  62. package/dist/priority.d.ts +1 -1
  63. package/dist/priority.d.ts.map +1 -1
  64. package/dist/streamUtils.d.ts +5 -5
  65. package/dist/streamUtils.d.ts.map +1 -1
  66. package/dist/streamUtils.js +5 -20
  67. package/dist/streamUtils.js.map +1 -1
  68. package/dist/sync.d.ts +8 -6
  69. package/dist/sync.d.ts.map +1 -1
  70. package/dist/sync.js +121 -74
  71. package/dist/sync.js.map +1 -1
  72. package/dist/tests/PeerState.test.js +0 -31
  73. package/dist/tests/PeerState.test.js.map +1 -1
  74. package/dist/tests/SyncStateManager.test.js +41 -6
  75. package/dist/tests/SyncStateManager.test.js.map +1 -1
  76. package/dist/tests/account.test.js +16 -0
  77. package/dist/tests/account.test.js.map +1 -1
  78. package/dist/tests/coList.test.js +19 -16
  79. package/dist/tests/coList.test.js.map +1 -1
  80. package/dist/tests/coMap.test.js +12 -13
  81. package/dist/tests/coMap.test.js.map +1 -1
  82. package/dist/tests/coPlainText.test.js +9 -10
  83. package/dist/tests/coPlainText.test.js.map +1 -1
  84. package/dist/tests/coStream.test.js +22 -17
  85. package/dist/tests/coStream.test.js.map +1 -1
  86. package/dist/tests/coValueCore.test.js +22 -28
  87. package/dist/tests/coValueCore.test.js.map +1 -1
  88. package/dist/tests/coValueCoreLoadingState.test.d.ts +2 -0
  89. package/dist/tests/coValueCoreLoadingState.test.d.ts.map +1 -0
  90. package/dist/tests/{coValueState.test.js → coValueCoreLoadingState.test.js} +62 -46
  91. package/dist/tests/coValueCoreLoadingState.test.js.map +1 -0
  92. package/dist/tests/group.test.js +42 -43
  93. package/dist/tests/group.test.js.map +1 -1
  94. package/dist/tests/messagesTestUtils.d.ts +2 -2
  95. package/dist/tests/messagesTestUtils.d.ts.map +1 -1
  96. package/dist/tests/messagesTestUtils.js +1 -1
  97. package/dist/tests/messagesTestUtils.js.map +1 -1
  98. package/dist/tests/permissions.test.js +224 -292
  99. package/dist/tests/permissions.test.js.map +1 -1
  100. package/dist/tests/priority.test.js +13 -14
  101. package/dist/tests/priority.test.js.map +1 -1
  102. package/dist/tests/sync.auth.test.d.ts +2 -0
  103. package/dist/tests/sync.auth.test.d.ts.map +1 -0
  104. package/dist/tests/sync.auth.test.js +190 -0
  105. package/dist/tests/sync.auth.test.js.map +1 -0
  106. package/dist/tests/sync.load.test.js +6 -6
  107. package/dist/tests/sync.load.test.js.map +1 -1
  108. package/dist/tests/sync.mesh.test.js +25 -12
  109. package/dist/tests/sync.mesh.test.js.map +1 -1
  110. package/dist/tests/sync.peerReconciliation.test.js +19 -19
  111. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  112. package/dist/tests/sync.storage.test.js +20 -13
  113. package/dist/tests/sync.storage.test.js.map +1 -1
  114. package/dist/tests/sync.test.js +32 -39
  115. package/dist/tests/sync.test.js.map +1 -1
  116. package/dist/tests/sync.upload.test.js +126 -37
  117. package/dist/tests/sync.upload.test.js.map +1 -1
  118. package/dist/tests/testUtils.d.ts +35 -17
  119. package/dist/tests/testUtils.d.ts.map +1 -1
  120. package/dist/tests/testUtils.js +103 -79
  121. package/dist/tests/testUtils.js.map +1 -1
  122. package/dist/typeUtils/expectGroup.js +1 -1
  123. package/dist/typeUtils/expectGroup.js.map +1 -1
  124. package/package.json +1 -1
  125. package/src/PeerState.ts +19 -40
  126. package/src/SyncStateManager.ts +2 -3
  127. package/src/coValue.ts +11 -8
  128. package/src/{coValueCore.ts → coValueCore/coValueCore.ts} +478 -422
  129. package/src/coValueCore/verifiedState.ts +376 -0
  130. package/src/coValues/account.ts +20 -25
  131. package/src/coValues/coList.ts +12 -6
  132. package/src/coValues/coMap.ts +9 -6
  133. package/src/coValues/coPlainText.ts +9 -6
  134. package/src/coValues/coStream.ts +9 -6
  135. package/src/coValues/group.ts +50 -28
  136. package/src/coreToCoValue.ts +14 -15
  137. package/src/exports.ts +9 -7
  138. package/src/localNode.ts +236 -275
  139. package/src/permissions.ts +18 -12
  140. package/src/priority.ts +1 -1
  141. package/src/streamUtils.ts +7 -34
  142. package/src/sync.ts +146 -84
  143. package/src/tests/PeerState.test.ts +0 -37
  144. package/src/tests/SyncStateManager.test.ts +56 -6
  145. package/src/tests/account.test.ts +24 -0
  146. package/src/tests/coList.test.ts +21 -15
  147. package/src/tests/coMap.test.ts +12 -13
  148. package/src/tests/coPlainText.test.ts +12 -9
  149. package/src/tests/coStream.test.ts +25 -16
  150. package/src/tests/coValueCore.test.ts +30 -27
  151. package/src/tests/{coValueState.test.ts → coValueCoreLoadingState.test.ts} +67 -57
  152. package/src/tests/group.test.ts +44 -69
  153. package/src/tests/messagesTestUtils.ts +3 -8
  154. package/src/tests/permissions.test.ts +283 -449
  155. package/src/tests/priority.test.ts +17 -13
  156. package/src/tests/sync.auth.test.ts +246 -0
  157. package/src/tests/sync.load.test.ts +7 -6
  158. package/src/tests/sync.mesh.test.ts +25 -12
  159. package/src/tests/sync.peerReconciliation.test.ts +25 -25
  160. package/src/tests/sync.storage.test.ts +20 -13
  161. package/src/tests/sync.test.ts +43 -43
  162. package/src/tests/sync.upload.test.ts +157 -37
  163. package/src/tests/testUtils.ts +143 -96
  164. package/src/typeUtils/expectGroup.ts +1 -1
  165. package/dist/CoValuesStore.d.ts +0 -14
  166. package/dist/CoValuesStore.d.ts.map +0 -1
  167. package/dist/CoValuesStore.js +0 -32
  168. package/dist/CoValuesStore.js.map +0 -1
  169. package/dist/coValueCore.d.ts +0 -142
  170. package/dist/coValueCore.d.ts.map +0 -1
  171. package/dist/coValueCore.js.map +0 -1
  172. package/dist/coValueState.d.ts +0 -34
  173. package/dist/coValueState.d.ts.map +0 -1
  174. package/dist/coValueState.js +0 -190
  175. package/dist/coValueState.js.map +0 -1
  176. package/dist/tests/coValueState.test.d.ts +0 -2
  177. package/dist/tests/coValueState.test.d.ts.map +0 -1
  178. package/dist/tests/coValueState.test.js.map +0 -1
  179. package/src/CoValuesStore.ts +0 -41
  180. package/src/coValueState.ts +0 -245
@@ -1,5 +1,6 @@
1
1
  import { CoID } from "./coValue.js";
2
- import { CoValueCore, Transaction } from "./coValueCore.js";
2
+ import { CoValueCore } from "./coValueCore/coValueCore.js";
3
+ import { Transaction } from "./coValueCore/verifiedState.js";
3
4
  import { RawAccount, RawAccountID, RawProfile } from "./coValues/account.js";
4
5
  import { MapOpPayload } from "./coValues/coMap.js";
5
6
  import {
@@ -64,19 +65,23 @@ export function determineValidTransactions(
64
65
  coValue: CoValueCore,
65
66
  knownTransactions?: CoValueKnownState["sessions"],
66
67
  ): { txID: TransactionID; tx: Transaction }[] {
67
- if (coValue.header.ruleset.type === "group") {
68
- const initialAdmin = coValue.header.ruleset.initialAdmin;
68
+ if (!coValue.isAvailable()) {
69
+ throw new Error("determineValidTransactions CoValue is not available");
70
+ }
71
+
72
+ if (coValue.verified.header.ruleset.type === "group") {
73
+ const initialAdmin = coValue.verified.header.ruleset.initialAdmin;
69
74
  if (!initialAdmin) {
70
75
  throw new Error("Group must have initialAdmin");
71
76
  }
72
77
 
73
78
  return determineValidTransactionsForGroup(coValue, initialAdmin)
74
79
  .validTransactions;
75
- } else if (coValue.header.ruleset.type === "ownedByGroup") {
80
+ } else if (coValue.verified.header.ruleset.type === "ownedByGroup") {
76
81
  const groupContent = expectGroup(
77
82
  coValue.node
78
83
  .expectCoValueLoaded(
79
- coValue.header.ruleset.group,
84
+ coValue.verified.header.ruleset.group,
80
85
  "Determining valid transaction in owned object but its group wasn't loaded",
81
86
  )
82
87
  .getCurrentContent(),
@@ -88,7 +93,7 @@ export function determineValidTransactions(
88
93
 
89
94
  const validTransactions: ValidTransactionsResult[] = [];
90
95
 
91
- for (const [sessionID, sessionLog] of coValue.sessionLogs.entries()) {
96
+ for (const [sessionID, sessionLog] of coValue.verified.sessions.entries()) {
92
97
  const transactor = accountOrAgentIDfromSessionID(sessionID);
93
98
  const knownTransactionsForSession = knownTransactions?.[sessionID] ?? -1;
94
99
 
@@ -123,10 +128,10 @@ export function determineValidTransactions(
123
128
  }
124
129
 
125
130
  return validTransactions;
126
- } else if (coValue.header.ruleset.type === "unsafeAllowAll") {
131
+ } else if (coValue.verified.header.ruleset.type === "unsafeAllowAll") {
127
132
  const validTransactions: ValidTransactionsResult[] = [];
128
133
 
129
- for (const [sessionID, sessionLog] of coValue.sessionLogs.entries()) {
134
+ for (const [sessionID, sessionLog] of coValue.verified.sessions.entries()) {
130
135
  const knownTransactionsForSession = knownTransactions?.[sessionID] ?? -1;
131
136
 
132
137
  sessionLog.transactions.forEach((tx, txIndex) => {
@@ -141,7 +146,7 @@ export function determineValidTransactions(
141
146
  } else {
142
147
  throw new Error(
143
148
  "Unknown ruleset type " +
144
- (coValue.header.ruleset as { type: string }).type,
149
+ (coValue.verified.header.ruleset as { type: string }).type,
145
150
  );
146
151
  }
147
152
  }
@@ -167,7 +172,7 @@ function resolveMemberStateFromParentReference(
167
172
  "Expected parent group to be loaded",
168
173
  );
169
174
 
170
- if (parentGroup.header.ruleset.type !== "group") {
175
+ if (parentGroup.verified.header.ruleset.type !== "group") {
171
176
  return;
172
177
  }
173
178
 
@@ -176,7 +181,7 @@ function resolveMemberStateFromParentReference(
176
181
  return;
177
182
  }
178
183
 
179
- const initialAdmin = parentGroup.header.ruleset.initialAdmin;
184
+ const initialAdmin = parentGroup.verified.header.ruleset.initialAdmin;
180
185
 
181
186
  if (!initialAdmin) {
182
187
  throw new Error("Group must have initialAdmin");
@@ -214,7 +219,8 @@ function determineValidTransactionsForGroup(
214
219
  tx: Transaction;
215
220
  }[] = [];
216
221
 
217
- for (const [sessionID, sessionLog] of coValue.sessionLogs.entries()) {
222
+ for (const [sessionID, sessionLog] of coValue.verified?.sessions.entries() ??
223
+ []) {
218
224
  sessionLog.transactions.forEach((tx, txIndex) => {
219
225
  allTransactionsSorted.push({ sessionID, txIndex, tx });
220
226
  });
package/src/priority.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CoValueHeader } from "./coValueCore.js";
1
+ import { type CoValueHeader } from "./coValueCore/verifiedState.js";
2
2
 
3
3
  /**
4
4
  * The priority of a `CoValue` determines how much priority is given
@@ -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
@@ -1,9 +1,11 @@
1
1
  import { Histogram, ValueType, metrics } from "@opentelemetry/api";
2
2
  import { PeerState } from "./PeerState.js";
3
3
  import { SyncStateManager } from "./SyncStateManager.js";
4
- import { CoValueHeader, Transaction } from "./coValueCore.js";
5
- import { CoValueCore } from "./coValueCore.js";
6
- import { CoValueState } from "./coValueState.js";
4
+ import {
5
+ AvailableCoValueCore,
6
+ CoValueCore,
7
+ } from "./coValueCore/coValueCore.js";
8
+ import { CoValueHeader, Transaction } from "./coValueCore/verifiedState.js";
7
9
  import { Signature } from "./crypto/crypto.js";
8
10
  import { RawCoID, SessionID } from "./ids.js";
9
11
  import { LocalNode } from "./localNode.js";
@@ -157,8 +159,14 @@ export class SyncManager {
157
159
  );
158
160
  }
159
161
 
162
+ hasStoragePeers(): boolean {
163
+ return this.getPeers().some(
164
+ (peer) => peer.role === "storage" && !peer.closed,
165
+ );
166
+ }
167
+
160
168
  handleSyncMessage(msg: SyncMessage, peer: PeerState) {
161
- if (this.local.coValuesStore.get(msg.id).isErroredInPeer(peer.id)) {
169
+ if (this.local.getCoValue(msg.id).isErroredInPeer(peer.id)) {
162
170
  logger.warn(
163
171
  `Skipping message ${msg.action} on errored coValue ${msg.id} from peer ${peer.id}`,
164
172
  );
@@ -197,13 +205,17 @@ export class SyncManager {
197
205
  }
198
206
 
199
207
  sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
200
- const coValue = this.local.expectCoValueLoaded(id);
208
+ const coValue = this.local.getCoValue(id);
209
+
210
+ if (!coValue.isAvailable()) {
211
+ return;
212
+ }
201
213
 
202
214
  coValue
203
215
  .getDependedOnCoValues()
204
216
  .map((id) => this.sendNewContentIncludingDependencies(id, peer));
205
217
 
206
- const newContentPieces = coValue.newContentSince(
218
+ const newContentPieces = coValue.verified.newContentSince(
207
219
  peer.optimisticKnownStates.get(id),
208
220
  );
209
221
 
@@ -212,16 +224,15 @@ export class SyncManager {
212
224
  this.trySendToPeer(peer, piece);
213
225
  }
214
226
 
215
- peer.toldKnownState.add(id);
216
227
  peer.combineOptimisticWith(id, coValue.knownState());
217
228
  } else if (!peer.toldKnownState.has(id)) {
218
229
  this.trySendToPeer(peer, {
219
230
  action: "known",
220
231
  ...coValue.knownState(),
221
232
  });
222
-
223
- peer.toldKnownState.add(id);
224
233
  }
234
+
235
+ peer.trackToldKnownState(id);
225
236
  }
226
237
 
227
238
  startPeerReconciliation(peer: PeerState) {
@@ -237,40 +248,38 @@ export class SyncManager {
237
248
  gathered.add(coValue.id);
238
249
 
239
250
  for (const id of coValue.getDependedOnCoValues()) {
240
- const entry = this.local.coValuesStore.get(id);
251
+ const coValue = this.local.getCoValue(id);
241
252
 
242
- if (entry.isAvailable()) {
243
- buildOrderedCoValueList(entry.core);
253
+ if (coValue.isAvailable()) {
254
+ buildOrderedCoValueList(coValue);
244
255
  }
245
256
  }
246
257
 
247
258
  coValuesOrderedByDependency.push(coValue);
248
259
  };
249
260
 
250
- for (const entry of this.local.coValuesStore.getValues()) {
251
- if (!entry.isAvailable()) {
261
+ for (const coValue of this.local.allCoValues()) {
262
+ if (!coValue.isAvailable()) {
252
263
  // If the coValue is unavailable and we never tried this peer
253
264
  // we try to load it from the peer
254
- if (!peer.toldKnownState.has(entry.id)) {
255
- peer.toldKnownState.add(entry.id);
265
+ if (!peer.loadRequestSent.has(coValue.id)) {
266
+ peer.trackLoadRequestSent(coValue.id);
256
267
  this.trySendToPeer(peer, {
257
268
  action: "load",
258
269
  header: false,
259
- id: entry.id,
270
+ id: coValue.id,
260
271
  sessions: {},
261
272
  });
262
273
  }
263
274
  } else {
264
- const coValue = entry.core;
265
-
266
275
  // Build the list of coValues ordered by dependency
267
276
  // so we can send the load message in the correct order
268
277
  buildOrderedCoValueList(coValue);
269
278
  }
270
279
 
271
280
  // Fill the missing known states with empty known states
272
- if (!peer.optimisticKnownStates.has(entry.id)) {
273
- peer.setOptimisticKnownState(entry.id, "empty");
281
+ if (!peer.optimisticKnownStates.has(coValue.id)) {
282
+ peer.setOptimisticKnownState(coValue.id, "empty");
274
283
  }
275
284
  }
276
285
 
@@ -281,7 +290,7 @@ export class SyncManager {
281
290
  * - Start the sync process in case we or the other peer
282
291
  * lacks some transactions
283
292
  */
284
- peer.toldKnownState.add(coValue.id);
293
+ peer.trackLoadRequestSent(coValue.id);
285
294
  this.trySendToPeer(peer, {
286
295
  action: "load",
287
296
  ...coValue.knownState(),
@@ -368,11 +377,11 @@ export class SyncManager {
368
377
  *
369
378
  */
370
379
  peer.setKnownState(msg.id, knownStateIn(msg));
371
- const entry = this.local.coValuesStore.get(msg.id);
380
+ const coValue = this.local.getCoValue(msg.id);
372
381
 
373
382
  if (
374
- entry.highLevelState === "unknown" ||
375
- entry.highLevelState === "unavailable"
383
+ coValue.loadingState === "unknown" ||
384
+ coValue.loadingState === "unavailable"
376
385
  ) {
377
386
  const eligiblePeers = this.getServerAndStoragePeers(peer.id);
378
387
 
@@ -380,8 +389,7 @@ export class SyncManager {
380
389
  // We don't have any eligible peers to load the coValue from
381
390
  // so we send a known state back to the sender to let it know
382
391
  // that the coValue is unavailable
383
- peer.toldKnownState.add(msg.id);
384
-
392
+ peer.trackToldKnownState(msg.id);
385
393
  this.trySendToPeer(peer, {
386
394
  action: "known",
387
395
  id: msg.id,
@@ -391,24 +399,25 @@ export class SyncManager {
391
399
 
392
400
  return;
393
401
  } else {
394
- // Should move the state to loading
395
- this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
396
- logger.error("Error loading coValue in handleLoad", { err: e });
397
- });
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
+ });
398
408
  }
399
409
  }
400
410
 
401
- if (entry.highLevelState === "loading") {
411
+ if (coValue.loadingState === "loading") {
402
412
  // We need to return from handleLoad immediately and wait for the CoValue to be loaded
403
413
  // in a new task, otherwise we might block further incoming content messages that would
404
414
  // resolve the CoValue as available. This can happen when we receive fresh
405
415
  // content from a client, but we are a server with our own upstream server(s)
406
- entry
407
- .getCoValue()
416
+ coValue
417
+ .waitForAvailableOrUnavailable()
408
418
  .then(async (value) => {
409
- if (value === "unavailable") {
410
- peer.toldKnownState.add(msg.id);
411
-
419
+ if (!value.isAvailable()) {
420
+ peer.trackToldKnownState(msg.id);
412
421
  this.trySendToPeer(peer, {
413
422
  action: "known",
414
423
  id: msg.id,
@@ -426,9 +435,10 @@ export class SyncManager {
426
435
  err: e,
427
436
  });
428
437
  });
429
- } else if (entry.isAvailable()) {
438
+ } else if (coValue.isAvailable()) {
430
439
  this.sendNewContentIncludingDependencies(msg.id, peer);
431
440
  } else {
441
+ peer.trackToldKnownState(msg.id);
432
442
  this.trySendToPeer(peer, {
433
443
  action: "known",
434
444
  id: msg.id,
@@ -439,7 +449,7 @@ export class SyncManager {
439
449
  }
440
450
 
441
451
  handleKnownState(msg: KnownStateMessage, peer: PeerState) {
442
- const entry = this.local.coValuesStore.get(msg.id);
452
+ const coValue = this.local.getCoValue(msg.id);
443
453
 
444
454
  peer.combineWith(msg.id, knownStateIn(msg));
445
455
 
@@ -448,10 +458,10 @@ export class SyncManager {
448
458
  const availableOnPeer = peer.optimisticKnownStates.get(msg.id)?.header;
449
459
 
450
460
  if (!availableOnPeer) {
451
- entry.markNotFoundInPeer(peer.id);
461
+ coValue.markNotFoundInPeer(peer.id);
452
462
  }
453
463
 
454
- if (entry.isAvailable()) {
464
+ if (coValue.isAvailable()) {
455
465
  this.sendNewContentIncludingDependencies(msg.id, peer);
456
466
  }
457
467
  }
@@ -470,11 +480,9 @@ export class SyncManager {
470
480
  }
471
481
 
472
482
  handleNewContent(msg: NewContentMessage, peer: PeerState) {
473
- const entry = this.local.coValuesStore.get(msg.id);
474
-
475
- let coValue: CoValueCore;
483
+ const coValue = this.local.getCoValue(msg.id);
476
484
 
477
- if (!entry.isAvailable()) {
485
+ if (!coValue.isAvailable()) {
478
486
  if (!msg.header) {
479
487
  this.trySendToPeer(peer, {
480
488
  action: "known",
@@ -487,12 +495,11 @@ export class SyncManager {
487
495
  }
488
496
 
489
497
  peer.updateHeader(msg.id, true);
498
+ coValue.markAvailable(msg.header, peer.id);
499
+ }
490
500
 
491
- coValue = new CoValueCore(msg.header, this.local);
492
-
493
- entry.markAvailable(coValue, peer.id);
494
- } else {
495
- coValue = entry.core;
501
+ if (!coValue.isAvailable()) {
502
+ throw new Error("Unreachable: CoValue should be available in every case");
496
503
  }
497
504
 
498
505
  let invalidStateAssumed = false;
@@ -502,7 +509,7 @@ export class SyncManager {
502
509
  SessionNewContent,
503
510
  ][]) {
504
511
  const ourKnownTxIdx =
505
- coValue.sessionLogs.get(sessionID)?.transactions.length;
512
+ coValue.verified.sessions.get(sessionID)?.transactions.length;
506
513
  const theirFirstNewTxIdx = newContentForSession.after;
507
514
 
508
515
  if ((ourKnownTxIdx || 0) < theirFirstNewTxIdx) {
@@ -526,16 +533,17 @@ export class SyncManager {
526
533
  newTransactions,
527
534
  undefined,
528
535
  newContentForSession.lastSignature,
536
+ "immediate", // TODO: can we change this to deferred?
529
537
  );
530
538
 
531
539
  if (result.isErr()) {
532
- logger.error("Failed to add transactions", {
540
+ console.error("Failed to add transactions", {
533
541
  peerId: peer.id,
534
542
  peerRole: peer.role,
535
543
  id: msg.id,
536
544
  err: result.error,
537
545
  });
538
- entry.markErrored(peer.id, result.error);
546
+ coValue.markErrored(peer.id, result.error);
539
547
  continue;
540
548
  }
541
549
 
@@ -555,7 +563,7 @@ export class SyncManager {
555
563
  isCorrection: true,
556
564
  ...coValue.knownState(),
557
565
  });
558
- peer.toldKnownState.add(msg.id);
566
+ peer.trackToldKnownState(msg.id);
559
567
  } else {
560
568
  /**
561
569
  * We are sending a known state message to the peer to acknowledge the
@@ -568,15 +576,50 @@ export class SyncManager {
568
576
  action: "known",
569
577
  ...coValue.knownState(),
570
578
  });
571
- peer.toldKnownState.add(msg.id);
579
+ peer.trackToldKnownState(msg.id);
572
580
  }
573
581
 
574
- /**
575
- * We do send a correction/ack message before syncing to give an immediate
576
- * response to the peers that are waiting for confirmation that a coValue is
577
- * fully synced
578
- */
579
- this.requestCoValueSync(coValue);
582
+ const sourcePeer = peer;
583
+ const syncedPeers = [];
584
+
585
+ for (const peer of this.peersInPriorityOrder()) {
586
+ /**
587
+ * We sync the content against the source peer if it is a client or server peers
588
+ * to upload any content that is available on the current node and not on the source peer.
589
+ *
590
+ * We don't need to do this with storage peers because we don't get updates from those peers,
591
+ * only load and store content.
592
+ */
593
+ if (peer.id === sourcePeer.id && sourcePeer.role === "storage") continue;
594
+ if (peer.closed) continue;
595
+ if (coValue.isErroredInPeer(peer.id)) continue;
596
+
597
+ // We directly forward the new content to peers that have an active subscription
598
+ if (peer.optimisticKnownStates.has(coValue.id)) {
599
+ this.sendNewContentIncludingDependencies(coValue.id, peer);
600
+ syncedPeers.push(peer);
601
+ } else if (
602
+ peer.isServerOrStoragePeer() &&
603
+ !peer.loadRequestSent.has(coValue.id)
604
+ ) {
605
+ const state = coValue.getStateForPeer(peer.id)?.type;
606
+
607
+ // Check if there is a inflight load operation and we
608
+ // are waiting for other peers to send the load request
609
+ if (state === "unknown" || state === undefined) {
610
+ this.trySendToPeer(peer, {
611
+ action: "load",
612
+ ...coValue.knownState(),
613
+ });
614
+ peer.trackLoadRequestSent(coValue.id);
615
+ syncedPeers.push(peer);
616
+ }
617
+ }
618
+ }
619
+
620
+ for (const peer of syncedPeers) {
621
+ this.syncState.triggerUpdate(peer.id, coValue.id);
622
+ }
580
623
  }
581
624
 
582
625
  handleCorrection(msg: KnownStateMessage, peer: PeerState) {
@@ -587,33 +630,27 @@ export class SyncManager {
587
630
 
588
631
  handleUnsubscribe(_msg: DoneMessage) {}
589
632
 
590
- requestedSyncs = new Map<RawCoID, Promise<void>>();
591
-
592
- async requestCoValueSync(coValue: CoValueCore) {
593
- 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
+ }
594
638
 
595
- if (promise) {
596
- return promise;
597
- } else {
598
- const promise = new Promise<void>((resolve) => {
599
- queueMicrotask(() => {
600
- this.requestedSyncs.delete(coValue.id);
601
- this.syncCoValue(coValue);
602
- resolve();
603
- });
604
- });
639
+ queueMicrotask(() => {
640
+ if (this.requestedSyncs.has(coValue.id)) {
641
+ this.syncCoValue(coValue);
642
+ }
643
+ });
605
644
 
606
- this.requestedSyncs.set(coValue.id, promise);
607
- return promise;
608
- }
645
+ this.requestedSyncs.add(coValue.id);
609
646
  }
610
647
 
611
648
  async syncCoValue(coValue: CoValueCore) {
612
- const entry = this.local.coValuesStore.get(coValue.id);
649
+ this.requestedSyncs.delete(coValue.id);
613
650
 
614
651
  for (const peer of this.peersInPriorityOrder()) {
615
652
  if (peer.closed) continue;
616
- if (entry.isErroredInPeer(peer.id)) continue;
653
+ if (coValue.isErroredInPeer(peer.id)) continue;
617
654
 
618
655
  // Only subscribed CoValues are synced to clients
619
656
  if (
@@ -641,6 +678,21 @@ export class SyncManager {
641
678
  return true;
642
679
  }
643
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
+
644
696
  return new Promise((resolve, reject) => {
645
697
  const unsubscribe = this.syncState.subscribeToPeerUpdates(
646
698
  peerId,
@@ -660,20 +712,30 @@ export class SyncManager {
660
712
  });
661
713
  }
662
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
+
663
725
  async waitForSync(id: RawCoID, timeout = 30_000) {
664
726
  const peers = this.getPeers();
665
727
 
666
- return Promise.all(
728
+ await Promise.all(
667
729
  peers.map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
668
730
  );
669
731
  }
670
732
 
671
733
  async waitForAllCoValuesSync(timeout = 60_000) {
672
- const coValues = this.local.coValuesStore.getValues();
734
+ const coValues = this.local.allCoValues();
673
735
  const validCoValues = Array.from(coValues).filter(
674
736
  (coValue) =>
675
- coValue.highLevelState === "available" ||
676
- coValue.highLevelState === "loading",
737
+ coValue.loadingState === "available" ||
738
+ coValue.loadingState === "loading",
677
739
  );
678
740
 
679
741
  return Promise.all(
@@ -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