UNPKG

@whatwg-node/node-fetch

Version:

Fetch API implementation for Node

404 lines (403 loc) • 15.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PonyfillReadableStream = void 0; const node_buffer_1 = require("node:buffer"); const node_events_1 = require("node:events"); const node_stream_1 = require("node:stream"); const promises_1 = require("node:stream/promises"); const promise_helpers_1 = require("@whatwg-node/promise-helpers"); const utils_js_1 = require("./utils.js"); function createController(desiredSize, readable) { let chunks = []; let _closed = false; let flushed = false; return { desiredSize, enqueue(chunk) { const buf = typeof chunk === 'string' ? node_buffer_1.Buffer.from(chunk) : chunk; if (!flushed) { chunks.push(buf); } else { readable.push(buf); } }, close() { if (chunks.length > 0) { this._flush(); } readable.push(null); _closed = true; }, error(error) { if (chunks.length > 0) { this._flush(); } readable.destroy(error); }, get _closed() { return _closed; }, _flush() { flushed = true; if (chunks.length > 0) { const concatenated = chunks.length > 1 ? node_buffer_1.Buffer.concat(chunks) : chunks[0]; readable.push(concatenated); chunks = []; } }, }; } function isNodeReadable(obj) { return obj?.read != null; } function isReadableStream(obj) { return obj?.getReader != null; } /** In-flight cancel(reason); keyed by underlying Readable (not stored on it). */ const pendingCancelReasons = new WeakMap(); const noop = () => { }; function isExpectedCancelError(err, reason) { const code = err?.code; return (Object.is(err, reason) || code === 'ERR_STREAM_PREMATURE_CLOSE' || err?.message === 'Premature close'); } class PonyfillReadableStream { readable; constructor(underlyingSource) { if (underlyingSource instanceof PonyfillReadableStream && underlyingSource.readable != null) { this.readable = underlyingSource.readable; } else if (isNodeReadable(underlyingSource)) { this.readable = underlyingSource; } else if (isReadableStream(underlyingSource)) { this.readable = node_stream_1.Readable.fromWeb(underlyingSource); } else { let started = false; let ongoing = false; const handleStart = (desiredSize) => { if (!started) { const controller = createController(desiredSize, this.readable); started = true; return (0, promise_helpers_1.handleMaybePromise)(() => underlyingSource?.start?.(controller), () => { controller._flush(); if (controller._closed) { return false; } return true; }); } return true; }; const readImpl = (desiredSize) => { return (0, promise_helpers_1.handleMaybePromise)(() => handleStart(desiredSize), shouldContinue => { if (!shouldContinue) { return; } const controller = createController(desiredSize, this.readable); return (0, promise_helpers_1.handleMaybePromise)(() => underlyingSource?.pull?.(controller), () => { controller._flush(); ongoing = false; }); }); }; this.readable = new node_stream_1.Readable({ read(desiredSize) { if (ongoing) { return; } ongoing = true; return readImpl(desiredSize); }, destroy(err, callback) { // cancel() sets a pending marker and destroy()s without an error for this path. // A racing destroy(otherErr) must still surface as a real failure. const pending = pendingCancelReasons.get(this); const fromCancel = pending != null && err == null; const cancelReason = fromCancel ? pending.value : undefined; if (fromCancel) { pendingCancelReasons.delete(this); } if (underlyingSource?.cancel) { try { const res$ = underlyingSource.cancel(fromCancel ? cancelReason : err); if (res$?.then) { return res$.then(() => { // Preserve real destroy(err) after the cancel hook runs. callback(fromCancel ? null : (err ?? null)); }, cancelErr => { callback(cancelErr); }); } } catch (cancelErr) { callback(cancelErr); return; } callback(fromCancel ? null : (err ?? null)); return; } // Explicit cancel must not surface as a stream failure if (fromCancel) { callback(null); return; } // No cancel hook: propagate destroy(err) so piped destinations observe the failure (#3011) callback(err ?? null); }, }); } } cancel(reason) { if (this.readable.destroyed) { const errored = this.readable.errored; if (errored != null) { return (0, promise_helpers_1.fakeRejectPromise)(errored); } return (0, utils_js_1.fakePromise)(); } pendingCancelReasons.set(this.readable, { value: reason }); const readable = this.readable; readable.once('error', noop); // Always destroy without an error: cancel intent is in the WeakMap for aware streams. // For wrapped streams, pipeThrough forwards cancel(reason) to the source explicitly. const whenDone = (0, promises_1.finished)(readable).then(() => undefined, err => { // Swallow intentional cancel artifacts: destroy(reason) on wrapped streams, and // ERR_STREAM_PREMATURE_CLOSE from destroy() before the readable is drained. // Still propagate cancel-hook failures and racing destroy(err) failures. if (isExpectedCancelError(err, reason)) { return undefined; } throw err; }); readable.destroy(); return whenDone.finally(() => { readable.off('error', noop); pendingCancelReasons.delete(readable); }); } locked = false; getReader(_options) { const iterator = this.readable[Symbol.asyncIterator](); this.locked = true; const thisReadable = this.readable; return { read() { return iterator.next(); }, releaseLock: () => { if (iterator.return) { const retResult$ = iterator.return(); if (retResult$.then) { retResult$.then(() => { this.locked = false; }); return; } } this.locked = false; }, cancel: (reason) => { this.locked = false; // Route through stream cancel so pipeThrough's cancel forwarding still runs. return this.cancel(reason); }, get closed() { return Promise.race([ (0, node_events_1.once)(thisReadable, 'end'), (0, node_events_1.once)(thisReadable, 'error').then(err => Promise.reject(err)), ]); }, }; } [Symbol.asyncIterator](options) { const preventCancel = options?.preventCancel === true; // Node's default async iterator destroys on return; honor preventCancel via destroyOnReturn. const iterator = (typeof this.readable.iterator === 'function' ? this.readable.iterator({ destroyOnReturn: !preventCancel }) : this.readable[Symbol.asyncIterator]()); const iterable = { [Symbol.asyncIterator]() { return this; }, [Symbol.asyncDispose]: async () => { await iterator.return?.(); if (!preventCancel && !this.readable.destroyed) { this.readable.destroy(); } }, next: () => iterator.next(), return: () => { if (!preventCancel && !this.readable.destroyed) { this.readable.destroy(); } return iterator.return?.() || (0, utils_js_1.fakePromise)({ done: true, value: undefined }); }, throw: (err) => { if (!this.readable.destroyed) { this.readable.destroy(err); } return iterator.throw?.(err) || (0, utils_js_1.fakePromise)({ done: true, value: undefined }); }, }; return iterable; } values(options) { return this[Symbol.asyncIterator](options); } tee() { throw new Error('Not implemented'); } async pipeToWriter(writer) { try { for await (const chunk of this) { await writer.write(chunk); } await writer.close(); } catch (err) { try { await writer.abort(err); } catch { // Ignore abort failures; still surface the original write/close error. } throw err; } } pipeTo(destination) { if (isPonyfillWritableStream(destination)) { // Prefer `.pipe()` over `stream/promises.pipeline`: the promise pipeline allocates an // AbortController and aborts it on teardown (DOMException + stack), which dominates // per-request cost for short pipeThrough TransformStreams (e.g. body-size limiting). // Still mirror pipeline's premature-close behavior: dest/src `close` without a normal // `finish` / end must reject so canceling a TransformStream readable cannot hang pipeTo. const src = this.readable; const dest = destination.writable; return new Promise((resolve, reject) => { let settled = false; let completed = false; const settle = (err) => { if (settled) { return; } settled = true; src.off('error', onError); dest.off('error', onError); dest.off('finish', onFinish); src.off('close', onSrcClose); dest.off('close', onDestClose); if (err != null) { reject(err); } else { resolve(); } }; const prematureCloseError = () => { const err = new Error('Premature close'); err.code = 'ERR_STREAM_PREMATURE_CLOSE'; return err; }; const onError = (err) => { if (!src.destroyed) { src.once('error', noop); src.destroy(err); } if (!dest.destroyed) { dest.once('error', noop); dest.destroy(err); } settle(err); }; const onFinish = () => { completed = true; settle(); }; const onSrcClose = () => { if (!completed && !src.readableEnded) { onError(prematureCloseError()); } }; const onDestClose = () => { if (!completed && !dest.writableFinished) { onError(prematureCloseError()); } }; src.once('error', onError); dest.once('error', onError); dest.once('finish', onFinish); src.once('close', onSrcClose); dest.once('close', onDestClose); src.pipe(dest, { end: true }); }); } const writer = destination.getWriter(); return this.pipeToWriter(writer); } pipeThrough({ writable, readable, }) { const pipePromise = this.pipeTo(writable); pipePromise.catch(err => { if (!this.readable.destroyed) { this.readable.once('error', noop); this.readable.destroy(err); } }); if (isPonyfillReadableStream(readable)) { const onError = (err) => { if (!this.readable.destroyed) { this.readable.once('error', noop); this.readable.destroy(err); } }; const onEnd = () => { if (!this.readable.destroyed && !this.readable.readableEnded) { this.readable.push(null); } }; readable.readable.once('error', onError); readable.readable.once('finish', onEnd); readable.readable.once('close', onEnd); // finally() re-settles like the original promise; must catch or it becomes unhandled. pipePromise .finally(() => { readable.readable.off('error', onError); readable.readable.off('finish', onEnd); readable.readable.off('close', onEnd); }) .catch(() => { }); // Forward cancel(reason) to the source first (sequential) so a racing pipe // destroy(err) cannot overwrite the cancel reason with ERR_STREAM_PREMATURE_CLOSE. const outCancel = readable.cancel.bind(readable); readable.cancel = async (reason) => { await this.cancel(reason); try { await outCancel(reason); } catch (err) { if (!isExpectedCancelError(err, reason)) { throw err; } } }; } return readable; } static [Symbol.hasInstance](instance) { return isReadableStream(instance); } static from(iterable) { return new PonyfillReadableStream(node_stream_1.Readable.from(iterable)); } [Symbol.toStringTag] = 'ReadableStream'; } exports.PonyfillReadableStream = PonyfillReadableStream; function isPonyfillReadableStream(obj) { return obj?.readable != null; } function isPonyfillWritableStream(obj) { return obj?.writable != null; }