@libp2p/yamux 7.0.4-a02cb0461

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.
Files changed (49) hide show
  1. package/README.md +127 -0
  2. package/dist/index.min.js +4 -0
  3. package/dist/index.min.js.map +7 -0
  4. package/dist/src/config.d.ts +30 -0
  5. package/dist/src/config.d.ts.map +1 -0
  6. package/dist/src/config.js +41 -0
  7. package/dist/src/config.js.map +1 -0
  8. package/dist/src/constants.d.ts +12 -0
  9. package/dist/src/constants.d.ts.map +1 -0
  10. package/dist/src/constants.js +22 -0
  11. package/dist/src/constants.js.map +1 -0
  12. package/dist/src/decode.d.ts +34 -0
  13. package/dist/src/decode.d.ts.map +1 -0
  14. package/dist/src/decode.js +76 -0
  15. package/dist/src/decode.js.map +1 -0
  16. package/dist/src/encode.d.ts +3 -0
  17. package/dist/src/encode.d.ts.map +1 -0
  18. package/dist/src/encode.js +19 -0
  19. package/dist/src/encode.js.map +1 -0
  20. package/dist/src/errors.d.ts +40 -0
  21. package/dist/src/errors.d.ts.map +1 -0
  22. package/dist/src/errors.js +70 -0
  23. package/dist/src/errors.js.map +1 -0
  24. package/dist/src/frame.d.ts +54 -0
  25. package/dist/src/frame.d.ts.map +1 -0
  26. package/dist/src/frame.js +36 -0
  27. package/dist/src/frame.js.map +1 -0
  28. package/dist/src/index.d.ts +85 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/index.js +84 -0
  31. package/dist/src/index.js.map +1 -0
  32. package/dist/src/muxer.d.ts +91 -0
  33. package/dist/src/muxer.d.ts.map +1 -0
  34. package/dist/src/muxer.js +428 -0
  35. package/dist/src/muxer.js.map +1 -0
  36. package/dist/src/stream.d.ts +96 -0
  37. package/dist/src/stream.d.ts.map +1 -0
  38. package/dist/src/stream.js +253 -0
  39. package/dist/src/stream.js.map +1 -0
  40. package/package.json +95 -0
  41. package/src/config.ts +72 -0
  42. package/src/constants.ts +25 -0
  43. package/src/decode.ts +105 -0
  44. package/src/encode.ts +26 -0
  45. package/src/errors.ts +89 -0
  46. package/src/frame.ts +64 -0
  47. package/src/index.ts +90 -0
  48. package/src/muxer.ts +507 -0
  49. package/src/stream.ts +313 -0
