cojson 0.13.2 → 0.13.7

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 (87) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/LICENSE.txt +1 -1
  4. package/dist/PeerState.d.ts +6 -0
  5. package/dist/PeerState.d.ts.map +1 -1
  6. package/dist/PeerState.js +43 -0
  7. package/dist/PeerState.js.map +1 -1
  8. package/dist/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore.js +1 -0
  10. package/dist/coValueCore.js.map +1 -1
  11. package/dist/coValueState.d.ts +1 -0
  12. package/dist/coValueState.d.ts.map +1 -1
  13. package/dist/coValueState.js +27 -2
  14. package/dist/coValueState.js.map +1 -1
  15. package/dist/coValues/group.d.ts +1 -0
  16. package/dist/coValues/group.d.ts.map +1 -1
  17. package/dist/coValues/group.js +45 -21
  18. package/dist/coValues/group.js.map +1 -1
  19. package/dist/crypto/crypto.d.ts +2 -2
  20. package/dist/crypto/crypto.d.ts.map +1 -1
  21. package/dist/permissions.d.ts +1 -0
  22. package/dist/permissions.d.ts.map +1 -1
  23. package/dist/permissions.js +19 -3
  24. package/dist/permissions.js.map +1 -1
  25. package/dist/storage/FileSystem.d.ts +2 -2
  26. package/dist/storage/FileSystem.d.ts.map +1 -1
  27. package/dist/sync.d.ts +14 -4
  28. package/dist/sync.d.ts.map +1 -1
  29. package/dist/sync.js +146 -146
  30. package/dist/sync.js.map +1 -1
  31. package/dist/tests/SyncStateManager.test.js +51 -46
  32. package/dist/tests/SyncStateManager.test.js.map +1 -1
  33. package/dist/tests/coValueCore.test.js +51 -2
  34. package/dist/tests/coValueCore.test.js.map +1 -1
  35. package/dist/tests/coValueState.test.js +31 -4
  36. package/dist/tests/coValueState.test.js.map +1 -1
  37. package/dist/tests/group.test.js +135 -2
  38. package/dist/tests/group.test.js.map +1 -1
  39. package/dist/tests/messagesTestUtils.d.ts +13 -0
  40. package/dist/tests/messagesTestUtils.d.ts.map +1 -0
  41. package/dist/tests/messagesTestUtils.js +42 -0
  42. package/dist/tests/messagesTestUtils.js.map +1 -0
  43. package/dist/tests/sync.load.test.d.ts +2 -0
  44. package/dist/tests/sync.load.test.d.ts.map +1 -0
  45. package/dist/tests/sync.load.test.js +249 -0
  46. package/dist/tests/sync.load.test.js.map +1 -0
  47. package/dist/tests/sync.mesh.test.d.ts +2 -0
  48. package/dist/tests/sync.mesh.test.d.ts.map +1 -0
  49. package/dist/tests/sync.mesh.test.js +157 -0
  50. package/dist/tests/sync.mesh.test.js.map +1 -0
  51. package/dist/tests/sync.peerReconciliation.test.d.ts +2 -0
  52. package/dist/tests/sync.peerReconciliation.test.d.ts.map +1 -0
  53. package/dist/tests/sync.peerReconciliation.test.js +130 -0
  54. package/dist/tests/sync.peerReconciliation.test.js.map +1 -0
  55. package/dist/tests/sync.storage.test.d.ts +2 -0
  56. package/dist/tests/sync.storage.test.d.ts.map +1 -0
  57. package/dist/tests/sync.storage.test.js +201 -0
  58. package/dist/tests/sync.storage.test.js.map +1 -0
  59. package/dist/tests/sync.test.js +139 -1048
  60. package/dist/tests/sync.test.js.map +1 -1
  61. package/dist/tests/sync.upload.test.d.ts +2 -0
  62. package/dist/tests/sync.upload.test.d.ts.map +1 -0
  63. package/dist/tests/sync.upload.test.js +156 -0
  64. package/dist/tests/sync.upload.test.js.map +1 -0
  65. package/dist/tests/testUtils.d.ts +76 -33
  66. package/dist/tests/testUtils.d.ts.map +1 -1
  67. package/dist/tests/testUtils.js +153 -47
  68. package/dist/tests/testUtils.js.map +1 -1
  69. package/package.json +3 -3
  70. package/src/PeerState.ts +59 -1
  71. package/src/coValueCore.ts +1 -0
  72. package/src/coValueState.ts +34 -3
  73. package/src/coValues/group.ts +83 -45
  74. package/src/permissions.ts +31 -3
  75. package/src/sync.ts +169 -185
  76. package/src/tests/SyncStateManager.test.ts +58 -70
  77. package/src/tests/coValueCore.test.ts +70 -1
  78. package/src/tests/coValueState.test.ts +59 -5
  79. package/src/tests/group.test.ts +250 -2
  80. package/src/tests/messagesTestUtils.ts +75 -0
  81. package/src/tests/sync.load.test.ts +327 -0
  82. package/src/tests/sync.mesh.test.ts +219 -0
  83. package/src/tests/sync.peerReconciliation.test.ts +201 -0
  84. package/src/tests/sync.storage.test.ts +259 -0
  85. package/src/tests/sync.test.ts +170 -1245
  86. package/src/tests/sync.upload.test.ts +202 -0
  87. package/src/tests/testUtils.ts +213 -61
