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/crypto.ts CHANGED
@@ -1,4 +1,12 @@
1
- import { ed25519, x25519 } from "@noble/curves/ed25519";
1
+ import {
2
+ initBundledOnce,
3
+ Ed25519SigningKey,
4
+ Ed25519VerifyingKey,
5
+ X25519StaticSecret,
6
+ Memory,
7
+ Ed25519Signature,
8
+ X25519PublicKey,
9
+ } from "@hazae41/berith";
2
10
  import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
3
11
  import { JsonValue } from "./jsonValue.js";
4
12
  import { base58 } from "@scure/base";
@@ -10,7 +18,13 @@ import { createBLAKE3 } from "hash-wasm";
10
18
  import { Stringified, parseJSON, stableStringify } from "./jsonStringify.js";
11
19
 
12
20
  let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
13
- let blake3HashOnce: (data: Uint8Array) => Uint8Array;
21
+ let blake3HashOnce: (data: Uint8Array) => Uint8Array = () => {
22
+ throw new Error(
23
+ "cojson WASM dependencies not yet loaded; Make sure to import `cojsonReady` from `cojson` and await it before using any cojson functionality:\n\n" +
24
+ 'import { cojsonReady } from "cojson";\n' +
25
+ "await cojsonReady;\n\n"
26
+ );
27
+ };
14
28
  let blake3HashOnceWithContext: (
15
29
  data: Uint8Array,
16
30
  { context }: { context: Uint8Array }
@@ -21,29 +35,36 @@ let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (
21
35
  ) => Uint8Array;
22
36
  let blake3digestForState: (state: Uint8Array) => Uint8Array;
23
37
 
24
- export const cryptoReady = new Promise<void>((resolve) => {
25
- createBLAKE3()
26
- .then((bl3) => {
27
- blake3Instance = bl3;
28
- blake3HashOnce = (data) => {
29
- return bl3.init().update(data).digest("binary");
30
- };
31
- blake3HashOnceWithContext = (data, { context }) => {
32
- return bl3.init().update(context).update(data).digest("binary");
33
- };
34
- blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
35
- bl3.load(state).update(data);
36
- return bl3.save();
37
- };
38
- blake3digestForState = (state) => {
39
- return bl3.load(state).digest("binary");
40
- };
41
- resolve();
42
- })
43
- .catch((e) =>
44
- console.error("Failed to load cryptography dependencies", e)
45
- );
46
- });
38
+ export const cryptoReady = Promise.all([
39
+ new Promise<void>((resolve) => {
40
+ createBLAKE3()
41
+ .then((bl3) => {
42
+ blake3Instance = bl3;
43
+ blake3HashOnce = (data) => {
44
+ return bl3.init().update(data).digest("binary");
45
+ };
46
+ blake3HashOnceWithContext = (data, { context }) => {
47
+ return bl3
48
+ .init()
49
+ .update(context)
50
+ .update(data)
51
+ .digest("binary");
52
+ };
53
+ blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
54
+ bl3.load(state).update(data);
55
+ return bl3.save();
56
+ };
57
+ blake3digestForState = (state) => {
58
+ return bl3.load(state).digest("binary");
59
+ };
60
+ resolve();
61
+ })
62
+ .catch((e) =>
63
+ console.error("Failed to load cryptography dependencies", e)
64
+ );
65
+ }),
66
+ initBundledOnce(),
67
+ ]);
47
68
 
48
69
  export type SignerSecret = `signerSecret_z${string}`;
49
70
  export type SignerID = `signer_z${string}`;
@@ -59,7 +80,9 @@ const textEncoder = new TextEncoder();
59
80
  const textDecoder = new TextDecoder();
60
81
 
61
82
  export function newRandomSigner(): SignerSecret {
62
- return `signerSecret_z${base58.encode(ed25519.utils.randomPrivateKey())}`;
83
+ return `signerSecret_z${base58.encode(
84
+ new Ed25519SigningKey().to_bytes().copyAndDispose()
85
+ )}`;
63
86
  }
64
87
 
