@peerbit/stream 4.4.0 → 4.4.1-94a82ff

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/package.json CHANGED
@@ -1,96 +1,96 @@
1
1
  {
2
- "name": "@peerbit/stream",
3
- "version": "4.4.0",
4
- "description": "A building block for direct streaming protocols",
5
- "sideEffects": false,
6
- "type": "module",
7
- "types": "./dist/src/index.d.ts",
8
- "typesVersions": {
9
- "*": {
10
- "*": [
11
- "*",
12
- "dist/*",
13
- "dist/src/*",
14
- "dist/src/*/index"
15
- ],
16
- "src/*": [
17
- "*",
18
- "dist/*",
19
- "dist/src/*",
20
- "dist/src/*/index"
21
- ]
22
- }
23
- },
24
- "files": [
25
- "src",
26
- "dist",
27
- "!dist/e2e",
28
- "!dist/test",
29
- "!**/*.tsbuildinfo"
30
- ],
31
- "exports": {
32
- ".": {
33
- "types": "./dist/src/index.d.ts",
34
- "import": "./dist/src/index.js"
35
- }
36
- },
37
- "eslintConfig": {
38
- "extends": "peerbit",
39
- "parserOptions": {
40
- "project": true,
41
- "sourceType": "module"
42
- },
43
- "ignorePatterns": [
44
- "!.aegir.js",
45
- "test/ts-use",
46
- "*.d.ts"
47
- ]
48
- },
49
- "publishConfig": {
50
- "access": "public"
51
- },
52
- "scripts": {
53
- "bench": "node --loader ts-node/esm ./benchmark/index.ts",
54
- "clean": "aegir clean",
55
- "build": "aegir build --no-bundle",
56
- "test": "aegir test --target node",
57
- "lint": "aegir lint"
58
- },
59
- "engines": {
60
- "node": ">=16.15.1"
61
- },
62
- "repository": {
63
- "type": "git",
64
- "url": "git+https://github.com/dao-xyz/peerbit.git"
65
- },
66
- "keywords": [
67
- "peerbit"
68
- ],
69
- "author": "dao.xyz",
70
- "license": "MIT",
71
- "bugs": {
72
- "url": "https://github.com/dao-xyz/peerbit/issues"
73
- },
74
- "homepage": "https://github.com/dao-xyz/peerbit#readme",
75
- "localMaintainers": [
76
- "dao.xyz"
77
- ],
78
- "devDependencies": {
79
- "@peerbit/libp2p-test-utils": "2.1.20",
80
- "@types/yallist": "^4.0.4",
81
- "@types/fast-fifo": "^1.0.2"
82
- },
83
- "dependencies": {
84
- "p-queue": "^8.0.1",
85
- "fast-fifo": "^1.3.2",
86
- "@dao-xyz/borsh": "^5.2.3",
87
- "@peerbit/cache": "2.1.4",
88
- "@peerbit/crypto": "2.3.11",
89
- "@peerbit/stream-interface": "^5.2.5",
90
- "@peerbit/time": "^2.2.0",
91
- "@peerbit/logger": "^1.0.4",
92
- "libp2p": "^2.10.0",
93
- "yallist": "^4.0.0",
94
- "abortable-iterator": "^5.0.1"
95
- }
2
+ "name": "@peerbit/stream",
3
+ "version": "4.4.1-94a82ff",
4
+ "description": "A building block for direct streaming protocols",
5
+ "sideEffects": false,
6
+ "type": "module",
7
+ "types": "./dist/src/index.d.ts",
8
+ "typesVersions": {
9
+ "*": {
10
+ "*": [
11
+ "*",
12
+ "dist/*",
13
+ "dist/src/*",
14
+ "dist/src/*/index"
15
+ ],
16
+ "src/*": [
17
+ "*",
18
+ "dist/*",
19
+ "dist/src/*",
20
+ "dist/src/*/index"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "src",
26
+ "dist",
27
+ "!dist/e2e",
28
+ "!dist/test",
29
+ "!**/*.tsbuildinfo"
30
+ ],
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/src/index.d.ts",
34
+ "import": "./dist/src/index.js"
35
+ }
36
+ },
37
+ "eslintConfig": {
38
+ "extends": "peerbit",
39
+ "parserOptions": {
40
+ "project": true,
41
+ "sourceType": "module"
42
+ },
43
+ "ignorePatterns": [
44
+ "!.aegir.js",
45
+ "test/ts-use",
46
+ "*.d.ts"
47
+ ]
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "bench": "node --loader ts-node/esm ./benchmark/index.ts",
54
+ "clean": "aegir clean",
55
+ "build": "aegir build --no-bundle",
56
+ "test": "aegir test --target node",
57
+ "lint": "aegir lint"
58
+ },
59
+ "engines": {
60
+ "node": ">=16.15.1"
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "git+https://github.com/dao-xyz/peerbit.git"
65
+ },
66
+ "keywords": [
67
+ "peerbit"
68
+ ],
69
+ "author": "dao.xyz",
70
+ "license": "MIT",
71
+ "bugs": {
72
+ "url": "https://github.com/dao-xyz/peerbit/issues"
73
+ },
74
+ "homepage": "https://github.com/dao-xyz/peerbit#readme",
75
+ "localMaintainers": [
76
+ "dao.xyz"
77
+ ],
78
+ "devDependencies": {
79
+ "@peerbit/libp2p-test-utils": "2.1.20-94a82ff",
80
+ "@types/yallist": "^4.0.4",
81
+ "@types/fast-fifo": "^1.0.2"
82
+ },
83
+ "dependencies": {
84
+ "p-queue": "^8.0.1",
85
+ "fast-fifo": "^1.3.2",
86
+ "@dao-xyz/borsh": "^5.2.3",
87
+ "@peerbit/cache": "2.1.4-94a82ff",
88
+ "@peerbit/crypto": "2.3.11-94a82ff",
89
+ "@peerbit/stream-interface": "5.2.5-94a82ff",
90
+ "@peerbit/time": "2.2.0-94a82ff",
91
+ "@peerbit/logger": "1.0.4-94a82ff",
92
+ "libp2p": "^2.10.0",
93
+ "yallist": "^4.0.0",
94
+ "abortable-iterator": "^5.0.1"
95
+ }
96
96
  }
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ import type {
60
60
  } from "@peerbit/stream-interface";
