webhoster 0.1.1 → 0.3.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.
Files changed (85) hide show
  1. package/.eslintrc.json +74 -58
  2. package/.github/copilot-instructions.md +100 -0
  3. package/.github/workflows/test-matrix.yml +37 -0
  4. package/.test/benchmark.js +28 -0
  5. package/.test/constants.js +4 -0
  6. package/{test → .test}/http2server.js +1 -1
  7. package/{test → .test}/httpserver.js +1 -1
  8. package/{test → .test}/index.js +178 -192
  9. package/.test/multipromise.js +32 -0
  10. package/{test → .test}/tls.js +3 -3
  11. package/{test → .test}/urlencoded.js +3 -0
  12. package/.vscode/launch.json +24 -3
  13. package/README.md +116 -90
  14. package/data/CookieObject.js +14 -14
  15. package/errata/socketio.js +6 -11
  16. package/examples/starter.js +11 -0
  17. package/helpers/HeadersParser.js +7 -8
  18. package/helpers/HttpListener.js +387 -42
  19. package/helpers/RequestHeaders.js +43 -36
  20. package/helpers/RequestReader.js +27 -26
  21. package/helpers/ResponseHeaders.js +47 -36
  22. package/jsconfig.json +1 -1
  23. package/lib/HttpHandler.js +447 -277
  24. package/lib/HttpRequest.js +383 -39
  25. package/lib/HttpResponse.js +316 -52
  26. package/lib/HttpTransaction.js +146 -0
  27. package/middleware/AutoHeadersMiddleware.js +73 -0
  28. package/middleware/CORSMiddleware.js +45 -47
  29. package/middleware/CaseInsensitiveHeadersMiddleware.js +5 -11
  30. package/middleware/ContentDecoderMiddleware.js +81 -35
  31. package/middleware/ContentEncoderMiddleware.js +179 -132
  32. package/middleware/ContentLengthMiddleware.js +66 -41
  33. package/middleware/ContentWriterMiddleware.js +5 -5
  34. package/middleware/HashMiddleware.js +68 -40
  35. package/middleware/HeadMethodMiddleware.js +24 -21
  36. package/middleware/MethodMiddleware.js +29 -36
  37. package/middleware/PathMiddleware.js +49 -66
  38. package/middleware/ReadFormData.js +99 -0
  39. package/middleware/SendJsonMiddleware.js +131 -0
  40. package/middleware/SendStringMiddleware.js +87 -0
  41. package/package.json +38 -29
  42. package/polyfill/FormData.js +164 -0
  43. package/rollup.config.js +0 -1
  44. package/scripts/test-all-sync.sh +6 -0
  45. package/scripts/test-all.sh +7 -0
  46. package/templates/starter.js +53 -0
  47. package/test/fixtures/stream.js +68 -0
  48. package/test/helpers/HttpListener/construct.js +18 -0
  49. package/test/helpers/HttpListener/customOptions.js +22 -0
  50. package/test/helpers/HttpListener/doubleCreate.js +40 -0
  51. package/test/helpers/HttpListener/events.js +77 -0
  52. package/test/helpers/HttpListener/http.js +31 -0
  53. package/test/helpers/HttpListener/http2.js +41 -0
  54. package/test/helpers/HttpListener/https.js +38 -0
  55. package/test/helpers/HttpListener/startAll.js +31 -0
  56. package/test/helpers/HttpListener/stopNotStarted.js +23 -0
  57. package/test/lib/HttpHandler/class.js +8 -0
  58. package/test/lib/HttpHandler/handleRequest.js +11 -0
  59. package/test/lib/HttpHandler/middleware.js +941 -0
  60. package/test/lib/HttpHandler/parse.js +41 -0
  61. package/test/lib/HttpRequest/class.js +8 -0
  62. package/test/lib/HttpRequest/downstream.js +171 -0
  63. package/test/lib/HttpRequest/properties.js +101 -0
  64. package/test/lib/HttpRequest/read.js +518 -0
  65. package/test/lib/HttpResponse/class.js +8 -0
  66. package/test/lib/HttpResponse/properties.js +59 -0
  67. package/test/lib/HttpResponse/send.js +275 -0
  68. package/test/lib/HttpTransaction/class.js +8 -0
  69. package/test/lib/HttpTransaction/ping.js +50 -0
  70. package/test/lib/HttpTransaction/push.js +89 -0
  71. package/test/middleware/SendJsonMiddleware.js +222 -0
  72. package/test/sanity.js +10 -0
  73. package/test/templates/starter.js +93 -0
  74. package/tsconfig.json +12 -0
  75. package/types/index.js +61 -34
  76. package/types/typings.d.ts +8 -9
  77. package/utils/AsyncObject.js +6 -3
  78. package/utils/CaseInsensitiveObject.js +2 -3
  79. package/utils/function.js +1 -7
  80. package/utils/headers.js +42 -0
  81. package/utils/qualityValues.js +1 -1
  82. package/utils/stream.js +4 -20
  83. package/index.cjs +0 -3190
  84. package/test/constants.js +0 -4
  85. /package/{test → .test}/cookietester.js +0 -0