65
88
  export function signerSecretToBytes(secret: SignerSecret): Uint8Array {
@@ -72,17 +95,22 @@ export function signerSecretFromBytes(bytes: Uint8Array): SignerSecret {
72
95
 
73
96
  export function getSignerID(secret: SignerSecret): SignerID {
74
97
  return `signer_z${base58.encode(
75
- ed25519.getPublicKey(
76
- base58.decode(secret.substring("signerSecret_z".length))
98
+ Ed25519SigningKey.from_bytes(
99
+ new Memory(base58.decode(secret.substring("signerSecret_z".length)))
77
100
  )
101
+ .public()
102
+ .to_bytes()
103
+ .copyAndDispose()
78
104
  )}`;
79
105
  }
80
106
 
81
107
  export function sign(secret: SignerSecret, message: JsonValue): Signature {
82
- const signature = ed25519.sign(
83
- textEncoder.encode(stableStringify(message)),
84
- base58.decode(secret.substring("signerSecret_z".length))
85
- );
108
+ const signature = Ed25519SigningKey.from_bytes(
109
+ new Memory(base58.decode(secret.substring("signerSecret_z".length)))
110
+ )
111
+ .sign(new Memory(textEncoder.encode(stableStringify(message))))
112
+ .to_bytes()
113
+ .copyAndDispose();
86
114
  return `signature_z${base58.encode(signature)}`;
87
115
  }
88
116
 
@@ -91,15 +119,20 @@ export function verify(
91
119
  message: JsonValue,
92
120
  id: SignerID
93
121
  ): boolean {
94
- return ed25519.verify(
95
- base58.decode(signature.substring("signature_z".length)),
96
- textEncoder.encode(stableStringify(message)),
97
- base58.decode(id.substring("signer_z".length))
122
+ return new Ed25519VerifyingKey(
123
+ new Memory(base58.decode(id.substring("signer_z".length)))
124
+ ).verify(
125
+ new Memory(textEncoder.encode(stableStringify(message))),
126
+ new Ed25519Signature(
127
+ new Memory(base58.decode(signature.substring("signature_z".length)))
128
+ )
98
129
  );
99
130
  }
100
131
 
101
132
  export function newRandomSealer(): SealerSecret {
102
- return `sealerSecret_z${base58.encode(x25519.utils.randomPrivateKey())}`;
133
+ return `sealerSecret_z${base58.encode(
134
+ new X25519StaticSecret().to_bytes().copyAndDispose()
135
+ )}`;
103
136
  }
104
137
 
105
138
  export function sealerSecretToBytes(secret: SealerSecret): Uint8Array {
@@ -112,9 +145,12 @@ export function sealerSecretFromBytes(bytes: Uint8Array): SealerSecret {
112
145
 
113
146
  export function getSealerID(secret: SealerSecret): SealerID {
114
147
  return `sealer_z${base58.encode(
115
- x25519.getPublicKey(
116
- base58.decode(secret.substring("sealerSecret_z".length))
148
+ X25519StaticSecret.from_bytes(
149
+ new Memory(base58.decode(secret.substring("sealerSecret_z".length)))
117
150
  )
151
+ .to_public()
152
+ .to_bytes()
153
+ .copyAndDispose()
118
154
  )}`;
119
155
  }
120
156
 
@@ -180,7 +216,10 @@ export function seal<T extends JsonValue>({
180
216
 
181
217
  const plaintext = textEncoder.encode(stableStringify(message));
182
218
 
183
- const sharedSecret = x25519.getSharedSecret(senderPriv, sealerPub);
219
+ const sharedSecret = X25519StaticSecret.from_bytes(new Memory(senderPriv))
220
+ .diffie_hellman(X25519PublicKey.from_bytes(new Memory(sealerPub)))
221
+ .to_bytes()
222
+ .copyAndDispose();
184
223
 
185
224
  const sealedBytes = xsalsa20_poly1305(sharedSecret, nOnce).encrypt(
186
225
  plaintext
@@ -205,7 +244,10 @@ export function unseal<T extends JsonValue>(
205
244
 
206
245
  const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
207
246
 
208
- const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
247
+ const sharedSecret = X25519StaticSecret.from_bytes(new Memory(sealerPriv))
248
+ .diffie_hellman(X25519PublicKey.from_bytes(new Memory(senderPub)))
249
+ .to_bytes()
250
+ .copyAndDispose();
209
251
 
210
252
  const plaintext = xsalsa20_poly1305(sharedSecret, nOnce).decrypt(
211
253
  sealedBytes
package/src/localNode.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  Group,
20
20
  secretSeedFromInviteSecret,
21
21
  } from "./coValues/group.js";
22
- import { Peer, SyncManager } from "./sync.js";
22
+ import { Peer, PeerID, SyncManager } from "./sync.js";
23
23
  import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
24
24
  import { CoID } from "./coValue.js";
25
25
  import {
@@ -153,6 +153,11 @@ export class LocalNode {
153
153
  }
154
154
 
155
155
  const account = await accountPromise;
156
+
157
+ if (account === "unavailable") {
158
+ throw new Error("Account unavailable from all peers");
159
+ }
160
+
156
161
  const controlledAccount = new ControlledAccount(
157
162
  account.core,
158
163
  accountSecret
@@ -199,14 +204,36 @@ export class LocalNode {
199
204
  }
200
205
 
201
206
  /** @internal */
202
- loadCoValue(id: RawCoID, onProgress?: (progress: number) => void): Promise<CoValueCore> {
207
+ async loadCoValueCore(
208
+ id: RawCoID,
209
+ options: {
210
+ dontLoadFrom?: PeerID;
211
+ dontWaitFor?: PeerID;
212
+ onProgress?: (progress: number) => void;
213
+ } = {}
214
+ ): Promise<CoValueCore | "unavailable"> {
203
215
  let entry = this.coValues[id];
204
216
  if (!entry) {
205
- entry = newLoadingState(onProgress);
217
+ const peersToWaitFor = new Set(
218
+ Object.values(this.syncManager.peers)
219
+ .filter((peer) => peer.role === "server")
220
+ .map((peer) => peer.id)
221
+ );
222
+ if (options.dontWaitFor) peersToWaitFor.delete(options.dontWaitFor);
223
+ entry = newLoadingState(peersToWaitFor, options.onProgress);
206
224
 
207
225
  this.coValues[id] = entry;
208
226
 
209
- this.syncManager.loadFromPeers(id);
227
+ this.syncManager
228
+ .loadFromPeers(id, options.dontLoadFrom)
229
+ .catch((e) => {
230
+ console.error(
231
+ "Error loading from peers",
232
+ id,
233
+
234
+ e
235
+ );
236
+ });
210
237
  }
211
238
  if (entry.state === "loaded") {
212
239
  return Promise.resolve(entry.coValue);
@@ -221,25 +248,38 @@ export class LocalNode {
221
248
  *
222
249
  * @category 3. Low-level
223
250
  */
224
- async load<T extends CoValue>(id: CoID<T>, onProgress?: (progress: number) => void): Promise<T> {
225
- return (await this.loadCoValue(id, onProgress)).getCurrentContent() as T;
251
+ async load<T extends CoValue>(
252
+ id: CoID<T>,
253
+ onProgress?: (progress: number) => void
254
+ ): Promise<T | "unavailable"> {
255
+ const core = await this.loadCoValueCore(id, { onProgress });
256
+
257
+ if (core === "unavailable") {
258
+ return "unavailable";
259
+ }
260
+
261
+ return core.getCurrentContent() as T;
226
262
  }
227
263
 
228
264
  /** @category 3. Low-level */
229
265
  subscribe<T extends CoValue>(
230
266
  id: CoID<T>,
231
- callback: (update: T) => void
267
+ callback: (update: T | "unavailable") => void
232
268
  ): () => void {
233
269
  let stopped = false;
234
270
  let unsubscribe!: () => void;
235
271
 
236
- console.log("Subscribing to " + id);
272
+ // console.log("Subscribing to " + id);
237
273
 
238
274
  this.load(id)
239
275
  .then((coValue) => {
240
276
  if (stopped) {
241
277
  return;
242
278
  }
279
+ if (coValue === "unavailable") {
280
+ callback("unavailable");
281
+ return;
282
+ }
243
283
  unsubscribe = coValue.subscribe(callback);
244
284
  })
245
285
  .catch((e) => {
@@ -260,6 +300,12 @@ export class LocalNode {
260
300
  ): Promise<void> {
261
301
  const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
262
302
 
303
+ if (groupOrOwnedValue === "unavailable") {
304
+ throw new Error(
305
+ "Trying to accept invite: Group/owned value unavailable from all peers"
306
+ );
307
+ }
308
+
263
309
  if (groupOrOwnedValue.core.header.ruleset.type === "ownedByGroup") {
264
310
  return this.acceptInvite(
265
311
  groupOrOwnedValue.core.header.ruleset.group as CoID<Group>,
@@ -325,7 +371,7 @@ export class LocalNode {
325
371
  : "reader"
326
372
  );
327
373
 
328
- group.core._sessions = groupAsInvite.core.sessions;
374
+ group.core._sessionLogs = groupAsInvite.core.sessionLogs;
329
375
  group.core._cachedContent = undefined;
330
376
 
331
377
  for (const groupListener of group.core.listeners) {
@@ -400,17 +446,6 @@ export class LocalNode {
400
446
  },
401
447
  });
402
448
 
403
- console.log(
404
- "Creating read key",
405
- getAgentSealerSecret(agentSecret),
406
- getAgentSealerID(accountAgentID),
407
- account.id,
408
- account.core.nextTransactionID(),
409
- "in session",
410
- account.core.node.currentSessionID,
411
- "=",
412
- sealed
413
- );
414
449
  editable.set(
415
450
  `${readKey.id}_for_${accountAgentID}`,
416
451
  sealed,
@@ -432,16 +467,13 @@ export class LocalNode {
432
467
 
433
468
  const accountOnThisNode = this.expectCoValueLoaded(account.id);
434
469
 
435
- accountOnThisNode._sessions = {
436
- ...account.core.sessions,
437
- };
470
+ accountOnThisNode._sessionLogs = new Map(account.core.sessionLogs);
471
+
438
472
  accountOnThisNode._cachedContent = undefined;
439
473
 
440
474
  const profileOnThisNode = this.createCoValue(profile.core.header);
441
475
 
442
- profileOnThisNode._sessions = {
443
- ...profile.core.sessions,
444
- };
476
+ profileOnThisNode._sessionLogs = new Map(profile.core.sessionLogs);
445
477
  profileOnThisNode._cachedContent = undefined;
446
478
 
447
479
  return new ControlledAccount(accountOnThisNode, agentSecret);
@@ -475,6 +507,41 @@ export class LocalNode {
475
507
  return new Account(coValue).getCurrentAgentID();
476
508
  }
477
509
 
510
+ async resolveAccountAgentAsync(
511
+ id: AccountID | AgentID,
512
+ expectation?: string
513
+ ): Promise<AgentID> {
514
+ if (isAgentID(id)) {
515
+ return id;
516
+ }
517
+
518
+ const coValue = await this.loadCoValueCore(id);
519
+
520
+ if (coValue === "unavailable") {
521
+ throw new Error(
522
+ `${
523
+ expectation ? expectation + ": " : ""
524
+ }Account ${id} is unavailable from all peers`
525
+ );
526
+ }
527
+
528
+ if (
529
+ coValue.header.type !== "comap" ||
530
+ coValue.header.ruleset.type !== "group" ||
531
+ !coValue.header.meta ||
532
+ !("type" in coValue.header.meta) ||
533
+ coValue.header.meta.type !== "account"
534
+ ) {
535
+ throw new Error(
536
+ `${
537
+ expectation ? expectation + ": " : ""
538
+ }CoValue ${id} is not an account`
539
+ );
540
+ }
541
+
542
+ return new Account(coValue).getCurrentAgentID();
543
+ }
544
+
478
545
  /**
479
546
  * @deprecated use Account.createGroup() instead
480
547
  */
@@ -543,7 +610,7 @@ export class LocalNode {
543
610
  const newCoValue = new CoValueCore(
544
611
  entry.coValue.header,
545
612
  newNode,
546
- { ...entry.coValue.sessions }
613
+ new Map(entry.coValue.sessionLogs)
547
614
  );
548
615
 
549
616
  newNode.coValues[coValueID as RawCoID] = {
@@ -575,17 +642,34 @@ export class LocalNode {
575
642
  type CoValueState =
576
643
  | {
577
644
  state: "loading";
578
- done: Promise<CoValueCore>;
579
- resolve: (coValue: CoValueCore) => void;
645
+ done: Promise<CoValueCore | "unavailable">;
646
+ resolve: (coValue: CoValueCore | "unavailable") => void;
580
647
  onProgress?: (progress: number) => void;
648
+ firstPeerState: {
649
+ [peerID: string]:
650
+ | {
651
+ type: "waiting";
652
+ done: Promise<void>;
653
+ resolve: () => void;
654
+ }
655
+ | { type: "available" }
656
+ | { type: "unavailable" };
657
+ };
581
658
  }
582
- | { state: "loaded"; coValue: CoValueCore; onProgress?: (progress: number) => void; };
659
+ | {
660
+ state: "loaded";
661
+ coValue: CoValueCore;
662
+ onProgress?: (progress: number) => void;
663
+ };
583
664
 
584
665
  /** @internal */
585
- export function newLoadingState(onProgress?: (progress: number) => void): CoValueState {
586
- let resolve: (coValue: CoValueCore) => void;
666
+ export function newLoadingState(
667
+ currentPeerIds: Set<PeerID>,
668
+ onProgress?: (progress: number) => void
669
+ ): CoValueState {
670
+ let resolve: (coValue: CoValueCore | "unavailable") => void;
587
671
 
588
- const promise = new Promise<CoValueCore>((r) => {
672
+ const promise = new Promise<CoValueCore | "unavailable">((r) => {
589
673
  resolve = r;
590
674
  });
591
675
 
@@ -593,6 +677,15 @@ export function newLoadingState(onProgress?: (progress: number) => void): CoValu
593
677
  state: "loading",
594
678
  done: promise,
595
679
  resolve: resolve!,
596
- onProgress
680
+ onProgress,
681
+ firstPeerState: Object.fromEntries(
682
+ [...currentPeerIds].map((id) => {
683
+ let resolve: () => void;
684
+ const done = new Promise<void>((r) => {
685
+ resolve = r;
686
+ });
687
+ return [id, { type: "waiting", done, resolve: resolve! }];
688
+ })
689
+ ),
597
690
  };
598
691
  }
@@ -2,10 +2,7 @@ import { CoID } from "./coValue.js";
2
2
  import { MapOpPayload } from "./coValues/coMap.js";
3
3
  import { JsonValue } from "./jsonValue.js";
4
4
  import { KeyID } from "./crypto.js";
5
- import {
6
- CoValueCore,
7
- Transaction,
8
- } from "./coValueCore.js";
5
+ import { CoValueCore, Transaction } from "./coValueCore.js";
9
6
  import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
10
7
  import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
11
8
  import { Account, AccountID, Profile } from "./coValues/account.js";
@@ -31,19 +28,19 @@ export function determineValidTransactions(
31
28
  coValue: CoValueCore
32
29
  ): { txID: TransactionID; tx: Transaction }[] {
33
30
  if (coValue.header.ruleset.type === "group") {
34
- const allTransactionsSorted = Object.entries(coValue.sessions).flatMap(
35
- ([sessionID, sessionLog]) => {
36
- return sessionLog.transactions.map((tx, txIndex) => ({
37
- sessionID,
38
- txIndex,
39
- tx,
40
- })) as {
41
- sessionID: SessionID;
42
- txIndex: number;
43
- tx: Transaction;
44
- }[];
45
- }
46
- );
31
+ const allTransactionsSorted = [
32
+ ...coValue.sessionLogs.entries(),
33
+ ].flatMap(([sessionID, sessionLog]) => {
34
+ return sessionLog.transactions.map((tx, txIndex) => ({
35
+ sessionID,
36
+ txIndex,
37
+ tx,
38
+ })) as {
39
+ sessionID: SessionID;
40
+ txIndex: number;
41
+ tx: Transaction;
42
+ }[];
43
+ });
47
44
 
48
45
  allTransactionsSorted.sort((a, b) => {
49
46
  return a.tx.madeAt - b.tx.madeAt;
@@ -242,11 +239,9 @@ export function determineValidTransactions(
242
239
  throw new Error("Group must be a map");
243
240
  }
244
241
 
245
- return Object.entries(coValue.sessions).flatMap(
242
+ return [...coValue.sessionLogs.entries()].flatMap(
246
243
  ([sessionID, sessionLog]) => {
247
- const transactor = accountOrAgentIDfromSessionID(
248
- sessionID as SessionID
249
- );
244
+ const transactor = accountOrAgentIDfromSessionID(sessionID);
250
245
 
251
246
  return sessionLog.transactions
252
247
  .filter((tx) => {
@@ -266,16 +261,16 @@ export function determineValidTransactions(
266
261
  );
267
262
  })
268
263
  .map((tx, txIndex) => ({
269
- txID: { sessionID: sessionID as SessionID, txIndex },
264
+ txID: { sessionID: sessionID, txIndex },
270
265
  tx,
271
266
  }));
272
267
  }
273
268
  );
274
269
  } else if (coValue.header.ruleset.type === "unsafeAllowAll") {
275
- return Object.entries(coValue.sessions).flatMap(
270
+ return [...coValue.sessionLogs.entries()].flatMap(
276
271
  ([sessionID, sessionLog]) => {
277
272
  return sessionLog.transactions.map((tx, txIndex) => ({
278
- txID: { sessionID: sessionID as SessionID, txIndex },
273
+ txID: { sessionID: sessionID, txIndex },
279
274
  tx,
280
275
  }));
281
276
  }
@@ -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");