Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/api/stream_iter.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1675,7 +1675,7 @@ Creates a classic [`stream.Writable`][] backed by a stream/iter Writer.

Each `_write()` / `_writev()` call attempts the Writer's synchronous method
first (`writeSync` / `writevSync`), falling back to the async method if the
sync path returns `false` or throws. Similarly, `_final()` tries `endSync()`
sync path returns `false`. Similarly, `_final()` tries `endSync()`
before `end()`. When the sync path succeeds, the callback is deferred via
`queueMicrotask` to preserve the async resolution contract.

Expand Down
67 changes: 16 additions & 51 deletions lib/internal/streams/iter/classic.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ const {
const {
toAsyncStreamable: kToAsyncStreamable,
kValidatedSource,
kSyncWriteAccepted,
drainableProtocol,
} = require('internal/streams/iter/types');

Expand DownExpand Up@@ -765,41 +764,11 @@ function toWritable(writer) {
const hasEndSync = hasEnd &&
typeof writer.endSync === 'function';
const hasFail = typeof writer.fail === 'function';
const hasSyncWriteAccepted =
typeof writer[kSyncWriteAccepted] === 'function';

function syncWriteAccepted() {
return hasSyncWriteAccepted && writer[kSyncWriteAccepted]();
}

function finishAfterSyncBackpressure(cb) {
let ondrain;
try {
if (typeof writer[drainableProtocol] === 'function') {
ondrain = writer[drainableProtocol]();
}
} catch (err) {
cb(err);
return;
}
if (ondrain !== null && ondrain !== undefined) {
PromisePrototypeThen(ondrain, (drained) => {
if (drained === false) {
cb(new ERR_INVALID_STATE.TypeError('Stream closed by consumer'));
return;
}
cb();
}, cb);
return;
}
queueMicrotask(cb);
}

// Try-sync-first pattern: attempt the synchronous method and fall back to the
// async method if it returns false without accepting the data, or if it
// throws. When the sync path succeeds, the callback is deferred via
// queueMicrotask to preserve the async resolution contract that Writable
// internals expect from _write/_writev/_final callbacks.
// async method if it returns false (data not accepted synchronously).
// When the sync path succeeds, the callback is deferred via queueMicrotask
// to preserve the async resolution contract that Writable internals expect
// from _write/_writev/_final callbacks.

function _write(chunk, encoding, cb) {
const bytes = typeof chunk === 'string' ?
Expand All@@ -810,13 +779,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunk was accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WriteSync returned false: not accepted, fall through to async.
} catch (err) {
Comment thread
jasnell marked this conversation as resolved.
cb(err);
return;
}
}
try {
Expand All@@ -839,13 +805,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
if (syncWriteAccepted()) {
// The chunks were accepted; false only signaled backpressure.
finishAfterSyncBackpressure(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// WritevSync returned false: not accepted, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand All@@ -867,8 +830,10 @@ function toWritable(writer) {
queueMicrotask(cb);
return;
}
} catch {
// Sync path threw -- fall through to async.
// Result < 0: can't end synchronously, fall through to async.
} catch (err) {
cb(err);
return;
}
}
try {
Expand Down
45 changes: 35 additions & 10 deletions lib/internal/streams/iter/consumers.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -480,6 +480,7 @@ function merge(...args) {
);
}

let primaryError;
try {
while (activeCount > 0 || ready.length > 0) {
signal?.throwIfAborted();
Expand All@@ -500,22 +501,46 @@ function merge(...args) {
});
}
}
} catch (err) {
primaryError = err;
} finally {
// Clean up: return all iterators
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch {
// Ignore return errors
}
}
});
// Clean up: return all iterators. Cleanup errors are not
// swallowed - a broken iterator.return() (e.g., failing to
// release a resource) should be visible to the caller.
await cleanupIterators(iterators, primaryError);
}
},
};
}

async function cleanupIterators(iterators, primaryError) {
let cleanupError;
await SafePromiseAllReturnVoid(iterators, async (iterator) => {
if (iterator.return) {
try {
await iterator.return();
} catch (err) {
// Keep the first cleanup error encountered.
cleanupError ??= err;
}
}
});
if (cleanupError !== undefined) {
if (primaryError !== undefined) {
// Both a primary error and a cleanup error occurred.
// Wrap in SuppressedError so neither is lost:
// .error = primaryError, .suppressed = cleanupError.
// eslint-disable-next-line no-restricted-syntax
throw new SuppressedError(primaryError, cleanupError);
}
// No primary error - the cleanup error is the only error.
throw cleanupError;
}
if (primaryError !== undefined) {
throw primaryError;
}
}

