h2lean 0.0.0-stage → 0.1.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.
@@ -0,0 +1,436 @@
1
+ 'use strict';
2
+
3
+ // Copied from 'undici'
4
+ //
5
+ // This file is deliberately a byte-for-byte copy of that source, with no local
6
+ // changes, so that it can be lifted out into a standalone npm package and have
7
+ // h2lite require it instead. Any deviation here would make that extraction a
8
+ // merge rather than a move, and would break the upstream tests that come with it.
9
+ //
10
+ // What h2lite uses it for: one shared 499 ms clock for all idle/keep-alive
11
+ // timeouts, instead of one libuv timer handle per connection. See
12
+ // Http2Session.setTimeout() in lib/session.js.
13
+
14
+ /**
15
+ * This module offers an optimized timer implementation designed for scenarios
16
+ * where high precision is not critical.
17
+ *
18
+ * The timer achieves faster performance by using a low-resolution approach,
19
+ * with an accuracy target of within 500ms. This makes it particularly useful
20
+ * for timers with delays of 1 second or more, where exact timing is less
21
+ * crucial.
22
+ *
23
+ * It's important to note that Node.js timers are inherently imprecise, as
24
+ * delays can occur due to the event loop being blocked by other operations.
25
+ * Consequently, timers may trigger later than their scheduled time.
26
+ */
27
+
28
+ /**
29
+ * The fastNow variable contains the internal fast timer clock value.
30
+ *
31
+ * @type {number}
32
+ */
33
+ let fastNow = 0;
34
+
35
+ /**
36
+ * RESOLUTION_MS represents the target resolution time in milliseconds.
37
+ *
38
+ * @type {number}
39
+ * @default 1000
40
+ */
41
+ const RESOLUTION_MS = 1e3;
42
+
43
+ /**
44
+ * TICK_MS defines the desired interval in milliseconds between each tick.
45
+ * The target value is set to half the resolution time, minus 1 ms, to account
46
+ * for potential event loop overhead.
47
+ *
48
+ * @type {number}
49
+ * @default 499
50
+ */
51
+ const TICK_MS = (RESOLUTION_MS >> 1) - 1;
52
+
53
+ /**
54
+ * fastNowTimeout is a Node.js timer used to manage and process
55
+ * the FastTimers stored in the `fastTimers` array.
56
+ *
57
+ * @type {NodeJS.Timeout}
58
+ */
59
+ let fastNowTimeout;
60
+
61
+ /**
62
+ * The kFastTimer symbol is used to identify FastTimer instances.
63
+ *
64
+ * @type {Symbol}
65
+ */
66
+ const kFastTimer = Symbol('kFastTimer');
67
+
68
+ /**
69
+ * The fastTimers array contains all active FastTimers.
70
+ *
71
+ * @type {FastTimer[]}
72
+ */
73
+ const fastTimers = [];
74
+
75
+ /**
76
+ * These constants represent the various states of a FastTimer.
77
+ */
78
+
79
+ /**
80
+ * The `NOT_IN_LIST` constant indicates that the FastTimer is not included
81
+ * in the `fastTimers` array. Timers with this status will not be processed
82
+ * during the next tick by the `onTick` function.
83
+ *
84
+ * A FastTimer can be re-added to the `fastTimers` array by invoking the
85
+ * `refresh` method on the FastTimer instance.
86
+ *
87
+ * @type {-2}
88
+ */
89
+ const NOT_IN_LIST = -2;
90
+
91
+ /**
92
+ * The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled
93
+ * for removal from the `fastTimers` array. A FastTimer in this state will
94
+ * be removed in the next tick by the `onTick` function and will no longer
95
+ * be processed.
96
+ *
97
+ * This status is also set when the `clear` method is called on the FastTimer instance.
98
+ *
99
+ * @type {-1}
100
+ */
101
+ const TO_BE_CLEARED = -1;
102
+
103
+ /**
104
+ * The `PENDING` constant signifies that the FastTimer is awaiting processing
105
+ * in the next tick by the `onTick` function. Timers with this status will have
106
+ * their `_idleStart` value set and their status updated to `ACTIVE` in the next tick.
107
+ *
108
+ * @type {0}
109
+ */
110
+ const PENDING = 0;
111
+
112
+ /**
113
+ * The `ACTIVE` constant indicates that the FastTimer is active and waiting
114
+ * for its timer to expire. During the next tick, the `onTick` function will
115
+ * check if the timer has expired, and if so, it will execute the associated callback.
116
+ *
117
+ * @type {1}
118
+ */
119
+ const ACTIVE = 1;
120
+
121
+ /**
122
+ * The onTick function processes the fastTimers array.
123
+ *
124
+ * @returns {void}
125
+ */
126
+ function onTick() {
127
+ /**
128
+ * Increment the fastNow value by the TICK_MS value, despite the actual time
129
+ * that has passed since the last tick. This approach ensures independence
130
+ * from the system clock and delays caused by a blocked event loop.
131
+ *
132
+ * @type {number}
133
+ */
134
+ fastNow += TICK_MS;
135
+
136
+ /**
137
+ * The `idx` variable is used to iterate over the `fastTimers` array.
138
+ * Expired timers are removed by replacing them with the last element in the array.
139
+ * Consequently, `idx` is only incremented when the current element is not removed.
140
+ *
141
+ * @type {number}
142
+ */
143
+ let idx = 0;
144
+
145
+ /**
146
+ * The len variable will contain the length of the fastTimers array
147
+ * and will be decremented when a FastTimer should be removed from the
148
+ * fastTimers array.
149
+ *
150
+ * @type {number}
151
+ */
152
+ let len = fastTimers.length;
153
+
154
+ while (idx < len) {
155
+ /**
156
+ * @type {FastTimer}
157
+ */
158
+ const timer = fastTimers[idx];
159
+
160
+ // If the timer is in the ACTIVE state and the timer has expired, it will
161
+ // be processed in the next tick.
162
+ if (timer._state === PENDING) {
163
+ // Set the _idleStart value to the fastNow value minus the TICK_MS value
164
+ // to account for the time the timer was in the PENDING state.
165
+ timer._idleStart = fastNow - TICK_MS;
166
+ timer._state = ACTIVE;
167
+ } else if (
168
+ timer._state === ACTIVE &&
169
+ fastNow >= timer._idleStart + timer._idleTimeout
170
+ ) {
171
+ timer._state = TO_BE_CLEARED;
172
+ timer._idleStart = -1;
173
+ timer._onTimeout(timer._timerArg);
174
+ }
175
+
176
+ if (timer._state === TO_BE_CLEARED) {
177
+ timer._state = NOT_IN_LIST;
178
+
179
+ // Move the last element to the current index and decrement len if it is
180
+ // not the only element in the array.
181
+ if (--len !== 0) {
182
+ fastTimers[idx] = fastTimers[len];
183
+ }
184
+ } else {
185
+ ++idx;
186
+ }
187
+ }
188
+
189
+ // Set the length of the fastTimers array to the new length and thus
190
+ // removing the excess FastTimers elements from the array.
191
+ fastTimers.length = len;
192
+
193
+ // If there are still active FastTimers in the array, refresh the Timer.
194
+ // If there are no active FastTimers, the timer will be refreshed again
195
+ // when a new FastTimer is instantiated.
196
+ if (fastTimers.length !== 0) {
197
+ refreshTimeout();
198
+ }
199
+ }
200
+
201
+ function refreshTimeout() {
202
+ // If the fastNowTimeout is already set and the Timer has the refresh()-
203
+ // method available, call it to refresh the timer.
204
+ // Some timer objects returned by setTimeout may not have a .refresh()
205
+ // method (e.g. mocked timers in tests).
206
+ if (fastNowTimeout?.refresh) {
207
+ fastNowTimeout.refresh();
208
+ // fastNowTimeout is not instantiated yet or refresh is not availabe,
209
+ // create a new Timer.
210
+ } else {
211
+ clearTimeout(fastNowTimeout);
212
+ fastNowTimeout = setTimeout(onTick, TICK_MS);
213
+ // If the Timer has an unref method, call it to allow the process to exit,
214
+ // if there are no other active handles. When using fake timers or mocked
215
+ // environments (like Jest), .unref() may not be defined,
216
+ fastNowTimeout?.unref();
217
+ }
218
+ }
219
+
220
+ /**
221
+ * The `FastTimer` class is a data structure designed to store and manage
222
+ * timer information.
223
+ */
224
+ class FastTimer {
225
+ [kFastTimer] = true;
226
+
227
+ /**
228
+ * The state of the timer, which can be one of the following:
229
+ * - NOT_IN_LIST (-2)
230
+ * - TO_BE_CLEARED (-1)
231
+ * - PENDING (0)
232
+ * - ACTIVE (1)
233
+ *
234
+ * @type {-2|-1|0|1}
235
+ * @private
236
+ */
237
+ _state = NOT_IN_LIST;
238
+
239
+ /**
240
+ * The number of milliseconds to wait before calling the callback.
241
+ *
242
+ * @type {number}
243
+ * @private
244
+ */
245
+ _idleTimeout = -1;
246
+
247
+ /**
248
+ * The time in milliseconds when the timer was started. This value is used to
249
+ * calculate when the timer should expire.
250
+ *
251
+ * @type {number}
252
+ * @default -1
253
+ * @private
254
+ */
255
+ _idleStart = -1;
256
+
257
+ /**
258
+ * The function to be executed when the timer expires.
259
+ * @type {Function}
260
+ * @private
261
+ */
262
+ _onTimeout;
263
+
264
+ /**
265
+ * The argument to be passed to the callback when the timer expires.
266
+ *
267
+ * @type {*}
268
+ * @private
269
+ */
270
+ _timerArg;
271
+
272
+ /**
273
+ * @constructor
274
+ * @param {Function} callback A function to be executed after the timer
275
+ * expires.
276
+ * @param {number} delay The time, in milliseconds that the timer should wait
277
+ * before the specified function or code is executed.
278
+ * @param {*} arg
279
+ */
280
+ constructor(callback, delay, arg) {
281
+ this._onTimeout = callback;
282
+ this._idleTimeout = delay;
283
+ this._timerArg = arg;
284
+
285
+ this.refresh();
286
+ }
287
+
288
+ /**
289
+ * Sets the timer's start time to the current time, and reschedules the timer
290
+ * to call its callback at the previously specified duration adjusted to the
291
+ * current time.
292
+ * Using this on a timer that has already called its callback will reactivate
293
+ * the timer.
294
+ *
295
+ * @returns {void}
296
+ */
297
+ refresh() {
298
+ // In the special case that the timer is not in the list of active timers,
299
+ // add it back to the array to be processed in the next tick by the onTick
300
+ // function.
301
+ if (this._state === NOT_IN_LIST) {
302
+ fastTimers.push(this);
303
+ }
304
+
305
+ // If the timer is the only active timer, refresh the fastNowTimeout for
306
+ // better resolution.
307
+ if (!fastNowTimeout || fastTimers.length === 1) {
308
+ refreshTimeout();
309
+ }
310
+
311
+ // Setting the state to PENDING will cause the timer to be reset in the
312
+ // next tick by the onTick function.
313
+ this._state = PENDING;
314
+ }
315
+
316
+ /**
317
+ * The `clear` method cancels the timer, preventing it from executing.
318
+ *
319
+ * @returns {void}
320
+ * @private
321
+ */
322
+ clear() {
323
+ // Set the state to TO_BE_CLEARED to mark the timer for removal in the next
324
+ // tick by the onTick function.
325
+ this._state = TO_BE_CLEARED;
326
+
327
+ // Reset the _idleStart value to -1 to indicate that the timer is no longer
328
+ // active.
329
+ this._idleStart = -1;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * This module exports a setTimeout and clearTimeout function that can be
335
+ * used as a drop-in replacement for the native functions.
336
+ */
337
+ module.exports = {
338
+ /**
339
+ * The setTimeout() method sets a timer which executes a function once the
340
+ * timer expires.
341
+ * @param {Function} callback A function to be executed after the timer
342
+ * expires.
343
+ * @param {number} delay The time, in milliseconds that the timer should
344
+ * wait before the specified function or code is executed.
345
+ * @param {*} [arg] An optional argument to be passed to the callback function
346
+ * when the timer expires.
347
+ * @returns {NodeJS.Timeout|FastTimer}
348
+ */
349
+ setTimeout(callback, delay, arg) {
350
+ // If the delay is less than or equal to the RESOLUTION_MS value return a
351
+ // native Node.js Timer instance.
352
+ return delay <= RESOLUTION_MS
353
+ ? setTimeout(callback, delay, arg)
354
+ : new FastTimer(callback, delay, arg);
355
+ },
356
+ /**
357
+ * The clearTimeout method cancels an instantiated Timer previously created
358
+ * by calling setTimeout.
359
+ *
360
+ * @param {NodeJS.Timeout|FastTimer} timeout
361
+ */
362
+ clearTimeout(timeout) {
363
+ // If the timeout is a FastTimer, call its own clear method.
364
+ if (timeout[kFastTimer]) {
365
+ /**
366
+ * @type {FastTimer}
367
+ */
368
+ timeout.clear();
369
+ // Otherwise it is an instance of a native NodeJS.Timeout, so call the
370
+ // Node.js native clearTimeout function.
371
+ } else {
372
+ clearTimeout(timeout);
373
+ }
374
+ },
375
+ /**
376
+ * The setFastTimeout() method sets a fastTimer which executes a function once
377
+ * the timer expires.
378
+ * @param {Function} callback A function to be executed after the timer
379
+ * expires.
380
+ * @param {number} delay The time, in milliseconds that the timer should
381
+ * wait before the specified function or code is executed.
382
+ * @param {*} [arg] An optional argument to be passed to the callback function
383
+ * when the timer expires.
384
+ * @returns {FastTimer}
385
+ */
386
+ setFastTimeout(callback, delay, arg) {
387
+ return new FastTimer(callback, delay, arg);
388
+ },
389
+ /**
390
+ * The clearTimeout method cancels an instantiated FastTimer previously
391
+ * created by calling setFastTimeout.
392
+ *
393
+ * @param {FastTimer} timeout
394
+ */
395
+ clearFastTimeout(timeout) {
396
+ timeout.clear();
397
+ },
398
+ /**
399
+ * The now method returns the value of the internal fast timer clock.
400
+ *
401
+ * @returns {number}
402
+ */
403
+ now() {
404
+ return fastNow;
405
+ },
406
+ /**
407
+ * Trigger the onTick function to process the fastTimers array.
408
+ * Exported for testing purposes only.
409
+ * Marking as deprecated to discourage any use outside of testing.
410
+ * @deprecated
411
+ * @param {number} [delay=0] The delay in milliseconds to add to the now value.
412
+ */
413
+ tick(delay = 0) {
414
+ fastNow += delay - RESOLUTION_MS + 1;
415
+ onTick();
416
+ onTick();
417
+ },
418
+ /**
419
+ * Reset FastTimers.
420
+ * Exported for testing purposes only.
421
+ * Marking as deprecated to discourage any use outside of testing.
422
+ * @deprecated
423
+ */
424
+ reset() {
425
+ fastNow = 0;
426
+ fastTimers.length = 0;
427
+ clearTimeout(fastNowTimeout);
428
+ fastNowTimeout = null;
429
+ },
430
+ /**
431
+ * Exporting for testing purposes only.
432
+ * Marking as deprecated to discourage any use outside of testing.
433
+ * @deprecated
434
+ */
435
+ kFastTimer
436
+ };
package/lib/frames.js ADDED
@@ -0,0 +1,127 @@
1
+ // Frame layer: constants and in-place header read/write. Deliberately tiny --
2
+ // a frame is (offset, length) into a caller-provided Buffer, never an object.
3
+ 'use strict';
4
+
5
+ const PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
6
+ const PREFACE_LEN = PREFACE.length;
7
+ const HEADER_LEN = 9;
8
+
9
+ // Frame types, RFC 9113 §6 and the IANA HTTP/2 frame registry. The numbers are the
10
+ // wire format, not an internal enumeration: a peer sends 9 when it splits a header
11
+ // block, and 10 is ALTSVC (RFC 7807, and RFC 9113 §5.5 says a frame of unknown or
12
+ // unsupported type MUST be discarded). Getting one of these wrong is invisible to our own
13
+ // tests -- both ends would use the same wrong number -- and looks to every other
14
+ // implementation like a connection error, so each value here was checked against a
15
+ // live node:http2 endpoint rather than recalled.
16
+ const DATA = 0, HEADERS = 1, PRIORITY = 2, RST_STREAM = 3, SETTINGS = 4;
17
+ const PUSH_PROMISE = 5, PING = 6, GOAWAY = 7, WINDOW_UPDATE = 8, CONTINUATION = 9;
18
+
19
+ const F_END_STREAM = 0x1;
20
+ const F_ACK = 0x1;
21
+ const F_END_HEADERS = 0x4;
22
+ const F_PADDED = 0x8;
23
+ const F_PRIORITY = 0x20;
24
+
25
+ // Wire error codes, RFC 9113 §7. Values verified against http2.constants
26
+ // (NGHTTP2_*), because rstCode/goawayCode are part of the observable API.
27
+ const NO_ERROR = 0, PROTOCOL_ERROR = 1, INTERNAL_ERROR = 2, FLOW_CONTROL_ERROR = 3;
28
+ const SETTINGS_TIMEOUT = 4, STREAM_CLOSED = 5, FRAME_SIZE_ERROR = 6;
29
+ const REFUSED_STREAM = 7, CANCEL = 8, COMPRESSION_ERROR = 9, CONNECT_ERROR = 10;
30
+ const ENHANCE_YOUR_CALM = 11, INADEQUATE_SECURITY = 12, HTTP_1_1_REQUIRED = 13;
31
+
32
+ // Stream state numbers as node reports them in stream.state.state.
33
+ const ST_IDLE = 1, ST_OPEN = 2, ST_RESERVED_LOCAL = 3, ST_RESERVED_REMOTE = 4;
34
+ const ST_HALF_CLOSED_LOCAL = 5, ST_HALF_CLOSED_REMOTE = 6, ST_CLOSED = 7;
35
+
36
+ // Results of readHeader land in these module-level slots instead of in a
37
+ // returned object: one allocation avoided per frame.
38
+ let FH_LEN = 0, FH_TYPE = 0, FH_FLAGS = 0, FH_SID = 0;
39
+
40
+ function readHeader(buf, off) {
41
+ FH_LEN = (buf[off] << 16) | (buf[off + 1] << 8) | buf[off + 2];
42
+ FH_TYPE = buf[off + 3];
43
+ FH_FLAGS = buf[off + 4];
44
+ FH_SID = buf.readUInt32BE(off + 5) & 0x7fffffff;
45
+ }
46
+
47
+ function writeHeader(dst, off, len, type, flags, streamId) {
48
+ dst[off] = len >> 16;
49
+ dst[off + 1] = len >> 8;
50
+ dst[off + 2] = len;
51
+ dst[off + 3] = type;
52
+ dst[off + 4] = flags;
53
+ dst.writeUInt32BE(streamId >>> 0, off + 5); // reserved bit stays 0
54
+ }
55
+
56
+ // RFC 9113 6.1: a padded DATA frame's payload is one octet of pad length, then
57
+ // the data, then that many octets of padding. The views a payload arrived in are
58
+ // therefore not the payload: the pad length has to come off the front and the
59
+ // padding off the back. The pad length is the payload's first octet, so it is
60
+ // known as soon as the payload starts -- there is nothing to wait for.
61
+ //
62
+ // The views are edited in place: the pad length octet is dropped from the first
63
+ // one, the padding from the last one, and the pieces that are left empty are
64
+ // dropped. A frame that arrived in pieces is handed to the application in
65
+ // pieces, and a copy made to trim it would undo that.
66
+ //
67
+ // Returns the number of data octets, or -1 when the frame is malformed: a padded
68
+ // frame with an empty payload has no pad length to read at all, and padding that
69
+ // does not fit in the payload is what RFC 9113 6.1 calls a connection error of
70
+ // type PROTOCOL_ERROR. Node answers both with GOAWAY(PROTOCOL_ERROR) (measured
71
+ // against a live node server, tools/probe-pad-bad.js).
72
+ function trimPadding(pieces) {
73
+ let first = 0;
74
+ while (first < pieces.length && pieces[first].length === 0) {
75
+ first++;
76
+ }
77
+ if (first === pieces.length) {
78
+ return -1;
79
+ } // no pad length octet
80
+ const pad = pieces[first][0];
81
+ pieces[first] = pieces[first].subarray(1); // the pad length is not data
82
+ let left = pad;
83
+ for (let i = pieces.length - 1; i >= 0 && left > 0; i--) {
84
+ const p = pieces[i];
85
+ if (left >= p.length) {
86
+ left -= p.length; pieces[i] = null;
87
+ } else {
88
+ pieces[i] = p.subarray(0, p.length - left); left = 0;
89
+ }
90
+ }
91
+ if (left > 0) {
92
+ return -1;
93
+ } // the padding does not fit
94
+ let total = 0;
95
+ for (let i = 0; i < pieces.length; i++) {
96
+ if (pieces[i] === null || pieces[i].length === 0) {
97
+ pieces[i] = null; continue;
98
+ }
99
+ total += pieces[i].length;
100
+ }
101
+ return total;
102
+ }
103
+
104
+ module.exports = {
105
+ PREFACE, PREFACE_LEN, HEADER_LEN,
106
+ DATA, HEADERS, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY,
107
+ WINDOW_UPDATE, CONTINUATION,
108
+ F_END_STREAM, F_ACK, F_END_HEADERS, F_PADDED, F_PRIORITY,
109
+ NO_ERROR, PROTOCOL_ERROR, INTERNAL_ERROR, FLOW_CONTROL_ERROR, SETTINGS_TIMEOUT,
110
+ STREAM_CLOSED, FRAME_SIZE_ERROR, REFUSED_STREAM, CANCEL, COMPRESSION_ERROR,
111
+ CONNECT_ERROR, ENHANCE_YOUR_CALM, INADEQUATE_SECURITY, HTTP_1_1_REQUIRED,
112
+ ST_IDLE, ST_OPEN, ST_RESERVED_LOCAL, ST_RESERVED_REMOTE,
113
+ ST_HALF_CLOSED_LOCAL, ST_HALF_CLOSED_REMOTE, ST_CLOSED,
114
+ readHeader, writeHeader, trimPadding,
115
+ get FH_LEN() {
116
+ return FH_LEN;
117
+ },
118
+ get FH_TYPE() {
119
+ return FH_TYPE;
120
+ },
121
+ get FH_FLAGS() {
122
+ return FH_FLAGS;
123
+ },
124
+ get FH_SID() {
125
+ return FH_SID;
126
+ }
127
+ };