In `Writer`, there are two queues that matter: the slot queue and the pending queue. The sync `writeSync`/`writevSync` only use the slot queue. If writes cannot be accepted directly into slots, they return `false`. The sync methods *never* enqueue into the pending queue. The async `write`/`writev` will first attempt to add to the slot queue; if it is full, then it will attempt to add to the pending queue; if that is also full, the backpressure policy kicks in. Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63697 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
721 lines
20 KiB
JavaScript
721 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
// New Streams API - Push Stream Implementation
|
|
//
|
|
// Creates a bonded pair of writer and async iterable for push-based streaming
|
|
// with built-in backpressure.
|
|
|
|
const {
|
|
ArrayIsArray,
|
|
ArrayPrototypePush,
|
|
MathMax,
|
|
PromiseReject,
|
|
PromiseResolve,
|
|
PromiseWithResolvers,
|
|
Symbol,
|
|
SymbolAsyncDispose,
|
|
SymbolAsyncIterator,
|
|
SymbolDispose,
|
|
TypedArrayPrototypeGetByteLength,
|
|
} = primordials;
|
|
|
|
const {
|
|
codes: {
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_STATE,
|
|
},
|
|
} = require('internal/errors');
|
|
const { isError, lazyDOMException } = require('internal/util');
|
|
const {
|
|
validateAbortSignal,
|
|
validateInteger,
|
|
} = require('internal/validators');
|
|
|
|
const {
|
|
drainableProtocol,
|
|
} = require('internal/streams/iter/types');
|
|
|
|
const {
|
|
kPushDefaultHWM,
|
|
kResolvedPromise,
|
|
clampHWM,
|
|
onSignalAbort,
|
|
toUint8Array,
|
|
convertChunks,
|
|
parsePullArgs,
|
|
validateBackpressure,
|
|
} = require('internal/streams/iter/utils');
|
|
|
|
const {
|
|
pull: pullWithTransforms,
|
|
} = require('internal/streams/iter/pull');
|
|
|
|
const {
|
|
RingBuffer,
|
|
} = require('internal/streams/iter/ringbuffer');
|
|
|
|
const kNoFailReason = Symbol('kNoFailReason');
|
|
|
|
// =============================================================================
|
|
// PushQueue - Internal Queue with Chunk-Based Backpressure
|
|
// =============================================================================
|
|
|
|
class PushQueue {
|
|
/** Buffered chunks (each slot is from one write/writev call) */
|
|
#slots = new RingBuffer();
|
|
/** Pending writes waiting for buffer space */
|
|
#pendingWrites = new RingBuffer();
|
|
/** Pending reads waiting for data */
|
|
#pendingReads = new RingBuffer();
|
|
/** Pending drains waiting for backpressure to clear */
|
|
#pendingDrains = [];
|
|
/** Writer state: 'open' | 'closing' | 'closed' | 'errored' */
|
|
#writerState = 'open';
|
|
/** Consumer state: 'active' | 'returned' | 'thrown' */
|
|
#consumerState = 'active';
|
|
/** Error that closed the stream */
|
|
#error = null;
|
|
/** Total bytes written */
|
|
#bytesWritten = 0;
|
|
/** Pending end promise (resolves when consumer drains past end sentinel) */
|
|
#pendingEnd = null;
|
|
|
|
/** Configuration */
|
|
#highWaterMark;
|
|
#backpressure;
|
|
#signal;
|
|
#abortHandler;
|
|
|
|
constructor(options = { __proto__: null }) {
|
|
const {
|
|
highWaterMark = kPushDefaultHWM,
|
|
backpressure = 'strict',
|
|
signal,
|
|
} = options;
|
|
validateInteger(highWaterMark, 'options.highWaterMark');
|
|
validateBackpressure(backpressure);
|
|
if (signal !== undefined) {
|
|
validateAbortSignal(signal, 'options.signal');
|
|
}
|
|
this.#highWaterMark = clampHWM(highWaterMark);
|
|
this.#backpressure = backpressure;
|
|
this.#signal = signal;
|
|
this.#abortHandler = undefined;
|
|
|
|
if (this.#signal) {
|
|
this.#abortHandler = () => {
|
|
this.fail(isError(this.#signal.reason) ?
|
|
this.#signal.reason :
|
|
lazyDOMException('Aborted', 'AbortError'));
|
|
};
|
|
onSignalAbort(this.#signal, this.#abortHandler);
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Writer Methods
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Get slots available before hitting highWaterMark.
|
|
* Returns null if writer is closed/errored or consumer has terminated.
|
|
* @returns {number | null}
|
|
*/
|
|
get desiredSize() {
|
|
if (this.#writerState !== 'open' || this.#consumerState !== 'active') {
|
|
return null;
|
|
}
|
|
return MathMax(0, this.#highWaterMark - this.#slots.length);
|
|
}
|
|
|
|
/**
|
|
* Check if a sync write would be accepted.
|
|
* @returns {boolean}
|
|
*/
|
|
canWriteSync() {
|
|
if (this.#writerState !== 'open') return false;
|
|
if (this.#consumerState !== 'active') return false;
|
|
if ((this.#backpressure === 'strict' ||
|
|
this.#backpressure === 'block') &&
|
|
this.#slots.length >= this.#highWaterMark) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Write chunks synchronously if possible.
|
|
* Returns true if write completed, false if buffer is full.
|
|
* @returns {boolean}
|
|
*/
|
|
writeSync(chunks) {
|
|
if (this.#writerState !== 'open') return false;
|
|
if (this.#consumerState !== 'active') return false;
|
|
|
|
if (this.#slots.length >= this.#highWaterMark) {
|
|
switch (this.#backpressure) {
|
|
case 'strict':
|
|
return false;
|
|
case 'block':
|
|
return false;
|
|
case 'drop-oldest':
|
|
if (this.#slots.length > 0) {
|
|
this.#slots.shift();
|
|
}
|
|
break;
|
|
case 'drop-newest':
|
|
// Discard this write, but return true
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
this.#slots.push(chunks);
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
|
|
}
|
|
|
|
this.#resolvePendingReads();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Write chunks asynchronously.
|
|
* If signal is provided, a write blocked on backpressure will reject
|
|
* immediately when the signal fires. The cancelled write is removed from
|
|
* pendingWrites so it does not occupy a slot. The queue itself is NOT put
|
|
* into an error state - this is per-operation cancellation, not terminal
|
|
* failure.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async writeAsync(chunks, signal) {
|
|
// Check writer state before signal (spec order: state, then signal)
|
|
if (this.#writerState === 'closed') {
|
|
throw new ERR_INVALID_STATE.TypeError('Writer is closed');
|
|
}
|
|
if (this.#writerState === 'closing') {
|
|
throw new ERR_INVALID_STATE.TypeError('Writer is closing');
|
|
}
|
|
if (this.#writerState === 'errored') {
|
|
throw this.#error;
|
|
}
|
|
if (this.#consumerState !== 'active') {
|
|
throw this.#consumerState === 'thrown' && this.#error ?
|
|
this.#error :
|
|
new ERR_INVALID_STATE.TypeError('Stream closed by consumer');
|
|
}
|
|
|
|
// Check for pre-aborted signal (after state checks per spec)
|
|
signal?.throwIfAborted();
|
|
|
|
// Try sync first
|
|
if (this.writeSync(chunks)) {
|
|
return;
|
|
}
|
|
|
|
// Buffer is full
|
|
switch (this.#backpressure) {
|
|
case 'strict':
|
|
if (this.#pendingWrites.length >= this.#highWaterMark) {
|
|
throw new ERR_INVALID_STATE.RangeError(
|
|
'Backpressure violation: too many pending writes. ' +
|
|
'Await each write() call to respect backpressure.');
|
|
}
|
|
return this.#createPendingWrite(chunks, signal);
|
|
case 'block':
|
|
return this.#createPendingWrite(chunks, signal);
|
|
default:
|
|
throw new ERR_INVALID_STATE(
|
|
'Unexpected: writeSync should have handled non-strict policy');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a pending write promise, optionally racing against a signal.
|
|
* If the signal fires, the entry is removed from pendingWrites and the
|
|
* promise rejects. Signal listeners are cleaned up on normal resolution.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
#createPendingWrite(chunks, signal) {
|
|
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
const entry = { __proto__: null, chunks, resolve, reject };
|
|
this.#pendingWrites.push(entry);
|
|
|
|
if (signal) {
|
|
const onAbort = () => {
|
|
// Remove from queue so it doesn't occupy a slot
|
|
const idx = this.#pendingWrites.indexOf(entry);
|
|
if (idx !== -1) this.#pendingWrites.removeAt(idx);
|
|
reject(signal.reason ?? lazyDOMException('Aborted', 'AbortError'));
|
|
};
|
|
|
|
// Wrap resolve/reject to clean up signal listener
|
|
entry.resolve = function() {
|
|
signal.removeEventListener('abort', onAbort);
|
|
resolve();
|
|
};
|
|
entry.reject = function(reason) {
|
|
signal.removeEventListener('abort', onAbort);
|
|
reject(reason);
|
|
};
|
|
|
|
signal.addEventListener('abort', onAbort, { __proto__: null, once: true });
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Signal end of stream. Returns total bytes written.
|
|
* @returns {number}
|
|
*/
|
|
end() {
|
|
if (this.#writerState === 'errored') {
|
|
return -2; // Signal to reject with stored error
|
|
}
|
|
if (this.#writerState === 'closing') {
|
|
return -3; // Signal to PushWriter: wait for drain to complete
|
|
}
|
|
if (this.#writerState === 'closed') {
|
|
return this.#bytesWritten; // Idempotent
|
|
}
|
|
|
|
this.#cleanup();
|
|
this.#rejectPendingWrites(
|
|
new ERR_INVALID_STATE.TypeError('Writer closed'));
|
|
this.#resolvePendingDrains(false);
|
|
|
|
// If buffer is empty, close immediately
|
|
if (this.#slots.length === 0) {
|
|
this.#writerState = 'closed';
|
|
this.#resolvePendingReads();
|
|
return this.#bytesWritten;
|
|
}
|
|
|
|
// Buffer has data: transition to closing, defer completion until drained
|
|
this.#writerState = 'closing';
|
|
return -3; // Signal to PushWriter: create deferred end promise
|
|
}
|
|
|
|
/**
|
|
* Called by the read path when the consumer has drained all data while
|
|
* the writer is in the 'closing' state. Transitions to 'closed' and
|
|
* resolves the pending end promise.
|
|
*/
|
|
endDrained() {
|
|
if (this.#writerState !== 'closing') return;
|
|
this.#writerState = 'closed';
|
|
if (this.#pendingEnd) {
|
|
this.#pendingEnd.resolve(this.#bytesWritten);
|
|
this.#pendingEnd = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Put queue into terminal error state.
|
|
* No-op if errored or closed (fully drained).
|
|
* If closing (draining), short-circuits the drain.
|
|
*/
|
|
fail(reason = kNoFailReason) {
|
|
if (this.#writerState === 'errored' || this.#writerState === 'closed') {
|
|
return;
|
|
}
|
|
|
|
const wasClosing = this.#writerState === 'closing';
|
|
this.#writerState = 'errored';
|
|
this.#error = reason === kNoFailReason ?
|
|
new ERR_INVALID_STATE('Failed') :
|
|
reason;
|
|
this.#cleanup();
|
|
this.#rejectPendingReads(this.#error);
|
|
this.#rejectPendingDrains(this.#error);
|
|
|
|
if (wasClosing) {
|
|
// Short-circuit the graceful drain: reject the pending end promise
|
|
if (this.#pendingEnd) {
|
|
this.#pendingEnd.reject(this.#error);
|
|
this.#pendingEnd = null;
|
|
}
|
|
} else {
|
|
this.#rejectPendingWrites(this.#error);
|
|
}
|
|
}
|
|
|
|
get totalBytesWritten() {
|
|
return this.#bytesWritten;
|
|
}
|
|
|
|
get error() {
|
|
return this.#error;
|
|
}
|
|
|
|
get backpressurePolicy() {
|
|
return this.#backpressure;
|
|
}
|
|
|
|
get writerState() {
|
|
return this.#writerState;
|
|
}
|
|
|
|
get pendingEndPromise() {
|
|
return this.#pendingEnd?.promise ?? null;
|
|
}
|
|
|
|
setPendingEnd(pending) {
|
|
this.#pendingEnd = pending;
|
|
}
|
|
|
|
/**
|
|
* Wait for backpressure to clear (desiredSize > 0).
|
|
* @returns {Promise<void>}
|
|
*/
|
|
waitForDrain() {
|
|
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
ArrayPrototypePush(this.#pendingDrains, { __proto__: null, resolve, reject });
|
|
return promise;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Consumer Methods
|
|
// ===========================================================================
|
|
|
|
async read() {
|
|
// If there's data in the buffer, return it immediately
|
|
if (this.#slots.length > 0) {
|
|
const result = this.#drain();
|
|
this.#resolvePendingWrites();
|
|
// After draining, check if writer was closing and buffer is now empty
|
|
if (this.#writerState === 'closing' && this.#slots.length === 0) {
|
|
this.endDrained();
|
|
}
|
|
return { __proto__: null, done: false, value: result };
|
|
}
|
|
|
|
// Buffer empty and writer closing = drain complete
|
|
if (this.#writerState === 'closing') {
|
|
this.endDrained();
|
|
return { __proto__: null, done: true, value: undefined };
|
|
}
|
|
|
|
if (this.#writerState === 'closed') {
|
|
return { __proto__: null, done: true, value: undefined };
|
|
}
|
|
|
|
if (this.#writerState === 'errored') {
|
|
throw this.#error;
|
|
}
|
|
|
|
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
this.#pendingReads.push({ __proto__: null, resolve, reject });
|
|
return promise;
|
|
}
|
|
|
|
consumerReturn() {
|
|
if (this.#consumerState !== 'active') return;
|
|
this.#consumerState = 'returned';
|
|
this.#cleanup();
|
|
this.#resolvePendingReads();
|
|
this.#rejectPendingWrites(
|
|
new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
|
|
// If closing, reject the pending end promise
|
|
if (this.#writerState === 'closing' && this.#pendingEnd) {
|
|
this.#pendingEnd.reject(
|
|
new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
|
|
this.#pendingEnd = null;
|
|
}
|
|
// Resolve pending drains with false - no more data will be consumed
|
|
this.#resolvePendingDrains(false);
|
|
}
|
|
|
|
consumerThrow(error) {
|
|
if (this.#consumerState !== 'active') return;
|
|
this.#consumerState = 'thrown';
|
|
this.#error = error;
|
|
this.#cleanup();
|
|
this.#rejectPendingReads(error);
|
|
this.#rejectPendingWrites(error);
|
|
if (this.#writerState === 'closing' && this.#pendingEnd) {
|
|
this.#pendingEnd.reject(error);
|
|
this.#pendingEnd = null;
|
|
}
|
|
// Reject pending drains - the consumer errored
|
|
this.#rejectPendingDrains(error);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Private Methods
|
|
// ===========================================================================
|
|
|
|
#drain() {
|
|
if (this.#slots.length === 1) {
|
|
return this.#slots.shift();
|
|
}
|
|
|
|
const result = [];
|
|
for (let i = 0; i < this.#slots.length; i++) {
|
|
const slot = this.#slots.get(i);
|
|
for (let j = 0; j < slot.length; j++) {
|
|
ArrayPrototypePush(result, slot[j]);
|
|
}
|
|
}
|
|
this.#slots.clear();
|
|
return result;
|
|
}
|
|
|
|
#resolvePendingReads() {
|
|
while (this.#pendingReads.length > 0) {
|
|
if (this.#slots.length > 0) {
|
|
const pending = this.#pendingReads.shift();
|
|
const result = this.#drain();
|
|
this.#resolvePendingWrites();
|
|
pending.resolve({ __proto__: null, done: false, value: result });
|
|
} else if (this.#writerState === 'closing' && this.#slots.length === 0) {
|
|
this.endDrained();
|
|
const pending = this.#pendingReads.shift();
|
|
pending.resolve({ __proto__: null, done: true, value: undefined });
|
|
} else if (this.#writerState === 'closed') {
|
|
const pending = this.#pendingReads.shift();
|
|
pending.resolve({ __proto__: null, done: true, value: undefined });
|
|
} else if (this.#writerState === 'errored') {
|
|
const pending = this.#pendingReads.shift();
|
|
pending.reject(this.#error);
|
|
} else if (this.#consumerState === 'returned') {
|
|
const pending = this.#pendingReads.shift();
|
|
pending.resolve({ __proto__: null, done: true, value: undefined });
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#resolvePendingWrites() {
|
|
while (this.#pendingWrites.length > 0 &&
|
|
this.#slots.length < this.#highWaterMark) {
|
|
const pending = this.#pendingWrites.shift();
|
|
this.#slots.push(pending.chunks);
|
|
for (let i = 0; i < pending.chunks.length; i++) {
|
|
this.#bytesWritten += TypedArrayPrototypeGetByteLength(pending.chunks[i]);
|
|
}
|
|
pending.resolve();
|
|
}
|
|
|
|
if (this.#slots.length < this.#highWaterMark) {
|
|
this.#resolvePendingDrains(true);
|
|
}
|
|
}
|
|
|
|
#resolvePendingDrains(canWrite) {
|
|
const drains = this.#pendingDrains;
|
|
this.#pendingDrains = [];
|
|
for (let i = 0; i < drains.length; i++) {
|
|
drains[i].resolve(canWrite);
|
|
}
|
|
}
|
|
|
|
#rejectPendingDrains(error) {
|
|
const drains = this.#pendingDrains;
|
|
this.#pendingDrains = [];
|
|
for (let i = 0; i < drains.length; i++) {
|
|
drains[i].reject(error);
|
|
}
|
|
}
|
|
|
|
#rejectPendingReads(error) {
|
|
while (this.#pendingReads.length > 0) {
|
|
this.#pendingReads.shift().reject(error);
|
|
}
|
|
}
|
|
|
|
#rejectPendingWrites(error) {
|
|
while (this.#pendingWrites.length > 0) {
|
|
this.#pendingWrites.shift().reject(error);
|
|
}
|
|
}
|
|
|
|
#cleanup() {
|
|
if (this.#signal && this.#abortHandler) {
|
|
this.#signal.removeEventListener('abort', this.#abortHandler);
|
|
this.#abortHandler = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// PushWriter Implementation
|
|
// =============================================================================
|
|
|
|
class PushWriter {
|
|
#queue;
|
|
|
|
constructor(queue) {
|
|
this.#queue = queue;
|
|
}
|
|
|
|
[drainableProtocol]() {
|
|
const desired = this.desiredSize;
|
|
if (desired === null) return null;
|
|
if (desired > 0) return PromiseResolve(true);
|
|
return this.#queue.waitForDrain();
|
|
}
|
|
|
|
get desiredSize() {
|
|
return this.#queue.desiredSize;
|
|
}
|
|
|
|
write(chunk, options) {
|
|
if (!options?.signal && this.#queue.canWriteSync()) {
|
|
const bytes = toUint8Array(chunk);
|
|
this.#queue.writeSync([bytes]);
|
|
return kResolvedPromise;
|
|
}
|
|
const bytes = toUint8Array(chunk);
|
|
return this.#queue.writeAsync([bytes], options?.signal);
|
|
}
|
|
|
|
writev(chunks, options) {
|
|
if (!ArrayIsArray(chunks)) {
|
|
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
|
|
}
|
|
if (!options?.signal && this.#queue.canWriteSync()) {
|
|
const bytes = convertChunks(chunks);
|
|
this.#queue.writeSync(bytes);
|
|
return kResolvedPromise;
|
|
}
|
|
const bytes = convertChunks(chunks);
|
|
return this.#queue.writeAsync(bytes, options?.signal);
|
|
}
|
|
|
|
writeSync(chunk) {
|
|
const bytes = toUint8Array(chunk);
|
|
return this.#queue.writeSync([bytes]);
|
|
}
|
|
|
|
writevSync(chunks) {
|
|
if (!ArrayIsArray(chunks)) {
|
|
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
|
|
}
|
|
const bytes = convertChunks(chunks);
|
|
return this.#queue.writeSync(bytes);
|
|
}
|
|
|
|
end(options) {
|
|
const result = this.#queue.end();
|
|
if (result === -2) {
|
|
// Errored: reject with stored error
|
|
return PromiseReject(this.#queue.error);
|
|
}
|
|
if (result === -3) {
|
|
// Closing: buffer has data, create deferred promise that resolves
|
|
// when consumer drains past the end sentinel
|
|
const pendingEndPromise = this.#queue.pendingEndPromise;
|
|
if (pendingEndPromise !== null) {
|
|
return pendingEndPromise;
|
|
}
|
|
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
this.#queue.setPendingEnd({ __proto__: null, promise, resolve, reject });
|
|
return promise;
|
|
}
|
|
// >= 0: byte count (immediate close or idempotent)
|
|
return PromiseResolve(result);
|
|
}
|
|
|
|
endSync() {
|
|
const result = this.#queue.end();
|
|
if (result === -2) return -1; // Errored
|
|
if (result === -3) return -1; // Buffer not empty, can't wait
|
|
return result;
|
|
}
|
|
|
|
fail(reason) {
|
|
this.#queue.fail(arguments.length === 0 ? kNoFailReason : reason);
|
|
}
|
|
|
|
[SymbolAsyncDispose]() {
|
|
const state = this.#queue.writerState;
|
|
if (state === 'closing') {
|
|
// Wait for graceful drain
|
|
return this.#queue.pendingEndPromise ?? PromiseResolve();
|
|
}
|
|
if (state === 'open') {
|
|
this.fail();
|
|
}
|
|
return PromiseResolve();
|
|
}
|
|
|
|
[SymbolDispose]() {
|
|
this.fail();
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Readable Implementation
|
|
// =============================================================================
|
|
|
|
function createReadable(queue) {
|
|
return {
|
|
__proto__: null,
|
|
[SymbolAsyncIterator]() {
|
|
return {
|
|
__proto__: null,
|
|
async next() {
|
|
return queue.read();
|
|
},
|
|
async return() {
|
|
queue.consumerReturn();
|
|
return { __proto__: null, done: true, value: undefined };
|
|
},
|
|
async throw(error) {
|
|
queue.consumerThrow(error);
|
|
return { __proto__: null, done: true, value: undefined };
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Stream.push() Factory
|
|
// =============================================================================
|
|
|
|
function parseArgs(args) {
|
|
const result = parsePullArgs(args);
|
|
// PushQueue constructor requires a non-undefined options object.
|
|
if (result.options === undefined) {
|
|
result.options = { __proto__: null };
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create a push stream with optional transforms.
|
|
* @param {...(Function|object)} args - Transforms, then options (optional)
|
|
* @returns {{ writer: Writer, readable: AsyncIterable<Uint8Array[]> }}
|
|
*/
|
|
function push(...args) {
|
|
const { transforms, options } = parseArgs(args);
|
|
|
|
const queue = new PushQueue(options);
|
|
const writer = new PushWriter(queue);
|
|
const rawReadable = createReadable(queue);
|
|
|
|
// Apply transforms lazily if provided
|
|
let readable;
|
|
if (transforms.length > 0) {
|
|
if (options.signal) {
|
|
readable = pullWithTransforms(
|
|
rawReadable, ...transforms, { __proto__: null, signal: options.signal });
|
|
} else {
|
|
readable = pullWithTransforms(rawReadable, ...transforms);
|
|
}
|
|
} else {
|
|
readable = rawReadable;
|
|
}
|
|
|
|
return { __proto__: null, writer, readable };
|
|
}
|
|
|
|
module.exports = {
|
|
push,
|
|
};
|