module.exports = {
array,
arrayBuffer,
Expand Down
56 changes: 5 additions & 51 deletions lib/internal/streams/iter/pull.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,8 +63,6 @@ const {
} = require('internal/streams/iter/utils');

const {
drainableProtocol,
kSyncWriteAcceptedOnFalse,
kValidatedSource,
kValidatedTransform,
toAsyncStreamable,
Expand DownExpand Up@@ -863,18 +861,6 @@ async function* createAsyncPipeline(source, transforms, signal) {
}
}

/**
* Check if a false sync write result means accepted backpressure.
* @param {object} writer - The writer whose sync method returned.
* @param {*} result - The return value from writeSync() or writevSync().
* @returns {boolean}
*/
function isAcceptedSyncWriteBackpressure(writer, result) {
return result === false &&
writer[kSyncWriteAcceptedOnFalse] === true &&
writer.desiredSize === 0;
}

// =============================================================================
// Public API: pull() and pullSync()
// =============================================================================
Expand DownExpand Up@@ -968,9 +954,7 @@ function pipeToSync(source, ...args) {
break;
}
if (hasWritevSync && batch.length > 1) {
const result = writer.writevSync(batch);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writevSync(batch) === false) {
break;
}
for (let i = 0; i < batch.length; i++) {
Expand All@@ -979,9 +963,7 @@ function pipeToSync(source, ...args) {
} else {
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = writer.writeSync(chunk);
if (result === false &&
!isAcceptedSyncWriteBackpressure(writer, result)) {
if (writer.writeSync(chunk) === false) {
canContinue = false;
break;
}
Expand DownExpand Up@@ -1037,28 +1019,13 @@ async function pipeTo(source, ...args) {
const hasWritevSync = typeof writer.writevSync === 'function';
const hasEndSync = typeof writer.endSync === 'function';

function waitForSyncBackpressure() {
const ondrain = writer[drainableProtocol];
return ondrain?.call(writer);
}

async function writeBatchAfterAcceptedBackpressure(batch, startIndex) {
await waitForSyncBackpressure();
await writeBatchAsyncFallback(batch, startIndex);
}

// Async fallback for writeBatch when sync write fails partway through.
// Continues writing from batch[startIndex] using async write().
async function writeBatchAsyncFallback(batch, startIndex) {
for (let i = startIndex; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (result) {
if (hasWriteSync && writer.writeSync(chunk)) {
// Sync retry succeeded
} else if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
await waitForSyncBackpressure();
continue;
} else {
const result = writer.write(
chunk, signal ? { __proto__: null, signal } : undefined);
Expand All@@ -1075,14 +1042,7 @@ async function pipeTo(source, ...args) {
// is required. Callers must check: const p = writeBatch(b); if (p) await p;
function writeBatch(batch) {
if (hasWritev && batch.length > 1) {
const result = hasWritevSync && writer.writevSync(batch);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
for (let i = 0; i < batch.length; i++) {
totalBytes += TypedArrayPrototypeGetByteLength(batch[i]);
}
return waitForSyncBackpressure();
}
if (!hasWritevSync || !writer.writevSync(batch)) {
const opts = signal ? { __proto__: null, signal } : undefined;
const writevResult = writer.writev(batch, opts);
if (writevResult === undefined) {
Expand All@@ -1104,14 +1064,8 @@ async function pipeTo(source, ...args) {
}
for (let i = 0; i < batch.length; i++) {
const chunk = batch[i];
const result = hasWriteSync && writer.writeSync(chunk);
if (!result) {
if (isAcceptedSyncWriteBackpressure(writer, result)) {
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
return writeBatchAfterAcceptedBackpressure(batch, i + 1);
}
if (!hasWriteSync || !writer.writeSync(chunk)) {
// Sync path failed at index i - fall back to async for the rest.
// Count bytes for chunks already written synchronously (0..i-1).
return writeBatchAsyncFallback(batch, i);
}
totalBytes += TypedArrayPrototypeGetByteLength(chunk);
Expand Down
48 changes: 2 additions & 46 deletions lib/internal/streams/iter/push.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,8 +33,6 @@ const {

const {
drainableProtocol,
kSyncWriteAccepted,
kSyncWriteAcceptedOnFalse,
} = require('internal/streams/iter/types');

const {
Expand DownExpand Up@@ -369,19 +367,6 @@ class PushQueue {
this.#pendingEnd = pending;
}

/**
* Force-enqueue chunks into the slots buffer, bypassing capacity checks.
* Used by PushWriter.writeSync() for 'block' policy where the data is
* accepted but false is returned as a backpressure signal.
*/
forceEnqueue(chunks) {
this.#slots.push(chunks);
for (let i = 0; i < chunks.length; i++) {
this.#bytesWritten += TypedArrayPrototypeGetByteLength(chunks[i]);
}
this.#resolvePendingReads();
}

/**
* Wait for backpressure to clear (desiredSize > 0).
* @returns {Promise<void>}
Expand DownExpand Up@@ -563,16 +548,11 @@ class PushQueue {

class PushWriter {
#queue;
#syncWriteAccepted = false;

constructor(queue) {
this.#queue = queue;
}

[kSyncWriteAccepted]() {
return this.#syncWriteAccepted;
}

[drainableProtocol]() {
const desired = this.desiredSize;
if (desired === null) return null;
Expand All@@ -584,10 +564,6 @@ class PushWriter {
return this.#queue.desiredSize;
}

get [kSyncWriteAcceptedOnFalse]() {
return this.#queue.backpressurePolicy === 'block';
}

write(chunk, options) {
if (!options?.signal && this.#queue.canWriteSync()) {
const bytes = toUint8Array(chunk);
Expand All@@ -612,36 +588,16 @@ class PushWriter {
}

writeSync(chunk) {
this.#syncWriteAccepted = false;
const bytes = toUint8Array(chunk);
const result = this.#queue.writeSync([bytes]);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
// Block policy: force-enqueue and return false as backpressure signal.
// Data IS accepted; false tells caller to slow down.
this.#queue.forceEnqueue([bytes]);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync([bytes]);
Comment thread
jasnell marked this conversation as resolved.
}

writevSync(chunks) {
this.#syncWriteAccepted = false;
if (!ArrayIsArray(chunks)) {
throw new ERR_INVALID_ARG_TYPE('chunks', 'Array', chunks);
}
const bytes = convertChunks(chunks);
const result = this.#queue.writeSync(bytes);
if (!result && this.#queue.backpressurePolicy === 'block' &&
this.#queue.desiredSize === 0) {
this.#queue.forceEnqueue(bytes);
this.#syncWriteAccepted = true;
return false;
}
this.#syncWriteAccepted = result;
return result;
return this.#queue.writeSync(bytes);
}

end(options) {
Expand Down
Loading
Loading