61
61
  import { AbortError, TimeoutError, delay } from "@peerbit/time";
62
62
  import { abortableSource } from "abortable-iterator";
63
+ import { anySignal } from "any-signal";
63
64
  import * as lp from "it-length-prefixed";
64
65
  import { pipe } from "it-pipe";
65
66
  import { type Pushable, pushable } from "it-pushable";
@@ -130,6 +131,8 @@ const DEFAULT_PRUNED_CONNNECTIONS_TIMEOUT = 30 * 1000;
130
131
 
131
132
  const ROUTE_UPDATE_DELAY_FACTOR = 3e4;
132
133
 
134
+ const DEFAULT_CREATE_OUTBOUND_STREAM_TIMEOUT = 30_000;
135
+
133
136
  const getLaneFromPriority = (priority: number) => {
134
137
  if (priority > 0) {
135
138
  return 0;
@@ -262,14 +265,31 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
262
265
  raw,
263
266
  ).catch((e: any) => {
264
267
  candidate.aborted = true;
268
+ try {
269
+ pushableInst.end(e);
270
+ } catch {}
265
271
  logError(e as { message: string } as any);
266
272
  });
267
273
  this.outboundStreams.push(candidate);
268
274
  const origAbort = raw.abort?.bind(raw);
269
275
  raw.abort = (err?: any) => {
270
276
  candidate.aborted = true;
277
+ try {
278
+ pushableInst.end(err);
279
+ } catch {}
271
280
  return origAbort?.(err);
272
281
  };
282
+
283
+ const origClose = raw.close?.bind(raw);
284
+ if (origClose) {
285
+ raw.close = (...args: any[]) => {
286
+ candidate.aborted = true;
287
+ try {
288
+ pushableInst.end();
289
+ } catch {}
290
+ return origClose(...args);
291
+ };
292
+ }
273
293
  return candidate;
274
294
  }
275
295
 
@@ -310,7 +330,7 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
310
330
  * Do we have a connection to write on?
311
331
  */
312
332
  get isWritable() {
313
- return this.outboundStreams.length > 0;
333
+ return this.outboundStreams.some((c) => !c.aborted);
314
334
  }
315
335
 