@@ -1,111 +1,375 @@
1
+ /**
2
+ * @see https://www.rfc-editor.org/rfc/rfc7230
3
+ * @see https://www.rfc-editor.org/rfc/rfc7231
4
+ */
5
+
1
6
  /** @typedef {import('stream').Writable} Writable */
2
7
 
8
+ import { PassThrough, Readable, pipeline } from 'node:stream';
9
+
10
+ import { isWritable } from '../utils/stream.js';
11
+
3
12
  /** @typedef {import('http').OutgoingHttpHeaders} OutgoingHttpHeaders */
13
+ /** @typedef {import('stream').Stream} Stream */
14
+ /** @typedef {import('../types/index.js').Middleware} Middleware */
15
+ /** @typedef {import('./HttpRequest.js').default} HttpRequest */
16
+ /** @typedef {import('../types/index.js').ResponseFinalizer} ResponseFinalizer */
4
17
 
5
18
  /**
6
19
  * @typedef {Object} HttpResponseOptions
20
+ * @prop {HttpRequest} [request]
7
21
  * @prop {OutgoingHttpHeaders} [headers]
8
22
  * @prop {function():boolean} [onHeadersSent]
9
- * @prop {function(boolean):void} [onSendHeaders]
23
+ * @prop {(flush:boolean, end:boolean) => any} [onSendHeaders]
10
24
  * @prop {number} [status]
11
25
  * @prop {Writable} stream
12
- * @prop {import('net').Socket|import('tls').TLSSocket} [socket]
13
- * @prop {boolean} [canPushPath]
14
- * @prop {function(string):Promise<any>} [onPushPath]
15
- * @prop {Object<string,any>} [locals]
16
- * @prop {boolean} [unsealed]
26
+ * @prop {Stream[]} [pipes]
27
+ * @prop {ResponseFinalizer[]} [finalizers] inline middleware for message body
28
+ * @prop {any} [body]
17
29
  */
18
30
 
