h2lean 0.0.0-stage → 0.1.1
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/LICENSE +201 -0
- package/README.md +265 -2
- package/index.js +72 -0
- package/lib/client-session.js +2068 -0
- package/lib/client-stream.js +166 -0
- package/lib/client.js +85 -0
- package/lib/compat.js +849 -0
- package/lib/fast-timer.js +436 -0
- package/lib/frames.js +127 -0
- package/lib/headers.js +463 -0
- package/lib/hpack-data.js +333 -0
- package/lib/hpack.js +1288 -0
- package/lib/pieces.js +134 -0
- package/lib/server.js +272 -0
- package/lib/session.js +2698 -0
- package/lib/stream.js +519 -0
- package/package.json +49 -4
package/lib/stream.js
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
// Http2Stream equivalent: the object a request handler is handed.
|
|
2
|
+
//
|
|
3
|
+
// v1 deliberately implements the parts of the contract that can be honoured
|
|
4
|
+
// honestly, and omits the ones that cannot:
|
|
5
|
+
// * the read side releases flow-control credit as the application takes the
|
|
6
|
+
// bytes. With no reader the peer's window stays shut, so an unread body
|
|
7
|
+
// costs the sender instead of our heap; data that arrives before anyone is
|
|
8
|
+
// listening is buffered and replayed in order, so late listeners lose
|
|
9
|
+
// nothing. pause()/resume()/isPaused() are implemented on that mechanism --
|
|
10
|
+
// there is no partial credit accounting, the side either is read or it isn't.
|
|
11
|
+
// * a delivered chunk stays readable for as long as the application holds it:
|
|
12
|
+
// it is either a view of the buffer node read off the socket, or the private
|
|
13
|
+
// buffer a frame that straddled two reads was assembled into. Neither is ever
|
|
14
|
+
// recycled underneath the application, so queueing a chunk nobody has read yet
|
|
15
|
+
// is safe and the receive path performs no copy.
|
|
16
|
+
// * write()/end() are real and respect both the stream and connection send
|
|
17
|
+
// windows, returning false when the window is exhausted. A body larger than the
|
|
18
|
+
// reference threshold is queued as a view of the caller's buffer and copied
|
|
19
|
+
// nowhere, so the caller must not write into it before the bytes have gone out --
|
|
20
|
+
// the same promise node:http2 makes.
|
|
21
|
+
// * a response the application never started is started for it: the first write()
|
|
22
|
+
// sends HEADERS{:status 200} before the DATA, as node does. An end() with no body at
|
|
23
|
+
// all is the one shape where h2lean answers (200, empty DATA/ES) and node says nothing
|
|
24
|
+
// -- measured, and recorded in AGENTS.md section 10.
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const { EventEmitter } = require('node:events');
|
|
28
|
+
const frames = require('./frames');
|
|
29
|
+
const hdr = require('./headers');
|
|
30
|
+
|
|
31
|
+
const EMPTY = Buffer.alloc(0);
|
|
32
|
+
|
|
33
|
+
// process.nextTick(fn, args) instead of an arrow closure per event: the arrow
|
|
34
|
+
// allocated a new function object for every request, and in the compat layer
|
|
35
|
+
// that showed up as GC time.
|
|
36
|
+
function emitNT(target, name, arg) {
|
|
37
|
+
if (arg === undefined) {
|
|
38
|
+
target.emit(name);
|
|
39
|
+
} else {
|
|
40
|
+
target.emit(name, arg);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function emitErrorCloseNT(target, err) {
|
|
45
|
+
target.emit('error', err);
|
|
46
|
+
target.emit('close');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 'end' and 'close' for a stream whose two halves finished in the same turn.
|
|
50
|
+
// They were already going to run in the same tick drain, so folding them into
|
|
51
|
+
// one callback only removes one TickObject per request.
|
|
52
|
+
function emitEndCloseNT(target) {
|
|
53
|
+
target.emit('end');
|
|
54
|
+
target.emit('close');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
ST_OPEN, ST_HALF_CLOSED_LOCAL, ST_HALF_CLOSED_REMOTE, ST_CLOSED, NO_ERROR, PROTOCOL_ERROR
|
|
59
|
+
} = frames;
|
|
60
|
+
|
|
61
|
+
class Http2Stream extends EventEmitter {
|
|
62
|
+
constructor(session, id) {
|
|
63
|
+
super();
|
|
64
|
+
this.id = id;
|
|
65
|
+
this.session = session;
|
|
66
|
+
this.rstCode = 0;
|
|
67
|
+
this.sentHeaders = undefined;
|
|
68
|
+
this.sentInfoHeaders = [];
|
|
69
|
+
this.sentTrailers = undefined;
|
|
70
|
+
this.headersSent = false;
|
|
71
|
+
this.pending = true;
|
|
72
|
+
this.aborted = false;
|
|
73
|
+
this.endAfterHeaders = false;
|
|
74
|
+
this.allowHalfOpen = false;
|
|
75
|
+
this.state = {
|
|
76
|
+
state: ST_OPEN,
|
|
77
|
+
weight: 16,
|
|
78
|
+
sumDependencyWeight: 0,
|
|
79
|
+
localClose: 0,
|
|
80
|
+
remoteClose: 0,
|
|
81
|
+
localWindowSize: session.localSettings.initialWindowSize
|
|
82
|
+
};
|
|
83
|
+
this._sendWin = session.remoteSettings.initialWindowSize;
|
|
84
|
+
this._recvUsed = 0; // inbound bytes not yet released as credit
|
|
85
|
+
this._unannounced = 0; // ...consumed but not yet sent back as credit
|
|
86
|
+
this._expLen = -1; // content-length promised by the request, -1 = nothing
|
|
87
|
+
this._bodyLen = 0; // ...octets of body actually received
|
|
88
|
+
this._queued = null; // Buffers waiting on window credit
|
|
89
|
+
this._pending = null; // inbound chunks nobody has read yet
|
|
90
|
+
this._endPending = false; // END_STREAM arrived with no reader
|
|
91
|
+
this._consumeState = 0; // 0 no reader, 1 delivering, 2 reader is full
|
|
92
|
+
this._endQueued = false;
|
|
93
|
+
this._waitForTrailers = false;
|
|
94
|
+
this._pendingTrailers = null; // trailer block waiting behind a queued body
|
|
95
|
+
this._destroyed = false;
|
|
96
|
+
// The compat layer's handler for this stream's events, or null when the
|
|
97
|
+
// application uses the stream API. See emit() below.
|
|
98
|
+
this._compatDispatch = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get destroyed() {
|
|
102
|
+
return this._destroyed;
|
|
103
|
+
}
|
|
104
|
+
get closed() {
|
|
105
|
+
return this.state.state === ST_CLOSED;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
respond(headers, options) {
|
|
109
|
+
if (this.headersSent) {
|
|
110
|
+
const err = errorOf('ERR_HTTP2_HEADERS_SENT', 'stream has already sent HEADERS');
|
|
111
|
+
this.emit('error', err);
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
const opts = options || {};
|
|
115
|
+
// node puts a `date` on every response of this API too: prepareResponseHeadersObject()
|
|
116
|
+
// (internal/http2/core.js:2702) adds one unless the application set one itself or
|
|
117
|
+
// `options.sendDate` says otherwise -- and its test is `sendDate == null || sendDate`,
|
|
118
|
+
// so the default is on and only an explicit false (or any other falsy value) turns it
|
|
119
|
+
// off. Measured against a live node server: respond({':status':200}) carries a date,
|
|
120
|
+
// respond({':status':204}) and 304 do too, and sendDate:false removes it.
|
|
121
|
+
const sendDate = opts.sendDate == null ? true : !!opts.sendDate;
|
|
122
|
+
const block = hdr.block(headers, opts.sensitiveHeaders,
|
|
123
|
+
{ sendDate }, this.session);
|
|
124
|
+
this.sentHeaders = headers;
|
|
125
|
+
this.headersSent = true;
|
|
126
|
+
this.pending = false;
|
|
127
|
+
this._waitForTrailers = !!opts.waitForTrailers;
|
|
128
|
+
// node does not infer END_STREAM from the request having ended: the local
|
|
129
|
+
// side only closes when the application says so
|
|
130
|
+
const empty = opts.endStream === true;
|
|
131
|
+
this.session.sendHeaders(this.id, block, empty);
|
|
132
|
+
if (empty) {
|
|
133
|
+
// This response has already carried END_STREAM, and nothing may follow it on this
|
|
134
|
+
// stream: RFC 9113 6.1 puts a trailing HEADERS *before* the close, and node shows
|
|
135
|
+
// what that means for this combination -- respond({waitForTrailers:true,
|
|
136
|
+
// endStream:true}) puts a single HEADERS/ES/EH on the wire and no trailer block.
|
|
137
|
+
// So the outstanding request for trailers is dropped instead of answered after the
|
|
138
|
+
// stream is closed, and 'wantTrailers' is not emitted for a stream that cannot send.
|
|
139
|
+
this._waitForTrailers = false;
|
|
140
|
+
this._endLocalSide();
|
|
141
|
+
}
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
write(chunk, encoding, cb) {
|
|
146
|
+
if (typeof encoding === 'function') {
|
|
147
|
+
cb = encoding; encoding = undefined;
|
|
148
|
+
}
|
|
149
|
+
if (this._endQueued || this.state.localClose === 1) {
|
|
150
|
+
const err = errorOf('ERR_HTTP2_STREAM_CANCEL', 'stream write after end');
|
|
151
|
+
if (cb) {
|
|
152
|
+
return process.nextTick(cb, err);
|
|
153
|
+
}
|
|
154
|
+
this.emit('error', err);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
// The response starts itself if the application never called respond(). Node does this on
|
|
158
|
+
// the first WRITE of a server stream, not on the first end(): measured, `write('x')` alone
|
|
159
|
+
// puts HEADERS{:status 200} on the wire ahead of the DATA. Doing it here as well is what
|
|
160
|
+
// keeps DATA from arriving on a stream that has no response headers yet -- a peer that sees
|
|
161
|
+
// DATA first has nothing to attach it to, and nghttp2 answers that with a stream error.
|
|
162
|
+
if (!this.headersSent) {
|
|
163
|
+
this.respond({ ':status': 200 });
|
|
164
|
+
}
|
|
165
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk, encoding || 'utf8') : chunk;
|
|
166
|
+
const canContinue = this.session.sendData(this, buf, false);
|
|
167
|
+
if (cb) {
|
|
168
|
+
process.nextTick(cb);
|
|
169
|
+
}
|
|
170
|
+
return canContinue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
end(chunk, encoding, cb) {
|
|
174
|
+
if (typeof chunk === 'function') {
|
|
175
|
+
cb = chunk; chunk = null;
|
|
176
|
+
} else if (typeof encoding === 'function') {
|
|
177
|
+
cb = encoding; encoding = null;
|
|
178
|
+
}
|
|
179
|
+
if (!this.headersSent) {
|
|
180
|
+
// A body takes the auto-respond of write() above; an empty end() is where h2lean and node
|
|
181
|
+
// part company, and the split is written down in AGENTS.md section 10: node puts NOTHING on
|
|
182
|
+
// the wire for `end()` before respond() -- no HEADERS, no DATA, no END_STREAM, and
|
|
183
|
+
// headersSent stays false -- while h2lean answers a plain 200 rather than leave a stream
|
|
184
|
+
// that no peer will ever see an answer on.
|
|
185
|
+
this.respond({ ':status': 200 });
|
|
186
|
+
}
|
|
187
|
+
const buf = chunk == null ? EMPTY : (typeof chunk === 'string'
|
|
188
|
+
? Buffer.from(chunk, encoding || 'utf8') : chunk);
|
|
189
|
+
// A stream that still owes a trailer block must not close the stream with its body.
|
|
190
|
+
// RFC 9113 6.1 puts a trailing HEADERS before END_STREAM, and node's frame sequence
|
|
191
|
+
// shows the placement: HEADERS/EH DATA HEADERS/ES/EH for a body, and no DATA at all
|
|
192
|
+
// for an empty one -- where h2lean used to send DATA/ES first and then a trailer block
|
|
193
|
+
// that no peer could still read. The application-level half-close happens here (that
|
|
194
|
+
// is what emits 'wantTrailers'), the wire-level one happens in sendTrailers().
|
|
195
|
+
const oweTrailers = this._waitForTrailers;
|
|
196
|
+
this.session.sendData(this, buf, !oweTrailers);
|
|
197
|
+
if (oweTrailers) {
|
|
198
|
+
this._endLocalSide();
|
|
199
|
+
}
|
|
200
|
+
if (cb) {
|
|
201
|
+
process.nextTick(cb);
|
|
202
|
+
}
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
sendTrailers(trailers) {
|
|
207
|
+
if (!this._waitForTrailers) {
|
|
208
|
+
this.emit('error', errorOf('ERR_HTTP2_TRAILERS_NOT_READY',
|
|
209
|
+
'trailers cannot be sent unless a request is made by setting waitForTrailers'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const block = hdr.block(trailers, undefined, { trailer: true }, this.session);
|
|
213
|
+
this.sentTrailers = trailers;
|
|
214
|
+
// the trailers are out: the flag that permitted this call must not keep asking for
|
|
215
|
+
// wantTrailers, whatever happens to the bytes below
|
|
216
|
+
this._waitForTrailers = false;
|
|
217
|
+
if (this._queued !== null) {
|
|
218
|
+
// The body is still waiting for credit or for the socket, and this block has to
|
|
219
|
+
// follow it: END_STREAM rides on the trailers, so a HEADERS sent now would outrun
|
|
220
|
+
// DATA that has not gone out. Stash a copy (the block is a view into shared scratch)
|
|
221
|
+
// and let the session put it on the wire when the queue drains.
|
|
222
|
+
this._pendingTrailers = Buffer.from(block);
|
|
223
|
+
return this;
|
|
224
|
+
}
|
|
225
|
+
this.session.sendHeaders(this.id, block, true);
|
|
226
|
+
// the local half closes for good now: the bytes that owed it are on the wire
|
|
227
|
+
this._endLocalSide();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
close(code = NO_ERROR, reason) {
|
|
231
|
+
this._destroy(code, reason, true);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
destroy(codeOrError, reason) {
|
|
235
|
+
const code = typeof codeOrError === 'number' ? codeOrError : NO_ERROR;
|
|
236
|
+
this._destroy(code, reason, true, typeof codeOrError === 'object' ? codeOrError : null);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// An informational (1xx) HEADERS block, sent before the final response.
|
|
240
|
+
// node: Http2Stream.additionalHeaders().
|
|
241
|
+
additionalHeaders(headers) {
|
|
242
|
+
// `{ info: true }` is node's separate rule for these blocks: :status must be 100..199
|
|
243
|
+
// (101 has its own error), there is no default status and no generated date.
|
|
244
|
+
const block = hdr.block(headers, undefined, { info: true }, this.session);
|
|
245
|
+
this.sentInfoHeaders.push(headers);
|
|
246
|
+
this.session.sendHeaders(this.id, block, false);
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
priority() {
|
|
251
|
+
// RFC 9218 deprecates the priority scheme; node still accepts the call.
|
|
252
|
+
// Returning true without reordering anything is the honest v1 answer.
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Readable-side control, node's contract: while paused no credit is released,
|
|
257
|
+
// so the sender is the one that waits, and whatever arrives in the meantime is
|
|
258
|
+
// buffered and replayed on resume(). Returns this, like the Duplex does.
|
|
259
|
+
pause() {
|
|
260
|
+
this._pauseConsuming();
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
resume() {
|
|
265
|
+
this._startConsuming();
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
isPaused() {
|
|
270
|
+
return this._consumeState === 2;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// node maps a per-stream timeout onto the connection's idle timeout.
|
|
274
|
+
setTimeout(ms, cb) {
|
|
275
|
+
this.session.setTimeout(ms, cb);
|
|
276
|
+
return this;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- internals used by the session ------------------------------------------
|
|
280
|
+
// Both halves have to converge: a handler that answers synchronously inside
|
|
281
|
+
// the 'stream' emit closes the local side BEFORE the peer's END_STREAM is
|
|
282
|
+
// reported, and vice versa. Whichever call sees both halves closed is the one
|
|
283
|
+
// that removes the stream from the session -- without it every request leaks
|
|
284
|
+
// an Http2Stream (and _liveStreams never falls, so close() never finishes).
|
|
285
|
+
_remoteEnded() {
|
|
286
|
+
if (this.state.remoteClose === 1) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// The other half of the content-length promise: too many octets is caught as they arrive
|
|
290
|
+
// (session _onDataFrame, a connection error), too few can only be seen here, where the
|
|
291
|
+
// message ends. Measured on node (h2spec http2/8.1.2.6): RST_STREAM(PROTOCOL_ERROR) and the
|
|
292
|
+
// application is told ERR_HTTP2_STREAM_ERROR -- the short body is one request's failure, not
|
|
293
|
+
// the session's, which is the same split nghttp2 makes and the reason the two directions of
|
|
294
|
+
// the same lie get two different codes.
|
|
295
|
+
if (this._expLen >= 0 && this._bodyLen < this._expLen) {
|
|
296
|
+
this.session._streamError(this, PROTOCOL_ERROR, `content-length ${this._expLen} with only ` +
|
|
297
|
+
`${this._bodyLen} octets received`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.state.remoteClose = 1;
|
|
301
|
+
this.endAfterHeaders = true;
|
|
302
|
+
// the readable side is done: 'end' after the last 'data', like a Readable.
|
|
303
|
+
// No auto-destroy: the application still has to answer. node keeps the
|
|
304
|
+
// writable side of an unanswered request open as well.
|
|
305
|
+
if (this.state.localClose === 1) {
|
|
306
|
+
this._closeNow();
|
|
307
|
+
process.nextTick(emitEndCloseNT, this);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (this.state.state === ST_OPEN) {
|
|
311
|
+
this.state.state = ST_HALF_CLOSED_REMOTE;
|
|
312
|
+
}
|
|
313
|
+
if (!this._isConsuming()) {
|
|
314
|
+
// Nothing has read the buffered bytes yet, so 'end' must not overtake them:
|
|
315
|
+
// a reader that attaches later is owed the whole sequence, EOF included.
|
|
316
|
+
this._endPending = true;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
process.nextTick(emitNT, this, 'end');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_endLocalSide() {
|
|
323
|
+
const first = this.state.localClose !== 1;
|
|
324
|
+
this.state.localClose = 1;
|
|
325
|
+
this._endQueued = false;
|
|
326
|
+
if (this.state.state === ST_OPEN) {
|
|
327
|
+
this.state.state = ST_HALF_CLOSED_LOCAL;
|
|
328
|
+
}
|
|
329
|
+
if (this._waitForTrailers) {
|
|
330
|
+
// one shot: a later end() must not ask for trailers a second time. The flag
|
|
331
|
+
// itself stays set, because sendTrailers() is only allowed while it is set.
|
|
332
|
+
if (first) {
|
|
333
|
+
process.nextTick(emitNT, this, 'wantTrailers');
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (this.state.remoteClose === 1) {
|
|
338
|
+
this._closeIfBothDone();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_closeIfBothDone() {
|
|
343
|
+
if (this._destroyed || this.state.localClose !== 1 || this.state.remoteClose !== 1) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this._closeNow();
|
|
347
|
+
// An EOF that arrived while nobody was reading is still owed to the application, and it
|
|
348
|
+
// goes out before 'close' -- the same pair the both-halves-in-one-turn path above uses.
|
|
349
|
+
// Without this the stream simply never reported 'end' for a request that ended on its
|
|
350
|
+
// HEADERS frame (END_STREAM on HEADERS is every GET): _remoteEnded() had parked the EOF
|
|
351
|
+
// in _endPending for a reader that never appeared, and the local close below was the last
|
|
352
|
+
// event the stream ever produced. Measured against node on exactly that shape: it emits
|
|
353
|
+
// stream.end then stream.close (test/event-order.test.js, scenario eof).
|
|
354
|
+
if (this._endPending) {
|
|
355
|
+
this._endPending = false;
|
|
356
|
+
process.nextTick(emitEndCloseNT, this);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
process.nextTick(emitNT, this, 'close');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_closeNow() {
|
|
363
|
+
this.state.state = ST_CLOSED;
|
|
364
|
+
this._destroyed = true;
|
|
365
|
+
this._pending = null;
|
|
366
|
+
this.session._dropStream(this, false);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
_destroy(code, reason, sendRst, err) {
|
|
370
|
+
if (this._destroyed) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this._destroyed = true;
|
|
374
|
+
this.rstCode = code;
|
|
375
|
+
// node's rule for 'aborted' is about the RESPONSE, not about the request:
|
|
376
|
+
// "if the writable side of the Http2Stream is still open, emit the 'aborted'
|
|
377
|
+
// event" (internal/http2/core.js, closeStream). Measured against a live node
|
|
378
|
+
// server: a peer that resets a stream whose request has already ended but
|
|
379
|
+
// whose response never started does report 'aborted', and a peer that resets
|
|
380
|
+
// a stream whose response was ended does not.
|
|
381
|
+
const responseOpen = this.state.localClose !== 1;
|
|
382
|
+
this.state.localClose = 1;
|
|
383
|
+
this.state.state = ST_CLOSED;
|
|
384
|
+
if (sendRst && this.session._open) {
|
|
385
|
+
this.session.sendRst(this.id, code, reason);
|
|
386
|
+
}
|
|
387
|
+
this._pending = null;
|
|
388
|
+
this.session._dropStream(this, true);
|
|
389
|
+
// The event is emitted synchronously -- before the 'end' that an already-queued
|
|
390
|
+
// tick would otherwise deliver first. Marking the remote half closed keeps a
|
|
391
|
+
// late END_STREAM from emitting 'end' after 'close'.
|
|
392
|
+
this.state.remoteClose = 1;
|
|
393
|
+
if (responseOpen) {
|
|
394
|
+
this.aborted = true;
|
|
395
|
+
this.emit('aborted');
|
|
396
|
+
}
|
|
397
|
+
if (err) {
|
|
398
|
+
process.nextTick(emitErrorCloseNT, this, err);
|
|
399
|
+
} else {
|
|
400
|
+
process.nextTick(emitNT, this, 'close');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- the compat layer's handlers ------------------------------------------------
|
|
405
|
+
// The compat layer does not subscribe to the stream's events: it installs one
|
|
406
|
+
// dispatch function and the stream calls it -- subscriptions are an order of
|
|
407
|
+
// magnitude more expensive per request than the dispatch (OPTIMIZATION.md
|
|
408
|
+
// item 5) -- and the handlers want the stream as `this` anyway.
|
|
409
|
+
//
|
|
410
|
+
// The dispatch runs BEFORE the event reaches the emitter, which is the order
|
|
411
|
+
// the subscriptions had: the compat layer's own handlers first, the
|
|
412
|
+
// application's listeners after.
|
|
413
|
+
emit(name, arg) {
|
|
414
|
+
const dispatch = this._compatDispatch;
|
|
415
|
+
if (dispatch !== null) {
|
|
416
|
+
dispatch.call(this, name, arg);
|
|
417
|
+
}
|
|
418
|
+
// The argument list is forwarded as it came: a listener that counts its
|
|
419
|
+
// arguments must not see an extra undefined.
|
|
420
|
+
if (arguments.length === 1) {
|
|
421
|
+
return super.emit(name);
|
|
422
|
+
}
|
|
423
|
+
if (arguments.length === 2) {
|
|
424
|
+
return super.emit(name, arg);
|
|
425
|
+
}
|
|
426
|
+
return super.emit.apply(this, arguments);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// --- inbound flow control ----------------------------------------------------
|
|
430
|
+
// Attaching a reader is how the application says "send me the body": replay
|
|
431
|
+
// whatever arrived before it did. attach after super.on(), or the replay would
|
|
432
|
+
// emit into an empty listener list. once()/addListener are covered by the
|
|
433
|
+
// listenerCount() check at delivery time.
|
|
434
|
+
on(name, fn) {
|
|
435
|
+
const r = super.on(name, fn);
|
|
436
|
+
if (name === 'data' || name === 'readable') {
|
|
437
|
+
this._startConsuming();
|
|
438
|
+
}
|
|
439
|
+
return r;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
once(name, fn) {
|
|
443
|
+
const r = super.once(name, fn);
|
|
444
|
+
if (name === 'data' || name === 'readable') {
|
|
445
|
+
this._startConsuming();
|
|
446
|
+
}
|
|
447
|
+
return r;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Credit goes back when the application takes the bytes, so a body nobody
|
|
451
|
+
// reads closes the sender's window instead of growing our heap. The buffer of
|
|
452
|
+
// unread chunks cannot exceed the window we advertised, because the peer is
|
|
453
|
+
// not allowed to send more than that without a credit we never gave.
|
|
454
|
+
_isConsuming() {
|
|
455
|
+
if (this._consumeState === 2) {
|
|
456
|
+
return false;
|
|
457
|
+
} // the reader asked us to stop
|
|
458
|
+
return this._consumeState === 1 || this.listenerCount('data') > 0 ||
|
|
459
|
+
this.listenerCount('readable') > 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Called when a reader shows up: replay what was buffered, in order, and let
|
|
463
|
+
// later frames through directly.
|
|
464
|
+
_startConsuming() {
|
|
465
|
+
this._consumeState = 1;
|
|
466
|
+
const pending = this._pending;
|
|
467
|
+
if (pending !== null) {
|
|
468
|
+
this._pending = null;
|
|
469
|
+
for (let i = 0; i < pending.length; i++) {
|
|
470
|
+
const chunk = pending[i];
|
|
471
|
+
this._release(chunk.length);
|
|
472
|
+
this.emit('data', chunk);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (this._endPending && !this._destroyed) {
|
|
476
|
+
this._endPending = false;
|
|
477
|
+
process.nextTick(emitNT, this, 'end');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Internal pause, used by the compat layer when its Readable is full. There is
|
|
482
|
+
// no public pause()/resume() yet, and this is the only reason it exists.
|
|
483
|
+
_pauseConsuming() {
|
|
484
|
+
this._consumeState = 2;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
_release(n) {
|
|
488
|
+
if (n > 0) {
|
|
489
|
+
this.session._consume(this, n);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
_recvData(buf) {
|
|
494
|
+
const n = buf.length;
|
|
495
|
+
if (n === 0) {
|
|
496
|
+
return;
|
|
497
|
+
} // an empty DATA frame carries no payload
|
|
498
|
+
this._recvUsed += n;
|
|
499
|
+
if (this._isConsuming()) {
|
|
500
|
+
this._release(n);
|
|
501
|
+
this.emit('data', buf);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
// the chunk is either a view of a socket read or a buffer built for this frame
|
|
505
|
+
// alone; both outlive the tick, so it can simply be queued
|
|
506
|
+
if (this._pending === null) {
|
|
507
|
+
this._pending = [];
|
|
508
|
+
}
|
|
509
|
+
this._pending.push(buf);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function errorOf(code, msg) {
|
|
514
|
+
const err = new Error(msg);
|
|
515
|
+
err.code = code;
|
|
516
|
+
return err;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
module.exports = { Http2Stream, errorOf, emitNT };
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "h2lean",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pure-JavaScript HTTP/2 for Node.js — a drop-in node:http2 replacement (server and client, h2c and TLS) with zero dependencies",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib/",
|
|
8
|
+
"index.js"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/MakeNodeGreatAgain/h2lean.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"http2",
|
|
16
|
+
"h2c",
|
|
17
|
+
"node:http2",
|
|
18
|
+
"nghttp2",
|
|
19
|
+
"server",
|
|
20
|
+
"client",
|
|
21
|
+
"pure-javascript",
|
|
22
|
+
"undici",
|
|
23
|
+
"fastify",
|
|
24
|
+
"http",
|
|
25
|
+
"grpc"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test --test-timeout=120000 test/*.test.js && node --test --test-timeout=120000 test/client/*.test.js",
|
|
29
|
+
"test:server": "node --test --test-timeout=120000 test/*.test.js",
|
|
30
|
+
"test:client": "node --test --test-timeout=120000 test/client/*.test.js",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"spec": "node tools/check-h2spec.js",
|
|
33
|
+
"bench:showcase": "node bench/showcase/matrix.js",
|
|
34
|
+
"bench:client": "node bench/client/matrix.js",
|
|
35
|
+
"gen": "node tools/gen-hpack-data.js"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"author": {
|
|
42
|
+
"name": "MakeNodeGreatAgain",
|
|
43
|
+
"email": "351629238+MakeNodeGreatAgain@users.noreply.github.com"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^10.0.1",
|
|
47
|
+
"eslint": "^10.14.0",
|
|
48
|
+
"fast-xml-parser": "^5.12.0",
|
|
49
|
+
"uWebSockets.js": "github:uNetworking/UWebSockets.js#v20.70.0"
|
|
50
|
+
}
|
|
51
|
+
}
|