@libp2p/utils 4.0.7-05b52d69c → 4.0.7-0b4a2ee79

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.
@@ -4,48 +4,71 @@
4
4
  */
5
5
  export function streamToMaConnection(props) {
6
6
  const { stream, remoteAddr, logger } = props;
7
- const { sink, source } = stream;
8
7
  const log = logger.forComponent('libp2p:stream:converter');
9
- const mapSource = (async function* () {
10
- for await (const list of source) {
11
- if (list instanceof Uint8Array) {
12
- yield list;
13
- }
14
- else {
15
- yield* list;
8
+ let closedRead = false;
9
+ let closedWrite = false;
10
+ // piggyback on `stream.close` invocations to close maconn
11
+ const streamClose = stream.close.bind(stream);
12
+ stream.close = async (options) => {
13
+ await streamClose(options);
14
+ close(true);
15
+ };
16
+ // piggyback on `stream.abort` invocations to close maconn
17
+ const streamAbort = stream.abort.bind(stream);
18
+ stream.abort = (err) => {
19
+ streamAbort(err);
20
+ close(true);
21
+ };
22
+ // piggyback on `stream.sink` invocations to close maconn
23
+ const streamSink = stream.sink.bind(stream);
24
+ stream.sink = async (source) => {
25
+ try {
26
+ await streamSink(source);
27
+ }
28
+ catch (err) {
29
+ // If aborted we can safely ignore
30
+ if (err.type !== 'aborted') {
31
+ // If the source errored the socket will already have been destroyed by
32
+ // toIterable.duplex(). If the socket errored it will already be
33
+ // destroyed. There's nothing to do here except log the error & return.
34
+ log(err);
16
35
  }
17
36
  }
18
- }());
37
+ finally {
38
+ closedWrite = true;
39
+ close();
40
+ }
41
+ };
19
42
  const maConn = {
20
- async sink(source) {
43
+ log,
44
+ sink: stream.sink,
45
+ source: (async function* () {
21
46
  try {
22
- await sink(source);
23
- close();
24
- }
25
- catch (err) {
26
- // If aborted we can safely ignore
27
- if (err.type !== 'aborted') {
28
- // If the source errored the socket will already have been destroyed by
29
- // toIterable.duplex(). If the socket errored it will already be
30
- // destroyed. There's nothing to do here except log the error & return.
31
- log(err);
47
+ for await (const list of stream.source) {
48
+ if (list instanceof Uint8Array) {
49
+ yield list;
50
+ }
51
+ else {
52
+ yield* list;
53
+ }
32
54
  }
33
55
  }
34
- },
35
- source: mapSource,
56
+ finally {
57
+ closedRead = true;
58
+ close();
59
+ }
60
+ }()),
36
61
  remoteAddr,
37
62
  timeline: { open: Date.now(), close: undefined },
38
- async close(options) {
39
- close();
40
- await stream.close(options);
41
- },
42
- abort(err) {
43
- close();
44
- stream.abort(err);
45
- }
63
+ close: stream.close,
64
+ abort: stream.abort
46
65
  };
47
- function close() {
48
- if (maConn.timeline.close == null) {
66
+ function close(force) {
67
+ if (force === true) {
68
+ closedRead = true;
69
+ closedWrite = true;
70
+ }
71
+ if (closedRead && closedWrite && maConn.timeline.close == null) {
49
72
  maConn.timeline.close = Date.now();
50
73
  }
51
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"stream-to-ma-conn.js","sourceRoot":"","sources":["../../src/stream-to-ma-conn.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAE,KAAuB;IAC3D,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAC5C,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;IAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,yBAAyB,CAAC,CAAA;IAE1D,MAAM,SAAS,GAAG,CAAC,KAAK,SAAU,CAAC;QACjC,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,EAAE;YAC/B,IAAI,IAAI,YAAY,UAAU,EAAE;gBAC9B,MAAM,IAAI,CAAA;aACX;iBAAM;gBACL,KAAM,CAAC,CAAC,IAAI,CAAA;aACb;SACF;IACH,CAAC,EAAE,CAAC,CAAA;IAEJ,MAAM,MAAM,GAAwB;QAClC,KAAK,CAAC,IAAI,CAAE,MAAM;YAChB,IAAI;gBACF,MAAM,IAAI,CAAC,MAAM,CAAC,CAAA;gBAClB,KAAK,EAAE,CAAA;aACR;YAAC,OAAO,GAAQ,EAAE;gBACjB,kCAAkC;gBAClC,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE;oBAC1B,uEAAuE;oBACvE,gEAAgE;oBAChE,uEAAuE;oBACvE,GAAG,CAAC,GAAG,CAAC,CAAA;iBACT;aACF;QACH,CAAC;QACD,MAAM,EAAE,SAAS;QACjB,UAAU;QACV,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;QAChD,KAAK,CAAC,KAAK,CAAE,OAAsB;YACjC,KAAK,EAAE,CAAA;YACP,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC7B,CAAC;QACD,KAAK,CAAE,GAAU;YACf,KAAK,EAAE,CAAA;YACP,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACnB,CAAC;KACF,CAAA;IAED,SAAS,KAAK;QACZ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE;YACjC,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;SACnC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
1
+ {"version":3,"file":"stream-to-ma-conn.js","sourceRoot":"","sources":["../../src/stream-to-ma-conn.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAE,KAAuB;IAC3D,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,KAAK,CAAA;IAC5C,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,yBAAyB,CAAC,CAAA;IAE1D,IAAI,UAAU,GAAG,KAAK,CAAA;IACtB,IAAI,WAAW,GAAG,KAAK,CAAA;IAEvB,0DAA0D;IAC1D,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC7C,MAAM,CAAC,KAAK,GAAG,KAAK,EAAE,OAAO,EAAE,EAAE;QAC/B,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;QAC1B,KAAK,CAAC,IAAI,CAAC,CAAA;IACb,CAAC,CAAA;IAED,0DAA0D;IAC1D,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC7C,MAAM,CAAC,KAAK,GAAG,CAAC,GAAG,EAAE,EAAE;QACrB,WAAW,CAAC,GAAG,CAAC,CAAA;QAChB,KAAK,CAAC,IAAI,CAAC,CAAA;IACb,CAAC,CAAA;IAED,yDAAyD;IACzD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC3C,MAAM,CAAC,IAAI,GAAG,KAAK,EAAE,MAAM,EAAE,EAAE;QAC7B,IAAI;YACF,MAAM,UAAU,CAAC,MAAM,CAAC,CAAA;SACzB;QAAC,OAAO,GAAQ,EAAE;YACjB,kCAAkC;YAClC,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE;gBAC1B,uEAAuE;gBACvE,gEAAgE;gBAChE,uEAAuE;gBACvE,GAAG,CAAC,GAAG,CAAC,CAAA;aACT;SACF;gBAAS;YACR,WAAW,GAAG,IAAI,CAAA;YAClB,KAAK,EAAE,CAAA;SACR;IACH,CAAC,CAAA;IAED,MAAM,MAAM,GAAwB;QAClC,GAAG;QACH,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,CAAC,KAAK,SAAU,CAAC;YACvB,IAAI;gBACF,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE;oBACtC,IAAI,IAAI,YAAY,UAAU,EAAE;wBAC9B,MAAM,IAAI,CAAA;qBACX;yBAAM;wBACL,KAAM,CAAC,CAAC,IAAI,CAAA;qBACb;iBACF;aACF;oBAAS;gBACR,UAAU,GAAG,IAAI,CAAA;gBACjB,KAAK,EAAE,CAAA;aACR;QACH,CAAC,EAAE,CAAC;QACJ,UAAU;QACV,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE;QAChD,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAA;IAED,SAAS,KAAK,CAAE,KAAe;QAC7B,IAAI,KAAK,KAAK,IAAI,EAAE;YAClB,UAAU,GAAG,IAAI,CAAA;YACjB,WAAW,GAAG,IAAI,CAAA;SACnB;QAED,IAAI,UAAU,IAAI,WAAW,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE;YAC9D,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;SACnC;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@libp2p/utils",
3
- "version": "4.0.7-05b52d69c",
3
+ "version": "4.0.7-0b4a2ee79",
4
4
  "description": "Package to aggregate shared logic and dependencies for the libp2p ecosystem",
5
5
  "license": "Apache-2.0 OR MIT",
6
- "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/utils#readme",
6
+ "homepage": "https://github.com/libp2p/js-libp2p/tree/main/packages/utils#readme",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/libp2p/js-libp2p.git"
@@ -40,6 +40,10 @@
40
40
  "types": "./src/index.d.ts",
41
41
  "import": "./dist/src/index.js"
42
42
  },
43
+ "./abstract-stream": {
44
+ "types": "./dist/src/abstract-stream.d.ts",
45
+ "import": "./dist/src/abstract-stream.js"
46
+ },
43
47
  "./address-sort": {
44
48
  "types": "./dist/src/address-sort.d.ts",
45
49
  "import": "./dist/src/address-sort.js"
@@ -48,10 +52,18 @@
48
52
  "types": "./dist/src/array-equals.d.ts",
49
53
  "import": "./dist/src/array-equals.js"
50
54
  },
55
+ "./close-source": {
56
+ "types": "./dist/src/close-source.d.ts",
57
+ "import": "./dist/src/close-source.js"
58
+ },
51
59
  "./ip-port-to-multiaddr": {
52
60
  "types": "./dist/src/ip-port-to-multiaddr.d.ts",
53
61
  "import": "./dist/src/ip-port-to-multiaddr.js"
54
62
  },
63
+ "./is-promise": {
64
+ "types": "./dist/src/is-promise.d.ts",
65
+ "import": "./dist/src/is-promise.js"
66
+ },
55
67
  "./multiaddr/is-loopback": {
56
68
  "types": "./dist/src/multiaddr/is-loopback.d.ts",
57
69
  "import": "./dist/src/multiaddr/is-loopback.js"
@@ -91,23 +103,30 @@
91
103
  },
92
104
  "dependencies": {
93
105
  "@chainsafe/is-ip": "^2.0.2",
94
- "@libp2p/interface": "0.1.6-05b52d69c",
106
+ "@libp2p/interface": "0.1.6-0b4a2ee79",
95
107
  "@multiformats/multiaddr": "^12.1.10",
96
108
  "@multiformats/multiaddr-matcher": "^1.1.0",
109
+ "get-iterator": "^2.0.1",
97
110
  "is-loopback-addr": "^2.0.1",
111
+ "it-pushable": "^3.2.2",
98
112
  "it-stream-types": "^2.0.1",
99
113
  "p-queue": "^7.4.1",
100
114
  "private-ip": "^3.0.1",
115
+ "race-signal": "^1.0.1",
101
116
  "uint8arraylist": "^2.4.3"
102
117
  },
103
118
  "devDependencies": {
104
- "@libp2p/logger": "3.1.0-05b52d69c",
105
- "@libp2p/peer-id-factory": "3.0.8-05b52d69c",
119
+ "@libp2p/logger": "3.1.0-0b4a2ee79",
120
+ "@libp2p/peer-id-factory": "3.0.8-0b4a2ee79",
106
121
  "aegir": "^41.0.2",
122
+ "delay": "^6.0.0",
107
123
  "it-all": "^3.0.3",
124
+ "it-drain": "^3.0.5",
108
125
  "it-pair": "^2.0.6",
109
126
  "it-pipe": "^3.0.1",
110
127
  "p-defer": "^4.0.0",
128
+ "sinon": "^17.0.1",
129
+ "sinon-ts": "^2.0.0",
111
130
  "uint8arrays": "^4.0.6"
112
131
  }
113
132
  }
@@ -0,0 +1,509 @@
1
+ import { CodeError } from '@libp2p/interface/errors'
2
+ import { type Pushable, pushable } from 'it-pushable'
3
+ import defer, { type DeferredPromise } from 'p-defer'
4
+ import { raceSignal } from 'race-signal'
5
+ import { Uint8ArrayList } from 'uint8arraylist'
6
+ import { closeSource } from './close-source.js'
7
+ import type { AbortOptions } from '@libp2p/interface'
8
+ import type { Direction, ReadStatus, Stream, StreamStatus, StreamTimeline, WriteStatus } from '@libp2p/interface/connection'
9
+ import type { Logger } from '@libp2p/logger'
10
+ import type { Source } from 'it-stream-types'
11
+
12
+ const ERR_STREAM_RESET = 'ERR_STREAM_RESET'
13
+ const ERR_SINK_INVALID_STATE = 'ERR_SINK_INVALID_STATE'
14
+ const DEFAULT_SEND_CLOSE_WRITE_TIMEOUT = 5000
15
+
16
+ export interface AbstractStreamInit {
17
+ /**
18
+ * A unique identifier for this stream
19
+ */
20
+ id: string
21
+
22
+ /**
23
+ * The stream direction
24
+ */
25
+ direction: Direction
26
+
27
+ /**
28
+ * A Logger implementation used to log stream-specific information
29
+ */
30
+ log: Logger
31
+
32
+ /**
33
+ * User specific stream metadata
34
+ */
35
+ metadata?: Record<string, unknown>
36
+
37
+ /**
38
+ * Invoked when the stream ends
39
+ */
40
+ onEnd?(err?: Error | undefined): void
41
+
42
+ /**
43
+ * Invoked when the readable end of the stream is closed
44
+ */
45
+ onCloseRead?(): void
46
+
47
+ /**
48
+ * Invoked when the writable end of the stream is closed
49
+ */
50
+ onCloseWrite?(): void
51
+
52
+ /**
53
+ * Invoked when the the stream has been reset by the remote
54
+ */
55
+ onReset?(): void
56
+
57
+ /**
58
+ * Invoked when the the stream has errored
59
+ */
60
+ onAbort?(err: Error): void
61
+
62
+ /**
63
+ * How long to wait in ms for stream data to be written to the underlying
64
+ * connection when closing the writable end of the stream. (default: 500)
65
+ */
66
+ closeTimeout?: number
67
+
68
+ /**
69
+ * After the stream sink has closed, a limit on how long it takes to send
70
+ * a close-write message to the remote peer.
71
+ */
72
+ sendCloseWriteTimeout?: number
73
+ }
74
+
75
+ function isPromise <T = unknown> (thing: any): thing is Promise<T> {
76
+ if (thing == null) {
77
+ return false
78
+ }
79
+
80
+ return typeof thing.then === 'function' &&
81
+ typeof thing.catch === 'function' &&
82
+ typeof thing.finally === 'function'
83
+ }
84
+
85
+ export abstract class AbstractStream implements Stream {
86
+ public id: string
87
+ public direction: Direction
88
+ public timeline: StreamTimeline
89
+ public protocol?: string
90
+ public metadata: Record<string, unknown>
91
+ public source: AsyncGenerator<Uint8ArrayList, void, unknown>
92
+ public status: StreamStatus
93
+ public readStatus: ReadStatus
94
+ public writeStatus: WriteStatus
95
+ public readonly log: Logger
96
+
97
+ private readonly sinkController: AbortController
98
+ private readonly sinkEnd: DeferredPromise<void>
99
+ private readonly closed: DeferredPromise<void>
100
+ private endErr: Error | undefined
101
+ private readonly streamSource: Pushable<Uint8ArrayList>
102
+ private readonly onEnd?: (err?: Error | undefined) => void
103
+ private readonly onCloseRead?: () => void
104
+ private readonly onCloseWrite?: () => void
105
+ private readonly onReset?: () => void
106
+ private readonly onAbort?: (err: Error) => void
107
+ private readonly sendCloseWriteTimeout: number
108
+
109
+ constructor (init: AbstractStreamInit) {
110
+ this.sinkController = new AbortController()
111
+ this.sinkEnd = defer()
112
+ this.closed = defer()
113
+ this.log = init.log
114
+
115
+ // stream status
116
+ this.status = 'open'
117
+ this.readStatus = 'ready'
118
+ this.writeStatus = 'ready'
119
+
120
+ this.id = init.id
121
+ this.metadata = init.metadata ?? {}
122
+ this.direction = init.direction
123
+ this.timeline = {
124
+ open: Date.now()
125
+ }
126
+ this.sendCloseWriteTimeout = init.sendCloseWriteTimeout ?? DEFAULT_SEND_CLOSE_WRITE_TIMEOUT
127
+
128
+ this.onEnd = init.onEnd
129
+ this.onCloseRead = init?.onCloseRead
130
+ this.onCloseWrite = init?.onCloseWrite
131
+ this.onReset = init?.onReset
132
+ this.onAbort = init?.onAbort
133
+
134
+ this.source = this.streamSource = pushable<Uint8ArrayList>({
135
+ onEnd: (err) => {
136
+ if (err != null) {
137
+ this.log.trace('source ended with error', err)
138
+ } else {
139
+ this.log.trace('source ended')
140
+ }
141
+
142
+ this.onSourceEnd(err)
143
+ }
144
+ })
145
+
146
+ // necessary because the libp2p upgrader wraps the sink function
147
+ this.sink = this.sink.bind(this)
148
+ }
149
+
150
+ async sink (source: Source<Uint8ArrayList | Uint8Array>): Promise<void> {
151
+ if (this.writeStatus !== 'ready') {
152
+ throw new CodeError(`writable end state is "${this.writeStatus}" not "ready"`, ERR_SINK_INVALID_STATE)
153
+ }
154
+
155
+ try {
156
+ this.writeStatus = 'writing'
157
+
158
+ const options: AbortOptions = {
159
+ signal: this.sinkController.signal
160
+ }
161
+
162
+ if (this.direction === 'outbound') { // If initiator, open a new stream
163
+ const res = this.sendNewStream(options)
164
+
165
+ if (isPromise(res)) {
166
+ await res
167
+ }
168
+ }
169
+
170
+ const abortListener = (): void => {
171
+ closeSource(source, this.log)
172
+ }
173
+
174
+ try {
175
+ this.sinkController.signal.addEventListener('abort', abortListener)
176
+
177
+ this.log.trace('sink reading from source')
178
+
179
+ for await (let data of source) {
180
+ data = data instanceof Uint8Array ? new Uint8ArrayList(data) : data
181
+
182
+ const res = this.sendData(data, options)
183
+
184
+ if (isPromise(res)) { // eslint-disable-line max-depth
185
+ await res
186
+ }
187
+ }
188
+ } finally {
189
+ this.sinkController.signal.removeEventListener('abort', abortListener)
190
+ }
191
+
192
+ this.log.trace('sink finished reading from source, write status is "%s"', this.writeStatus)
193
+
194
+ if (this.writeStatus === 'writing') {
195
+ this.writeStatus = 'closing'
196
+
197
+ this.log.trace('send close write to remote')
198
+ await this.sendCloseWrite({
199
+ signal: AbortSignal.timeout(this.sendCloseWriteTimeout)
200
+ })
201
+
202
+ this.writeStatus = 'closed'
203
+ }
204
+
205
+ this.onSinkEnd()
206
+ } catch (err: any) {
207
+ this.log.trace('sink ended with error, calling abort with error', err)
208
+ this.abort(err)
209
+
210
+ throw err
211
+ } finally {
212
+ this.log.trace('resolve sink end')
213
+ this.sinkEnd.resolve()
214
+ }
215
+ }
216
+
217
+ protected onSourceEnd (err?: Error): void {
218
+ if (this.timeline.closeRead != null) {
219
+ return
220
+ }
221
+
222
+ this.timeline.closeRead = Date.now()
223
+ this.readStatus = 'closed'
224
+
225
+ if (err != null && this.endErr == null) {
226
+ this.endErr = err
227
+ }
228
+
229
+ this.onCloseRead?.()
230
+
231
+ if (this.timeline.closeWrite != null) {
232
+ this.log.trace('source and sink ended')
233
+ this.timeline.close = Date.now()
234
+
235
+ if (this.status !== 'aborted' && this.status !== 'reset') {
236
+ this.status = 'closed'
237
+ }
238
+
239
+ if (this.onEnd != null) {
240
+ this.onEnd(this.endErr)
241
+ }
242
+
243
+ this.closed.resolve()
244
+ } else {
245
+ this.log.trace('source ended, waiting for sink to end')
246
+ }
247
+ }
248
+
249
+ protected onSinkEnd (err?: Error): void {
250
+ if (this.timeline.closeWrite != null) {
251
+ return
252
+ }
253
+
254
+ this.timeline.closeWrite = Date.now()
255
+ this.writeStatus = 'closed'
256
+
257
+ if (err != null && this.endErr == null) {
258
+ this.endErr = err
259
+ }
260
+
261
+ this.onCloseWrite?.()
262
+
263
+ if (this.timeline.closeRead != null) {
264
+ this.log.trace('sink and source ended')
265
+ this.timeline.close = Date.now()
266
+
267
+ if (this.status !== 'aborted' && this.status !== 'reset') {
268
+ this.status = 'closed'
269
+ }
270
+
271
+ if (this.onEnd != null) {
272
+ this.onEnd(this.endErr)
273
+ }
274
+
275
+ this.closed.resolve()
276
+ } else {
277
+ this.log.trace('sink ended, waiting for source to end')
278
+ }
279
+ }
280
+
281
+ // Close for both Reading and Writing
282
+ async close (options?: AbortOptions): Promise<void> {
283
+ this.log.trace('closing gracefully')
284
+
285
+ this.status = 'closing'
286
+
287
+ await Promise.all([
288
+ this.closeRead(options),
289
+ this.closeWrite(options)
290
+ ])
291
+
292
+ // wait for read and write ends to close
293
+ await raceSignal(this.closed.promise, options?.signal)
294
+
295
+ this.status = 'closed'
296
+
297
+ this.log.trace('closed gracefully')
298
+ }
299
+
300
+ async closeRead (options: AbortOptions = {}): Promise<void> {
301
+ if (this.readStatus === 'closing' || this.readStatus === 'closed') {
302
+ return
303
+ }
304
+
305
+ this.log.trace('closing readable end of stream with starting read status "%s"', this.readStatus)
306
+
307
+ const readStatus = this.readStatus
308
+ this.readStatus = 'closing'
309
+
310
+ if (this.status !== 'reset' && this.status !== 'aborted' && this.timeline.closeRead == null) {
311
+ this.log.trace('send close read to remote')
312
+ await this.sendCloseRead(options)
313
+ }
314
+
315
+ if (readStatus === 'ready') {
316
+ this.log.trace('ending internal source queue with %d queued bytes', this.streamSource.readableLength)
317
+ this.streamSource.end()
318
+ }
319
+
320
+ this.log.trace('closed readable end of stream')
321
+ }
322
+
323
+ async closeWrite (options: AbortOptions = {}): Promise<void> {
324
+ if (this.writeStatus === 'closing' || this.writeStatus === 'closed') {
325
+ return
326
+ }
327
+
328
+ this.log.trace('closing writable end of stream with starting write status "%s"', this.writeStatus)
329
+
330
+ if (this.writeStatus === 'ready') {
331
+ this.log.trace('sink was never sunk, sink an empty array')
332
+
333
+ await raceSignal(this.sink([]), options.signal)
334
+ }
335
+
336
+ if (this.writeStatus === 'writing') {
337
+ // stop reading from the source passed to `.sink` in the microtask queue
338
+ // - this lets any data queued by the user in the current tick get read
339
+ // before we exit
340
+ await new Promise((resolve, reject) => {
341
+ queueMicrotask(() => {
342
+ this.log.trace('aborting source passed to .sink')
343
+ this.sinkController.abort()
344
+ raceSignal(this.sinkEnd.promise, options.signal)
345
+ .then(resolve, reject)
346
+ })
347
+ })
348
+ }
349
+
350
+ this.writeStatus = 'closed'
351
+
352
+ this.log.trace('closed writable end of stream')
353
+ }
354
+
355
+ /**
356
+ * Close immediately for reading and writing and send a reset message (local
357
+ * error)
358
+ */
359
+ abort (err: Error): void {
360
+ if (this.status === 'closed' || this.status === 'aborted' || this.status === 'reset') {
361
+ return
362
+ }
363
+
364
+ this.log('abort with error', err)
365
+
366
+ // try to send a reset message
367
+ this.log('try to send reset to remote')
368
+ const res = this.sendReset()
369
+
370
+ if (isPromise(res)) {
371
+ res.catch((err) => {
372
+ this.log.error('error sending reset message', err)
373
+ })
374
+ }
375
+
376
+ this.status = 'aborted'
377
+ this.timeline.abort = Date.now()
378
+ this._closeSinkAndSource(err)
379
+ this.onAbort?.(err)
380
+ }
381
+
382
+ /**
383
+ * Receive a reset message - close immediately for reading and writing (remote
384
+ * error)
385
+ */
386
+ reset (): void {
387
+ if (this.status === 'closed' || this.status === 'aborted' || this.status === 'reset') {
388
+ return
389
+ }
390
+
391
+ const err = new CodeError('stream reset', ERR_STREAM_RESET)
392
+
393
+ this.status = 'reset'
394
+ this.timeline.reset = Date.now()
395
+ this._closeSinkAndSource(err)
396
+ this.onReset?.()
397
+ }
398
+
399
+ _closeSinkAndSource (err?: Error): void {
400
+ this._closeSink(err)
401
+ this._closeSource(err)
402
+ }
403
+
404
+ _closeSink (err?: Error): void {
405
+ // if the sink function is running, cause it to end
406
+ if (this.writeStatus === 'writing') {
407
+ this.log.trace('end sink source')
408
+ this.sinkController.abort()
409
+ }
410
+
411
+ this.onSinkEnd(err)
412
+ }
413
+
414
+ _closeSource (err?: Error): void {
415
+ // if the source is not ending, end it
416
+ if (this.readStatus !== 'closing' && this.readStatus !== 'closed') {
417
+ this.log.trace('ending source with %d bytes to be read by consumer', this.streamSource.readableLength)
418
+ this.readStatus = 'closing'
419
+ this.streamSource.end(err)
420
+ }
421
+ }
422
+
423
+ /**
424
+ * The remote closed for writing so we should expect to receive no more
425
+ * messages
426
+ */
427
+ remoteCloseWrite (): void {
428
+ if (this.readStatus === 'closing' || this.readStatus === 'closed') {
429
+ this.log('received remote close write but local source is already closed')
430
+ return
431
+ }
432
+
433
+ this.log.trace('remote close write')
434
+ this._closeSource()
435
+ }
436
+
437
+ /**
438
+ * The remote closed for reading so we should not send any more
439
+ * messages
440
+ */
441
+ remoteCloseRead (): void {
442
+ if (this.writeStatus === 'closing' || this.writeStatus === 'closed') {
443
+ this.log('received remote close read but local sink is already closed')
444
+ return
445
+ }
446
+
447
+ this.log.trace('remote close read')
448
+ this._closeSink()
449
+ }
450
+
451
+ /**
452
+ * The underlying muxer has closed, no more messages can be sent or will
453
+ * be received, close immediately to free up resources
454
+ */
455
+ destroy (): void {
456
+ if (this.status === 'closed' || this.status === 'aborted' || this.status === 'reset') {
457
+ this.log('received destroy but we are already closed')
458
+ return
459
+ }
460
+
461
+ this.log.trace('stream destroyed')
462
+
463
+ this._closeSinkAndSource()
464
+ }
465
+
466
+ /**
467
+ * When an extending class reads data from it's implementation-specific source,
468
+ * call this method to allow the stream consumer to read the data.
469
+ */
470
+ sourcePush (data: Uint8ArrayList): void {
471
+ this.streamSource.push(data)
472
+ }
473
+
474
+ /**
475
+ * Returns the amount of unread data - can be used to prevent large amounts of
476
+ * data building up when the stream consumer is too slow.
477
+ */
478
+ sourceReadableLength (): number {
479
+ return this.streamSource.readableLength
480
+ }
481
+
482
+ /**
483
+ * Send a message to the remote muxer informing them a new stream is being
484
+ * opened
485
+ */
486
+ abstract sendNewStream (options?: AbortOptions): void | Promise<void>
487
+
488
+ /**
489
+ * Send a data message to the remote muxer
490
+ */
491
+ abstract sendData (buf: Uint8ArrayList, options?: AbortOptions): void | Promise<void>
492
+
493
+ /**
494
+ * Send a reset message to the remote muxer
495
+ */
496
+ abstract sendReset (options?: AbortOptions): void | Promise<void>
497
+
498
+ /**
499
+ * Send a message to the remote muxer, informing them no more data messages
500
+ * will be sent by this end of the stream
501
+ */
502
+ abstract sendCloseWrite (options?: AbortOptions): void | Promise<void>
503
+
504
+ /**
505
+ * Send a message to the remote muxer, informing them no more data messages
506
+ * will be read by this end of the stream
507
+ */
508
+ abstract sendCloseRead (options?: AbortOptions): void | Promise<void>
509
+ }