@peerbit/stream 5.0.0 → 5.0.1-3dcfc85
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/dist/src/index.d.ts +46 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +248 -46
- package/dist/src/index.js.map +1 -1
- package/dist/src/pushable-lanes.d.ts +11 -0
- package/dist/src/pushable-lanes.d.ts.map +1 -1
- package/dist/src/pushable-lanes.js +73 -3
- package/dist/src/pushable-lanes.js.map +1 -1
- package/package.json +10 -9
- package/src/index.ts +414 -82
- package/src/pushable-lanes.ts +98 -3
package/src/index.ts
CHANGED
|
@@ -48,10 +48,12 @@ import {
|
|
|
48
48
|
} from "@peerbit/stream-interface";
|
|
49
49
|
import type {
|
|
50
50
|
DirectStreamAckRouteHint,
|
|
51
|
+
ExpiresAtOptions,
|
|
51
52
|
IdOptions,
|
|
52
53
|
PeerRefs,
|
|
53
54
|
PriorityOptions,
|
|
54
55
|
PublicKeyFromHashResolver,
|
|
56
|
+
ResponsePriorityOptions,
|
|
55
57
|
StreamEvents,
|
|
56
58
|
WaitForAnyOpts,
|
|
57
59
|
WaitForBaseOpts,
|
|
@@ -144,6 +146,7 @@ export interface PeerStreamsInit {
|
|
|
144
146
|
publicKey: PublicSignKey;
|
|
145
147
|
protocol: string;
|
|
146
148
|
connId: string;
|
|
149
|
+
outboundQueue?: PeerOutboundQueueOptions;
|
|
147
150
|
}
|
|
148
151
|
const DEFAULT_SEEK_MESSAGE_REDUDANCY = 2;
|
|
149
152
|
const DEFAULT_SILENT_MESSAGE_REDUDANCY = 1;
|
|
@@ -156,6 +159,7 @@ const isWebsocketConnection = (c: Connection) =>
|
|
|
156
159
|
export interface PeerStreamEvents {
|
|
157
160
|
"stream:inbound": CustomEvent<never>;
|
|
158
161
|
"stream:outbound": CustomEvent<never>;
|
|
162
|
+
"queue:outbound": CustomEvent<never>;
|
|
159
163
|
close: CustomEvent<never>;
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -165,6 +169,8 @@ const MAX_DATA_LENGTH_IN = 15e6 + 1000; // 15 mb and some metadata
|
|
|
165
169
|
const MAX_DATA_LENGTH_OUT = 1e7 + 1000; // 10 mb and some metadata
|
|
166
170
|
|
|
167
171
|
const MAX_QUEUED_BYTES = MAX_DATA_LENGTH_IN * 50;
|
|
172
|
+
const DEFAULT_OUTBOUND_QUEUE_MAX_BYTES = MAX_DATA_LENGTH_OUT * 2;
|
|
173
|
+
const DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES = 1024 * 1024;
|
|
168
174
|
|
|
169
175
|
const DEFAULT_PRUNE_CONNECTIONS_INTERVAL = 2e4;
|
|
170
176
|
const DEFAULT_MIN_CONNECTIONS = 2;
|
|
@@ -186,6 +192,16 @@ const getLaneFromPriority = (priority: number) => {
|
|
|
186
192
|
const clampedPriority = Math.max(0, Math.min(maxLane, Math.floor(priority)));
|
|
187
193
|
return maxLane - clampedPriority;
|
|
188
194
|
};
|
|
195
|
+
type OutboundQueueOptions = {
|
|
196
|
+
maxBufferedBytes: number;
|
|
197
|
+
reservedPriorityBytes: number;
|
|
198
|
+
maxTotalBufferedBytes: number;
|
|
199
|
+
reservedTotalPriorityBytes: number;
|
|
200
|
+
};
|
|
201
|
+
type PeerOutboundQueueOptions = Pick<
|
|
202
|
+
OutboundQueueOptions,
|
|
203
|
+
"maxBufferedBytes" | "reservedPriorityBytes"
|
|
204
|
+
>;
|
|
189
205
|
interface OutboundCandidate {
|
|
190
206
|
raw: Stream;
|
|
191
207
|
pushable: PushableLanes<Uint8Array>;
|
|
@@ -202,6 +218,36 @@ export interface InboundStreamRecord {
|
|
|
202
218
|
lastActivity: number;
|
|
203
219
|
bytesReceived: number;
|
|
204
220
|
}
|
|
221
|
+
|
|
222
|
+
export class BackpressureError extends Error {
|
|
223
|
+
readonly scope: "peer" | "node";
|
|
224
|
+
readonly peerId: PeerId;
|
|
225
|
+
readonly priority: number;
|
|
226
|
+
readonly limitBytes: number;
|
|
227
|
+
readonly currentBufferedBytes: number;
|
|
228
|
+
readonly attemptedBytes: number;
|
|
229
|
+
|
|
230
|
+
constructor(options: {
|
|
231
|
+
scope: "peer" | "node";
|
|
232
|
+
peerId: PeerId;
|
|
233
|
+
priority: number;
|
|
234
|
+
limitBytes: number;
|
|
235
|
+
currentBufferedBytes: number;
|
|
236
|
+
attemptedBytes: number;
|
|
237
|
+
}) {
|
|
238
|
+
super(
|
|
239
|
+
`Outbound ${options.scope} queue full for ${options.peerId.toString()} on priority ${options.priority}: ` +
|
|
240
|
+
`${options.currentBufferedBytes} buffered + ${options.attemptedBytes} attempted > ${options.limitBytes} limit`,
|
|
241
|
+
);
|
|
242
|
+
this.name = "BackpressureError";
|
|
243
|
+
this.scope = options.scope;
|
|
244
|
+
this.peerId = options.peerId;
|
|
245
|
+
this.priority = options.priority;
|
|
246
|
+
this.limitBytes = options.limitBytes;
|
|
247
|
+
this.currentBufferedBytes = options.currentBufferedBytes;
|
|
248
|
+
this.attemptedBytes = options.attemptedBytes;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
205
251
|
// Hook for tests to override queued length measurement (peerStreams, default impl)
|
|
206
252
|
export let measureOutboundQueuedBytes: (
|
|
207
253
|
ps: PeerStreams, // return queued bytes for active outbound (all lanes) or 0 if none
|
|
@@ -254,6 +300,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
254
300
|
public seekedOnce: boolean;
|
|
255
301
|
|
|
256
302
|
private usedBandWidthTracker: BandwidthTracker;
|
|
303
|
+
private readonly outboundQueue?: PeerOutboundQueueOptions;
|
|
257
304
|
|
|
258
305
|
// Unified outbound streams list (during grace may contain >1; after pruning length==1)
|
|
259
306
|
private outboundStreams: OutboundCandidate[] = [];
|
|
@@ -306,6 +353,11 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
306
353
|
if (existing) return existing;
|
|
307
354
|
const pushableInst = pushableLanes<Uint8Array>({
|
|
308
355
|
lanes: PRIORITY_LANES,
|
|
356
|
+
maxBufferedBytes: this.outboundQueue?.maxBufferedBytes,
|
|
357
|
+
overflow: "throw",
|
|
358
|
+
onBufferSize: () => {
|
|
359
|
+
this.dispatchEvent(new CustomEvent("queue:outbound"));
|
|
360
|
+
},
|
|
309
361
|
onPush: (val: Uint8Array) => {
|
|
310
362
|
candidate.bytesDelivered += val.length || val.byteLength || 0;
|
|
311
363
|
},
|
|
@@ -397,6 +449,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
397
449
|
this.connId = init.connId;
|
|
398
450
|
this.usedBandWidthTracker = new BandwidthTracker(10);
|
|
399
451
|
this.usedBandWidthTracker.start();
|
|
452
|
+
this.outboundQueue = init.outboundQueue;
|
|
400
453
|
}
|
|
401
454
|
|
|
402
455
|
/**
|
|
@@ -417,6 +470,65 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
417
470
|
return this.usedBandWidthTracker.value;
|
|
418
471
|
}
|
|
419
472
|
|
|
473
|
+
private getQueueAdmissionLimitBytes(lane: number): number | undefined {
|
|
474
|
+
if (!this.outboundQueue) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
if (lane === getLaneFromPriority(0)) {
|
|
478
|
+
return Math.max(
|
|
479
|
+
0,
|
|
480
|
+
this.outboundQueue.maxBufferedBytes -
|
|
481
|
+
this.outboundQueue.reservedPriorityBytes,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
return this.outboundQueue.maxBufferedBytes;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private assertQueueCapacity(
|
|
488
|
+
candidate: OutboundCandidate,
|
|
489
|
+
payloadBytes: number,
|
|
490
|
+
priority: number,
|
|
491
|
+
) {
|
|
492
|
+
const lane = getLaneFromPriority(priority);
|
|
493
|
+
const limitBytes = this.getQueueAdmissionLimitBytes(lane);
|
|
494
|
+
if (limitBytes == null) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const currentBufferedBytes = candidate.pushable.getReadableLength();
|
|
498
|
+
if (currentBufferedBytes + payloadBytes > limitBytes) {
|
|
499
|
+
throw new BackpressureError({
|
|
500
|
+
scope: "peer",
|
|
501
|
+
peerId: this.peerId,
|
|
502
|
+
priority,
|
|
503
|
+
limitBytes,
|
|
504
|
+
currentBufferedBytes,
|
|
505
|
+
attemptedBytes: payloadBytes,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private async waitForQueueCapacity(
|
|
511
|
+
payloadBytes: number,
|
|
512
|
+
priority: number,
|
|
513
|
+
signal?: AbortSignal,
|
|
514
|
+
) {
|
|
515
|
+
const lane = getLaneFromPriority(priority);
|
|
516
|
+
const limitBytes = this.getQueueAdmissionLimitBytes(lane);
|
|
517
|
+
if (limitBytes == null) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const threshold = Math.max(0, limitBytes - payloadBytes);
|
|
521
|
+
const waiters = this.outboundStreams
|
|
522
|
+
.filter((candidate) => !candidate.aborted)
|
|
523
|
+
.map((candidate) =>
|
|
524
|
+
candidate.pushable.onBufferedBelow(threshold, { signal }),
|
|
525
|
+
);
|
|
526
|
+
if (waiters.length === 0) {
|
|
527
|
+
throw new Error("No writable connection to " + this.peerId.toString());
|
|
528
|
+
}
|
|
529
|
+
await Promise.race(waiters);
|
|
530
|
+
}
|
|
531
|
+
|
|
420
532
|
/**
|
|
421
533
|
* Send a message to this peer.
|
|
422
534
|
* Throws if there is no `stream` to write to available.
|
|
@@ -434,8 +546,6 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
434
546
|
throw new Error("No writable connection to " + this.peerId.toString());
|
|
435
547
|
}
|
|
436
548
|
|
|
437
|
-
this.usedBandWidthTracker.add(data.byteLength);
|
|
438
|
-
|
|
439
549
|
// Write to all current outbound streams (normally 1, but >1 during grace)
|
|
440
550
|
const payload = data instanceof Uint8Array ? data : data.subarray();
|
|
441
551
|
let successes = 0;
|
|
@@ -449,6 +559,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
449
559
|
}
|
|
450
560
|
|
|
451
561
|
try {
|
|
562
|
+
this.assertQueueCapacity(c, payload.byteLength, priority);
|
|
452
563
|
c.pushable.push(payload, getLaneFromPriority(priority));
|
|
453
564
|
successes++;
|
|
454
565
|
} catch (e) {
|
|
@@ -458,6 +569,12 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
458
569
|
}
|
|
459
570
|
}
|
|
460
571
|
if (successes === 0) {
|
|
572
|
+
const backpressureFailures = failures.filter(
|
|
573
|
+
(f) => f instanceof BackpressureError,
|
|
574
|
+
);
|
|
575
|
+
if (backpressureFailures.length === failures.length) {
|
|
576
|
+
throw backpressureFailures[0];
|
|
577
|
+
}
|
|
461
578
|
throw new Error(
|
|
462
579
|
"All outbound writes failed (" +
|
|
463
580
|
failures.map((f) => f?.message).join(", ") +
|
|
@@ -489,6 +606,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
489
606
|
else this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
490
607
|
}
|
|
491
608
|
}
|
|
609
|
+
this.usedBandWidthTracker.add(payload.byteLength);
|
|
492
610
|
}
|
|
493
611
|
|
|
494
612
|
/**
|
|
@@ -506,54 +624,63 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
506
624
|
return;
|
|
507
625
|
}
|
|
508
626
|
|
|
509
|
-
|
|
510
|
-
this.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
627
|
+
while (true) {
|
|
628
|
+
if (!this.isWritable) {
|
|
629
|
+
// Outbound stream negotiation can legitimately take several seconds in CI
|
|
630
|
+
// (identify/protocol discovery, resource contention, etc). Keep this fairly
|
|
631
|
+
// generous so control-plane messages (joins/subscriptions) don't flap.
|
|
632
|
+
const timeoutMs = 10_000;
|
|
633
|
+
|
|
634
|
+
await new Promise<void>((resolve, reject) => {
|
|
635
|
+
const onOutbound = () => {
|
|
636
|
+
cleanup();
|
|
637
|
+
resolve();
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const onAbortOrClose = () => {
|
|
641
|
+
cleanup();
|
|
642
|
+
reject(new AbortError("Closed"));
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const onTimeout = () => {
|
|
646
|
+
cleanup();
|
|
647
|
+
reject(
|
|
648
|
+
new TimeoutError("Failed to deliver message, never reachable"),
|
|
649
|
+
);
|
|
650
|
+
};
|
|
518
651
|
|
|
519
|
-
|
|
520
|
-
const onOutbound = () => {
|
|
521
|
-
cleanup();
|
|
522
|
-
resolve();
|
|
523
|
-
};
|
|
652
|
+
const timerId = setTimeout(onTimeout, timeoutMs);
|
|
524
653
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
cleanup();
|
|
532
|
-
reject(new TimeoutError("Failed to deliver message, never reachable"));
|
|
533
|
-
};
|
|
654
|
+
const cleanup = () => {
|
|
655
|
+
clearTimeout(timerId);
|
|
656
|
+
this.removeEventListener("stream:outbound", onOutbound);
|
|
657
|
+
this.removeEventListener("close", onAbortOrClose);
|
|
658
|
+
signal?.removeEventListener("abort", onAbortOrClose);
|
|
659
|
+
};
|
|
534
660
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
};
|
|
661
|
+
this.addEventListener("stream:outbound", onOutbound, { once: true });
|
|
662
|
+
this.addEventListener("close", onAbortOrClose, { once: true });
|
|
663
|
+
if (signal?.aborted) {
|
|
664
|
+
onAbortOrClose();
|
|
665
|
+
} else {
|
|
666
|
+
signal?.addEventListener("abort", onAbortOrClose, { once: true });
|
|
667
|
+
}
|
|
543
668
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
onAbortOrClose();
|
|
548
|
-
} else {
|
|
549
|
-
signal?.addEventListener("abort", onAbortOrClose, { once: true });
|
|
669
|
+
// Catch a race where writability flips after the first check.
|
|
670
|
+
if (this.isWritable) onOutbound();
|
|
671
|
+
});
|
|
550
672
|
}
|
|
551
673
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
674
|
+
try {
|
|
675
|
+
this.write(bytes, priority);
|
|
676
|
+
return;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (!(error instanceof BackpressureError)) {
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
await this.waitForQueueCapacity(bytes.byteLength, priority, signal);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
557
684
|
}
|
|
558
685
|
|
|
559
686
|
/**
|
|
@@ -858,6 +985,15 @@ export type ConnectionManagerArguments =
|
|
|
858
985
|
} & { dialer?: Partial<DialerOptions> | false })
|
|
859
986
|
| false;
|
|
860
987
|
|
|
988
|
+
type OutboundQueueArguments =
|
|
989
|
+
| {
|
|
990
|
+
maxBufferedBytes?: number;
|
|
991
|
+
reservedPriorityBytes?: number;
|
|
992
|
+
maxTotalBufferedBytes?: number;
|
|
993
|
+
reservedTotalPriorityBytes?: number;
|
|
994
|
+
}
|
|
995
|
+
| false;
|
|
996
|
+
|
|
861
997
|
export type DirectStreamOptions = {
|
|
862
998
|
canRelayMessage?: boolean;
|
|
863
999
|
messageProcessingConcurrency?: number;
|
|
@@ -886,6 +1022,7 @@ export type DirectStreamOptions = {
|
|
|
886
1022
|
sharedRouting?: boolean;
|
|
887
1023
|
seenCacheMax?: number;
|
|
888
1024
|
seenCacheTtlMs?: number;
|
|
1025
|
+
outboundQueue?: OutboundQueueArguments;
|
|
889
1026
|
};
|
|
890
1027
|
|
|
891
1028
|
type ConnectionManagerLike = {
|
|
@@ -926,6 +1063,8 @@ const sharedRoutingByPrivateKey = new WeakMap<PrivateKey, SharedRoutingState>();
|
|
|
926
1063
|
|
|
927
1064
|
export type PublishOptions = (WithMode | WithTo) &
|
|
928
1065
|
PriorityOptions &
|
|
1066
|
+
ResponsePriorityOptions &
|
|
1067
|
+
ExpiresAtOptions &
|
|
929
1068
|
WithExtraSigners;
|
|
930
1069
|
|
|
931
1070
|
export abstract class DirectStream<
|
|
@@ -975,6 +1114,11 @@ export abstract class DirectStream<
|
|
|
975
1114
|
private routeCacheMaxTargetsPerFrom?: number;
|
|
976
1115
|
private routeCacheMaxRelaysPerTarget?: number;
|
|
977
1116
|
private readonly sharedRouting: boolean;
|
|
1117
|
+
private readonly outboundQueueOptions?: OutboundQueueOptions;
|
|
1118
|
+
private readonly totalOutboundQueueWaiters: Set<{
|
|
1119
|
+
limitBytes: number;
|
|
1120
|
+
deferred: DeferredPromise<void>;
|
|
1121
|
+
}> = new Set();
|
|
978
1122
|
private sharedRoutingKey?: PrivateKey;
|
|
979
1123
|
private sharedRoutingState?: SharedRoutingState;
|
|
980
1124
|
|
|
@@ -1023,6 +1167,7 @@ export abstract class DirectStream<
|
|
|
1023
1167
|
seenCacheMax = 1e6,
|
|
1024
1168
|
seenCacheTtlMs = 10 * 60 * 1e3,
|
|
1025
1169
|
inboundIdleTimeout,
|
|
1170
|
+
outboundQueue,
|
|
1026
1171
|
} = options || {};
|
|
1027
1172
|
|
|
1028
1173
|
const signKey = getKeypairFromPrivateKey(components.privateKey);
|
|
@@ -1088,6 +1233,50 @@ export abstract class DirectStream<
|
|
|
1088
1233
|
: undefined,
|
|
1089
1234
|
};
|
|
1090
1235
|
}
|
|
1236
|
+
if (outboundQueue === false) {
|
|
1237
|
+
this.outboundQueueOptions = undefined;
|
|
1238
|
+
} else {
|
|
1239
|
+
const maxBufferedBytes = Math.max(
|
|
1240
|
+
MAX_DATA_LENGTH_OUT,
|
|
1241
|
+
Math.floor(
|
|
1242
|
+
outboundQueue?.maxBufferedBytes ?? DEFAULT_OUTBOUND_QUEUE_MAX_BYTES,
|
|
1243
|
+
),
|
|
1244
|
+
);
|
|
1245
|
+
const reservedPriorityBytes = Math.max(
|
|
1246
|
+
0,
|
|
1247
|
+
Math.min(
|
|
1248
|
+
maxBufferedBytes,
|
|
1249
|
+
Math.floor(
|
|
1250
|
+
outboundQueue?.reservedPriorityBytes ??
|
|
1251
|
+
DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES,
|
|
1252
|
+
),
|
|
1253
|
+
),
|
|
1254
|
+
);
|
|
1255
|
+
const maxTotalBufferedBytes = Math.max(
|
|
1256
|
+
maxBufferedBytes,
|
|
1257
|
+
Math.floor(
|
|
1258
|
+
outboundQueue?.maxTotalBufferedBytes ??
|
|
1259
|
+
this.connectionManagerOptions.pruner?.maxBuffer ??
|
|
1260
|
+
MAX_QUEUED_BYTES,
|
|
1261
|
+
),
|
|
1262
|
+
);
|
|
1263
|
+
const reservedTotalPriorityBytes = Math.max(
|
|
1264
|
+
0,
|
|
1265
|
+
Math.min(
|
|
1266
|
+
maxTotalBufferedBytes,
|
|
1267
|
+
Math.floor(
|
|
1268
|
+
outboundQueue?.reservedTotalPriorityBytes ??
|
|
1269
|
+
DEFAULT_OUTBOUND_QUEUE_RESERVED_PRIORITY_BYTES,
|
|
1270
|
+
),
|
|
1271
|
+
),
|
|
1272
|
+
);
|
|
1273
|
+
this.outboundQueueOptions = {
|
|
1274
|
+
maxBufferedBytes,
|
|
1275
|
+
reservedPriorityBytes,
|
|
1276
|
+
maxTotalBufferedBytes,
|
|
1277
|
+
reservedTotalPriorityBytes,
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1091
1280
|
|
|
1092
1281
|
this.recentDials = this.connectionManagerOptions.dialer
|
|
1093
1282
|
? new Cache({
|
|
@@ -1802,30 +1991,35 @@ export abstract class DirectStream<
|
|
|
1802
1991
|
publicKey,
|
|
1803
1992
|
protocol,
|
|
1804
1993
|
connId,
|
|
1994
|
+
outboundQueue: this.outboundQueueOptions,
|
|
1805
1995
|
});
|
|
1806
1996
|
|
|
1807
1997
|
this.peers.set(publicKeyHash, peerStreams);
|
|
1808
1998
|
this.updateSession(publicKey, -1);
|
|
1809
1999
|
|
|
1810
2000
|
// Propagate per-peer stream readiness events to the parent emitter
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2001
|
+
const forwardOutbound = () =>
|
|
2002
|
+
this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
2003
|
+
const forwardInbound = () =>
|
|
2004
|
+
this.dispatchEvent(new CustomEvent("stream:inbound"));
|
|
2005
|
+
const forwardQueue = () => this.notifyTotalOutboundQueueWaiters();
|
|
2006
|
+
peerStreams.addEventListener("stream:outbound", forwardOutbound);
|
|
2007
|
+
peerStreams.addEventListener("stream:inbound", forwardInbound);
|
|
2008
|
+
peerStreams.addEventListener("queue:outbound", forwardQueue);
|
|
2009
|
+
|
|
2010
|
+
peerStreams.addEventListener("close", () => this._removePeer(publicKey), {
|
|
2011
|
+
once: true,
|
|
1820
2012
|
});
|
|
1821
2013
|
peerStreams.addEventListener(
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2014
|
+
"close",
|
|
2015
|
+
() => {
|
|
2016
|
+
peerStreams.removeEventListener("stream:outbound", forwardOutbound);
|
|
2017
|
+
peerStreams.removeEventListener("stream:inbound", forwardInbound);
|
|
2018
|
+
peerStreams.removeEventListener("queue:outbound", forwardQueue);
|
|
2019
|
+
this.notifyTotalOutboundQueueWaiters();
|
|
2020
|
+
},
|
|
2021
|
+
{ once: true },
|
|
2022
|
+
);
|
|
1829
2023
|
|
|
1830
2024
|
this.addRouteConnection(
|
|
1831
2025
|
this.publicKeyHash,
|
|
@@ -2228,7 +2422,6 @@ export abstract class DirectStream<
|
|
|
2228
2422
|
}
|
|
2229
2423
|
}
|
|
2230
2424
|
}
|
|
2231
|
-
|
|
2232
2425
|
async onGoodBye(
|
|
2233
2426
|
publicKey: PublicSignKey,
|
|
2234
2427
|
peerStream: PeerStreams,
|
|
@@ -2328,6 +2521,8 @@ export abstract class DirectStream<
|
|
|
2328
2521
|
data: Uint8Array | Uint8ArrayList | undefined,
|
|
2329
2522
|
options: (WithTo | WithMode) &
|
|
2330
2523
|
PriorityOptions &
|
|
2524
|
+
ResponsePriorityOptions &
|
|
2525
|
+
ExpiresAtOptions &
|
|
2331
2526
|
IdOptions & { skipRecipientValidation?: boolean } & WithExtraSigners,
|
|
2332
2527
|
) {
|
|
2333
2528
|
// dispatch the event if we are interested
|
|
@@ -2377,6 +2572,8 @@ export abstract class DirectStream<
|
|
|
2377
2572
|
mode,
|
|
2378
2573
|
session: this.session,
|
|
2379
2574
|
priority: options.priority,
|
|
2575
|
+
responsePriority: options.responsePriority,
|
|
2576
|
+
expires: options.expiresAt,
|
|
2380
2577
|
}),
|
|
2381
2578
|
});
|
|
2382
2579
|
|
|
@@ -2827,14 +3024,24 @@ export abstract class DirectStream<
|
|
|
2827
3024
|
for (const [neighbour, _distantPeers] of fanout) {
|
|
2828
3025
|
const stream = this.peers.get(neighbour);
|
|
2829
3026
|
if (!stream) continue;
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
3027
|
+
if (message.header.mode instanceof SilentDelivery) {
|
|
3028
|
+
message.header.mode.to = [..._distantPeers.keys()];
|
|
3029
|
+
promises.push(
|
|
3030
|
+
this.waitForPeerWrite(
|
|
3031
|
+
stream,
|
|
3032
|
+
message.bytes(),
|
|
3033
|
+
message.header.priority,
|
|
3034
|
+
),
|
|
3035
|
+
);
|
|
3036
|
+
} else {
|
|
3037
|
+
promises.push(
|
|
3038
|
+
this.waitForPeerWrite(
|
|
3039
|
+
stream,
|
|
3040
|
+
bytes,
|
|
3041
|
+
message.header.priority,
|
|
3042
|
+
),
|
|
3043
|
+
);
|
|
3044
|
+
}
|
|
2838
3045
|
usedNeighbours.add(neighbour);
|
|
2839
3046
|
}
|
|
2840
3047
|
if (message.header.mode instanceof SilentDelivery) {
|
|
@@ -2853,12 +3060,18 @@ export abstract class DirectStream<
|
|
|
2853
3060
|
for (const [neighbour, stream] of this.peers) {
|
|
2854
3061
|
if (usedNeighbours.size >= message.header.mode.redundancy) {
|
|
2855
3062
|
break;
|
|
3063
|
+
}
|
|
3064
|
+
if (usedNeighbours.has(neighbour)) continue;
|
|
3065
|
+
usedNeighbours.add(neighbour);
|
|
3066
|
+
promises.push(
|
|
3067
|
+
this.waitForPeerWrite(
|
|
3068
|
+
stream,
|
|
3069
|
+
bytes,
|
|
3070
|
+
message.header.priority,
|
|
3071
|
+
),
|
|
3072
|
+
);
|
|
2856
3073
|
}
|
|
2857
|
-
if (usedNeighbours.has(neighbour)) continue;
|
|
2858
|
-
usedNeighbours.add(neighbour);
|
|
2859
|
-
promises.push(stream.waitForWrite(bytes, message.header.priority));
|
|
2860
3074
|
}
|
|
2861
|
-
}
|
|
2862
3075
|
|
|
2863
3076
|
await Promise.all(promises);
|
|
2864
3077
|
return delivereyPromise;
|
|
@@ -2882,12 +3095,16 @@ export abstract class DirectStream<
|
|
|
2882
3095
|
)
|
|
2883
3096
|
) {
|
|
2884
3097
|
continue; // recipient already signed/seen this message
|
|
3098
|
+
}
|
|
3099
|
+
message.header.mode.to = [recipient];
|
|
3100
|
+
promises.push(
|
|
3101
|
+
this.waitForPeerWrite(
|
|
3102
|
+
stream,
|
|
3103
|
+
message.bytes(),
|
|
3104
|
+
message.header.priority,
|
|
3105
|
+
),
|
|
3106
|
+
);
|
|
2885
3107
|
}
|
|
2886
|
-
message.header.mode.to = [recipient];
|
|
2887
|
-
promises.push(
|
|
2888
|
-
stream.waitForWrite(message.bytes(), message.header.priority),
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
2891
3108
|
message.header.mode.to = originalTo;
|
|
2892
3109
|
if (promises.length > 0) {
|
|
2893
3110
|
await Promise.all(promises);
|
|
@@ -2924,11 +3141,11 @@ export abstract class DirectStream<
|
|
|
2924
3141
|
)
|
|
2925
3142
|
) {
|
|
2926
3143
|
continue;
|
|
2927
|
-
|
|
3144
|
+
}
|
|
2928
3145
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3146
|
+
sentOnce = true;
|
|
3147
|
+
promises.push(this.waitForPeerWrite(id, bytes, message.header.priority));
|
|
3148
|
+
}
|
|
2932
3149
|
await Promise.all(promises);
|
|
2933
3150
|
|
|
2934
3151
|
if (!sentOnce) {
|
|
@@ -3188,6 +3405,121 @@ export abstract class DirectStream<
|
|
|
3188
3405
|
return this.components.connectionManager.closeConnections(stream.peerId);
|
|
3189
3406
|
}
|
|
3190
3407
|
|
|
3408
|
+
private getTotalQueueAdmissionLimitBytes(
|
|
3409
|
+
priority: number,
|
|
3410
|
+
): number | undefined {
|
|
3411
|
+
const limitBytes = this.outboundQueueOptions?.maxTotalBufferedBytes;
|
|
3412
|
+
if (limitBytes == null) {
|
|
3413
|
+
return undefined;
|
|
3414
|
+
}
|
|
3415
|
+
if (getLaneFromPriority(priority) === getLaneFromPriority(0)) {
|
|
3416
|
+
return Math.max(
|
|
3417
|
+
0,
|
|
3418
|
+
limitBytes - this.outboundQueueOptions.reservedTotalPriorityBytes,
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
return limitBytes;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
private assertTotalQueueCapacity(
|
|
3425
|
+
peerId: PeerId,
|
|
3426
|
+
payloadBytes: number,
|
|
3427
|
+
priority: number,
|
|
3428
|
+
) {
|
|
3429
|
+
const limitBytes = this.getTotalQueueAdmissionLimitBytes(priority);
|
|
3430
|
+
if (limitBytes == null) {
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
const currentBufferedBytes = this.getQueuedBytes();
|
|
3434
|
+
if (currentBufferedBytes + payloadBytes > limitBytes) {
|
|
3435
|
+
throw new BackpressureError({
|
|
3436
|
+
scope: "node",
|
|
3437
|
+
peerId,
|
|
3438
|
+
priority,
|
|
3439
|
+
limitBytes,
|
|
3440
|
+
currentBufferedBytes,
|
|
3441
|
+
attemptedBytes: payloadBytes,
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
private notifyTotalOutboundQueueWaiters() {
|
|
3447
|
+
if (this.totalOutboundQueueWaiters.size === 0) {
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
const queuedBytes = this.getQueuedBytes();
|
|
3451
|
+
for (const waiter of [...this.totalOutboundQueueWaiters]) {
|
|
3452
|
+
if (queuedBytes <= waiter.limitBytes) {
|
|
3453
|
+
this.totalOutboundQueueWaiters.delete(waiter);
|
|
3454
|
+
waiter.deferred.resolve();
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
private async waitForTotalQueueCapacity(
|
|
3460
|
+
payloadBytes: number,
|
|
3461
|
+
priority: number,
|
|
3462
|
+
signal?: AbortSignal,
|
|
3463
|
+
) {
|
|
3464
|
+
const limitBytes = this.getTotalQueueAdmissionLimitBytes(priority);
|
|
3465
|
+
if (limitBytes == null) {
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
const threshold = Math.max(0, limitBytes - payloadBytes);
|
|
3469
|
+
if (this.getQueuedBytes() <= threshold) {
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
const waiter = {
|
|
3473
|
+
limitBytes: threshold,
|
|
3474
|
+
deferred: pDefer<void>(),
|
|
3475
|
+
};
|
|
3476
|
+
this.totalOutboundQueueWaiters.add(waiter);
|
|
3477
|
+
|
|
3478
|
+
let cancel: Promise<void> | undefined;
|
|
3479
|
+
let listener: (() => void) | undefined;
|
|
3480
|
+
|
|
3481
|
+
if (signal != null) {
|
|
3482
|
+
cancel = new Promise<void>((_resolve, reject) => {
|
|
3483
|
+
listener = () => {
|
|
3484
|
+
this.totalOutboundQueueWaiters.delete(waiter);
|
|
3485
|
+
reject(new AbortError());
|
|
3486
|
+
};
|
|
3487
|
+
signal.addEventListener("abort", listener!);
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
try {
|
|
3492
|
+
await Promise.race(
|
|
3493
|
+
cancel != null ? [waiter.deferred.promise, cancel] : [waiter.deferred.promise],
|
|
3494
|
+
);
|
|
3495
|
+
} finally {
|
|
3496
|
+
this.totalOutboundQueueWaiters.delete(waiter);
|
|
3497
|
+
if (listener != null) {
|
|
3498
|
+
signal?.removeEventListener("abort", listener);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
protected async waitForPeerWrite(
|
|
3504
|
+
stream: PeerStreams,
|
|
3505
|
+
bytes: Uint8Array | Uint8ArrayList,
|
|
3506
|
+
priority = 0,
|
|
3507
|
+
signal?: AbortSignal,
|
|
3508
|
+
) {
|
|
3509
|
+
while (true) {
|
|
3510
|
+
try {
|
|
3511
|
+
this.assertTotalQueueCapacity(stream.peerId, bytes.byteLength, priority);
|
|
3512
|
+
await stream.waitForWrite(bytes, priority, signal);
|
|
3513
|
+
return;
|
|
3514
|
+
} catch (error) {
|
|
3515
|
+
if (!(error instanceof BackpressureError) || error.scope !== "node") {
|
|
3516
|
+
throw error;
|
|
3517
|
+
}
|
|
3518
|
+
await this.waitForTotalQueueCapacity(bytes.byteLength, priority, signal);
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3191
3523
|
getQueuedBytes(): number {
|
|
3192
3524
|
let sum = 0;
|
|
3193
3525
|
for (const [_k, ps] of this.peers) {
|