316
336
  get usedBandwidth() {
@@ -342,6 +362,12 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
342
362
  let failures: any[] = [];
343
363
  const failed: OutboundCandidate[] = [];
344
364
  for (const c of this.outboundStreams) {
365
+ if (c.aborted) {
366
+ failures.push(new Error("aborted"));
367
+ failed.push(c);
368
+ continue;
369
+ }
370
+
345
371
  try {
346
372
  c.pushable.push(
347
373
  payload,
@@ -373,6 +399,9 @@ export class PeerStreams extends TypedEventEmitter<PeerStreamEvents> {
373
399
  (c) => !failed.includes(c),
374
400
  );
375
401
  for (const f of failed) {
402
+ try {
403
+ f.pushable.end(new AbortError("Failed write" as any));
404
+ } catch {}
376
405
  try {
377
406
  f.raw.abort?.(new AbortError("Failed write" as any));
378
407
  } catch {}
@@ -812,7 +841,7 @@ export abstract class DirectStream<
812
841
  private routeMaxRetentionPeriod: number;
813
842
 
814
843
  // for sequential creation of outbound streams
815
- private outboundInflightQueue: Pushable<{
844
+ public outboundInflightQueue: Pushable<{
816
845
  connection: Connection;
817
846
  peerId: PeerId;
818
847
  }>;
@@ -821,6 +850,7 @@ export abstract class DirectStream<
821
850
  seekTimeout: number;
822
851
  closeController: AbortController;
823
852
  session: number;
853
+ _outboundPump: ReturnType<typeof pipe> | undefined;
824
854
 
825
855
  private _ackCallbacks: Map<
826
856
  string,
@@ -937,16 +967,76 @@ export abstract class DirectStream<
937
967
  this.closeController = new AbortController();
938
968
 
939
969
  this.outboundInflightQueue = pushable({ objectMode: true });
940
- pipe(this.outboundInflightQueue, async (source) => {
970
+
971
+ const drainOutbound = async (
972
+ source: AsyncIterable<{ peerId: PeerId; connection: Connection }>,
973
+ ) => {
941
974
  for await (const { peerId, connection } of source) {
942
- if (this.stopping || this.started === false) {
943
- return;
975
+ if (this.stopping || !this.started) break; // do not 'return' – finish loop cleanly
976
+
977
+ // Skip closed/closing connections
978
+ if (connection?.timeline?.close != null) {
979
+ logger.debug(
980
+ "skip outbound stream on closed connection %s",
981
+ connection.remoteAddr?.toString(),
982
+ );
983
+ continue;
984
+ }
985
+
986
+ try {
987
+ // Pass an abort + timeout into your stream open so it cannot hang forever
988
+ const attemptSignal = anySignal([
989
+ this.closeController.signal,
990
+ AbortSignal.timeout(DEFAULT_CREATE_OUTBOUND_STREAM_TIMEOUT), // pick a sensible per-attempt cap
991
+ ]);
992
+ try {
993
+ await this.createOutboundStream(peerId, connection, {
994
+ signal: attemptSignal,
995
+ });
996
+ } finally {
997
+ attemptSignal.clear?.();
998
+ }
999
+ } catch (e: any) {
1000
+ // Treat common shutdowny errors as transient – do NOT crash the pump
1001
+ const msg = String(e?.message ?? e);
1002
+ if (
1003
+ e?.code === "ERR_STREAM_RESET" ||
1004
+ /unexpected end of input|ECONNRESET|EPIPE|Muxer closed|Premature close/i.test(
1005
+ msg,
1006
+ )
1007
+ ) {
1008
+ logger.debug(
1009
+ "createOutboundStream transient failure (%s): %s",
1010
+ connection?.remoteAddr,
1011
+ msg,
1012
+ );
1013
+ } else {
1014
+ logger.warn(
1015
+ "createOutboundStream failed (%s): %o",
1016
+ connection?.remoteAddr,
1017
+ e,
1018
+ );
1019
+ }
1020
+ // continue to next item
944
1021
  }
945
- await this.createOutboundStream(peerId, connection);
946
1022
  }
947
- }).catch((e) => {
948
- logger.error("outbound inflight queue error: " + e?.toString());
949
- });
1023
+ };
1024
+
1025
+ this._outboundPump = pipe(this.outboundInflightQueue, drainOutbound).catch(
1026
+ (e) => {
1027
+ // Only log if we didn't intentionally abort
1028
+ if (!this.closeController.signal.aborted) {
1029
+ logger.error("outbound inflight pipeline crashed: %o", e);
1030
+ // Optional: restart the pump to self-heal
1031
+ this._outboundPump = pipe(
1032
+ this.outboundInflightQueue,
1033
+ drainOutbound,
1034
+ ).catch((err) =>
1035
+ logger.error("outbound pump crashed again: %o", err),
1036
+ );
1037
+ }
1038
+ },
1039
+ );
950
1040
 
951
1041
  this.closeController.signal.addEventListener("abort", () => {
952
1042
  this.outboundInflightQueue.return();
@@ -1123,7 +1213,11 @@ export abstract class DirectStream<
1123
1213
  await this.outboundInflightQueue.push({ peerId, connection });
1124
1214
  }
1125
1215
 
1126
- protected async createOutboundStream(peerId: PeerId, connection: Connection) {
1216
+ protected async createOutboundStream(
1217
+ peerId: PeerId,
1218
+ connection: Connection,
1219
+ opts?: { signal?: AbortSignal },
1220
+ ) {
1127
1221
  for (const existingStreams of connection.streams) {
1128
1222
  if (
1129
1223
  existingStreams.protocol &&
@@ -1150,7 +1244,10 @@ export abstract class DirectStream<
1150
1244
  // research whether we can do without this so we can push data without beeing able to send
1151
1245
  // more info here https://github.com/libp2p/js-libp2p/issues/2321
1152
1246
  negotiateFully: true,
1153
- signal: this.closeController.signal,
1247
+ signal: anySignal([
1248
+ this.closeController.signal,
1249
+ ...(opts?.signal ? [opts.signal] : []),
1250
+ ]),
1154
1251
  });
1155
1252
 
1156
1253
  if (stream.protocol == null) {