cojson 0.4.13 → 0.5.0

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.
package/src/sync.ts CHANGED
@@ -2,7 +2,6 @@ import { Signature } from "./crypto.js";
2
2
  import { CoValueHeader, Transaction } from "./coValueCore.js";
3
3
  import { CoValueCore } from "./coValueCore.js";
4
4
  import { LocalNode } from "./localNode.js";
5
- import { newLoadingState } from "./localNode.js";
6
5
  import {
7
6
  ReadableStream,
8
7
  WritableStream,
@@ -67,6 +66,7 @@ export interface Peer {
67
66
  outgoing: WritableStream<SyncMessage>;
68
67
  role: "peer" | "server" | "client";
69
68
  delayOnError?: number;
69
+ priority?: number;
70
70
  }
71
71
 
72
72
  export interface PeerState {
@@ -77,6 +77,7 @@ export interface PeerState {
77
77
  outgoing: WritableStreamDefaultWriter<SyncMessage>;
78
78
  role: "peer" | "server" | "client";
79
79
  delayOnError?: number;
80
+ priority?: number;
80
81
  }
81
82
 
82
83
  export function combinedKnownStates(
@@ -107,13 +108,30 @@ export function combinedKnownStates(
107
108
  export class SyncManager {
108
109
  peers: { [key: PeerID]: PeerState } = {};
109
110
  local: LocalNode;
111
+ requestedSyncs: { [id: RawCoID]: {done: Promise<void>, nRequestsThisTick: number} | undefined } = {};
110
112
 
111
113
  constructor(local: LocalNode) {
112
114
  this.local = local;
113
115
  }
114
116
 
115
- loadFromPeers(id: RawCoID) {
116
- for (const peer of Object.values(this.peers)) {
117
+ peersInPriorityOrder(): PeerState[] {
118
+ return Object.values(this.peers).sort((a, b) => {
119
+ const aPriority = a.priority || 0;
120
+ const bPriority = b.priority || 0;
121
+
122
+ return bPriority - aPriority;
123
+ });
124
+ }
125
+
126
+ async loadFromPeers(id: RawCoID, excludePeer?: PeerID) {
127
+ for (const peer of this.peersInPriorityOrder()) {
128
+ if (peer.id === excludePeer) {
129
+ continue;
130
+ }
131
+ if (peer.role !== "server") {
132
+ continue;
133
+ }
134
+ // console.log("loading", id, "from", peer.id);
117
135
  peer.outgoing
118
136
  .write({
119
137
  action: "load",
@@ -124,6 +142,44 @@ export class SyncManager {
124
142
  .catch((e) => {
125
143
  console.error("Error writing to peer", e);
126
144
  });
145
+ const coValueEntry = this.local.coValues[id];
146
+ if (coValueEntry?.state !== "loading") {
147
+ continue;
148
+ }
149
+ const firstStateEntry = coValueEntry.firstPeerState[peer.id];
150
+ if (firstStateEntry?.type !== "waiting") {
151
+ throw new Error("Expected firstPeerState to be waiting " + id);
152
+ }
153
+ await new Promise<void>((resolve) => {
154
+ const timeout = setTimeout(() => {
155
+ if (this.local.coValues[id]?.state === "loading") {
156
+ // console.warn(
157
+ // "Timeout waiting for peer to load",
158
+ // id,
159
+ // "from",
160
+ // peer.id,
161
+ // "and it hasn't loaded from other peers yet"
162
+ // );
163
+ }
164
+ resolve();
165
+ }, 1000);
166
+ firstStateEntry.done
167
+ .then(() => {
168
+ clearTimeout(timeout);
169
+ resolve();
170
+ })
171
+ .catch((e) => {
172
+ clearTimeout(timeout);
173
+ console.error(
174
+ "Error waiting for peer to load",
175
+ id,
176
+ "from",
177
+ peer.id,
178
+ e
179
+ );
180
+ resolve();
181
+ });
182
+ });
127
183
  }
128
184
  }
129
185
 
@@ -139,6 +195,7 @@ export class SyncManager {
139
195
  return await this.handleKnownState(msg, peer);
140
196
  }
141
197
  case "content":
198
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
142
199
  return await this.handleNewContent(msg, peer);
143
200
  case "done":
144
201
  return await this.handleUnsubscribe(msg);
@@ -190,13 +247,11 @@ export class SyncManager {
190
247
  ) {
191
248
  const coValue = this.local.expectCoValueLoaded(id);
192
249
 
193
- for (const dependentCoID of coValue.getDependedOnCoValues()) {
194
- await this.tellUntoldKnownStateIncludingDependencies(
195
- dependentCoID,
196
- peer,
197
- asDependencyOf || id
198
- );
199
- }
250
+ await Promise.all(coValue.getDependedOnCoValues().map(dependentCoID => this.tellUntoldKnownStateIncludingDependencies(
251
+ dependentCoID,
252
+ peer,
253
+ asDependencyOf || id
254
+ )));
200
255
 
201
256
  if (!peer.toldKnownState.has(id)) {
202
257
  await this.trySendToPeer(peer, {
@@ -212,9 +267,7 @@ export class SyncManager {
212
267
  async sendNewContentIncludingDependencies(id: RawCoID, peer: PeerState) {
213
268
  const coValue = this.local.expectCoValueLoaded(id);
214
269
 
215
- for (const id of coValue.getDependedOnCoValues()) {
216
- await this.sendNewContentIncludingDependencies(id, peer);
217
- }
270
+ await Promise.all(coValue.getDependedOnCoValues().map(id => this.sendNewContentIncludingDependencies(id, peer)));
218
271
 
219
272
  const newContentPieces = coValue.newContentSince(
220
273
  peer.optimisticKnownStates[id]
@@ -263,6 +316,7 @@ export class SyncManager {
263
316
  toldKnownState: new Set(),
264
317
  role: peer.role,
265
318
  delayOnError: peer.delayOnError,
319
+ priority: peer.priority,
266
320
  };
267
321
  this.peers[peer.id] = peerState;
268
322
 
@@ -271,6 +325,7 @@ export class SyncManager {
271
325
  for (const id of Object.keys(
272
326
  this.local.coValues
273
327
  ) as RawCoID[]) {
328
+ // console.log("subscribing to after peer added", id, peer.id)
274
329
  await this.subscribeToIncludingDependencies(id, peerState);
275
330
 
276
331
  peerState.optimisticKnownStates[id] = {
@@ -287,7 +342,19 @@ export class SyncManager {
287
342
  try {
288
343
  for await (const msg of peerState.incoming) {
289
344
  try {
290
- await this.handleSyncMessage(msg, peerState);
345
+ // await this.handleSyncMessage(msg, peerState);
346
+ this.handleSyncMessage(msg, peerState).catch((e) => {
347
+ console.error(
348
+ new Date(),
349
+ `Error reading from peer ${peer.id}, handling msg`,
350
+ JSON.stringify(msg, (k, v) =>
351
+ k === "changes" || k === "encryptedChanges"
352
+ ? v.slice(0, 20) + "..."
353
+ : v
354
+ ),
355
+ e
356
+ );
357
+ });
291
358
  await new Promise<void>((resolve) => {
292
359
  setTimeout(resolve, 0);
293
360
  });
@@ -321,18 +388,25 @@ export class SyncManager {
321
388
  }
322
389
 
323
390
  trySendToPeer(peer: PeerState, msg: SyncMessage) {
391
+ if (!this.peers[peer.id]) {
392
+ // already disconnected, return to drain potential queue
393
+ return Promise.resolve();
394
+ }
395
+
324
396
  return new Promise<void>((resolve) => {
325
- const start = Date.now()
397
+ const start = Date.now();
326
398
  peer.outgoing
327
399
  .write(msg)
328
400
  .then(() => {
329
401
  const end = Date.now();
330
402
  if (end - start > 1000) {
331
- console.error(
332
- new Error(
333
- `Writing to peer "${peer.id}" took ${Math.round((Date.now() - start)/100)/10}s - this should never happen as write should resolve quickly or error`
334
- )
335
- );
403
+ // console.error(
404
+ // new Error(
405
+ // `Writing to peer "${peer.id}" took ${
406
+ // Math.round((Date.now() - start) / 100) / 10
407
+ // }s - this should never happen as write should resolve quickly or error`
408
+ // )
409
+ // );
336
410
  } else {
337
411
  resolve();
338
412
  }
@@ -352,42 +426,42 @@ export class SyncManager {
352
426
  }
353
427
 
354
428
  async handleLoad(msg: LoadMessage, peer: PeerState) {
355
- const entry = this.local.coValues[msg.id];
429
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
430
+ let entry = this.local.coValues[msg.id];
356
431
 
357
- if (!entry || entry.state === "loading") {
358
- if (!entry) {
359
- await new Promise<void>((resolve) => {
360
- this.local
361
- .loadCoValue(msg.id)
362
- .then(() => resolve())
363
- .catch((e) => {
364
- console.error(
365
- "Error loading coValue in handleLoad",
366
- e
367
- );
368
- resolve();
369
- });
370
- setTimeout(resolve, 1000);
432
+ if (!entry) {
433
+ // console.log(`Loading ${msg.id} from all peers except ${peer.id}`);
434
+ this.local
435
+ .loadCoValueCore(msg.id, {
436
+ dontLoadFrom: peer.id,
437
+ dontWaitFor: peer.id,
438
+ })
439
+ .catch((e) => {
440
+ console.error("Error loading coValue in handleLoad", e);
371
441
  });
372
- }
373
442
 
374
- peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
375
- peer.toldKnownState.add(msg.id);
443
+ entry = this.local.coValues[msg.id]!;
444
+ }
376
445
 
377
- await this.trySendToPeer(peer, {
378
- action: "known",
379
- id: msg.id,
380
- header: false,
381
- sessions: {},
382
- });
446
+ if (entry.state === "loading") {
447
+ const loaded = await entry.done;
383
448
 
384
- return;
385
- }
449
+ if (loaded === "unavailable") {
450
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
451
+ peer.toldKnownState.add(msg.id);
386
452
 
387
- peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
453
+ await this.trySendToPeer(peer, {
454
+ action: "known",
455
+ id: msg.id,
456
+ header: false,
457
+ sessions: {},
458
+ });
388
459
 
389
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
460
+ return;
461
+ }
462
+ }
390
463
 
464
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
391
465
  await this.sendNewContentIncludingDependencies(msg.id, peer);
392
466
  }
393
467
 
@@ -402,9 +476,15 @@ export class SyncManager {
402
476
  if (!entry) {
403
477
  if (msg.asDependencyOf) {
404
478
  if (this.local.coValues[msg.asDependencyOf]) {
405
- entry = newLoadingState();
406
-
407
- this.local.coValues[msg.id] = entry;
479
+ this.local
480
+ .loadCoValueCore(msg.id, { dontLoadFrom: peer.id })
481
+ .catch((e) => {
482
+ console.error(
483
+ `Error loading coValue ${msg.id} to create loading state, as dependency of ${msg.asDependencyOf}`,
484
+ e
485
+ );
486
+ });
487
+ entry = this.local.coValues[msg.id]!; // must exist after loadCoValueCore
408
488
  } else {
409
489
  throw new Error(
410
490
  "Expected coValue dependency entry to be created, missing subscribe?"
@@ -418,6 +498,29 @@ export class SyncManager {
418
498
  }
419
499
 
420
500
  if (entry.state === "loading") {
501
+ const availableOnPeer = peer.optimisticKnownStates[msg.id]?.header;
502
+ const firstPeerStateEntry = entry.firstPeerState[peer.id];
503
+ if (firstPeerStateEntry?.type === "waiting") {
504
+ firstPeerStateEntry.resolve();
505
+ }
506
+ entry.firstPeerState[peer.id] = availableOnPeer
507
+ ? { type: "available" }
508
+ : { type: "unavailable" };
509
+ // console.log(
510
+ // "Marking",
511
+ // msg.id,
512
+ // "as",
513
+ // entry.firstPeerState[peer.id]?.type,
514
+ // "from",
515
+ // peer.id
516
+ // );
517
+ if (
518
+ Object.values(entry.firstPeerState).every(
519
+ (s) => s.type === "unavailable"
520
+ )
521
+ ) {
522
+ entry.resolve("unavailable");
523
+ }
421
524
  return [];
422
525
  }
423
526
 
@@ -449,6 +552,12 @@ export class SyncManager {
449
552
  throw new Error("Expected header to be sent in first message");
450
553
  }
451
554
 
555
+ const firstPeerStateEntry = entry.firstPeerState[peer.id];
556
+ if (firstPeerStateEntry?.type === "waiting") {
557
+ firstPeerStateEntry.resolve();
558
+ entry.firstPeerState[peer.id] = { type: "available" };
559
+ }
560
+
452
561
  peerOptimisticKnownState.header = true;
453
562
 
454
563
  const coValue = new CoValueCore(msg.header, this.local);
@@ -458,7 +567,7 @@ export class SyncManager {
458
567
  entry = {
459
568
  state: "loaded",
460
569
  coValue: coValue,
461
- onProgress: entry.onProgress
570
+ onProgress: entry.onProgress,
462
571
  };
463
572
 
464
573
  this.local.coValues[msg.id] = entry;
@@ -472,7 +581,7 @@ export class SyncManager {
472
581
  msg.new
473
582
  ) as [SessionID, SessionNewContent][]) {
474
583
  const ourKnownTxIdx =
475
- coValue.sessions[sessionID]?.transactions.length;
584
+ coValue.sessionLogs.get(sessionID)?.transactions.length;
476
585
  const theirFirstNewTxIdx = newContentForSession.after;
477
586
 
478
587
  if ((ourKnownTxIdx || 0) < theirFirstNewTxIdx) {
@@ -499,7 +608,7 @@ export class SyncManager {
499
608
  newContentForSession.lastSignature
500
609
  );
501
610
  const after = performance.now();
502
- if (after - before > 10) {
611
+ if (after - before > 80) {
503
612
  const totalTxLength = newTransactions
504
613
  .map((t) =>
505
614
  t.privacy === "private"
@@ -518,8 +627,13 @@ export class SyncManager {
518
627
  );
519
628
  }
520
629
 
521
- const theirTotalnTxs = Object.values(peer.optimisticKnownStates[msg.id]?.sessions || {}).reduce((sum, nTxs) => sum + nTxs, 0);
522
- const ourTotalnTxs = Object.values(coValue.sessions).reduce((sum, session) => sum + session.transactions.length, 0);
630
+ const theirTotalnTxs = Object.values(
631
+ peer.optimisticKnownStates[msg.id]?.sessions || {}
632
+ ).reduce((sum, nTxs) => sum + nTxs, 0);
633
+ const ourTotalnTxs = [...coValue.sessionLogs.values()].reduce(
634
+ (sum, session) => sum + session.transactions.length,
635
+ 0
636
+ );
523
637
 
524
638
  entry.onProgress?.(ourTotalnTxs / theirTotalnTxs);
525
639
 
@@ -536,9 +650,11 @@ export class SyncManager {
536
650
  continue;
537
651
  }
538
652
 
539
- peerOptimisticKnownState.sessions[sessionID] = Math.max(peerOptimisticKnownState.sessions[sessionID] || 0,
653
+ peerOptimisticKnownState.sessions[sessionID] = Math.max(
654
+ peerOptimisticKnownState.sessions[sessionID] || 0,
540
655
  newContentForSession.after +
541
- newContentForSession.newTransactions.length);
656
+ newContentForSession.newTransactions.length
657
+ );
542
658
  }
543
659
 
544
660
  if (resolveAfterDone) {
@@ -567,7 +683,38 @@ export class SyncManager {
567
683
  }
568
684
 
569
685
  async syncCoValue(coValue: CoValueCore) {
570
- for (const peer of Object.values(this.peers)) {
686
+ if (this.requestedSyncs[coValue.id]) {
687
+ this.requestedSyncs[coValue.id]!.nRequestsThisTick++;
688
+ return this.requestedSyncs[coValue.id]!.done;
689
+ } else {
690
+ const done = new Promise<void>((resolve) => {
691
+ setTimeout(async () => {
692
+ delete this.requestedSyncs[coValue.id];
693
+ if (entry.nRequestsThisTick >= 2) {
694
+ console.log("Syncing", coValue.id, "for", entry.nRequestsThisTick, "requests");
695
+ }
696
+ await this.actuallySyncCoValue(coValue);
697
+ resolve();
698
+ }, 0);
699
+ });
700
+ const entry = {
701
+ done,
702
+ nRequestsThisTick: 1,
703
+ };
704
+ this.requestedSyncs[coValue.id] = entry;
705
+ return done;
706
+ }
707
+ }
708
+
709
+ async actuallySyncCoValue(coValue: CoValueCore) {
710
+ let blockingSince = performance.now();
711
+ for (const peer of this.peersInPriorityOrder()) {
712
+ if (performance.now() - blockingSince > 5) {
713
+ await new Promise<void>((resolve) => {
714
+ setTimeout(resolve, 0);
715
+ });
716
+ blockingSince = performance.now();
717
+ }
571
718
  const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
572
719
 
573
720
  if (optimisticKnownState) {
@@ -1,9 +1,9 @@
1
1
  import { expectList, expectMap, expectStream } from "../coValue.js";
2
- import { accountOrAgentIDfromSessionID } from "../coValueCore.js";
3
2
  import { BinaryCoStream } from "../coValues/coStream.js";
4
3
  import { createdNowUnique } from "../crypto.js";
5
4
  import { MAX_RECOMMENDED_TX_SIZE, cojsonReady } from "../index.js";
6
5
  import { LocalNode } from "../localNode.js";
6
+ import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
7
7
  import { randomAnonymousAccountAndSessionID } from "./testUtils.js";
8
8
 
9
9
  beforeEach(async () => {
@@ -423,7 +423,7 @@ test("When adding large transactions (small fraction of MAX_RECOMMENDED_TX_SIZE)
423
423
  editable.endBinaryStream("trusting");
424
424
  });
425
425
 
426
- const sessionEntry = coValue._sessions[node.currentSessionID]!;
426
+ const sessionEntry = coValue.sessionLogs.get(node.currentSessionID)!;
427
427
  expect(sessionEntry.transactions.length).toEqual(12);
428
428
  expect(sessionEntry.signatureAfter[0]).not.toBeDefined();
429
429
  expect(sessionEntry.signatureAfter[1]).not.toBeDefined();
@@ -499,7 +499,7 @@ test("When adding large transactions (bigger than MAX_RECOMMENDED_TX_SIZE), we s
499
499
  editable.endBinaryStream("trusting");
500
500
  });
501
501
 
502
- const sessionEntry = coValue._sessions[node.currentSessionID]!;
502
+ const sessionEntry = coValue.sessionLogs.get(node.currentSessionID)!;
503
503
  expect(sessionEntry.transactions.length).toEqual(5);
504
504
  expect(sessionEntry.signatureAfter[0]).not.toBeDefined();
505
505
  expect(sessionEntry.signatureAfter[1]).toBeDefined();