|
1 | 1 | #!/usr/bin/env node |
2 | 2 | import process from 'node:process'; |
3 | 3 | import { createHash, randomBytes } from 'node:crypto'; |
4 | | -import { close as closeFd, createReadStream, realpathSync, write as writeFd } from 'node:fs'; |
| 4 | +import { closeSync as closeFdSync, createReadStream, createWriteStream, realpathSync } from 'node:fs'; |
5 | 5 | import { fileURLToPath } from 'node:url'; |
6 | 6 | import { join, resolve, sep } from 'node:path'; |
7 | 7 |
|
@@ -247,33 +247,55 @@ export function readInternalEnvelope(fd = 3, options = {}) { |
247 | 247 | stream.once('end', () => finish(() => { try { resolvePromise(JSON.parse(data)); } catch { reject(authorizationInputError()); } })); |
248 | 248 | }); |
249 | 249 | } |
250 | | -/** @param {unknown} value @param {number} [fd] @param {{maxBytes?:number,timeoutMs?:number,write?:(fd:number,buffer:Buffer,offset:number,length:number,position:null,callback:(error:NodeJS.ErrnoException|null,bytesWritten:number)=>void)=>void,close?:(fd:number,callback:(error?:NodeJS.ErrnoException|null)=>void)=>void}} [options] */ |
| 250 | +/** @param {unknown} value @param {number} [fd] @param {{maxBytes?:number,timeoutMs?:number,write?:(fd:number,buffer:Buffer,offset:number,length:number,position:null,callback:(error:NodeJS.ErrnoException|null,bytesWritten:number)=>void)=>void|{cancel?:()=>void},close?:(fd:number,callback:(error?:NodeJS.ErrnoException|null)=>void)=>void}} [options] */ |
251 | 251 | export function writeInternalResponse(value, fd = 4, options = {}) { |
252 | 252 | const maxBytes = options.maxBytes ?? 1024 * 1024; const timeoutMs = options.timeoutMs ?? 1_000; |
253 | 253 | if (!Number.isSafeInteger(fd) || fd < 3 || !Number.isSafeInteger(maxBytes) || maxBytes <= 0 || maxBytes > 1024 * 1024 || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) return Promise.reject(new PluginError('INTERNAL_RESPONSE_OPTIONS_INVALID', 'Internal response writer options are invalid.', { category: 'validation', remedy: 'Use a protected descriptor, a limit up to 1 MiB, and a positive deadline.' })); |
254 | 254 | const data = Buffer.from(`${JSON.stringify(value)}\n`); |
255 | 255 | if (data.length > maxBytes) return Promise.reject(new PluginError('INTERNAL_RESPONSE_TOO_LARGE', 'Internal response exceeded its limit.', { category: 'runtime', remedy: 'Inspect the job through status/result.' })); |
256 | | - const write = options.write ?? writeFd; const close = options.close ?? closeFd; |
| 256 | + /** @type {import('node:fs').WriteStream|null} */ |
| 257 | + let stream = null; |
| 258 | + const write = options.write ?? ((_fd, buffer, offset, length, _position, callback) => { |
| 259 | + if (!stream) { stream = createWriteStream(/** @type {any} */ (null), { fd, autoClose: false }); stream.on('error', () => {}); } |
| 260 | + stream.write(buffer.subarray(offset, offset + length), (error) => callback(error ? /** @type {NodeJS.ErrnoException} */ (error) : null, error ? 0 : length)); |
| 261 | + return { cancel: () => stream?.destroy() }; |
| 262 | + }); |
| 263 | + const close = options.close ?? ((targetFd, callback) => { |
| 264 | + try { closeFdSync(targetFd); callback(); } catch (error) { callback(/** @type {NodeJS.ErrnoException} */ (error)); } |
| 265 | + }); |
257 | 266 | return new Promise((resolvePromise, reject) => { |
258 | 267 | let offset = 0; let settled = false; let closing = false; |
| 268 | + /** @type {(()=>void)|null} */ |
| 269 | + let cancelPending = null; |
| 270 | + const dispose = () => { if (stream && !stream.destroyed) stream.destroy(); }; |
259 | 271 | /** @param {unknown} [error] */ |
260 | | - const finish = (error) => { if (settled) return; settled = true; clearTimeout(timer); if (error) reject(error); else resolvePromise(undefined); }; |
| 272 | + const finish = (error) => { if (settled) return; settled = true; clearTimeout(timer); dispose(); if (error) reject(error); else resolvePromise(undefined); }; |
261 | 273 | /** @param {unknown} cause @param {string} [code] */ |
262 | 274 | const failure = (cause, code = 'INTERNAL_RESPONSE_WRITE_FAILED') => new PluginError(code, 'Could not deliver the protected internal response.', { category: code.endsWith('TIMEOUT') ? 'timeout' : 'runtime', remedy: 'Retry the command through its installed skill.', cause }); |
263 | 275 | const timer = setTimeout(() => { |
264 | 276 | if (settled || closing) return; closing = true; |
| 277 | + const cancel = cancelPending; cancelPending = null; |
| 278 | + try { cancel?.(); } catch { /* best effort abort */ } |
265 | 279 | try { close(fd, () => {}); } catch { /* best effort abort */ } |
266 | 280 | finish(failure(new Error('Internal response write timed out.'), 'INTERNAL_RESPONSE_WRITE_TIMEOUT')); |
267 | 281 | }, timeoutMs); |
268 | 282 | const next = () => { |
269 | 283 | if (settled) return; |
270 | | - write(fd, data, offset, data.length - offset, null, (error, bytesWritten) => { |
| 284 | + let completed = false; |
| 285 | + /** @type {(()=>void)|null} */ |
| 286 | + let cancel = null; |
| 287 | + const callback = (/** @type {NodeJS.ErrnoException|null} */ error, /** @type {number} */ bytesWritten) => { |
| 288 | + completed = true; |
| 289 | + if (cancelPending === cancel) cancelPending = null; |
271 | 290 | if (settled) return; |
272 | 291 | if (error) return finish(failure(error)); |
273 | 292 | if (!Number.isSafeInteger(bytesWritten) || bytesWritten <= 0) return finish(failure(new Error('Internal response writer made no progress.'))); |
274 | 293 | offset += bytesWritten; |
275 | 294 | if (offset >= data.length) finish(); else queueMicrotask(next); |
276 | | - }); |
| 295 | + }; |
| 296 | + const operation = write(fd, data, offset, data.length - offset, null, callback); |
| 297 | + cancel = typeof operation?.cancel === 'function' ? operation.cancel : null; |
| 298 | + if (!completed) cancelPending = cancel; |
277 | 299 | }; |
278 | 300 | next(); |
279 | 301 | }); |
|
0 commit comments