cojson 0.4.8 → 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.
@@ -18,11 +18,11 @@ export function connectedPeers(
18
18
  peer2role?: Peer["role"];
19
19
  } = {}
20
20
  ): [Peer, Peer] {
21
- const [inRx1, inTx1] = newStreamPair<SyncMessage>();
22
- const [outRx1, outTx1] = newStreamPair<SyncMessage>();
21
+ const [inRx1, inTx1] = newStreamPair<SyncMessage>(peer1id + "_in");
22
+ const [outRx1, outTx1] = newStreamPair<SyncMessage>(peer1id + "_out");
23
23
 
24
- const [inRx2, inTx2] = newStreamPair<SyncMessage>();
25
- const [outRx2, outTx2] = newStreamPair<SyncMessage>();
24
+ const [inRx2, inTx2] = newStreamPair<SyncMessage>(peer2id + "_in");
25
+ const [outRx2, outTx2] = newStreamPair<SyncMessage>(peer2id + "_out");
26
26
 
27
27
  void outRx2
28
28
  .pipeThrough(
@@ -37,7 +37,7 @@ export function connectedPeers(
37
37
  JSON.stringify(
38
38
  chunk,
39
39
  (k, v) =>
40
- (k === "changes" || k === "encryptedChanges")
40
+ k === "changes" || k === "encryptedChanges"
41
41
  ? v.slice(0, 20) + "..."
42
42
  : v,
43
43
  2
@@ -62,7 +62,7 @@ export function connectedPeers(
62
62
  JSON.stringify(
63
63
  chunk,
64
64
  (k, v) =>
65
- (k === "changes" || k === "encryptedChanges")
65
+ k === "changes" || k === "encryptedChanges"
66
66
  ? v.slice(0, 20) + "..."
67
67
  : v,
68
68
  2
@@ -91,7 +91,10 @@ export function connectedPeers(
91
91
  return [peer1AsPeer, peer2AsPeer];
92
92
  }
93
93
 
94
- export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
94
+ export function newStreamPair<T>(
95
+ pairName?: string
96
+ ): [ReadableStream<T>, WritableStream<T>] {
97
+ let queueLength = 0;
95
98
  let readerClosed = false;
96
99
 
97
100
  let resolveEnqueue: (enqueue: (item: T) => void) => void;
@@ -104,6 +107,22 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
104
107
  resolveClose = resolve;
105
108
  });
106
109
 
110
+ let queueWasOverflowing = false;
111
+
112
+ function maybeReportQueueLength() {
113
+ if (queueLength >= 100) {
114
+ queueWasOverflowing = true;
115
+ if (queueLength % 100 === 0) {
116
+ console.warn(pairName, "overflowing queue length", queueLength);
117
+ }
118
+ } else {
119
+ if (queueWasOverflowing) {
120
+ console.debug(pairName, "ok queue length", queueLength);
121
+ queueWasOverflowing = false;
122
+ }
123
+ }
124
+ }
125
+
107
126
  const readable = new ReadableStream<T>({
108
127
  async start(controller) {
109
128
  resolveEnqueue(controller.enqueue.bind(controller));
@@ -114,12 +133,26 @@ export function newStreamPair<T>(): [ReadableStream<T>, WritableStream<T>] {
114
133
  console.log("Manually closing reader");
115
134
  readerClosed = true;
116
135
  },
117
- });
136
+ }).pipeThrough(
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ new TransformStream<any, any>({
139
+ transform(
140
+ chunk: SyncMessage,
141
+ controller: { enqueue: (msg: SyncMessage) => void }
142
+ ) {
143
+ queueLength -= 1;
144
+ maybeReportQueueLength();
145
+ controller.enqueue(chunk);
146
+ },
147
+ })
148
+ ) as ReadableStream<T>;
118
149
 
119
150
  let lastWritePromise = Promise.resolve();
120
151
 
121
152
  const writable = new WritableStream<T>({
122
153
  async write(chunk) {
154
+ queueLength += 1;
155
+ maybeReportQueueLength();
123
156
  const enqueue = await enqueuePromise;
124
157
  if (readerClosed) {
125
158
  throw new Error("Reader closed");
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]
@@ -225,12 +278,19 @@ export class SyncManager {
225
278
  peer.optimisticKnownStates[id] || emptyKnownState(id);
226
279
 
227
280
  const sendPieces = async () => {
281
+ let lastYield = performance.now();
228
282
  for (const [_i, piece] of newContentPieces.entries()) {
229
283
  // console.log(
230
284
  // `${id} -> ${peer.id}: Sending content piece ${i + 1}/${newContentPieces.length} header: ${!!piece.header}`,
231
285
  // // Object.values(piece.new).map((s) => s.newTransactions)
232
286
  // );
233
287
  await this.trySendToPeer(peer, piece);
288
+ if (performance.now() - lastYield > 10) {
289
+ await new Promise<void>((resolve) => {
290
+ setTimeout(resolve, 0);
291
+ });
292
+ lastYield = performance.now();
293
+ }
234
294
  }
235
295
  };
236
296
 
@@ -256,6 +316,7 @@ export class SyncManager {
256
316
  toldKnownState: new Set(),
257
317
  role: peer.role,
258
318
  delayOnError: peer.delayOnError,
319
+ priority: peer.priority,
259
320
  };
260
321
  this.peers[peer.id] = peerState;
261
322
 
@@ -264,6 +325,7 @@ export class SyncManager {
264
325
  for (const id of Object.keys(
265
326
  this.local.coValues
266
327
  ) as RawCoID[]) {
328
+ // console.log("subscribing to after peer added", id, peer.id)
267
329
  await this.subscribeToIncludingDependencies(id, peerState);
268
330
 
269
331
  peerState.optimisticKnownStates[id] = {
@@ -280,7 +342,19 @@ export class SyncManager {
280
342
  try {
281
343
  for await (const msg of peerState.incoming) {
282
344
  try {
283
- 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
+ });
284
358
  await new Promise<void>((resolve) => {
285
359
  setTimeout(resolve, 0);
286
360
  });
@@ -314,20 +388,28 @@ export class SyncManager {
314
388
  }
315
389
 
316
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
+
317
396
  return new Promise<void>((resolve) => {
318
- const timeout = setTimeout(() => {
319
- console.error(
320
- new Error(
321
- `Writing to peer ${peer.id} took >1s - this should never happen as write should resolve quickly or error`
322
- )
323
- );
324
- resolve();
325
- }, 1000);
397
+ const start = Date.now();
326
398
  peer.outgoing
327
399
  .write(msg)
328
400
  .then(() => {
329
- clearTimeout(timeout);
330
- resolve();
401
+ const end = Date.now();
402
+ if (end - start > 1000) {
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
+ // );
410
+ } else {
411
+ resolve();
412
+ }
331
413
  })
332
414
  .catch((e) => {
333
415
  console.error(
@@ -344,42 +426,42 @@ export class SyncManager {
344
426
  }
345
427
 
346
428
  async handleLoad(msg: LoadMessage, peer: PeerState) {
347
- const entry = this.local.coValues[msg.id];
429
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
430
+ let entry = this.local.coValues[msg.id];
348
431
 
349
- if (!entry || entry.state === "loading") {
350
- if (!entry) {
351
- await new Promise<void>((resolve) => {
352
- this.local
353
- .loadCoValue(msg.id)
354
- .then(() => resolve())
355
- .catch((e) => {
356
- console.error(
357
- "Error loading coValue in handleLoad",
358
- e
359
- );
360
- resolve();
361
- });
362
- 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);
363
441
  });
364
- }
365
442
 
366
- peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
367
- peer.toldKnownState.add(msg.id);
443
+ entry = this.local.coValues[msg.id]!;
444
+ }
368
445
 
369
- await this.trySendToPeer(peer, {
370
- action: "known",
371
- id: msg.id,
372
- header: false,
373
- sessions: {},
374
- });
446
+ if (entry.state === "loading") {
447
+ const loaded = await entry.done;
375
448
 
376
- return;
377
- }
449
+ if (loaded === "unavailable") {
450
+ peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
451
+ peer.toldKnownState.add(msg.id);
378
452
 
379
- peer.optimisticKnownStates[msg.id] = knownStateIn(msg);
453
+ await this.trySendToPeer(peer, {
454
+ action: "known",
455
+ id: msg.id,
456
+ header: false,
457
+ sessions: {},
458
+ });
380
459
 
381
- await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
460
+ return;
461
+ }
462
+ }
382
463
 
464
+ await this.tellUntoldKnownStateIncludingDependencies(msg.id, peer);
383
465
  await this.sendNewContentIncludingDependencies(msg.id, peer);
384
466
  }
385
467
 
@@ -394,9 +476,15 @@ export class SyncManager {
394
476
  if (!entry) {
395
477
  if (msg.asDependencyOf) {
396
478
  if (this.local.coValues[msg.asDependencyOf]) {
397
- entry = newLoadingState();
398
-
399
- 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
400
488
  } else {
401
489
  throw new Error(
402
490
  "Expected coValue dependency entry to be created, missing subscribe?"
@@ -410,6 +498,29 @@ export class SyncManager {
410
498
  }
411
499
 
412
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
+ }
413
524
  return [];
414
525
  }
415
526
 
@@ -441,6 +552,12 @@ export class SyncManager {
441
552
  throw new Error("Expected header to be sent in first message");
442
553
  }
443
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
+
444
561
  peerOptimisticKnownState.header = true;
445
562
 
446
563
  const coValue = new CoValueCore(msg.header, this.local);
@@ -450,6 +567,7 @@ export class SyncManager {
450
567
  entry = {
451
568
  state: "loaded",
452
569
  coValue: coValue,
570
+ onProgress: entry.onProgress,
453
571
  };
454
572
 
455
573
  this.local.coValues[msg.id] = entry;
@@ -463,7 +581,7 @@ export class SyncManager {
463
581
  msg.new
464
582
  ) as [SessionID, SessionNewContent][]) {
465
583
  const ourKnownTxIdx =
466
- coValue.sessions[sessionID]?.transactions.length;
584
+ coValue.sessionLogs.get(sessionID)?.transactions.length;
467
585
  const theirFirstNewTxIdx = newContentForSession.after;
468
586
 
469
587
  if ((ourKnownTxIdx || 0) < theirFirstNewTxIdx) {
@@ -490,7 +608,7 @@ export class SyncManager {
490
608
  newContentForSession.lastSignature
491
609
  );
492
610
  const after = performance.now();
493
- if (after - before > 10) {
611
+ if (after - before > 80) {
494
612
  const totalTxLength = newTransactions
495
613
  .map((t) =>
496
614
  t.privacy === "private"
@@ -509,6 +627,16 @@ export class SyncManager {
509
627
  );
510
628
  }
511
629
 
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
+ );
637
+
638
+ entry.onProgress?.(ourTotalnTxs / theirTotalnTxs);
639
+
512
640
  if (!success) {
513
641
  console.error(
514
642
  "Failed to add transactions",
@@ -522,9 +650,11 @@ export class SyncManager {
522
650
  continue;
523
651
  }
524
652
 
525
- peerOptimisticKnownState.sessions[sessionID] =
653
+ peerOptimisticKnownState.sessions[sessionID] = Math.max(
654
+ peerOptimisticKnownState.sessions[sessionID] || 0,
526
655
  newContentForSession.after +
527
- newContentForSession.newTransactions.length;
656
+ newContentForSession.newTransactions.length
657
+ );
528
658
  }
529
659
 
530
660
  if (resolveAfterDone) {
@@ -553,7 +683,38 @@ export class SyncManager {
553
683
  }
554
684
 
555
685
  async syncCoValue(coValue: CoValueCore) {
556
- 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
+ }
557
718
  const optimisticKnownState = peer.optimisticKnownStates[coValue.id];
558
719
 
559
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();