@@ -225,6 +225,7 @@ function determineValidTransactionsForGroup(
225
225
  });
226
226
 
227
227
  const memberState: MemberState = {};
228
+ const writeOnlyKeys: Record<RawAccountID | AgentID, KeyID> = {};
228
229
  const validTransactions: ValidTransactionsResult[] = [];
229
230
 
230
231
  const keyRevelations = new Set<string>();
@@ -302,7 +303,8 @@ function determineValidTransactionsForGroup(
302
303
  memberState[transactor] !== "adminInvite" &&
303
304
  memberState[transactor] !== "writerInvite" &&
304
305
  memberState[transactor] !== "readerInvite" &&
305
- memberState[transactor] !== "writeOnlyInvite"
306
+ memberState[transactor] !== "writeOnlyInvite" &&
307
+ !isOwnWriteKeyRevelation(change.key, transactor, writeOnlyKeys)
306
308
  ) {
307
309
  logPermissionError("Only admins can reveal keys");
308
310
  continue;
@@ -370,14 +372,19 @@ function determineValidTransactionsForGroup(
370
372
  validTransactions.push({ txID: { sessionID, txIndex }, tx });
371
373
  continue;
372
374
  } else if (isWriteKeyForMember(change.key)) {
375
+ const memberKey = getAccountOrAgentFromWriteKeyForMember(change.key);
376
+
373
377
  if (
374
378
  memberState[transactor] !== "admin" &&
375
- memberState[transactor] !== "writeOnlyInvite"
379
+ memberState[transactor] !== "writeOnlyInvite" &&
380
+ memberKey !== transactor
376
381
  ) {
377
382
  logPermissionError("Only admins can set writeKeys");
378
383
  continue;
379
384
  }
380
385
 
386
+ writeOnlyKeys[memberKey] = change.value as KeyID;
387
+
381
388
  /**
382
389
  * writeOnlyInvite need to be able to set writeKeys because every new writeOnly
383
390
  * member comes with their own write key.
@@ -422,11 +429,12 @@ function determineValidTransactionsForGroup(
422
429
  !(
423
430
  change.value === "reader" ||
424
431
  change.value === "writer" ||
432
+ change.value === "writeOnly" ||
425
433
  change.value === "revoked"
426
434
  )
427
435
  ) {
428
436
  logPermissionError(
429
- "Everyone can only be set to reader, writer or revoked",
437
+ "Everyone can only be set to reader, writer, writeOnly or revoked",
430
438
  );
431
439
  continue;
432
440
  }
@@ -499,6 +507,12 @@ export function isWriteKeyForMember(
499
507
  return co.startsWith("writeKeyFor_");
500
508
  }
501
509
 
510
+ export function getAccountOrAgentFromWriteKeyForMember(
511
+ co: `writeKeyFor_${RawAccountID | AgentID}`,
512
+ ): RawAccountID | AgentID {
513
+ return co.slice("writeKeyFor_".length) as RawAccountID | AgentID;
514
+ }
515
+
502
516
  export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
503
517
  return co.startsWith("key_") && co.includes("_for_key");
504
518
  }
@@ -520,3 +534,17 @@ function isParentExtension(key: string): key is `parent_${CoID<RawGroup>}` {
520
534
  function isChildExtension(key: string): key is `child_${CoID<RawGroup>}` {
521
535
  return key.startsWith("child_");
522
536
  }
537
+
538
+ function isOwnWriteKeyRevelation(
539
+ key: `${KeyID}_for_${string}`,
540
+ memberKey: RawAccountID | AgentID,
541
+ writeOnlyKeys: Record<RawAccountID | AgentID, KeyID>,
542
+ ): key is `${KeyID}_for_${RawAccountID | AgentID}` {
543
+ if (Object.keys(writeOnlyKeys).length === 0) {
544
+ return false;
545
+ }
546
+
547
+ const keyID = key.slice(0, key.indexOf("_for_"));
548
+
549
+ return writeOnlyKeys[memberKey] === keyID;
550
+ }
package/src/sync.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { ValueType, metrics } from "@opentelemetry/api";
1
+ import { Histogram, ValueType, metrics } from "@opentelemetry/api";
2
2
  import { PeerState } from "./PeerState.js";
3
3
  import { SyncStateManager } from "./SyncStateManager.js";
4
4
  import { CoValueHeader, Transaction } from "./coValueCore.js";
5
5
  import { CoValueCore } from "./coValueCore.js";
6
+ import { CoValueState } from "./coValueState.js";
6
7
  import { Signature } from "./crypto/crypto.js";
7
8
  import { RawCoID, SessionID } from "./ids.js";
8
9
  import { LocalNode } from "./localNode.js";
@@ -35,8 +36,8 @@ export type LoadMessage = {
35
36
 
36
37
  export type KnownStateMessage = {
37
38
  action: "known";
38
- asDependencyOf?: RawCoID;
39
39
  isCorrection?: boolean;
40
+ asDependencyOf?: RawCoID;
40
41
  } & CoValueKnownState;
41
42
 
42
43
  export type NewContentMessage = {
@@ -122,10 +123,19 @@ export class SyncManager {
122
123
  valueType: ValueType.INT,
123
124
  unit: "peer",
124
125
  });
126
+ private transactionsSizeHistogram: Histogram;
125
127
 
126
128
  constructor(local: LocalNode) {
127
129
  this.local = local;
128
130
  this.syncState = new SyncStateManager(this);
131
+
132
+ this.transactionsSizeHistogram = metrics
133
+ .getMeter("cojson")
134
+ .createHistogram("jazz.transactions.size", {
135
+ description: "The size of transactions in a covalue",
136
+ unit: "bytes",
137
+ valueType: ValueType.INT,
138
+ });
129
139
  }
130
140
 
131
141
  syncState: SyncStateManager;
@@ -145,7 +155,10 @@ export class SyncManager {
145
155
 
146
156
  getServerAndStoragePeers(excludePeerId?: PeerID): PeerState[] {
147
157
  return this.peersInPriorityOrder().filter(
148
- (peer) => peer.isServerOrStoragePeer() && peer.id !== excludePeerId,
158
+ (peer) =>
159
+ peer.isServerOrStoragePeer() &&
160
+ peer.id !== excludePeerId &&
161
+ !peer.closed,
149
162
  );
150
163
  }
151
164
 
@@ -189,56 +202,35 @@ export class SyncManager {
189
202
  }
190
203
  }
191
204
 
192
- async subscribeToIncludingDependencies(id: RawCoID, peer: PeerState) {
193
- const entry = this.local.coValuesStore.get(id);
194
-
195
- if (entry.state.type !== "available") {
196
- entry.loadFromPeers([peer]).catch((e: unknown) => {
197
- logger.error("Error sending load", { err: e });
198
- });
199
- return;
200
- }
201
-
202
- const coValue = entry.state.coValue;
203
-
204
- for (const id of coValue.getDependedOnCoValues()) {
205
- await this.subscribeToIncludingDependencies(id, peer);
206
- }
207
-
208
- if (!peer.toldKnownState.has(id)) {
209
- peer.toldKnownState.add(id);
210
- this.trySendToPeer(peer, {
211
- action: "load",
212
- ...coValue.knownState(),
213
- }).catch((e: unknown) => {
214
- logger.error("Error sending load", { err: e });
215
- });
216
- }
217
- }
218
-
219
- async tellUntoldKnownStateIncludingDependencies(
220
- id: RawCoID,
221
- peer: PeerState,
222
- asDependencyOf?: RawCoID,
223
- ) {
205
+ async sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
224
206
  const coValue = this.local.expectCoValueLoaded(id);
225
207
 
226
208
  await Promise.all(
227
209
  coValue
228
210
  .getDependedOnCoValues()
229
- .map((dependentCoID) =>
230
- this.tellUntoldKnownStateIncludingDependencies(
231
- dependentCoID,
232
- peer,
233
- asDependencyOf || id,
234
- ),
235
- ),
211
+ .map((id) => this.sendNewContentIncludingDependencies(id, peer)),
212
+ );
213
+
214
+ const newContentPieces = coValue.newContentSince(
215
+ peer.optimisticKnownStates.get(id),
236
216
  );
237
217
 
238
- if (!peer.toldKnownState.has(id)) {
218
+ if (newContentPieces) {
219
+ for (const piece of newContentPieces) {
220
+ this.trySendToPeer(peer, piece).catch((e: unknown) => {
221
+ logger.error("Error sending content piece", { err: e });
222
+ });
223
+ }
224
+
225
+ peer.toldKnownState.add(id);
226
+ peer.optimisticKnownStates.dispatch({
227
+ type: "COMBINE_WITH",
228
+ id: id,
229
+ value: coValue.knownState(),
230
+ });
231
+ } else if (!peer.toldKnownState.has(id)) {
239
232
  this.trySendToPeer(peer, {
240
233
  action: "known",
241
- asDependencyOf,
242
234
  ...coValue.knownState(),
243
235
  }).catch((e: unknown) => {
244
236
  logger.error("Error sending known state", { err: e });
@@ -248,66 +240,101 @@ export class SyncManager {
248
240
  }
249
241
  }
250
242
 
251
- async sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
252
- const coValue = this.local.expectCoValueLoaded(id);
243
+ async startPeerReconciliation(peer: PeerState) {
244
+ const coValuesOrderedByDependency: CoValueCore[] = [];
253
245
 
254
- await Promise.all(
255
- coValue
256
- .getDependedOnCoValues()
257
- .map((id) => this.sendNewContentIncludingDependencies(id, peer)),
258
- );
246
+ const gathered = new Set<string>();
259
247
 
260
- const newContentPieces = coValue.newContentSince(
261
- peer.optimisticKnownStates.get(id),
262
- );
248
+ const buildOrderedCoValueList = (coValue: CoValueCore) => {
249
+ if (gathered.has(coValue.id)) {
250
+ return;
251
+ }
263
252
 
264
- if (newContentPieces) {
265
- const optimisticKnownStateBefore =
266
- peer.optimisticKnownStates.get(id) || emptyKnownState(id);
267
-
268
- const sendPieces = async () => {
269
- let lastYield = performance.now();
270
- for (const [_i, piece] of newContentPieces.entries()) {
271
- this.trySendToPeer(peer, piece).catch((e: unknown) => {
272
- logger.error("Error sending content piece", { err: e });
273
- });
253
+ gathered.add(coValue.id);
254
+
255
+ for (const id of coValue.getDependedOnCoValues()) {
256
+ const entry = this.local.coValuesStore.get(id);
274
257
 
275
- if (performance.now() - lastYield > 10) {
276
- await new Promise<void>((resolve) => {
277
- setTimeout(resolve, 0);
258
+ if (entry.state.type === "available") {
259
+ buildOrderedCoValueList(entry.state.coValue);
260
+ }
261
+ }
262
+
263
+ coValuesOrderedByDependency.push(coValue);
264
+ };
265
+
266
+ for (const entry of this.local.coValuesStore.getValues()) {
267
+ switch (entry.state.type) {
268
+ case "unavailable":
269
+ // If the coValue is unavailable and we never tried this peer
270
+ // we try to load it from the peer
271
+ if (!peer.toldKnownState.has(entry.id)) {
272
+ await entry.loadFromPeers([peer]).catch((e: unknown) => {
273
+ logger.error("Error sending load", { err: e });
278
274
  });
279
- lastYield = performance.now();
280
275
  }
281
- }
282
- };
276
+ break;
277
+ case "available":
278
+ const coValue = entry.state.coValue;
279
+
280
+ // Build the list of coValues ordered by dependency
281
+ // so we can send the load message in the correct order
282
+ buildOrderedCoValueList(coValue);
283
+ break;
284
+ }
283
285
 
284
- sendPieces().catch((e) => {
285
- logger.error("Error sending new content piece, retrying", { err: e });
286
+ // Fill the missing known states with empty known states
287
+ if (!peer.optimisticKnownStates.has(entry.id)) {
286
288
  peer.optimisticKnownStates.dispatch({
287
- type: "SET",
288
- id,
289
- value: optimisticKnownStateBefore ?? emptyKnownState(id),
289
+ type: "SET_AS_EMPTY",
290
+ id: entry.id,
290
291
  });
291
- return this.sendNewContentIncludingDependencies(id, peer);
292
- });
292
+ }
293
+ }
293
294
 
294
- peer.optimisticKnownStates.dispatch({
295
- type: "COMBINE_WITH",
296
- id,
297
- value: coValue.knownState(),
295
+ for (const coValue of coValuesOrderedByDependency) {
296
+ /**
297
+ * We send the load messages to:
298
+ * - Subscribe to the coValue updates
299
+ * - Start the sync process in case we or the other peer
300
+ * lacks some transactions
301
+ */
302
+ peer.toldKnownState.add(coValue.id);
303
+ this.trySendToPeer(peer, {
304
+ action: "load",
305
+ ...coValue.knownState(),
306
+ }).catch((e: unknown) => {
307
+ logger.error("Error sending load", { err: e });
298
308
  });
299
309
  }
300
310
  }