package/src/frame.ts ADDED
@@ -0,0 +1,64 @@
1
+ export enum FrameType {
2
+ /** Used to transmit data. May transmit zero length payloads depending on the flags. */
3
+ Data = 0x0,
4
+ /** Used to updated the senders receive window size. This is used to implement per-session flow control. */
5
+ WindowUpdate = 0x1,
6
+ /** Used to measure RTT. It can also be used to heart-beat and do keep-alive over TCP. */
7
+ Ping = 0x2,
8
+ /** Used to close a session. */
9
+ GoAway = 0x3
10
+ }
11
+
12
+ export enum Flag {
13
+ /** Signals the start of a new stream. May be sent with a data or window update message. Also sent with a ping to indicate outbound. */
14
+ SYN = 0x1,
15
+ /** Acknowledges the start of a new stream. May be sent with a data or window update message. Also sent with a ping to indicate response. */
16
+ ACK = 0x2,
17
+ /** Performs a half-close of a stream. May be sent with a data message or window update. */
18
+ FIN = 0x4,
19
+ /** Reset a stream immediately. May be sent with a data or window update message. */
20
+ RST = 0x8
21
+ }
22
+
23
+ const flagCodes = Object.values(Flag).filter((x) => typeof x !== 'string') as Flag[]
24
+
25
+ export const YAMUX_VERSION = 0
26
+
27
+ export enum GoAwayCode {
28
+ NormalTermination = 0x0,
29
+ ProtocolError = 0x1,
30
+ InternalError = 0x2
31
+ }
32
+
33
+ export const HEADER_LENGTH = 12
34
+
35
+ export interface FrameHeader {
36
+ /**
37
+ * The version field is used for future backward compatibility.
38
+ * At the current time, the field is always set to 0, to indicate the initial version.
39
+ */
40
+ version?: number
41
+ /** The type field is used to switch the frame message type. */
42
+ type: FrameType
43
+ /** The flags field is used to provide additional information related to the message type. */
44
+ flag: number
45
+ /**
46
+ * The StreamID field is used to identify the logical stream the frame is addressing.
47
+ * The client side should use odd ID's, and the server even.
48
+ * This prevents any collisions. Additionally, the 0 ID is reserved to represent the session.
49
+ */
50
+ streamID: number
51
+ /**
52
+ * The meaning of the length field depends on the message type:
53
+ * Data - provides the length of bytes following the header
54
+ * Window update - provides a delta update to the window size
55
+ * Ping - Contains an opaque value, echoed back
56
+ * Go Away - Contains an error code
57
+ */
58
+ length: number
59
+ }
60
+
61
+ export function stringifyHeader (header: FrameHeader): string {
62
+ const flags = flagCodes.filter(f => (header.flag & f) === f).map(f => Flag[f]).join('|')
63
+ return `streamID=${header.streamID} type=${FrameType[header.type]} flag=${flags} length=${header.length}`
64
+ }
package/src/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * This module is a JavaScript implementation of [Yamux from Hashicorp](https://github.com/hashicorp/yamux/blob/master/spec.md) designed to be used with [js-libp2p](https://github.com/libp2p/js-libp2p).
5
+ *
6
+ * @example Configure libp2p with Yamux
7
+ *
8
+ * ```typescript
9
+ * import { createLibp2p } from 'libp2p'
10
+ * import { yamux } from '@libp2p/yamux'
11
+ *
12
+ * const node = await createLibp2p({
13
+ * // ... other options
14
+ * streamMuxers: [
15
+ * yamux()
16
+ * ]
17
+ * })
18
+ * ```
19
+ *
20
+ * @example Using the low-level API
21
+ *
22
+ * ```js
23
+ * import { yamux } from '@libp2p/yamux'
24
+ * import { pipe } from 'it-pipe'
25
+ * import { duplexPair } from 'it-pair/duplex'
26
+ * import all from 'it-all'
27
+ *
28
+ * // Connect two yamux muxers to demo basic stream multiplexing functionality
29
+ *
30
+ * const clientMuxer = yamux({
31
+ * client: true,
32
+ * onIncomingStream: stream => {
33
+ * // echo data on incoming streams
34
+ * pipe(stream, stream)
35
+ * },
36
+ * onStreamEnd: stream => {
37
+ * // do nothing
38
+ * }
39
+ * })()
40
+ *
41
+ * const serverMuxer = yamux({
42
+ * client: false,
43
+ * onIncomingStream: stream => {
44
+ * // echo data on incoming streams
45
+ * pipe(stream, stream)
46
+ * },
47
+ * onStreamEnd: stream => {
48
+ * // do nothing
49
+ * }
50
+ * })()
51
+ *
52
+ * // `p` is our "connections", what we use to connect the two sides
53
+ * // In a real application, a connection is usually to a remote computer
54
+ * const p = duplexPair()
55
+ *
56
+ * // connect the muxers together
57
+ * pipe(p[0], clientMuxer, p[0])
58
+ * pipe(p[1], serverMuxer, p[1])
59
+ *
60
+ * // now either side can open streams
61
+ * const stream0 = clientMuxer.newStream()
62
+ * const stream1 = serverMuxer.newStream()
63
+ *
64
+ * // Send some data to the other side
65
+ * const encoder = new TextEncoder()
66
+ * const data = [encoder.encode('hello'), encoder.encode('world')]
67
+ * pipe(data, stream0)
68
+ *
69
+ * // Receive data back
70
+ * const result = await pipe(stream0, all)
71
+ *
72
+ * // close a stream
73
+ * stream1.close()
74
+ *
75
+ * // close the muxer
76
+ * clientMuxer.close()
77
+ * ```
78
+ */
79
+
80
+ import { Yamux } from './muxer.js'
81
+ import type { YamuxMuxer, YamuxMuxerInit } from './muxer.js'
82
+ import type { StreamMuxerFactory } from '@libp2p/interface'
83
+
84
+ export { GoAwayCode } from './frame.js'
85
+ export type { FrameHeader, FrameType } from './frame.js'
86
+ export type { YamuxMuxerInit }
87
+
88
+ export function yamux (init: YamuxMuxerInit = {}): () => StreamMuxerFactory<YamuxMuxer> {
89
+ return () => new Yamux(init)
90
+ }
package/src/muxer.ts ADDED
@@ -0,0 +1,507 @@
1
+ import { InvalidParametersError, MuxerClosedError, TooManyOutboundProtocolStreamsError, serviceCapabilities } from '@libp2p/interface'
2
+ import { AbstractStreamMuxer, repeatingTask } from '@libp2p/utils'
3
+ import { raceSignal } from 'race-signal'
4
+ import { Uint8ArrayList } from 'uint8arraylist'
5
+ import { defaultConfig, verifyConfig } from './config.js'
6
+ import { Decoder } from './decode.js'
7
+ import { encodeHeader } from './encode.js'
8
+ import { InvalidFrameError, isProtocolError, NotMatchingPingError, UnRequestedPingError } from './errors.js'
9
+ import { Flag, FrameType, GoAwayCode } from './frame.js'
10
+ import { StreamState, YamuxStream } from './stream.js'
11
+ import type { Config } from './config.js'
12
+ import type { Frame } from './decode.js'
13
+ import type { FrameHeader } from './frame.js'
14
+ import type { AbortOptions, MessageStream, StreamMuxerFactory } from '@libp2p/interface'
15
+ import type { RepeatingTask } from '@libp2p/utils'
16
+
17
+ function debugFrame (header: FrameHeader): any {
18
+ return {
19
+ type: FrameType[header.type],
20
+ flags: [
21
+ (header.flag & Flag.SYN) === Flag.SYN ? 'SYN' : undefined,
22
+ (header.flag & Flag.ACK) === Flag.ACK ? 'ACK' : undefined,
23
+ (header.flag & Flag.FIN) === Flag.FIN ? 'FIN' : undefined,
24
+ (header.flag & Flag.RST) === Flag.RST ? 'RST' : undefined
25
+ ].filter(Boolean),
26
+ streamID: header.streamID,
27
+ length: header.length
28
+ }
29
+ }
30
+
31
+ const YAMUX_PROTOCOL_ID = '/yamux/1.0.0'
32
+
33
+ export interface YamuxMuxerInit extends Partial<Config> {
34
+ }
35
+
36
+ export class Yamux implements StreamMuxerFactory {
37
+ protocol = YAMUX_PROTOCOL_ID
38
+ private readonly _init: Partial<Config>
39
+
40
+ constructor (init: Partial<Config> = {}) {
41
+ this._init = init
42
+ }
43
+
44
+ readonly [Symbol.toStringTag] = '@libp2p/yamux'
45
+
46
+ readonly [serviceCapabilities]: string[] = [
47
+ '@libp2p/stream-multiplexing'
48
+ ]
49
+
50
+ createStreamMuxer (maConn: MessageStream): YamuxMuxer {
51
+ return new YamuxMuxer(maConn, {
52
+ ...this._init
53
+ })
54
+ }
55
+ }
56
+
57
+ export interface CloseOptions extends AbortOptions {
58
+ reason?: GoAwayCode
59
+ }
60
+
61
+ export interface ActivePing extends PromiseWithResolvers<number> {
62
+ id: number
63
+ start: number
64
+ }
65
+
66
+ export class YamuxMuxer extends AbstractStreamMuxer<YamuxStream> {
67
+ /** The next stream id to be used when initiating a new stream */
68
+ private nextStreamID: number
69
+
70
+ /** The next ping id to be used when pinging */
71
+ private nextPingID: number
72
+ /** Tracking info for the currently active ping */
73
+ private activePing?: ActivePing
74
+ /** Round trip time */
75
+ private rtt: number
76
+
77
+ /** True if client, false if server */
78
+ private client: boolean
79
+
80
+ private localGoAway?: GoAwayCode
81
+ private remoteGoAway?: GoAwayCode
82
+
83
+ /** Number of tracked inbound streams */
84
+ private numInboundStreams: number
85
+ /** Number of tracked outbound streams */
86
+ private numOutboundStreams: number
87
+
88
+ private decoder: Decoder
89
+ private keepAlive?: RepeatingTask
90
+
91
+ private enableKeepAlive: boolean
92
+ private keepAliveInterval: number
93
+ private maxInboundStreams: number
94
+ private maxOutboundStreams: number
95
+
96
+ constructor (maConn: MessageStream, init: YamuxMuxerInit = {}) {
97
+ super(maConn, {
98
+ ...init,
99
+ protocol: YAMUX_PROTOCOL_ID,
100
+ name: 'yamux'
101
+ })
102
+
103
+ this.client = maConn.direction === 'outbound'
104
+ verifyConfig(init)
105
+
106
+ this.enableKeepAlive = init.enableKeepAlive ?? defaultConfig.enableKeepAlive
107
+ this.keepAliveInterval = init.keepAliveInterval ?? defaultConfig.keepAliveInterval
108
+ this.maxInboundStreams = init.maxInboundStreams ?? defaultConfig.maxInboundStreams
109
+ this.maxOutboundStreams = init.maxOutboundStreams ?? defaultConfig.maxOutboundStreams
110
+
111
+ this.decoder = new Decoder()
112
+
113
+ this.numInboundStreams = 0
114
+ this.numOutboundStreams = 0
115
+
116
+ // client uses odd streamIDs, server uses even streamIDs
117
+ this.nextStreamID = this.client ? 1 : 2
118
+
119
+ this.nextPingID = 0
120
+ this.rtt = -1
121
+
122
+ this.log.trace('muxer created')
123
+
124
+ if (this.enableKeepAlive) {
125
+ this.log.trace('muxer keepalive enabled interval=%s', this.keepAliveInterval)
126
+ this.keepAlive = repeatingTask(async (options) => {
127
+ try {
128
+ await this.ping(options)
129
+ } catch (err: any) {
130
+ // TODO: should abort here?
131
+ this.log.error('ping error: %s', err)
132
+ }
133
+ }, this.keepAliveInterval, {
134
+ // send an initial ping to establish RTT
135
+ runImmediately: true
136
+ })
137
+ this.keepAlive.start()
138
+ }
139
+ }
140
+
141
+ onData (buf: Uint8Array | Uint8ArrayList): void {
142
+ for (const frame of this.decoder.emitFrames(buf)) {
143
+ this.handleFrame(frame)
144
+ }
145
+ }
146
+
147
+ onCreateStream (): YamuxStream {
148
+ if (this.remoteGoAway !== undefined) {
149
+ throw new MuxerClosedError('Muxer closed remotely')
150
+ }
151
+
152
+ if (this.localGoAway !== undefined) {
153
+ throw new MuxerClosedError('Muxer closed locally')
154
+ }
155
+
156
+ const id = this.nextStreamID
157
+ this.nextStreamID += 2
158
+
159
+ // check against our configured maximum number of outbound streams
160
+ if (this.numOutboundStreams >= this.maxOutboundStreams) {
161
+ throw new TooManyOutboundProtocolStreamsError('max outbound streams exceeded')
162
+ }
163
+
164
+ this.log.trace('new outgoing stream id=%s', id)
165
+
166
+ const stream = this._newStream(id, StreamState.Init, 'outbound')
167
+
168
+ this.numOutboundStreams++
169
+
170
+ // send a window update to open the stream on the receiver end. do this in a
171
+ // microtask so the stream gets added to the streams array by the superclass
172
+ // before we send the SYN flag, otherwise we create a race condition whereby
173
+ // we can receive the ACK before the stream is added to the streams list
174
+ queueMicrotask(() => {
175
+ stream.sendWindowUpdate()
176
+ })
177
+
178
+ return stream
179
+ }
180
+
181
+ /**
182
+ * Initiate a ping and wait for a response
183
+ *
184
+ * Note: only a single ping will be initiated at a time.
185
+ * If a ping is already in progress, a new ping will not be initiated.
186
+ *
187
+ * @returns the round-trip-time in milliseconds
188
+ */
189
+ async ping (options?: AbortOptions): Promise<number> {
190
+ if (this.remoteGoAway !== undefined) {
191
+ throw new MuxerClosedError('Muxer closed remotely')
192
+ }
193
+ if (this.localGoAway !== undefined) {
194
+ throw new MuxerClosedError('Muxer closed locally')
195
+ }
196
+
197
+ if (this.activePing != null) {
198
+ // an active ping is already in progress, piggyback off that
199
+ return raceSignal(this.activePing.promise, options?.signal)
200
+ }
201
+
202
+ // An active ping does not yet exist, handle the process here
203
+ // create active ping
204
+ this.activePing = Object.assign(Promise.withResolvers<number>(), {
205
+ id: this.nextPingID++,
206
+ start: Date.now()
207
+ })
208
+ // send ping
209
+ this.sendPing(this.activePing.id)
210
+ // await pong
211
+ try {
212
+ this.rtt = await raceSignal(this.activePing.promise, options?.signal)
213
+ } finally {
214
+ // clean-up active ping
215
+ this.activePing = undefined
216
+ }
217
+
218
+ return this.rtt
219
+ }
220
+
221
+ /**
222
+ * Get the ping round trip time
223
+ *
224
+ * Note: Will return 0 if no successful ping has yet been completed
225
+ *
226
+ * @returns the round-trip-time in milliseconds
227
+ */
228
+ getRTT (): number {
229
+ return this.rtt
230
+ }
231
+
232
+ /**
233
+ * Close the muxer
234
+ */
235
+ async close (options: CloseOptions = {}): Promise<void> {
236
+ if (this.status !== 'open') {
237
+ // already closed
238
+ return
239
+ }
240
+
241
+ try {
242
+ const reason = options?.reason ?? GoAwayCode.NormalTermination
243
+
244
+ this.log.trace('muxer close reason=%s', GoAwayCode[reason])
245
+
246
+ await super.close(options)
247
+
248
+ // send reason to the other side, allow the other side to close gracefully
249
+ this.sendGoAway(reason)
250
+ } finally {
251
+ this.keepAlive?.stop()
252
+ }
253
+ }
254
+
255
+ abort (err: Error): void {
256
+ if (this.status !== 'open') {
257
+ // already closed
258
+ return
259
+ }
260
+
261
+ try {
262
+ super.abort(err)
263
+
264
+ let reason = GoAwayCode.InternalError
265
+
266
+ if (isProtocolError(err)) {
267
+ reason = err.reason
268
+ }
269
+
270
+ // If reason was provided, use that, otherwise use the presence of `err` to determine the reason
271
+ this.log.error('muxer abort reason=%s error=%s', reason, err)
272
+
273
+ // send reason to the other side, allow the other side to close gracefully
274
+ this.sendGoAway(reason)
275
+ } finally {
276
+ this.keepAlive?.stop()
277
+ }
278
+ }
279
+
280
+ onTransportClosed (): void {
281
+ try {
282
+ super.onTransportClosed()
283
+ } finally {
284
+ this.keepAlive?.stop()
285
+ }
286
+ }
287
+
288
+ /** Create a new stream */
289
+ private _newStream (streamId: number, state: StreamState, direction: 'inbound' | 'outbound'): YamuxStream {
290
+ if (this.streams.find(s => s.streamId === streamId) != null) {
291
+ throw new InvalidParametersError('Stream already exists with that id')
292
+ }
293
+
294
+ const stream = new YamuxStream({
295
+ ...this.streamOptions,
296
+ id: `${streamId}`,
297
+ streamId,
298
+ state,
299
+ direction,
300
+ sendFrame: this.sendFrame.bind(this),
301
+ log: this.log.newScope(`${direction}:${streamId}`),
302
+ getRTT: this.getRTT.bind(this)
303
+ })
304
+
305
+ stream.addEventListener('close', () => {
306
+ this.closeStream(streamId)
307
+ }, {
308
+ once: true
309
+ })
310
+
311
+ return stream
312
+ }
313
+
314
+ /**
315
+ * closeStream is used to close a stream once both sides have
316
+ * issued a close.
317
+ */
318
+ private closeStream (id: number): void {
319
+ if (this.client === (id % 2 === 0)) {
320
+ this.numInboundStreams--
321
+ } else {
322
+ this.numOutboundStreams--
323
+ }
324
+ }
325
+
326
+ private handleFrame (frame: Frame): void {
327
+ const {
328
+ streamID,
329
+ type,
330
+ length
331
+ } = frame.header
332
+
333
+ this.log.trace('received frame %o', debugFrame(frame.header))
334
+
335
+ if (streamID === 0) {
336
+ switch (type) {
337
+ case FrameType.Ping:
338
+ { this.handlePing(frame.header); return }
339
+ case FrameType.GoAway:
340
+ { this.handleGoAway(length); return }
341
+ default:
342
+ // Invalid state
343
+ throw new InvalidFrameError('Invalid frame type')
344
+ }
345
+ } else {
346
+ switch (frame.header.type) {
347
+ case FrameType.Data:
348
+ case FrameType.WindowUpdate:
349
+ { this.handleStreamMessage(frame); return }
350
+ default:
351
+ // Invalid state
352
+ throw new InvalidFrameError('Invalid frame type')
353
+ }
354
+ }
355
+ }
356
+
357
+ private handlePing (header: FrameHeader): void {
358
+ // If the ping is initiated by the sender, send a response
359
+ if (header.flag === Flag.SYN) {
360
+ this.log.trace('received ping request pingId=%s', header.length)
361
+ this.sendPing(header.length, Flag.ACK)
362
+ } else if (header.flag === Flag.ACK) {
363
+ this.log.trace('received ping response pingId=%s', header.length)
364
+ this.handlePingResponse(header.length)
365
+ } else {
366
+ // Invalid state
367
+ throw new InvalidFrameError('Invalid frame flag')
368
+ }
369
+ }
370
+
371
+ private handlePingResponse (pingId: number): void {
372
+ if (this.activePing === undefined) {
373
+ // this ping was not requested
374
+ throw new UnRequestedPingError('ping not requested')
375
+ }
376
+ if (this.activePing.id !== pingId) {
377
+ // this ping doesn't match our active ping request
378
+ throw new NotMatchingPingError('ping doesn\'t match our id')
379
+ }
380
+
381
+ // valid ping response
382
+ this.activePing.resolve(Date.now() - this.activePing.start)
383
+ }
384
+
385
+ private handleGoAway (reason: GoAwayCode): void {
386
+ this.log.trace('received GoAway reason=%s', GoAwayCode[reason] ?? 'unknown')
387
+ this.remoteGoAway = reason
388
+
389
+ if (reason === GoAwayCode.NormalTermination) {
390
+ this.onTransportClosed()
391
+ } else {
392
+ // reset any streams that are still open and close the muxer
393
+ this.abort(new Error('Remote sent GoAway'))
394
+ }
395
+ }
396
+
397
+ private handleStreamMessage (frame: Frame): void {
398
+ const { streamID, flag, type } = frame.header
399
+
400
+ if ((flag & Flag.SYN) === Flag.SYN) {
401
+ this.incomingStream(streamID)
402
+ }
403
+
404
+ const stream = this.streams.find(s => s.streamId === streamID)
405
+ if (stream === undefined) {
406
+ this.log.trace('frame for missing stream id=%s', streamID)
407
+
408
+ return
409
+ }
410
+
411
+ switch (type) {
412
+ case FrameType.WindowUpdate: {
413
+ stream.handleWindowUpdate(frame); return
414
+ }
415
+ case FrameType.Data: {
416
+ stream.handleData(frame); return
417
+ }
418
+ default:
419
+ throw new Error('unreachable')
420
+ }
421
+ }
422
+
423
+ private incomingStream (id: number): void {
424
+ if (this.client !== (id % 2 === 0)) {
425
+ throw new InvalidParametersError('Both endpoints are clients')
426
+ }
427
+ if (this.streams.find(s => s.streamId === id)) {
428
+ return
429
+ }
430
+
431
+ this.log.trace('new incoming stream id=%s', id)
432
+
433
+ if (this.localGoAway !== undefined) {
434
+ // reject (reset) immediately if we are doing a go away
435
+ this.sendFrame({
436
+ type: FrameType.WindowUpdate,
437
+ flag: Flag.RST,
438
+ streamID: id,
439
+ length: 0
440
+ })
441
+ return
442
+ }
443
+
444
+ // check against our configured maximum number of inbound streams
445
+ if (this.numInboundStreams >= this.maxInboundStreams) {
446
+ this.log('maxIncomingStreams exceeded, forcing stream reset')
447
+ this.sendFrame({
448
+ type: FrameType.WindowUpdate,
449
+ flag: Flag.RST,
450
+ streamID: id,
451
+ length: 0
452
+ })
453
+ return
454
+ }
455
+
456
+ // allocate a new stream
457
+ const stream = this._newStream(id, StreamState.SYNReceived, 'inbound')
458
+
459
+ this.numInboundStreams++
460
+
461
+ // the stream should now be tracked
462
+ this.onRemoteStream(stream)
463
+ }
464
+
465
+ private sendFrame (header: FrameHeader, data?: Uint8ArrayList): boolean {
466
+ let encoded: Uint8Array | Uint8ArrayList
467
+
468
+ if (header.type === FrameType.Data) {
469
+ if (data == null) {
470
+ throw new InvalidFrameError('Invalid frame')
471
+ }
472
+
473
+ encoded = new Uint8ArrayList(encodeHeader(header), data)
474
+ } else {
475
+ encoded = encodeHeader(header)
476
+ }
477
+
478
+ this.log.trace('sending frame %o', debugFrame(header))
479
+
480
+ return this.send(encoded)
481
+ }
482
+
483
+ private sendPing (pingId: number, flag: Flag = Flag.SYN): void {
484
+ if (flag === Flag.SYN) {
485
+ this.log.trace('sending ping request pingId=%s', pingId)
486
+ } else {
487
+ this.log.trace('sending ping response pingId=%s', pingId)
488
+ }
489
+ this.sendFrame({
490
+ type: FrameType.Ping,
491
+ flag,
492
+ streamID: 0,
493
+ length: pingId
494
+ })
495
+ }
496
+
497
+ private sendGoAway (reason: GoAwayCode = GoAwayCode.NormalTermination): void {
498
+ this.log('sending GoAway reason=%s', GoAwayCode[reason])
499
+ this.localGoAway = reason
500
+ this.sendFrame({
501
+ type: FrameType.GoAway,
502
+ flag: 0,
503
+ streamID: 0,
504
+ length: reason
505
+ })
506
+ }
507
+ }