@octanejs/app-core 0.0.54 → 0.0.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/app-core",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -79,10 +79,10 @@
79
79
  "esbuild": "^0.28.1"
80
80
  },
81
81
  "peerDependencies": {
82
- "octane": "^0.4.0"
82
+ "octane": "^0.5.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^24.13.3",
86
- "octane": "0.4.0"
86
+ "octane": "0.5.0"
87
87
  }
88
88
  }
@@ -129,11 +129,14 @@ function shouldGzip(request, status, headers, hasBody) {
129
129
  }
130
130
 
131
131
  /**
132
- * Convert a Node.js IncomingMessage to a Web Request.
132
+ * Convert a Node.js IncomingMessage to a Web Request. Passing its response
133
+ * keeps request.signal active through streaming and cancels it on disconnect.
134
+ * Without a response, cancellation only covers interrupted request uploads.
133
135
  * @param {import('node:http').IncomingMessage} nodeRequest
136
+ * @param {import('node:http').ServerResponse} [nodeResponse]
134
137
  * @returns {Request}
135
138
  */
136
- export function nodeRequestToWebRequest(nodeRequest) {
139
+ export function nodeRequestToWebRequest(nodeRequest, nodeResponse) {
137
140
  const host = nodeRequest.headers.host || 'localhost';
138
141
  const url = new URL(nodeRequest.url || '/', `http://${host}`);
139
142
 
@@ -151,29 +154,60 @@ export function nodeRequestToWebRequest(nodeRequest) {
151
154
  /** @type {RequestInit & { duplex?: 'half' }} */
152
155
  const init = { method, headers };
153
156
  const abortController = new AbortController();
154
- const abortRequest = () => {
155
- if (!abortController.signal.aborted) {
156
- abortController.abort(new Error('The client disconnected before the request completed.'));
157
- }
158
- };
159
- if (nodeRequest.aborted || (nodeRequest.destroyed && !nodeRequest.complete)) {
160
- abortRequest();
161
- } else {
162
- nodeRequest.once('aborted', abortRequest);
163
- nodeRequest.once('close', () => {
164
- if (!nodeRequest.complete) abortRequest();
165
- });
157
+ const disconnected =
158
+ nodeRequest.aborted ||
159
+ (nodeRequest.destroyed && !nodeRequest.complete) ||
160
+ (nodeResponse?.destroyed && !nodeResponse.writableFinished);
161
+ if (disconnected) {
162
+ abortController.abort(new Error('The client disconnected before the request completed.'));
166
163
  }
167
164
  init.signal = abortController.signal;
168
165
  if (method !== 'GET' && method !== 'HEAD') {
169
- // node:stream/web's ReadableStream and the DOM lib's are structurally the
170
- // same at runtime; the lib types disagree on BYOB details.
171
- init.body = /** @type {ReadableStream} */ (
172
- /** @type {unknown} */ (Readable.toWeb(nodeRequest))
173
- );
166
+ // A destroyed upload is already disturbed and cannot become a Request body.
167
+ // Keep its read failure observable when dev middleware converts it late.
168
+ if (disconnected) {
169
+ init.body = new ReadableStream({
170
+ start(controller) {
171
+ controller.error(abortController.signal.reason);
172
+ },
173
+ });
174
+ } else {
175
+ // node:stream/web's ReadableStream and the DOM lib's are structurally the
176
+ // same at runtime; the lib types disagree on BYOB details.
177
+ init.body = /** @type {ReadableStream} */ (
178
+ /** @type {unknown} */ (Readable.toWeb(nodeRequest))
179
+ );
180
+ }
174
181
  init.duplex = 'half';
175
182
  }
176
- return new Request(url, init);
183
+ const request = new Request(url, init);
184
+ const cleanup = () => {
185
+ nodeRequest.off('aborted', abortRequest);
186
+ nodeRequest.off('close', onRequestClose);
187
+ nodeResponse?.off('close', onResponseClose);
188
+ nodeResponse?.off('finish', cleanup);
189
+ };
190
+ const abortRequest = () => {
191
+ cleanup();
192
+ abortController.abort(new Error('The client disconnected before the request completed.'));
193
+ };
194
+ const onRequestClose = () => {
195
+ if (!nodeRequest.complete) abortRequest();
196
+ else if (nodeResponse === undefined) cleanup();
197
+ };
198
+ const onResponseClose = () => {
199
+ if (nodeResponse?.writableFinished) cleanup();
200
+ else abortRequest();
201
+ };
202
+ // IncomingMessage closes when its body is consumed. The response owns the
203
+ // remaining lifetime, including async handler work and streaming output.
204
+ if (!disconnected && !nodeResponse?.writableFinished) {
205
+ nodeRequest.once('aborted', abortRequest);
206
+ nodeRequest.once('close', onRequestClose);
207
+ nodeResponse?.once('close', onResponseClose);
208
+ nodeResponse?.once('finish', cleanup);
209
+ }
210
+ return request;
177
211
  }
178
212
 
179
213
  /**
@@ -452,7 +486,7 @@ export function createNodeServer(handler, options = {}) {
452
486
  ) {
453
487
  return;
454
488
  }
455
- const response = await handler(nodeRequestToWebRequest(req));
489
+ const response = await handler(nodeRequestToWebRequest(req, res));
456
490
  await sendWebResponseForRequest(res, response, req);
457
491
  })().catch((error) => {
458
492
  console.error('[octane] Request error:', error);
@@ -471,7 +471,7 @@ export const handler = createHandler(
471
471
  */
472
472
  export async function nodeHandler(req, res) {
473
473
  try {
474
- const response = await handler(nodeRequestToWebRequest(req));
474
+ const response = await handler(nodeRequestToWebRequest(req, res));
475
475
  await sendWebResponse(res, response);
476
476
  } catch (error) {
477
477
  console.error('[octane] Request error:', error);
package/types/node.d.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import type { IncomingMessage, ServerResponse, Server } from 'node:http';
2
2
 
3
- /** Convert a Node.js IncomingMessage to a Web Request. */
4
- export function nodeRequestToWebRequest(nodeRequest: IncomingMessage): Request;
3
+ /**
4
+ * Convert a Node.js IncomingMessage to a Web Request. Pass its response to
5
+ * cancel request.signal on a disconnect until the response finishes, including
6
+ * after the request body has been consumed. Without a response, cancellation
7
+ * only covers interrupted request uploads.
8
+ */
9
+ export function nodeRequestToWebRequest(
10
+ nodeRequest: IncomingMessage,
11
+ nodeResponse?: ServerResponse,
12
+ ): Request;
5
13
 
6
14
  /**
7
15
  * Pipe a Web Response to a Node.js ServerResponse, streaming chunk-by-chunk