301
311
 
302
- addPeer(peer: Peer) {
312
+ nextPeer: Map<PeerID, Peer> = new Map();
313
+
314
+ async addPeer(peer: Peer) {
303
315
  const prevPeer = this.peers[peer.id];
304
- const peerState = new PeerState(peer, prevPeer?.knownStates);
305
- this.peers[peer.id] = peerState;
306
316
 
307
- if (prevPeer && !prevPeer.closed) {
308
- prevPeer.gracefulShutdown();
317
+ if (prevPeer) {
318
+ // Assign to nextPeer to check against race conditions
319
+ prevPeer.nextPeer = peer;
320
+
321
+ if (!prevPeer.closed) {
322
+ prevPeer.gracefulShutdown();
323
+ }
324
+
325
+ // Wait for the previous peer to finish processing the incoming messages
326
+ await prevPeer.incomingMessagesProcessingPromise?.catch((e) => {});
327
+
328
+ // If another peer was added in the meantime, we close this peer
329
+ if (prevPeer.nextPeer !== peer) {
330
+ peer.outgoing.close();
331
+ return;
332
+ }
309
333
  }
310
334
 
335
+ const peerState = new PeerState(peer, prevPeer?.knownStates);
336
+ this.peers[peer.id] = peerState;
337
+
311
338
  this.peersCounter.add(1, { role: peer.role });
312
339
 
313
340
  const unsubscribeFromKnownStatesUpdates = peerState.knownStates.subscribe(
@@ -317,43 +344,13 @@ export class SyncManager {
317
344
  );
318
345
 
319
346
  if (peerState.isServerOrStoragePeer()) {
320
- const initialSync = async () => {
321
- for (const entry of this.local.coValuesStore.getValues()) {
322
- await this.subscribeToIncludingDependencies(entry.id, peerState);
323
-
324
- if (entry.state.type === "available") {
325
- await this.sendNewContentIncludingDependencies(entry.id, peerState);
326
- }
327
-
328
- if (!peerState.optimisticKnownStates.has(entry.id)) {
329
- peerState.optimisticKnownStates.dispatch({
330
- type: "SET_AS_EMPTY",
331
- id: entry.id,
332
- });
333
- }
334
- }
335
- };
336
- void initialSync();
347
+ void this.startPeerReconciliation(peerState);
337
348
  }
338
349
 
339
- const processMessages = async () => {
340
- for await (const msg of peerState.incoming) {
341
- if (msg === "Disconnected") {
342
- return;
343
- }
344
- if (msg === "PingTimeout") {
345
- logger.error("Ping timeout from peer", {
346
- peerId: peer.id,
347
- peerRole: peer.role,
348
- });
349
- return;
350
- }
351
-
350
+ peerState
351
+ .processIncomingMessages(async (msg) => {
352
352
  await this.handleSyncMessage(msg, peerState);
353
- }
354
- };
355
-
356
- processMessages()
353
+ })
357
354
  .then(() => {
358
355
  if (peer.crashOnClose) {
359
356
  logger.error("Unexepcted close from peer", {
@@ -370,18 +367,18 @@ export class SyncManager {
370
367
  peerId: peer.id,
371
368
  peerRole: peer.role,
372
369
  });
370
+
373
371
  if (peer.crashOnClose) {
374
372
  this.local.crashed = e;
375
373
  throw new Error(e);
376
374
  }
377
375
  })
378
376
  .finally(() => {
379
- const state = this.peers[peer.id];
380
- state?.gracefulShutdown();
377
+ peerState.gracefulShutdown();
381
378
  unsubscribeFromKnownStatesUpdates();
382
379
  this.peersCounter.add(-1, { role: peer.role });
383
380
 
384
- if (peer.deletePeerStateOnClose) {
381
+ if (peer.deletePeerStateOnClose && this.peers[peer.id] === peerState) {
385
382
  delete this.peers[peer.id];
386
383
  }
387
384
  });
@@ -391,7 +388,21 @@ export class SyncManager {
391
388
  return peer.pushOutgoingMessage(msg);
392
389
  }
393
390
 
391
+ /**
392
+ * Handles the load message from a peer.
393
+ *
394
+ * Differences with the known state message:
395
+ * - The load message triggers the CoValue loading process on the other peer
396
+ * - The peer known state is stored as-is instead of being merged
397
+ * - The load message always replies with a known state message
398
+ */
394
399
  async handleLoad(msg: LoadMessage, peer: PeerState) {
400
+ /**
401
+ * We use the msg sessions as source of truth for the known states
402
+ *
403
+ * This way we can track part of the data loss that may occur when the other peer is restarted
404
+ *
405
+ */
395
406
  peer.dispatchToKnownStates({
396
407
  type: "SET",
397
408
  id: msg.id,
@@ -403,30 +414,23 @@ export class SyncManager {
403
414
  const eligiblePeers = this.getServerAndStoragePeers(peer.id);
404
415
 
405
416
  if (eligiblePeers.length === 0) {
406
- // If the load request contains a header or any session data
407
- // and we don't have any eligible peers to load the coValue from
408
- // we try to load it from the sender because it is the only place
409
- // where we can get informations about the coValue
410
- if (msg.header || Object.keys(msg.sessions).length > 0) {
411
- entry.loadFromPeers([peer]).catch((e) => {
412
- logger.error("Error loading coValue in handleLoad", { err: e });
413
- });
414
- } else {
415
- // We don't have any eligible peers to load the coValue from
416
- // so we send a known state back to the sender to let it know
417
- // that the coValue is unavailable
418
- this.trySendToPeer(peer, {
419
- action: "known",
420
- id: msg.id,
421
- header: false,
422
- sessions: {},
423
- }).catch((e) => {
424
- logger.error("Error sending known state back", { err: e });
425
- });
426
- }
417
+ // We don't have any eligible peers to load the coValue from
418
+ // so we send a known state back to the sender to let it know
419
+ // that the coValue is unavailable
420
+ peer.toldKnownState.add(msg.id);
421
+
422
+ this.trySendToPeer(peer, {
423
+ action: "known",
424
+ id: msg.id,
425
+ header: false,
426
+ sessions: {},
427
+ }).catch((e) => {
428
+ logger.error("Error sending known state back", { err: e });
429
+ });
427
430
 
428
431
  return;
429
432
  } else {
433
+ // Should move the state to loading
430
434
  this.local.loadCoValueCore(msg.id, peer.id).catch((e) => {
431
435
  logger.error("Error loading coValue in handleLoad", { err: e });
432
436
  });
@@ -442,11 +446,6 @@ export class SyncManager {
442
446
  .getCoValue()
443
447
  .then(async (value) => {
444
448
  if (value === "unavailable") {
445
- peer.dispatchToKnownStates({
446
- type: "SET",
447
- id: msg.id,
448
- value: knownStateIn(msg),
449
- });
450
449
  peer.toldKnownState.add(msg.id);
451
450
 
452
451
  this.trySendToPeer(peer, {
@@ -461,7 +460,6 @@ export class SyncManager {
461
460
  return;
462
461
  }
463
462
 
464
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
465
463
  await this.sendNewContentIncludingDependencies(msg.id, peer);
466
464
  })
467
465
  .catch((e) => {
@@ -469,7 +467,9 @@ export class SyncManager {
469
467
  err: e,
470
468
  });
471
469
  });
472
- } else if (entry.state.type === "unavailable") {
470
+ } else if (entry.state.type === "available") {
471
+ await this.sendNewContentIncludingDependencies(msg.id, peer);
472
+ } else {
473
473
  this.trySendToPeer(peer, {
474
474
  action: "known",
475
475
  id: msg.id,
@@ -477,11 +477,6 @@ export class SyncManager {
477
477
  sessions: {},
478
478
  });
479
479
  }
480
-
481
- if (entry.state.type === "available") {
482
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
483
- await this.sendNewContentIncludingDependencies(msg.id, peer);
484
- }
485
480
  }
486
481
 
487
482
  async handleKnownState(msg: KnownStateMessage, peer: PeerState) {
@@ -493,31 +488,6 @@ export class SyncManager {
493
488
  value: knownStateIn(msg),
494
489
  });
495
490
 
496
- if (entry.state.type === "unknown" || entry.state.type === "unavailable") {
497
- if (msg.asDependencyOf) {
498
- const dependencyEntry = this.local.coValuesStore.get(
499
- msg.asDependencyOf,
500
- );
501
-
502
- if (
503
- dependencyEntry.state.type === "available" ||
504
- dependencyEntry.state.type === "loading"
505
- ) {
506
- this.local
507
- .loadCoValueCore(
508
- msg.id,
509
- peer.role === "storage" ? undefined : peer.id,
510
- )
511
- .catch((e) => {
512
- logger.error(
513
- `Error loading coValue ${msg.id} to create loading state, as dependency of ${msg.asDependencyOf}`,
514
- { err: e },
515
- );
516
- });
517
- }
518
- }
519
- }
520
-
521
491
  // The header is a boolean value that tells us if the other peer do have information about the header.
522
492
  // If it's false in this point it means that the coValue is unavailable on the other peer.
523
493
  if (entry.state.type !== "available") {
@@ -534,11 +504,23 @@ export class SyncManager {
534
504
  }
535
505
 
536
506
  if (entry.state.type === "available") {
537
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
538
507
  await this.sendNewContentIncludingDependencies(msg.id, peer);
539
508
  }
540
509
  }
541
510
 
511
+ recordTransactionsSize(newTransactions: Transaction[], source: string) {
512
+ for (const tx of newTransactions) {
513
+ const txLength =
514
+ tx.privacy === "private"
515
+ ? tx.encryptedChanges.length
516
+ : tx.changes.length;
517
+
518
+ this.transactionsSizeHistogram.record(txLength, {
519
+ source,
520
+ });
521
+ }
522
+ }
523
+
542
524
  async handleNewContent(msg: NewContentMessage, peer: PeerState) {
543
525
  const entry = this.local.coValuesStore.get(msg.id);
544
526
 
@@ -638,6 +620,8 @@ export class SyncManager {
638
620
  continue;
639
621
  }
640
622
 
623
+ this.recordTransactionsSize(newTransactions, peer.role);
624
+
641
625
  peer.dispatchToKnownStates({
642
626
  type: "UPDATE_SESSION_COUNTER",
643
627
  id: msg.id,
@@ -660,6 +644,7 @@ export class SyncManager {
660
644
  err: e,
661
645
  });
662
646
  });
647
+ peer.toldKnownState.add(msg.id);
663
648
  } else {
664
649
  /**
665
650
  * We are sending a known state message to the peer to acknowledge the
@@ -678,6 +663,7 @@ export class SyncManager {
678
663
  err: e,
679
664
  });
680
665
  });
666
+ peer.toldKnownState.add(msg.id);
681
667
  }
682
668
 
683
669
  /**
@@ -685,7 +671,7 @@ export class SyncManager {
685
671
  * response to the peers that are waiting for confirmation that a coValue is
686
672
  * fully synced
687
673
  */
688
- await this.syncCoValue(coValue);
674
+ this.syncCoValue(coValue);
689
675
  }
690
676
 
691
677
  async handleCorrection(msg: KnownStateMessage, peer: PeerState) {
@@ -729,10 +715,8 @@ export class SyncManager {
729
715
  if (peer.erroredCoValues.has(coValue.id)) continue;
730
716
 
731
717
  if (peer.optimisticKnownStates.has(coValue.id)) {
732
- await this.tellUntoldKnownStateIncludingDependencies(coValue.id, peer);
733
718
  await this.sendNewContentIncludingDependencies(coValue.id, peer);
734
719
  } else if (peer.isServerOrStoragePeer()) {
735
- await this.subscribeToIncludingDependencies(coValue.id, peer);
736
720
  await this.sendNewContentIncludingDependencies(coValue.id, peer);
737
721
  }
738
722
  }