19
31
  export default class HttpResponse {
20
32
  /** @type {function():boolean} */
21
33
  #onHeadersSent = null;
22
34
 
23
- /** @type {function(boolean):void} */
35
+ /** @type {(flush:boolean, end:boolean) => any} */
24
36
  #onSendHeaders = null;
25
37
 
26
- /** @type {function(string):Promise<any>} */
27
- #onPushPath = null;
28
-
29
- /** @type {boolean} */
30
38
  #headersSent = false;
31
39
 
32
- /** @type {Array<string>} */
33
- #pushedPaths = [];
40
+ /** @type {Writable} */
41
+ #pipeline;
42
+
43
+ #pipelineComplete = false;
34
44
 
35
- #canPushPath = false;
45
+ /** @type {Function[]} */
46
+ #pipelineCallbacks = [];
47
+
48
+ #endCalled = false;
36
49
 
37
50
  /** @param {HttpResponseOptions} options */
38
51
  constructor(options) {
52
+ this.request = options.request;
39
53
  /** @type {OutgoingHttpHeaders} */
40
- this.headers = options.headers || {};
54
+ this.headers = options.headers ?? {};
55
+ this.isStreaming = false;
41
56
  this.stream = options.stream;
42
- this.socket = options.socket;
57
+ this.pipes = options.pipes ?? [];
58
+ /** @type {ResponseFinalizer[]} */
59
+ this.finalizers = options.finalizers ?? [];
43
60
  this.status = options.status;
44
- this.#onPushPath = options.onPushPath;
61
+ /** @type {any} */
62
+ this.body = options.body;
45
63
  this.#onHeadersSent = options.onHeadersSent;
46
64
  this.#onSendHeaders = options.onSendHeaders;
47
- this.#canPushPath = options.canPushPath ?? false;
48
- this.locals = options.locals || {};
49
- this.unsealed = options.unsealed ?? false;
50
- if (!this.unsealed) {
51
- Object.seal(this);
52
- }
53
65
  }
54
66
 
55
- get headersSent() {
56
- if (this.#onHeadersSent) {
57
- return this.#onHeadersSent();
58
- }
59
- return this.#headersSent;
67
+ get statusCode() {
68
+ return this.status;
69
+ }
70
+
71
+ set statusCode(number) {
72
+ this.status = number;
60
73
  }
61
74
 
62
75
  /**
63
- * @param {Writable} stream
64
- * @return {Writable} previousStream
76
+ * A boolean indicating whether the response was successful (status in the range 200–299) or not.
77
+ * @return {boolean}
65
78
  */
66
- replaceStream(stream) {
67
- const previousStream = this.stream;
68
- this.stream = stream;
69
- return previousStream;
79
+ get ok() {
80
+ return this.status >= 200 && this.status <= 299;
81
+ }
82
+
83
+ get headersSent() {
84
+ if (this.#headersSent) return true;
85
+ if (!this.#onHeadersSent) return false;
86
+ return this.#onHeadersSent();
70
87
  }
71
88
 
72
89
  /**
73
- * @param {boolean} [flush]
90
+ * @param {boolean} [flush] Flush headers
91
+ * @param {boolean} [end] End stream
74
92
  * @return {void}
75
93
  */
76
- sendHeaders(flush) {
94
+ sendHeaders(flush, end) {
77
95
  if (this.headersSent) {
78
- throw new Error('ALREADY_SENT');
96
+ throw new Error('HEADER_SENT');
79
97
  }
80
98
  if (!this.#onSendHeaders) {
81
99
  throw new Error('NOT_IMPLEMENTED');
82
100
  }
83
- this.#onSendHeaders(flush);
101
+ this.#onSendHeaders(flush, end);
84
102
  this.#headersSent = true;
85
103
  }
86
104
 
87
- get pushedPaths() {
88
- return this.#pushedPaths;
105
+ /**
106
+ * @param {number} [status]
107
+ * @return {Promise<0>} HttpHandler.END
108
+ */
109
+ async sendStatusOnly(status) {
110
+ if (this.headersSent) throw new Error('ERR_HEADER_SENT');
111
+ if (!isWritable(this.stream)) throw new Error('NOT_WRITABLE');
112
+ if (status) {
113
+ this.status = status;
114
+ }
115
+ await this.sendHeaders(true, true);
116
+ return 0;
117
+ }
118
+
119
+ /**
120
+ * Send message body to response stream without finalizers
121
+ * @param {any} [body]
122
+ * @return {Promise<0>}
123
+ */
124
+ async sendRaw(body) {
125
+ this.body = body;
126
+ await new Promise((resolve, reject) => {
127
+ this.stream.end(body, (err) => (err ? reject(err) : resolve()));
128
+ });
129
+ return 0;
89
130
  }
90
131
 
91
- get canPushPath() {
92
- return this.#canPushPath;
132
+ /** @return {boolean} */
133
+ hasPipeline() {
134
+ return this.#pipeline != null;
135
+ }
136
+
137
+ /**
138
+ * @param {Transform} stream
139
+ * @return {this}
140
+ */
141
+ addUpstream(stream) {
142
+ this.pipes.push(stream);
143
+ return this;
93
144
  }
94
145
 
95
146
  /**
96
- * @param {string} [path]
97
- * @return {Promise<any>}
147
+ * @param {(err: NodeJS.ErrnoException | null) => void} [callback] pipeline completion
148
+ * @return {Writable}
98
149
  */
99
- pushPath(path) {
100
- if (this.#pushedPaths.includes(path)) {
101
- return Promise.reject(new Error('ALREADY_PUSHED'));
150
+ getPipeline(callback) {
151
+ if (callback) {
152
+ if (this.#pipelineComplete) {
153
+ setTimeout(callback, 0);
154
+ } else {
155
+ this.#pipelineCallbacks.push(callback);
156
+ }
102
157
  }
103
- if (!this.#onPushPath) {
104
- return Promise.reject(new Error('NOT_IMPLEMENTED'));
158
+
159
+ if (this.#pipeline) return this.#pipeline;
160
+
161
+ if (!this.isStreaming) {
162
+ // Called directly by user and needs finalizer calls
163
+
164
+ this.isStreaming = true;
165
+ for (let i = 0; i < this.finalizers.length; i++) {
166
+ const process = this.finalizers[i];
167
+ const result = process(this);
168
+ if (result === false) {
169
+ break;
170
+ }
171
+ }
105
172
  }
106
- if (!this.#canPushPath) {
107
- return Promise.reject(new Error('NOT_SUPPORTED'));
173
+ let array;
174
+ if (this.pipes.length) {
175
+ array = [
176
+ ...this.pipes,
177
+ this.stream,
178
+ ];
179
+ } else {
180
+ array = [
181
+ new PassThrough({ objectMode: true }),
182
+ this.stream,
183
+ ];
108
184
  }
109
- return this.#onPushPath(path);
185
+
186
+ this.#pipeline = array[0];
187
+ // @ts-ignore Bad typings
188
+ pipeline(array, (err) => {
189
+ this.#pipelineComplete = true;
190
+ let nextCallback;
191
+ while ((nextCallback = this.#pipelineCallbacks.shift()) != null) {
192
+ nextCallback(err);
193
+ }
194
+ });
195
+ return this.#pipeline;
196
+ }
197
+
198
+ /**
199
+ * Asynchronously sends message body
200
+ * Returns on completions
201
+ * @throws {Error}
202
+ * @param {any} [body]
203
+ * @return {Promise<0>} HttpHandler.END
204
+ */
205
+ async send(body) {
206
+ if (!isWritable(this.stream)) throw new Error('NOT_WRITABLE');
207
+ if (this.isStreaming) throw new Error('ALREADY STREAMING');
208
+
209
+ if (body !== undefined) {
210
+ this.body = body;
211
+ }
212
+ if (this.body instanceof Readable) {
213
+ this.isStreaming = true;
214
+ this.pipes.push(this.body);
215
+ }
216
+
217
+ for (const process of this.finalizers) {
218
+ const result = process(this);
219
+ if (result === true || result == null) {
220
+ continue;
221
+ }
222
+ if (result === false) {
223
+ break;
224
+ }
225
+ // eslint-disable-next-line no-await-in-loop
226
+ const promiseResult = await result;
227
+ if (promiseResult === true || result == null) {
228
+ continue;
229
+ }
230
+ if (promiseResult === false) {
231
+ break;
232
+ }
233
+ }
234
+ if (!isWritable(this.stream)) return 0;
235
+ if (this.isStreaming) {
236
+ await new Promise((resolve, reject) => {
237
+ this.getPipeline((err) => {
238
+ if (err) reject(err);
239
+ resolve();
240
+ });
241
+ });
242
+ } else {
243
+ if (!this.headersSent) this.sendHeaders();
244
+ await this.sendRaw(this.body);
245
+ }
246
+ return 0;
247
+ }
248
+
249
+ wasEndCalled() {
250
+ return this.#endCalled;
251
+ }
252
+
253
+ /**
254
+ * Synchronously ends stream with message body. Ignores errors
255
+ * @param {any} [body]
256
+ * @return {0} HttpHandler.END
257
+ */
258
+ end(body) {
259
+ this.#endCalled = true;
260
+ if (!isWritable(this.stream)) {
261
+ return 0;
262
+ }
263
+ if (this.isStreaming) {
264
+ throw new Error('ALREADY STREAMING');
265
+ }
266
+
267
+ if (body !== undefined) {
268
+ this.body = body;
269
+ }
270
+
271
+ if (this.body instanceof Readable) {
272
+ this.isStreaming = true;
273
+ this.pipes.push(this.body);
274
+ }
275
+
276
+ // Process
277
+ /** @type {ResponseFinalizer[]} */
278
+ const pendingProcessors = [];
279
+ let needsAsync = false;
280
+ /** @type {void|Promise<boolean|void>} */
281
+ let pendingPromise;
282
+ let i;
283
+ for (i = 0; i < this.finalizers.length; i++) {
284
+ const process = this.finalizers[i];
285
+ if (needsAsync) {
286
+ pendingProcessors.push(process);
287
+ continue;
288
+ }
289
+
290
+ const result = process(this);
291
+ if (result === true || result == null) {
292
+ continue;
293
+ }
294
+ if (result === false) {
295
+ break;
296
+ }
297
+ pendingPromise = result;
298
+ needsAsync = true;
299
+ continue;
300
+ }
301
+ if (pendingPromise) {
302
+ pendingPromise.then(async (initialResult) => {
303
+ if (initialResult !== false) {
304
+ for (i = 0; i < pendingProcessors.length; i++) {
305
+ const process = pendingProcessors[i];
306
+ const result = process(this);
307
+ if (result === true || result == null) {
308
+ continue;
309
+ }
310
+ if (result === false) {
311
+ break;
312
+ }
313
+ // eslint-disable-next-line no-await-in-loop
314
+ const promiseResult = await result;
315
+ if (promiseResult === true || result == null) {
316
+ continue;
317
+ }
318
+ if (promiseResult === false) {
319
+ break;
320
+ }
321
+ }
322
+ }
323
+
324
+ if (this.isStreaming) {
325
+ this.getPipeline();
326
+ } else {
327
+ if (!this.headersSent) this.sendHeaders();
328
+ if (this.body == null) {
329
+ this.stream.end();
330
+ } else {
331
+ this.stream.end(this.body);
332
+ }
333
+ }
334
+ }).catch((error) => {
335
+ // console.error(error);
336
+ this.stream.destroy(error);
337
+ });
338
+ return 0;
339
+ }
340
+
341
+ if (this.isStreaming) {
342
+ this.getPipeline();
343
+ } else {
344
+ if (!this.headersSent) this.sendHeaders();
345
+ if (this.body == null) {
346
+ this.stream.end();
347
+ } else {
348
+ this.stream.end(this.body);
349
+ }
350
+ }
351
+ return 0;
352
+ }
353
+
354
+ // Alias
355
+
356
+ /**
357
+ * @param {number} status
358
+ * @return {this}
359
+ */
360
+ code(status) {
361
+ this.status = status;
362
+ return this;
363
+ }
364
+
365
+ /**
366
+ * @param {number} status
367
+ * @throws {Error<ERR_HEADER_SENT>}
368
+ * @return {this}
369
+ */
370
+ setStatus(status) {
371
+ if (this.headersSent) throw new Error('ERR_HEADER_SENT');
372
+ this.status = status;
373
+ return this;
110
374
  }
111
375
  }
@@ -0,0 +1,146 @@
1
+ /** @typedef {import('./HttpRequest.js').default} HttpRequest */
2
+ /** @typedef {import('./HttpResponse.js').default} HttpResponse */
3
+ /** @typedef {import('stream').Stream} Stream */
4
+
5
+ /** @typedef {import('../types/index.js').MediaType} MediaType */
6
+ /** @typedef {import('../types/index.js').Middleware} Middleware */
7
+ /** @typedef {import('stream').Writable} Writable */
8
+
9
+ /** @typedef {Partial<MediaType> & {parse:(this:HttpRequest)=>any|PromiseLike<any>, test?:(this:HttpRequest, mediaType: MediaType)=>boolean}} ContentReaderRegistration */
10
+
11
+ /**
12
+ * @typedef {Object} PathHistoryEntry
13
+ * @prop {string} base
14
+ * @prop {number[]} treeIndex
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} PathState
19
+ * @prop {PathHistoryEntry[]} history
20
+ * @prop {string} currentPath
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} HttpTransactionState
25
+ * @prop {number[]} treeIndex Middleware level
26
+ * @prop {PathState} path
27
+ */
28
+
29
+ /**
30
+ * @template {Object<string,any>} T
31
+ * @typedef HttpTransactionOptions
32
+ * @prop {string} httpVersion
33
+ * @prop {HttpRequest} [request]
34
+ * @prop {HttpResponse} [response]
35
+ * @prop {import('net').Socket|import('tls').TLSSocket} socket
36
+ * @prop {boolean} [canPing]
37
+ * @prop {(function():Promise<any>)|(function():any)} [onPing]
38
+ * @prop {boolean|function():boolean} [canPushPath]
39
+ * @prop {(path:string) => Promise<any>} [onPushPath]
40
+ * @prop {function():boolean} [onHeadersSent]
41
+ * @prop {(flush:boolean, end:boolean) => boolean} [onSendHeaders]
42
+ * @prop {T} [locals]
43
+ * @prop {HttpTransactionState} [state]
44
+ * @prop {Error} [error]
45
+ */
46
+
47
+ /**
48
+ * @template {Object<string,any>} [T=any]
49
+ */
50
+ export default class HttpTransaction {
51
+ /** @type {boolean|(()=>boolean)} */
52
+ #canPing = false;
53
+
54
+ /** @type {function():Promise<any>} */
55
+ #onPing = null;
56
+
57
+ /** @type {(path:string) => Promise<any>} */
58
+ #onPushPath = null;
59
+
60
+ /** @type {Array<string>} */
61
+ #pushedPaths = [];
62
+
63
+ /** @type {boolean|(()=>boolean)} */
64
+ #canPushPath = false;
65
+
66
+ #isErrorHandlerState = false;
67
+
68
+ /** @param {HttpTransactionOptions<T>} options */
69
+ constructor(options) {
70
+ this.request = options.request;
71
+ this.response = options.response;
72
+
73
+ /** @type {T} */
74
+ this.locals = options.locals || /** @type {T} */ ({});
75
+ this.state = options.state || {
76
+ treeIndex: [],
77
+ path: null,
78
+ };
79
+
80
+ this.socket = options.socket;
81
+ this.httpVersion = options.httpVersion;
82
+
83
+ this.#canPing = options.canPing ?? false;
84
+ this.#onPing = options.onPing;
85
+
86
+ this.#onPushPath = options.onPushPath;
87
+ this.#canPushPath = options.canPushPath ?? false;
88
+
89
+ this.error = options.error;
90
+ }
91
+
92
+ setErrorHandlerState() {
93
+ this.#isErrorHandlerState = true;
94
+ }
95
+
96
+ isErrorHandlerState() {
97
+ return this.#isErrorHandlerState;
98
+ }
99
+
100
+ get canPing() {
101
+ if (!this.#canPing) return false;
102
+ if (this.#canPing === true) return true;
103
+ return this.#canPing();
104
+ }
105
+
106
+ /**
107
+ * @return {Promise<any>}
108
+ */
109
+ ping() {
110
+ if (!this.#canPing) {
111
+ return Promise.reject(new Error('NOT_SUPPORTED'));
112
+ }
113
+ if (!this.#onPing) {
114
+ return Promise.reject(new Error('NOT_IMPLEMENTED'));
115
+ }
116
+ return this.#onPing();
117
+ }
118
+
119
+ get pushedPaths() {
120
+ return this.#pushedPaths;
121
+ }
122
+
123
+ get canPushPath() {
124
+ if (!this.#canPushPath) return false;
125
+ if (this.#canPushPath === true) return true;
126
+ return this.#canPushPath();
127
+ }
128
+
129
+ /**
130
+ * @param {string} [path]
131
+ * @return {Promise<any>}
132
+ */
133
+ async pushPath(path) {
134
+ if (this.#pushedPaths.includes(path)) {
135
+ throw new Error('ALREADY_PUSHED');
136
+ }
137
+ if (!this.#canPushPath) {
138
+ throw new Error('NOT_SUPPORTED');
139
+ }
140
+ if (!this.#onPushPath) {
141
+ throw new Error('NOT_IMPLEMENTED');
142
+ }
143
+ await this.#onPushPath(path);
144
+ this.#pushedPaths.push(path);
145
+ }
146
+ }
@@ -0,0 +1,73 @@
1
+ /** @typedef {import('../lib/HttpResponse.js').default} HttpResponse */
2
+ /** @typedef {import('../types').ResponseFinalizer} ResponseFinalizer */
3
+ /** @typedef {import('../types').MiddlewareFunction} MiddlewareFunction */
4
+
5
+ import { Transform } from 'node:stream';
6
+
7
+ /**
8
+ * @typedef {Object} AutoHeadersMiddlewareOptions
9
+ * @prop {boolean} [setStatus=true]
10
+ * Automatically set `200` or `204` status if not set
11
+ */
12
+
13
+ export default class AutoHeadersMiddleware {
14
+ /** @param {AutoHeadersMiddlewareOptions} options */
15
+ constructor(options = {}) {
16
+ this.setStatus = options.setStatus !== false;
17
+ this.finalizeResponse = this.finalizeResponse.bind(this);
18
+ }
19
+
20
+ /**
21
+ * @param {HttpResponse} response
22
+ * @return {void}
23
+ */
24
+ addTransformStream(response) {
25
+ let firstChunk = false;
26
+ response.pipes.push(new Transform({
27
+ transform: (chunk, encoding, callback) => {
28
+ if (!firstChunk) {
29
+ firstChunk = true;
30
+ if (!response.headersSent) {
31
+ if (response.statusCode == null) {
32
+ if (!this.setStatus) {
33
+ callback(new Error('NO_STATUS'));
34
+ return;
35
+ }
36
+ response.status = 200;
37
+ }
38
+ response.sendHeaders(false);
39
+ }
40
+ }
41
+ callback(null, chunk);
42
+ },
43
+ final: (callback) => {
44
+ if (!response.headersSent) {
45
+ if (this.setStatus && response.statusCode == null) {
46
+ response.status = 204;
47
+ }
48
+ response.sendHeaders(false);
49
+ }
50
+ callback();
51
+ },
52
+ }));
53
+ }
54
+
55
+ /** @type {ResponseFinalizer} */
56
+ finalizeResponse(response) {
57
+ if (response.headersSent) return;
58
+ if (response.isStreaming) {
59
+ this.addTransformStream(response);
60
+ return;
61
+ }
62
+
63
+ if (response.status == null && this.setStatus && Buffer.isBuffer(response.body)) {
64
+ response.status = response.body.byteLength ? 200 : 204;
65
+ }
66
+ response.sendHeaders();
67
+ }
68
+
69
+ /** @type {MiddlewareFunction} */
70
+ execute({ response }) {
71
+ response.finalizers.push(this.finalizeResponse);
72
+ }
73
+ }