cojson 0.4.13 → 0.5.1

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 {
@@ -109,12 +109,26 @@ export class LocalNode {
109
109
 
110
110
  if (migration) {
111
111
  migration(accountOnNodeWithAccount, profile as P);
112
- nodeWithAccount.account = new ControlledAccount(
113
- accountOnNodeWithAccount.core,
114
- accountOnNodeWithAccount.agentSecret
115
- );
116
112
  }
117
113
 
114
+ nodeWithAccount.account = new ControlledAccount(
115
+ accountOnNodeWithAccount.core,
116
+ accountOnNodeWithAccount.agentSecret
117
+ );
118
+
119
+ // we shouldn't need this, but it fixes account data not syncing for new accounts
120
+ function syncAllCoValuesAfterCreateAccount() {
121
+ for (const coValueEntry of Object.values(nodeWithAccount.coValues)) {
122
+ if (coValueEntry.state === "loaded") {
123
+ void nodeWithAccount.syncManager.syncCoValue(coValueEntry.coValue);
124
+ }
125
+ }
126
+ }
127
+
128
+ syncAllCoValuesAfterCreateAccount();
129
+
130
+ setTimeout(syncAllCoValuesAfterCreateAccount, 500);
131
+
118
132
  return {
119
133
  node: nodeWithAccount,
120
134
  accountID: accountOnNodeWithAccount.id,
@@ -153,6 +167,11 @@ export class LocalNode {
153
167
  }
154
168
 
155
169
  const account = await accountPromise;
170
+
171
+ if (account === "unavailable") {
172
+ throw new Error("Account unavailable from all peers");
173
+ }
174
+
156
175
  const controlledAccount = new ControlledAccount(
157
176
  account.core,
158
177
  accountSecret
@@ -199,14 +218,36 @@ export class LocalNode {
199
218
  }
200
219
 
201
220
  /** @internal */
202
- loadCoValue(id: RawCoID, onProgress?: (progress: number) => void): Promise<CoValueCore> {
221
+ async loadCoValueCore(
222
+ id: RawCoID,
223
+ options: {
224
+ dontLoadFrom?: PeerID;
225
+ dontWaitFor?: PeerID;
226
+ onProgress?: (progress: number) => void;
227
+ } = {}
228
+ ): Promise<CoValueCore | "unavailable"> {
203
229
  let entry = this.coValues[id];
204
230
  if (!entry) {
205
- entry = newLoadingState(onProgress);
231
+ const peersToWaitFor = new Set(
232
+ Object.values(this.syncManager.peers)
233
+ .filter((peer) => peer.role === "server")
234
+ .map((peer) => peer.id)
235
+ );
236
+ if (options.dontWaitFor) peersToWaitFor.delete(options.dontWaitFor);
237
+ entry = newLoadingState(peersToWaitFor, options.onProgress);
206
238
 
207
239
  this.coValues[id] = entry;
208
240
 
209
- this.syncManager.loadFromPeers(id);
241
+ this.syncManager
242
+ .loadFromPeers(id, options.dontLoadFrom)
243
+ .catch((e) => {
244
+ console.error(
245
+ "Error loading from peers",
246
+ id,
247
+
248
+ e
249
+ );
250
+ });
210
251
  }
211
252
  if (entry.state === "loaded") {
212
253
  return Promise.resolve(entry.coValue);
@@ -221,25 +262,38 @@ export class LocalNode {
221
262
  *
222
263
  * @category 3. Low-level
223
264
  */
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;
265
+ async load<T extends CoValue>(
266
+ id: CoID<T>,
267
+ onProgress?: (progress: number) => void
268
+ ): Promise<T | "unavailable"> {
269
+ const core = await this.loadCoValueCore(id, { onProgress });
270
+
271
+ if (core === "unavailable") {
272
+ return "unavailable";
273
+ }
274
+
275
+ return core.getCurrentContent() as T;
226
276
  }
227
277
 
228
278
  /** @category 3. Low-level */
229
279
  subscribe<T extends CoValue>(
230
280
  id: CoID<T>,
231
- callback: (update: T) => void
281
+ callback: (update: T | "unavailable") => void
232
282
  ): () => void {
233
283
  let stopped = false;
234
284
  let unsubscribe!: () => void;
235
285
 
236
- console.log("Subscribing to " + id);
286
+ // console.log("Subscribing to " + id);
237
287
 
238
288
  this.load(id)
239
289
  .then((coValue) => {
240
290
  if (stopped) {
241
291
  return;
242
292
  }
293
+ if (coValue === "unavailable") {
294
+ callback("unavailable");
295
+ return;
296
+ }
243
297
  unsubscribe = coValue.subscribe(callback);
244
298
  })
245
299
  .catch((e) => {
@@ -260,6 +314,12 @@ export class LocalNode {
260
314
  ): Promise<void> {
261
315
  const groupOrOwnedValue = await this.load(groupOrOwnedValueID);
262
316
 
317
+ if (groupOrOwnedValue === "unavailable") {
318
+ throw new Error(
319
+ "Trying to accept invite: Group/owned value unavailable from all peers"
320
+ );
321
+ }
322
+
263
323
  if (groupOrOwnedValue.core.header.ruleset.type === "ownedByGroup") {
264
324
  return this.acceptInvite(
265
325
  groupOrOwnedValue.core.header.ruleset.group as CoID<Group>,
@@ -325,7 +385,7 @@ export class LocalNode {
325
385
  : "reader"
326
386
  );
327
387
 
328
- group.core._sessions = groupAsInvite.core.sessions;
388
+ group.core._sessionLogs = groupAsInvite.core.sessionLogs;
329
389
  group.core._cachedContent = undefined;
330
390
 
331
391
  for (const groupListener of group.core.listeners) {
@@ -400,17 +460,6 @@ export class LocalNode {
400
460
  },
401
461
  });
402
462
 
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
463
  editable.set(
415
464
  `${readKey.id}_for_${accountAgentID}`,
416
465
  sealed,
@@ -432,16 +481,13 @@ export class LocalNode {
432
481
 
433
482
  const accountOnThisNode = this.expectCoValueLoaded(account.id);
434
483
 
435
- accountOnThisNode._sessions = {
436
- ...account.core.sessions,
437
- };
484
+ accountOnThisNode._sessionLogs = new Map(account.core.sessionLogs);
485
+
438
486
  accountOnThisNode._cachedContent = undefined;
439
487
 
440
488
  const profileOnThisNode = this.createCoValue(profile.core.header);
441
489
 
442
- profileOnThisNode._sessions = {
443
- ...profile.core.sessions,
444
- };
490
+ profileOnThisNode._sessionLogs = new Map(profile.core.sessionLogs);
445
491
  profileOnThisNode._cachedContent = undefined;
446
492
 
447
493
  return new ControlledAccount(accountOnThisNode, agentSecret);
@@ -475,6 +521,41 @@ export class LocalNode {
475
521
  return new Account(coValue).getCurrentAgentID();
476
522
  }
477
523
 
524
+ async resolveAccountAgentAsync(
525
+ id: AccountID | AgentID,
526
+ expectation?: string
527
+ ): Promise<AgentID> {
528
+ if (isAgentID(id)) {
529
+ return id;
530
+ }
531
+
532
+ const coValue = await this.loadCoValueCore(id);
533
+
534
+ if (coValue === "unavailable") {
535
+ throw new Error(
536
+ `${
537
+ expectation ? expectation + ": " : ""
538
+ }Account ${id} is unavailable from all peers`
539
+ );
540
+ }
541
+
542
+ if (
543
+ coValue.header.type !== "comap" ||
544
+ coValue.header.ruleset.type !== "group" ||
545
+ !coValue.header.meta ||
546
+ !("type" in coValue.header.meta) ||
547
+ coValue.header.meta.type !== "account"
548
+ ) {
549
+ throw new Error(
550
+ `${
551
+ expectation ? expectation + ": " : ""
552
+ }CoValue ${id} is not an account`
553
+ );
554
+ }
555
+
556
+ return new Account(coValue).getCurrentAgentID();
557
+ }
558
+
478
559
  /**
479
560
  * @deprecated use Account.createGroup() instead
480
561
  */
@@ -543,7 +624,7 @@ export class LocalNode {
543
624
  const newCoValue = new CoValueCore(
544
625
  entry.coValue.header,
545
626
  newNode,
546
- { ...entry.coValue.sessions }
627
+ new Map(entry.coValue.sessionLogs)
547
628
  );
548
629
 
549
630
  newNode.coValues[coValueID as RawCoID] = {
@@ -575,17 +656,34 @@ export class LocalNode {
575
656
  type CoValueState =
576
657
  | {
577
658
  state: "loading";
578
- done: Promise<CoValueCore>;
579
- resolve: (coValue: CoValueCore) => void;
659
+ done: Promise<CoValueCore | "unavailable">;
660
+ resolve: (coValue: CoValueCore | "unavailable") => void;
580
661
  onProgress?: (progress: number) => void;
662
+ firstPeerState: {
663
+ [peerID: string]:
664
+ | {
665
+ type: "waiting";
666
+ done: Promise<void>;
667
+ resolve: () => void;
668
+ }
669
+ | { type: "available" }
670
+ | { type: "unavailable" };
671
+ };
581
672
  }
582
- | { state: "loaded"; coValue: CoValueCore; onProgress?: (progress: number) => void; };
673
+ | {
674
+ state: "loaded";
675
+ coValue: CoValueCore;
676
+ onProgress?: (progress: number) => void;
677
+ };
583
678
 
584
679
  /** @internal */
585
- export function newLoadingState(onProgress?: (progress: number) => void): CoValueState {
586
- let resolve: (coValue: CoValueCore) => void;
680
+ export function newLoadingState(
681
+ currentPeerIds: Set<PeerID>,
682
+ onProgress?: (progress: number) => void
683
+ ): CoValueState {
684
+ let resolve: (coValue: CoValueCore | "unavailable") => void;
587
685
 
588
- const promise = new Promise<CoValueCore>((r) => {
686
+ const promise = new Promise<CoValueCore | "unavailable">((r) => {
589
687
  resolve = r;
590
688
  });
591
689
 
@@ -593,6 +691,15 @@ export function newLoadingState(onProgress?: (progress: number) => void): CoValu
593
691
  state: "loading",
594
692
  done: promise,
595
693
  resolve: resolve!,
596
- onProgress
694
+ onProgress,
695
+ firstPeerState: Object.fromEntries(
696
+ [...currentPeerIds].map((id) => {
697
+ let resolve: () => void;
698
+ const done = new Promise<void>((r) => {
699
+ resolve = r;
700
+ });
701
+ return [id, { type: "waiting", done, resolve: resolve! }];
702
+ })
703
+ ),
597
704
  };
598
705
  }
@@ -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");