@peerbit/stream 4.3.10 → 4.4.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/dist/src/index.d.ts +52 -18
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +377 -83
- package/dist/src/index.js.map +1 -1
- package/dist/src/pushable-lanes.d.ts +6 -0
- package/dist/src/pushable-lanes.d.ts.map +1 -1
- package/dist/src/pushable-lanes.js +3 -1
- package/dist/src/pushable-lanes.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +434 -114
- package/src/pushable-lanes.ts +7 -1
package/src/index.ts
CHANGED
|
@@ -23,13 +23,13 @@ import { Circuit } from "@multiformats/multiaddr-matcher";
|
|
|
23
23
|
import { Cache } from "@peerbit/cache";
|
|
24
24
|
import {
|
|
25
25
|
PublicSignKey,
|
|
26
|
-
type SignatureWithKey,
|
|
27
26
|
getKeypairFromPrivateKey,
|
|
28
27
|
getPublicKeyFromPeerId,
|
|
29
28
|
ready,
|
|
30
29
|
sha256Base64,
|
|
31
30
|
toBase64,
|
|
32
31
|
} from "@peerbit/crypto";
|
|
32
|
+
import type { SignatureWithKey } from "@peerbit/crypto";
|
|
33
33
|
import {
|
|
34
34
|
ACK,
|
|
35
35
|
AcknowledgeDelivery,
|
|
@@ -37,25 +37,27 @@ import {
|
|
|
37
37
|
DataMessage,
|
|
38
38
|
DeliveryError,
|
|
39
39
|
Goodbye,
|
|
40
|
-
type IdOptions,
|
|
41
40
|
InvalidMessageError,
|
|
42
41
|
Message,
|
|
43
42
|
MessageHeader,
|
|
44
43
|
MultiAddrinfo,
|
|
45
44
|
NotStartedError,
|
|
46
|
-
type PriorityOptions,
|
|
47
|
-
type PublicKeyFromHashResolver,
|
|
48
45
|
SeekDelivery,
|
|
49
46
|
SilentDelivery,
|
|
50
|
-
type StreamEvents,
|
|
51
47
|
TracedDelivery,
|
|
52
|
-
type WaitForPeer,
|
|
53
|
-
type WithExtraSigners,
|
|
54
|
-
type WithMode,
|
|
55
|
-
type WithTo,
|
|
56
48
|
deliveryModeHasReceiver,
|
|
57
49
|
getMsgId,
|
|
58
50
|
} from "@peerbit/stream-interface";
|
|
51
|
+
import type {
|
|
52
|
+
IdOptions,
|
|
53
|
+
PriorityOptions,
|
|
54
|
+
PublicKeyFromHashResolver,
|
|
55
|
+
StreamEvents,
|
|
56
|
+
WaitForPeer,
|
|
57
|
+
WithExtraSigners,
|
|
58
|
+
WithMode,
|
|
59
|
+
WithTo,
|
|
60
|
+
} from "@peerbit/stream-interface";
|
|
59
61
|
import { AbortError, TimeoutError, delay } from "@peerbit/time";
|
|
60
62
|
import { abortableSource } from "abortable-iterator";
|
|
61
63
|
import * as lp from "it-length-prefixed";
|
|
@@ -86,7 +88,7 @@ export const dontThrowIfDeliveryError = (e: any) => {
|
|
|
86
88
|
throw e;
|
|
87
89
|
};
|
|
88
90
|
|
|
89
|
-
const logError = (e?:
|
|
91
|
+
const logError = (e?: any) => {
|
|
90
92
|
if (e?.message === "Cannot push value onto an ended pushable") {
|
|
91
93
|
return; // ignore since we are trying to push to a closed stream
|
|
92
94
|
}
|
|
@@ -134,6 +136,41 @@ const getLaneFromPriority = (priority: number) => {
|
|
|
134
136
|
}
|
|
135
137
|
return 1;
|
|
136
138
|
};
|
|
139
|
+
interface OutboundCandidate {
|
|
140
|
+
raw: Stream;
|
|
141
|
+
pushable: PushableLanes<Uint8Array>;
|
|
142
|
+
created: number;
|
|
143
|
+
bytesDelivered: number;
|
|
144
|
+
aborted: boolean;
|
|
145
|
+
existing: boolean;
|
|
146
|
+
}
|
|
147
|
+
export interface InboundStreamRecord {
|
|
148
|
+
raw: Stream;
|
|
149
|
+
iterable: AsyncIterable<Uint8ArrayList>;
|
|
150
|
+
abortController: AbortController;
|
|
151
|
+
created: number;
|
|
152
|
+
lastActivity: number;
|
|
153
|
+
bytesReceived: number;
|
|
154
|
+
}
|
|
155
|
+
// Hook for tests to override queued length measurement (peerStreams, default impl)
|
|
156
|
+
export let measureOutboundQueuedBytes: (
|
|
157
|
+
ps: PeerStreams, // return queued bytes for active outbound (lane 0) or 0 if none
|
|
158
|
+
) => number = (ps: PeerStreams) => {
|
|
159
|
+
const active = ps._getActiveOutboundPushable();
|
|
160
|
+
if (!active) return 0;
|
|
161
|
+
// Prefer lane-aware helper if present
|
|
162
|
+
// @ts-ignore - optional test helper
|
|
163
|
+
if (typeof active.getReadableLength === "function") {
|
|
164
|
+
try {
|
|
165
|
+
// lane 0 only
|
|
166
|
+
return active.getReadableLength(0) || 0;
|
|
167
|
+
} catch {
|
|
168
|
+
// ignore
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// @ts-ignore fallback for vanilla pushable
|
|
172
|
+
return active.readableLength || 0;
|
|
173
|
+
};
|
|
137
174
|
/**
|
|
138
175
|
* Thin wrapper around a peer's inbound / outbound pubsub streams
|
|
139
176
|
*/
|
|
@@ -141,27 +178,24 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
141
178
|
public readonly peerId: PeerId;
|
|
142
179
|
public readonly publicKey: PublicSignKey;
|
|
143
180
|
public readonly protocol: string;
|
|
144
|
-
|
|
145
|
-
* Write stream - it's preferable to use the write method
|
|
146
|
-
*/
|
|
147
|
-
public outboundStream?: PushableLanes<Uint8Array>;
|
|
181
|
+
// Removed dedicated outboundStream; first element of outboundStreams[] is active
|
|
148
182
|
|
|
149
183
|
/**
|
|
150
|
-
*
|
|
184
|
+
* Backwards compatible single inbound references (points to first inbound candidate)
|
|
151
185
|
*/
|
|
152
186
|
public inboundStream?: AsyncIterable<Uint8ArrayList>;
|
|
153
|
-
/**
|
|
154
|
-
* The raw outbound stream, as retrieved from conn.newStream
|
|
155
|
-
*/
|
|
156
|
-
public rawOutboundStream?: Stream;
|
|
157
|
-
/**
|
|
158
|
-
* The raw inbound stream, as retrieved from the callback from libp2p.handle
|
|
159
|
-
*/
|
|
160
187
|
public rawInboundStream?: Stream;
|
|
188
|
+
|
|
161
189
|
/**
|
|
162
|
-
*
|
|
190
|
+
* Multiple inbound stream support (more permissive than outbound)
|
|
191
|
+
* We retain concurrent inbound streams to avoid races during migration; inactive ones can later be pruned.
|
|
163
192
|
*/
|
|
164
|
-
|
|
193
|
+
public inboundStreams: InboundStreamRecord[] = [];
|
|
194
|
+
|
|
195
|
+
private _inboundPruneTimer?: ReturnType<typeof setTimeout>;
|
|
196
|
+
public static INBOUND_IDLE_MS = 10_000; // configurable grace for inactivity (made public for tests)
|
|
197
|
+
static MAX_INBOUND_STREAMS = 8; // sensible default to prevent flood
|
|
198
|
+
|
|
165
199
|
private outboundAbortController: AbortController;
|
|
166
200
|
|
|
167
201
|
private closed: boolean;
|
|
@@ -171,13 +205,92 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
171
205
|
public seekedOnce: boolean;
|
|
172
206
|
|
|
173
207
|
private usedBandWidthTracker: BandwidthTracker;
|
|
208
|
+
|
|
209
|
+
// Unified outbound streams list (during grace may contain >1; after pruning length==1)
|
|
210
|
+
private outboundStreams: OutboundCandidate[] = [];
|
|
211
|
+
// Public debug exposure of current raw outbound streams (during grace may contain >1)
|
|
212
|
+
public get rawOutboundStreams(): Stream[] {
|
|
213
|
+
return this.outboundStreams.map((c) => c.raw);
|
|
214
|
+
}
|
|
215
|
+
public _getActiveOutboundPushable(): PushableLanes<Uint8Array> | undefined {
|
|
216
|
+
return this.outboundStreams[0]?.pushable;
|
|
217
|
+
}
|
|
218
|
+
public _getOutboundCount() {
|
|
219
|
+
return this.outboundStreams.length;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public _getInboundCount() {
|
|
223
|
+
return this.inboundStreams.length;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public _debugInboundStats(): {
|
|
227
|
+
id: string;
|
|
228
|
+
created: number;
|
|
229
|
+
lastActivity: number;
|
|
230
|
+
bytesReceived: number;
|
|
231
|
+
}[] {
|
|
232
|
+
return this.inboundStreams.map((c) => ({
|
|
233
|
+
id: c.raw.id,
|
|
234
|
+
created: c.created,
|
|
235
|
+
lastActivity: c.lastActivity,
|
|
236
|
+
bytesReceived: c.bytesReceived,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
private _outboundPruneTimer?: ReturnType<typeof setTimeout>;
|
|
240
|
+
private static readonly OUTBOUND_GRACE_MS = 500; // TODO configurable
|
|
241
|
+
|
|
242
|
+
private _addOutboundCandidate(raw: Stream): OutboundCandidate {
|
|
243
|
+
const existing = this.outboundStreams.find((c) => c.raw === raw);
|
|
244
|
+
if (existing) return existing;
|
|
245
|
+
const pushableInst = pushableLanes<Uint8Array>({
|
|
246
|
+
lanes: 2,
|
|
247
|
+
onPush: (val: Uint8Array) => {
|
|
248
|
+
candidate.bytesDelivered += val.length || val.byteLength || 0;
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const candidate: OutboundCandidate = {
|
|
252
|
+
raw,
|
|
253
|
+
pushable: pushableInst,
|
|
254
|
+
created: Date.now(),
|
|
255
|
+
bytesDelivered: 0,
|
|
256
|
+
aborted: false,
|
|
257
|
+
existing: false,
|
|
258
|
+
};
|
|
259
|
+
pipe(
|
|
260
|
+
pushableInst,
|
|
261
|
+
(source) => lp.encode(source, { maxDataLength: MAX_DATA_LENGTH_OUT }),
|
|
262
|
+
raw,
|
|
263
|
+
).catch((e: any) => {
|
|
264
|
+
candidate.aborted = true;
|
|
265
|
+
logError(e as { message: string } as any);
|
|
266
|
+
});
|
|
267
|
+
this.outboundStreams.push(candidate);
|
|
268
|
+
const origAbort = raw.abort?.bind(raw);
|
|
269
|
+
raw.abort = (err?: any) => {
|
|
270
|
+
candidate.aborted = true;
|
|
271
|
+
return origAbort?.(err);
|
|
272
|
+
};
|
|
273
|
+
return candidate;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private _scheduleOutboundPrune(reset = true) {
|
|
277
|
+
if (this.outboundStreams.length <= 1) return;
|
|
278
|
+
if (reset && this._outboundPruneTimer) {
|
|
279
|
+
clearTimeout(this._outboundPruneTimer);
|
|
280
|
+
}
|
|
281
|
+
if (!this._outboundPruneTimer) {
|
|
282
|
+
this._outboundPruneTimer = setTimeout(
|
|
283
|
+
() => this.pruneOutboundCandidates(),
|
|
284
|
+
PeerStreams.OUTBOUND_GRACE_MS,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
174
288
|
constructor(init: PeerStreamsInit) {
|
|
175
289
|
super();
|
|
176
290
|
|
|
177
291
|
this.peerId = init.peerId;
|
|
178
292
|
this.publicKey = init.publicKey;
|
|
179
293
|
this.protocol = init.protocol;
|
|
180
|
-
this.inboundAbortController = new AbortController();
|
|
181
294
|
this.outboundAbortController = new AbortController();
|
|
182
295
|
|
|
183
296
|
this.closed = false;
|
|
@@ -190,14 +303,14 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
190
303
|
* Do we have a connection to read from?
|
|
191
304
|
*/
|
|
192
305
|
get isReadable() {
|
|
193
|
-
return
|
|
306
|
+
return this.inboundStreams.length > 0;
|
|
194
307
|
}
|
|
195
308
|
|
|
196
309
|
/**
|
|
197
310
|
* Do we have a connection to write on?
|
|
198
311
|
*/
|
|
199
312
|
get isWritable() {
|
|
200
|
-
return
|
|
313
|
+
return this.outboundStreams.length > 0;
|
|
201
314
|
}
|
|
202
315
|
|
|
203
316
|
get usedBandwidth() {
|
|
@@ -216,19 +329,62 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
216
329
|
} mb`,
|
|
217
330
|
);
|
|
218
331
|
}
|
|
219
|
-
if (this.
|
|
332
|
+
if (!this.isWritable) {
|
|
220
333
|
logger.error("No writable connection to " + this.peerId.toString());
|
|
221
334
|
throw new Error("No writable connection to " + this.peerId.toString());
|
|
222
335
|
}
|
|
223
336
|
|
|
224
337
|
this.usedBandWidthTracker.add(data.byteLength);
|
|
225
338
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
)
|
|
339
|
+
// Write to all current outbound streams (normally 1, but >1 during grace)
|
|
340
|
+
const payload = data instanceof Uint8Array ? data : data.subarray();
|
|
341
|
+
let successes = 0;
|
|
342
|
+
let failures: any[] = [];
|
|
343
|
+
const failed: OutboundCandidate[] = [];
|
|
344
|
+
for (const c of this.outboundStreams) {
|
|
345
|
+
try {
|
|
346
|
+
c.pushable.push(
|
|
347
|
+
payload,
|
|
348
|
+
c.pushable.getReadableLength(0) === 0
|
|
349
|
+
? 0
|
|
350
|
+
: getLaneFromPriority(priority),
|
|
351
|
+
);
|
|
352
|
+
successes++;
|
|
353
|
+
} catch (e) {
|
|
354
|
+
failures.push(e);
|
|
355
|
+
failed.push(c);
|
|
356
|
+
logError(e);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (successes === 0) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
"All outbound writes failed (" +
|
|
362
|
+
failures.map((f) => f?.message).join(", ") +
|
|
363
|
+
")",
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (failures.length > 0) {
|
|
367
|
+
logger.warn(
|
|
368
|
+
`Partial outbound write failure: ${failures.length} failed, ${successes} succeeded`,
|
|
369
|
+
);
|
|
370
|
+
// Remove failed streams immediately (best-effort)
|
|
371
|
+
if (failed.length) {
|
|
372
|
+
this.outboundStreams = this.outboundStreams.filter(
|
|
373
|
+
(c) => !failed.includes(c),
|
|
374
|
+
);
|
|
375
|
+
for (const f of failed) {
|
|
376
|
+
try {
|
|
377
|
+
f.raw.abort?.(new AbortError("Failed write" as any));
|
|
378
|
+
} catch {}
|
|
379
|
+
try {
|
|
380
|
+
f.raw.close?.();
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|
|
383
|
+
// If more than one remains schedule prune; else ensure outbound event raised
|
|
384
|
+
if (this.outboundStreams.length > 1) this._scheduleOutboundPrune(true);
|
|
385
|
+
else this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
232
388
|
}
|
|
233
389
|
|
|
234
390
|
/**
|
|
@@ -296,41 +452,108 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
296
452
|
/**
|
|
297
453
|
* Attach a raw inbound stream and setup a read stream
|
|
298
454
|
*/
|
|
299
|
-
attachInboundStream(stream: Stream) {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
455
|
+
attachInboundStream(stream: Stream): InboundStreamRecord {
|
|
456
|
+
// Support multiple concurrent inbound streams with inactivity pruning.
|
|
457
|
+
// Enforce max inbound streams (drop least recently active)
|
|
458
|
+
if (this.inboundStreams.length >= PeerStreams.MAX_INBOUND_STREAMS) {
|
|
459
|
+
let dropIndex = 0;
|
|
460
|
+
for (let i = 1; i < this.inboundStreams.length; i++) {
|
|
461
|
+
const a = this.inboundStreams[i];
|
|
462
|
+
const b = this.inboundStreams[dropIndex];
|
|
463
|
+
if (
|
|
464
|
+
a.lastActivity < b.lastActivity ||
|
|
465
|
+
(a.lastActivity === b.lastActivity && a.created < b.created)
|
|
466
|
+
) {
|
|
467
|
+
dropIndex = i;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const [drop] = this.inboundStreams.splice(dropIndex, 1);
|
|
471
|
+
try {
|
|
472
|
+
drop.abortController.abort();
|
|
473
|
+
} catch {
|
|
474
|
+
logger.error("Failed to abort inbound stream");
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
drop.raw.close?.();
|
|
478
|
+
} catch {
|
|
479
|
+
logger.error("Failed to close inbound stream");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const abortController = new AbortController();
|
|
483
|
+
const decoded = pipe(stream, (source) =>
|
|
484
|
+
lp.decode(source, { maxDataLength: MAX_DATA_LENGTH_IN }),
|
|
316
485
|
);
|
|
486
|
+
const iterable = abortableSource(decoded, abortController.signal, {
|
|
487
|
+
returnOnAbort: true,
|
|
488
|
+
onReturnError: (err) => {
|
|
489
|
+
logger.error("Inbound stream error", err?.message);
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
const record: InboundStreamRecord = {
|
|
493
|
+
raw: stream,
|
|
494
|
+
iterable,
|
|
495
|
+
abortController,
|
|
496
|
+
created: Date.now(),
|
|
497
|
+
lastActivity: Date.now(),
|
|
498
|
+
bytesReceived: 0,
|
|
499
|
+
};
|
|
500
|
+
this.inboundStreams.push(record);
|
|
501
|
+
this._scheduleInboundPrune();
|
|
502
|
+
// Backwards compatibility: keep first inbound as public properties
|
|
503
|
+
if (this.inboundStreams.length === 1) {
|
|
504
|
+
this.rawInboundStream = stream;
|
|
505
|
+
this.inboundStream = iterable;
|
|
506
|
+
}
|
|
507
|
+
this.dispatchEvent(new CustomEvent("stream:inbound"));
|
|
508
|
+
return record;
|
|
509
|
+
}
|
|
317
510
|
|
|
318
|
-
|
|
319
|
-
this.
|
|
320
|
-
this.
|
|
321
|
-
this.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
511
|
+
private _scheduleInboundPrune() {
|
|
512
|
+
if (this._inboundPruneTimer) return; // already scheduled
|
|
513
|
+
this._inboundPruneTimer = setTimeout(() => {
|
|
514
|
+
this._inboundPruneTimer = undefined;
|
|
515
|
+
this._pruneInboundInactive();
|
|
516
|
+
if (this.inboundStreams.length > 1) {
|
|
517
|
+
// schedule again if still multiple
|
|
518
|
+
this._scheduleInboundPrune();
|
|
519
|
+
}
|
|
520
|
+
}, PeerStreams.INBOUND_IDLE_MS);
|
|
521
|
+
}
|
|
326
522
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
this.
|
|
333
|
-
|
|
523
|
+
private _pruneInboundInactive() {
|
|
524
|
+
if (this.inboundStreams.length <= 1) return;
|
|
525
|
+
const now = Date.now();
|
|
526
|
+
// Keep at least one (the most recently active)
|
|
527
|
+
this.inboundStreams.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
528
|
+
const keep = this.inboundStreams[0];
|
|
529
|
+
const survivors: typeof this.inboundStreams = [keep];
|
|
530
|
+
for (let i = 1; i < this.inboundStreams.length; i++) {
|
|
531
|
+
const candidate = this.inboundStreams[i];
|
|
532
|
+
if (now - candidate.lastActivity <= PeerStreams.INBOUND_IDLE_MS) {
|
|
533
|
+
survivors.push(candidate);
|
|
534
|
+
continue; // still active
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
candidate.abortController.abort();
|
|
538
|
+
} catch {}
|
|
539
|
+
try {
|
|
540
|
+
candidate.raw.close?.();
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
543
|
+
this.inboundStreams = survivors;
|
|
544
|
+
// update legacy references if they were pruned
|
|
545
|
+
if (!this.inboundStreams.includes(keep)) {
|
|
546
|
+
this.rawInboundStream = this.inboundStreams[0]?.raw;
|
|
547
|
+
this.inboundStream = this.inboundStreams[0]?.iterable;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
public forcePruneInbound() {
|
|
552
|
+
if (this._inboundPruneTimer) {
|
|
553
|
+
clearTimeout(this._inboundPruneTimer);
|
|
554
|
+
this._inboundPruneTimer = undefined;
|
|
555
|
+
}
|
|
556
|
+
this._pruneInboundInactive();
|
|
334
557
|
}
|
|
335
558
|
|
|
336
559
|
/**
|
|
@@ -338,39 +561,111 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
338
561
|
*/
|
|
339
562
|
|
|
340
563
|
async attachOutboundStream(stream: Stream) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
564
|
+
if (this.outboundStreams[0] && stream.id === this.outboundStreams[0].raw.id)
|
|
565
|
+
return; // duplicate
|
|
566
|
+
this._addOutboundCandidate(stream);
|
|
567
|
+
if (this.outboundStreams.length === 1) {
|
|
568
|
+
this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
this._scheduleOutboundPrune(true);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private pruneOutboundCandidates() {
|
|
575
|
+
try {
|
|
576
|
+
const candidates = this.outboundStreams;
|
|
577
|
+
if (!candidates.length) return;
|
|
578
|
+
const now = Date.now();
|
|
579
|
+
const healthy = candidates.filter(
|
|
580
|
+
(c: OutboundCandidate) => !c.aborted && c.bytesDelivered > 0,
|
|
348
581
|
);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
582
|
+
let chosen: OutboundCandidate | undefined;
|
|
583
|
+
if (healthy.length === 0) {
|
|
584
|
+
chosen = candidates.reduce(
|
|
585
|
+
(a: OutboundCandidate, b: OutboundCandidate) =>
|
|
586
|
+
b.created > a.created ? b : a,
|
|
352
587
|
);
|
|
353
|
-
}
|
|
354
|
-
|
|
588
|
+
} else {
|
|
589
|
+
let bestScore = -Infinity;
|
|
590
|
+
for (const c of healthy) {
|
|
591
|
+
const age = now - c.created || 1;
|
|
592
|
+
const score = c.bytesDelivered / age;
|
|
593
|
+
if (
|
|
594
|
+
score > bestScore ||
|
|
595
|
+
(score === bestScore && chosen && c.created > chosen.created)
|
|
596
|
+
) {
|
|
597
|
+
bestScore = score;
|
|
598
|
+
chosen = c;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!chosen) return;
|
|
603
|
+
for (const c of candidates) {
|
|
604
|
+
if (c === chosen) continue; // never abort chosen
|
|
605
|
+
try {
|
|
606
|
+
c.raw.abort?.(new AbortError("Replaced outbound stream" as any));
|
|
607
|
+
} catch {
|
|
608
|
+
logger.error("Failed to abort outbound stream");
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
c.pushable.return?.();
|
|
612
|
+
} catch {
|
|
613
|
+
logger.error("Failed to close outbound pushable");
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
c.raw.close?.();
|
|
617
|
+
} catch {
|
|
618
|
+
logger.error("Failed to close outbound stream");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
this.outboundStreams = [chosen];
|
|
622
|
+
} catch (e) {
|
|
623
|
+
logger.error(
|
|
624
|
+
"Error promoting outbound candidate: " + (e as any)?.message,
|
|
625
|
+
);
|
|
626
|
+
} finally {
|
|
627
|
+
this.dispatchEvent(new CustomEvent("stream:outbound"));
|
|
355
628
|
}
|
|
629
|
+
}
|
|
356
630
|
|
|
357
|
-
|
|
358
|
-
this.
|
|
631
|
+
public forcePruneOutbound() {
|
|
632
|
+
if (this._outboundPruneTimer) {
|
|
633
|
+
clearTimeout(this._outboundPruneTimer);
|
|
634
|
+
this._outboundPruneTimer = undefined;
|
|
635
|
+
}
|
|
636
|
+
this.pruneOutboundCandidates();
|
|
637
|
+
}
|
|
359
638
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
});
|
|
639
|
+
/**
|
|
640
|
+
* Internal helper to perform the actual outbound replacement & piping.
|
|
641
|
+
*/
|
|
642
|
+
// _replaceOutboundStream removed (legacy path)
|
|
365
643
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.
|
|
370
|
-
|
|
644
|
+
// Debug/testing helper: list active outbound raw stream ids
|
|
645
|
+
public _debugActiveOutboundIds(): string[] {
|
|
646
|
+
if (this.outboundStreams.length) {
|
|
647
|
+
return this.outboundStreams.map((c) => c.raw.id);
|
|
648
|
+
}
|
|
649
|
+
return this.outboundStreams.map((c) => c.raw.id);
|
|
650
|
+
}
|
|
371
651
|
|
|
372
|
-
|
|
373
|
-
|
|
652
|
+
public _debugOutboundStats(): {
|
|
653
|
+
id: string;
|
|
654
|
+
bytes: number;
|
|
655
|
+
aborted: boolean;
|
|
656
|
+
}[] {
|
|
657
|
+
if (this.outboundStreams.length) {
|
|
658
|
+
return this.outboundStreams.map((c) => ({
|
|
659
|
+
id: c.raw.id,
|
|
660
|
+
bytes: c.bytesDelivered,
|
|
661
|
+
aborted: !!c.aborted,
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
return this.outboundStreams.map((c) => ({
|
|
665
|
+
id: c.raw.id,
|
|
666
|
+
bytes: c.bytesDelivered,
|
|
667
|
+
aborted: !!c.aborted,
|
|
668
|
+
}));
|
|
374
669
|
}
|
|
375
670
|
|
|
376
671
|
/**
|
|
@@ -382,29 +677,49 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
|
|
|
382
677
|
}
|
|
383
678
|
|
|
384
679
|
this.closed = true;
|
|
680
|
+
if (this._outboundPruneTimer) {
|
|
681
|
+
clearTimeout(this._outboundPruneTimer);
|
|
682
|
+
this._outboundPruneTimer = undefined;
|
|
683
|
+
}
|
|
385
684
|
|
|
386
685
|
// End the outbound stream
|
|
387
|
-
if (this.
|
|
388
|
-
|
|
389
|
-
|
|
686
|
+
if (this.outboundStreams.length) {
|
|
687
|
+
for (const c of this.outboundStreams) {
|
|
688
|
+
try {
|
|
689
|
+
await c.pushable.return?.();
|
|
690
|
+
} catch {}
|
|
691
|
+
try {
|
|
692
|
+
c.raw.abort?.(new AbortError("Closed"));
|
|
693
|
+
} catch {}
|
|
694
|
+
}
|
|
390
695
|
this.outboundAbortController.abort();
|
|
391
696
|
}
|
|
392
697
|
|
|
393
|
-
// End
|
|
394
|
-
if (this.
|
|
395
|
-
this.
|
|
396
|
-
|
|
698
|
+
// End inbound streams
|
|
699
|
+
if (this.inboundStreams.length) {
|
|
700
|
+
for (const inbound of this.inboundStreams) {
|
|
701
|
+
try {
|
|
702
|
+
inbound.abortController.abort();
|
|
703
|
+
} catch {
|
|
704
|
+
logger.error("Failed to abort inbound stream");
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
await inbound.raw.close?.();
|
|
708
|
+
} catch {
|
|
709
|
+
logger.error("Failed to close inbound stream");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
397
712
|
}
|
|
398
713
|
|
|
399
714
|
this.usedBandWidthTracker.stop();
|
|
400
715
|
|
|
401
716
|
this.dispatchEvent(new CustomEvent("close"));
|
|
402
717
|
|
|
403
|
-
this.
|
|
404
|
-
this.outboundStream = undefined;
|
|
718
|
+
this.outboundStreams = [];
|
|
405
719
|
|
|
406
720
|
this.rawInboundStream = undefined;
|
|
407
721
|
this.inboundStream = undefined;
|
|
722
|
+
this.inboundStreams = [];
|
|
408
723
|
}
|
|
409
724
|
}
|
|
410
725
|
|
|
@@ -436,6 +751,7 @@ export type DirectStreamOptions = {
|
|
|
436
751
|
messageProcessingConcurrency?: number;
|
|
437
752
|
maxInboundStreams?: number;
|
|
438
753
|
maxOutboundStreams?: number;
|
|
754
|
+
inboundIdleTimeout?: number; // override PeerStreams.INBOUND_IDLE_MS
|
|
439
755
|
connectionManager?: ConnectionManagerArguments;
|
|
440
756
|
routeSeekInterval?: number;
|
|
441
757
|
seekTimeout?: number;
|
|
@@ -534,6 +850,7 @@ export abstract class DirectStream<
|
|
|
534
850
|
routeSeekInterval = ROUTE_UPDATE_DELAY_FACTOR,
|
|
535
851
|
seekTimeout = SEEK_DELIVERY_TIMEOUT,
|
|
536
852
|
routeMaxRetentionPeriod = ROUTE_MAX_RETANTION_PERIOD,
|
|
853
|
+
inboundIdleTimeout,
|
|
537
854
|
} = options || {};
|
|
538
855
|
|
|
539
856
|
const signKey = getKeypairFromPrivateKey(components.privateKey);
|
|
@@ -541,6 +858,10 @@ export abstract class DirectStream<
|
|
|
541
858
|
this.sign = signKey.sign.bind(signKey);
|
|
542
859
|
this.peerId = components.peerId;
|
|
543
860
|
this.publicKey = signKey.publicKey;
|
|
861
|
+
if (inboundIdleTimeout != null)
|
|
862
|
+
PeerStreams.INBOUND_IDLE_MS = inboundIdleTimeout;
|
|
863
|
+
if (maxInboundStreams != null)
|
|
864
|
+
PeerStreams.MAX_INBOUND_STREAMS = maxInboundStreams;
|
|
544
865
|
this.publicKeyHash = signKey.publicKey.hashcode();
|
|
545
866
|
this.multicodecs = multicodecs;
|
|
546
867
|
this.started = false;
|
|
@@ -795,8 +1116,8 @@ export abstract class DirectStream<
|
|
|
795
1116
|
);
|
|
796
1117
|
|
|
797
1118
|
// handle inbound
|
|
798
|
-
const
|
|
799
|
-
this.processMessages(peer.publicKey,
|
|
1119
|
+
const inboundRecord = peer.attachInboundStream(stream);
|
|
1120
|
+
this.processMessages(peer.publicKey, inboundRecord, peer).catch(logError);
|
|
800
1121
|
|
|
801
1122
|
// try to create outbound stream
|
|
802
1123
|
await this.outboundInflightQueue.push({ peerId, connection });
|
|
@@ -1109,17 +1430,17 @@ export abstract class DirectStream<
|
|
|
1109
1430
|
*/
|
|
1110
1431
|
async processMessages(
|
|
1111
1432
|
peerId: PublicSignKey,
|
|
1112
|
-
|
|
1433
|
+
record: InboundStreamRecord,
|
|
1113
1434
|
peerStreams: PeerStreams,
|
|
1114
1435
|
) {
|
|
1115
1436
|
try {
|
|
1116
|
-
await
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1437
|
+
for await (const data of record.iterable) {
|
|
1438
|
+
const now = Date.now();
|
|
1439
|
+
record.lastActivity = now;
|
|
1440
|
+
record.bytesReceived += data.length || data.byteLength || 0;
|
|
1441
|
+
|
|
1442
|
+
this.processRpc(peerId, peerStreams, data).catch((e) => logError(e));
|
|
1443
|
+
}
|
|
1123
1444
|
} catch (err: any) {
|
|
1124
1445
|
if (err?.code === "ERR_STREAM_RESET") {
|
|
1125
1446
|
// only send stream reset messages to info
|
|
@@ -2241,9 +2562,8 @@ export abstract class DirectStream<
|
|
|
2241
2562
|
|
|
2242
2563
|
getQueuedBytes(): number {
|
|
2243
2564
|
let sum = 0;
|
|
2244
|
-
for (const
|
|
2245
|
-
|
|
2246
|
-
sum += out ? out.readableLength : 0;
|
|
2565
|
+
for (const [_k, ps] of this.peers) {
|
|
2566
|
+
sum += measureOutboundQueuedBytes(ps as any); // cast to access hook
|
|
2247
2567
|
}
|
|
2248
2568
|
return sum;
|
|
2249
2569
|
}
|