From c713b84c1fe5a2eeddd25a51a930f26c496c5b10 Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Sun, 18 Oct 2026 13:45:08 -0700 Subject: [PATCH 1/3] benchmark: add URLSearchParams iterator consumers benchmark Assisted-by: claude:opus-5.5 Signed-off-by: Kevin Gibbons --- .../url-searchparams-iterator-consumers.js | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 benchmark/url/url-searchparams-iterator-consumers.js diff --git a/benchmark/url/url-searchparams-iterator-consumers.js b/benchmark/url/url-searchparams-iterator-consumers.js new file mode 100644 index 00000000000..62fff749959 --- /dev/null +++ b/benchmark/url/url-searchparams-iterator-consumers.js @@ -0,0 +1,53 @@ +'use strict'; +const common = require('../common.js'); +const assert = require('assert'); + +// Consumes a URLSearchParams iterator with builtins that have fast paths for +// iterator result objects (Array.from, Map constructor, Object.fromEntries), +// plus for-of for comparison. +const bench = common.createBenchmark(main, { + method: ['Array.from', 'Map', 'Object.fromEntries', 'for-of'], + n: [1e6], +}); + +const str = 'one=single&two=first&three=first&two=2nd&three=2nd&three=3rd'; + +function main({ method, n }) { + const params = new URLSearchParams(str); + let noDead; + + switch (method) { + case 'Array.from': + bench.start(); + for (let i = 0; i < n; i += 1) + noDead = Array.from(params); + bench.end(n); + assert.strictEqual(noDead.length, 6); + break; + case 'Map': + bench.start(); + for (let i = 0; i < n; i += 1) + noDead = new Map(params); + bench.end(n); + assert.strictEqual(noDead.size, 3); + break; + case 'Object.fromEntries': + bench.start(); + for (let i = 0; i < n; i += 1) + noDead = Object.fromEntries(params); + bench.end(n); + assert.strictEqual(noDead.three, '3rd'); + break; + case 'for-of': + bench.start(); + for (let i = 0; i < n; i += 1) { + for (const pair of params) + noDead = pair; + } + bench.end(n); + assert.strictEqual(noDead[1], '3rd'); + break; + default: + throw new Error(`Unknown method ${method}`); + } +} From b6bd705f27a4e29b0dc8b1f240566fa700b35f66 Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Sun, 18 Oct 2026 13:46:06 -0700 Subject: [PATCH 2/3] tools: require `value` before `done` in iterator results V8 has a fast path for `{ value, done }` literals, but it must be that order. Also, the ES spec technically requires that order. Assisted-by: claude:opus-5.5 Signed-off-by: Kevin Gibbons --- lib/eslint.config_partial.mjs | 2 +- .../test-eslint-iterator-result-done-first.js | 60 ------------------- ...test-eslint-iterator-result-value-first.js | 60 +++++++++++++++++++ ...irst.js => iterator-result-value-first.js} | 10 +++- 4 files changed, 68 insertions(+), 64 deletions(-) delete mode 100644 test/parallel/test-eslint-iterator-result-done-first.js create mode 100644 test/parallel/test-eslint-iterator-result-value-first.js rename tools/eslint-rules/{iterator-result-done-first.js => iterator-result-value-first.js} (68%) diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index 9240b00ac70..fcd4dd4aa78 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -435,7 +435,7 @@ export default [ 'node-core/alphabetize-errors': 'error', 'node-core/alphabetize-primordials': 'error', 'node-core/avoid-prototype-pollution': 'error', - 'node-core/iterator-result-done-first': 'error', + 'node-core/iterator-result-value-first': 'error', 'node-core/lowercase-name-for-primitive': 'error', 'node-core/non-ascii-character': 'error', 'node-core/no-array-destructuring': 'error', diff --git a/test/parallel/test-eslint-iterator-result-done-first.js b/test/parallel/test-eslint-iterator-result-done-first.js deleted file mode 100644 index f65f6bd72eb..00000000000 --- a/test/parallel/test-eslint-iterator-result-done-first.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const common = require('../common'); -if ((!common.hasCrypto) || (!common.hasIntl)) { - common.skip('ESLint tests require crypto and Intl'); -} - -common.skipIfEslintMissing(); - -const { RuleTester } = require('../../tools/eslint/node_modules/eslint'); -const rule = require('../../tools/eslint-rules/iterator-result-done-first'); - -const message = 'Iterator result objects should place `done` before `value`.'; - -new RuleTester().run('iterator-result-done-first', rule, { - valid: [ - 'function next() { return { done: true, value: undefined }; }', - 'function next() { return { __proto__: null, done: false, value: chunk }; }', - 'function next() { return { done, value }; }', - 'function next() { return { value }; }', - 'function next() { return { done }; }', - 'function next() { return { value: 1, other: 2 }; }', - 'function next() { return { [value]: 1, done: true }; }', - 'function next() { return { value: 1, [done]: true }; }', - 'function next() { return { "done": true, "value": undefined }; }', - 'function next() { return { ["done"]: true, ["value"]: undefined }; }', - ], - invalid: [ - { - code: 'function next() { return { value: undefined, done: true }; }', - errors: [{ message }], - output: 'function next() { return { done: true, value: undefined }; }', - }, - { - code: 'function next() { return { __proto__: null, value: chunk, done: false }; }', - errors: [{ message }], - output: 'function next() { return { __proto__: null, done: false, value: chunk }; }', - }, - { - code: 'function next() { return { value, done }; }', - errors: [{ message }], - output: 'function next() { return { done, value }; }', - }, - { - code: 'function next() { return { "value": undefined, "done": true }; }', - errors: [{ message }], - output: 'function next() { return { "done": true, "value": undefined }; }', - }, - { - code: 'function next() { return { ["value"]: undefined, ["done"]: true }; }', - errors: [{ message }], - output: 'function next() { return { ["done"]: true, ["value"]: undefined }; }', - }, - { - code: 'function next() { return { value: result, extra: true, done: false }; }', - errors: [{ message }], - output: 'function next() { return { done: false, extra: true, value: result }; }', - }, - ], -}); diff --git a/test/parallel/test-eslint-iterator-result-value-first.js b/test/parallel/test-eslint-iterator-result-value-first.js new file mode 100644 index 00000000000..24dc56be143 --- /dev/null +++ b/test/parallel/test-eslint-iterator-result-value-first.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../common'); +if ((!common.hasCrypto) || (!common.hasIntl)) { + common.skip('ESLint tests require crypto and Intl'); +} + +common.skipIfEslintMissing(); + +const { RuleTester } = require('../../tools/eslint/node_modules/eslint'); +const rule = require('../../tools/eslint-rules/iterator-result-value-first'); + +const message = 'Iterator result objects should place `value` before `done`.'; + +new RuleTester().run('iterator-result-value-first', rule, { + valid: [ + 'function next() { return { value: undefined, done: true }; }', + 'function next() { return { __proto__: null, value: chunk, done: false }; }', + 'function next() { return { value, done }; }', + 'function next() { return { value }; }', + 'function next() { return { done }; }', + 'function next() { return { done: 1, other: 2 }; }', + 'function next() { return { [done]: 1, value: true }; }', + 'function next() { return { done: 1, [value]: true }; }', + 'function next() { return { "value": undefined, "done": true }; }', + 'function next() { return { ["value"]: undefined, ["done"]: true }; }', + ], + invalid: [ + { + code: 'function next() { return { done: true, value: undefined }; }', + errors: [{ message }], + output: 'function next() { return { value: undefined, done: true }; }', + }, + { + code: 'function next() { return { __proto__: null, done: false, value: chunk }; }', + errors: [{ message }], + output: 'function next() { return { __proto__: null, value: chunk, done: false }; }', + }, + { + code: 'function next() { return { done, value }; }', + errors: [{ message }], + output: 'function next() { return { value, done }; }', + }, + { + code: 'function next() { return { "done": true, "value": undefined }; }', + errors: [{ message }], + output: 'function next() { return { "value": undefined, "done": true }; }', + }, + { + code: 'function next() { return { ["done"]: true, ["value"]: undefined }; }', + errors: [{ message }], + output: 'function next() { return { ["value"]: undefined, ["done"]: true }; }', + }, + { + code: 'function next() { return { done: false, extra: true, value: result }; }', + errors: [{ message }], + output: 'function next() { return { value: result, extra: true, done: false }; }', + }, + ], +}); diff --git a/tools/eslint-rules/iterator-result-done-first.js b/tools/eslint-rules/iterator-result-value-first.js similarity index 68% rename from tools/eslint-rules/iterator-result-done-first.js rename to tools/eslint-rules/iterator-result-value-first.js index 1c3ee9035e9..09d6fbe3027 100644 --- a/tools/eslint-rules/iterator-result-done-first.js +++ b/tools/eslint-rules/iterator-result-value-first.js @@ -1,6 +1,10 @@ 'use strict'; -const MESSAGE = 'Iterator result objects should place `done` before `value`.'; +// V8 has a fast path for `{ value, done }` literals, but it must be that order. +// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/init/bootstrapper.cc;l=746-754;drc=e8ac4914ee6b24442aed773d3a041451692b6a9c +// Also, the ES spec technically requires that order. +// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-createiterresultobject +const MESSAGE = 'Iterator result objects should place `value` before `done`.'; function getStaticPropertyName(property) { const { key } = property; @@ -48,9 +52,9 @@ module.exports = { } } - if (doneProperty && valueProperty && valueProperty.range[0] < doneProperty.range[0]) { + if (doneProperty && valueProperty && doneProperty.range[0] < valueProperty.range[0]) { context.report({ - node: valueProperty, + node: doneProperty, message: MESSAGE, fix(fixer) { return [ From 71922e3672ffcfdefb6ca6766a9eb1473e37c2d0 Mon Sep 17 00:00:00 2001 From: Kevin Gibbons Date: Sun, 18 Oct 2026 13:49:14 -0700 Subject: [PATCH 3/3] lib: place `value` before `done` in iterator results Apply the iterator-result-value-first lint fixer. Assisted-by: claude:opus-5.5 Signed-off-by: Kevin Gibbons --- lib/events.js | 2 +- lib/internal/fs/promises.js | 10 +++---- .../source_map/source_map_cache_map.js | 2 +- lib/internal/streams/readable.js | 18 ++++++------ lib/internal/url.js | 4 +-- lib/internal/vfs/watcher.js | 12 ++++---- lib/internal/webstreams/readablestream.js | 28 +++++++++---------- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/events.js b/lib/events.js index 8b20ea994d6..a2395e6dfa0 100644 --- a/lib/events.js +++ b/lib/events.js @@ -1043,7 +1043,7 @@ async function once(emitter, name, options = kEmptyObject) { } function createIterResult(value, done) { - return { done, value }; + return { value, done }; } function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) { diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index c21b1e5740f..bc9206dd993 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -672,7 +672,7 @@ if (getOptionValue('--experimental-stream-iter')) { __proto__: null, next() { if (done) { - return { done: true, value: undefined }; + return { value: undefined, done: true }; } if (!started) { // Lock lazily, on the first read, so that an iterable that is @@ -684,7 +684,7 @@ if (getOptionValue('--experimental-stream-iter')) { if (remaining === 0) { done = true; cleanup(); - return { done: true, value: undefined }; + return { value: undefined, done: true }; } const toRead = remaining > 0 ? MathMin(readSize, remaining) : readSize; @@ -700,20 +700,20 @@ if (getOptionValue('--experimental-stream-iter')) { if (bytesRead === 0) { done = true; cleanup(); - return { done: true, value: undefined }; + return { value: undefined, done: true }; } if (pos >= 0) pos += bytesRead; if (remaining > 0) remaining -= bytesRead; const chunk = bytesRead < toRead ? buf.subarray(0, bytesRead) : buf; - return { done: false, value: [chunk] }; + return { value: [chunk], done: false }; }, return() { if (!done) { done = true; if (started) cleanup(); } - return { done: true, value: undefined }; + return { value: undefined, done: true }; }, }; }, diff --git a/lib/internal/source_map/source_map_cache_map.js b/lib/internal/source_map/source_map_cache_map.js index 10561acffc4..5c144818ca7 100644 --- a/lib/internal/source_map/source_map_cache_map.js +++ b/lib/internal/source_map/source_map_cache_map.js @@ -104,7 +104,7 @@ class SourceMapCacheMap { const moduleInstance = weakRef.deref(); if (moduleInstance == null) return next(); const value = moduleInstance[source_map_data_private_symbol]; - return { done: false, value: [key, value] }; + return { value: [key, value], done: false }; }; return { diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js index e584757dd9a..9bd7ea63691 100644 --- a/lib/internal/streams/readable.js +++ b/lib/internal/streams/readable.js @@ -1656,7 +1656,7 @@ function createAsyncIterator(stream, options) { function onChunkFulfilled(value) { inFlight = false; if (queue !== null) drain(); - return { done: false, value }; + return { value, done: false }; } function onChunkRejected(err) { @@ -1679,7 +1679,7 @@ function createAsyncIterator(stream, options) { if (typeof then === 'function') { FunctionPrototypeCall(then, chunk, (value) => { inFlight = false; - resolve({ done: false, value }); + resolve({ value, done: false }); if (queue !== null) drain(); }, (err) => { inFlight = false; @@ -1689,7 +1689,7 @@ function createAsyncIterator(stream, options) { return; } inFlight = false; - resolve({ done: false, value: chunk }); + resolve({ value: chunk, done: false }); if (queue !== null) drain(); } else if (error) { inFlight = false; @@ -1698,7 +1698,7 @@ function createAsyncIterator(stream, options) { } else if (error === null) { inFlight = false; finalize(); - resolve({ done: true, value: undefined }); + resolve({ value: undefined, done: true }); if (queue !== null) drain(); } else { // No data buffered yet; wait for 'readable' or end-of-stream and @@ -1709,7 +1709,7 @@ function createAsyncIterator(stream, options) { function processNext(resolve, reject) { if (completed) { - resolve({ done: true, value: undefined }); + resolve({ value: undefined, done: true }); return; } if (!started) start(); @@ -1726,7 +1726,7 @@ function createAsyncIterator(stream, options) { completed = true; } } - resolve({ done: true, value }); + resolve({ value, done: true }); } function processThrow(err, reject) { @@ -1755,7 +1755,7 @@ function createAsyncIterator(stream, options) { return FunctionPrototypeCall( then, chunk, onChunkFulfilled, onChunkRejected); } - return PromiseResolve({ done: false, value: chunk }); + return PromiseResolve({ value: chunk, done: false }); } if (error) { finalize(); @@ -1763,7 +1763,7 @@ function createAsyncIterator(stream, options) { } if (error === null) { finalize(); - return PromiseResolve({ done: true, value: undefined }); + return PromiseResolve({ value: undefined, done: true }); } // No data buffered yet; wait for 'readable' or end-of-stream. inFlight = true; @@ -1776,7 +1776,7 @@ function createAsyncIterator(stream, options) { queue ??= new FixedQueue(); queue.push({ __proto__: null, type: 'next', value: undefined, resolve, reject }); } else { - resolve({ done: true, value: undefined }); + resolve({ value: undefined, done: true }); } }); }, diff --git a/lib/internal/url.js b/lib/internal/url.js index 8c04ac70c07..456afd2242d 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -278,8 +278,8 @@ class URLSearchParamsIterator { const len = values.length; if (index >= len) { return { - done: true, value: undefined, + done: true, }; } @@ -297,8 +297,8 @@ class URLSearchParamsIterator { } return { - done: false, value: result, + done: false, }; } diff --git a/lib/internal/vfs/watcher.js b/lib/internal/vfs/watcher.js index 0f23c1588b1..e48533493ef 100644 --- a/lib/internal/vfs/watcher.js +++ b/lib/internal/vfs/watcher.js @@ -600,7 +600,7 @@ class VFSWatchAsyncIterable { const event = { eventType, filename }; if (this.#pendingResolvers.length > 0) { const { resolve } = this.#pendingResolvers.shift(); - resolve({ done: false, value: event }); + resolve({ value: event, done: false }); } else if (this.#pendingEvents.length < kMaxPendingEvents) { ArrayPrototypePush(this.#pendingEvents, event); } @@ -612,7 +612,7 @@ class VFSWatchAsyncIterable { // Resolve any pending iterators while (this.#pendingResolvers.length > 0) { const { resolve } = this.#pendingResolvers.shift(); - resolve({ done: true, value: undefined }); + resolve({ value: undefined, done: true }); } }); @@ -649,12 +649,12 @@ class VFSWatchAsyncIterable { */ next() { if (this.#closed) { - return PromiseResolve({ done: true, value: undefined }); + return PromiseResolve({ value: undefined, done: true }); } if (this.#pendingEvents.length > 0) { const event = this.#pendingEvents.shift(); - return PromiseResolve({ done: false, value: event }); + return PromiseResolve({ value: event, done: false }); } return new Promise((resolve, reject) => { @@ -668,7 +668,7 @@ class VFSWatchAsyncIterable { */ return() { this.#watcher.close(); - return PromiseResolve({ done: true, value: undefined }); + return PromiseResolve({ value: undefined, done: true }); } /** @@ -678,7 +678,7 @@ class VFSWatchAsyncIterable { */ throw(error) { this.#watcher.close(); - return PromiseResolve({ done: true, value: undefined }); + return PromiseResolve({ value: undefined, done: true }); } } diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index f19337f5b6b..a5c4415785d 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -710,14 +710,14 @@ class ReadableStreamAsyncIteratorReadRequest { [kChunk](chunk) { this.current = undefined; - this.promise.resolve({ done: false, value: chunk }); + this.promise.resolve({ value: chunk, done: false }); } [kClose]() { this.current = undefined; this.done = true; readableStreamReaderGenericRelease(this.reader); - this.promise.resolve({ done: true, value: undefined }); + this.promise.resolve({ value: undefined, done: true }); } [kError](error) { @@ -733,7 +733,7 @@ class ReadableStreamAsyncIteratorReadRequest { // promise allocations. function readableStreamAsyncIteratorNextSteps(state) { if (state.done) - return PromiseResolve({ done: true, value: undefined }); + return PromiseResolve({ value: undefined, done: true }); const reader = state.reader; if (reader[kState].stream === undefined) { @@ -751,7 +751,7 @@ function readableStreamAsyncIteratorNextSteps(state) { // Not an async function either: the cancel path settles one microtask // after the cancel promise, exactly as `await` would. function readableStreamAsyncIteratorReturnSteps(state, value) { - const iterResult = { done: true, value }; + const iterResult = { value, done: true }; if (state.done) return PromiseResolve(iterResult); state.done = true; @@ -839,16 +839,16 @@ class ReadableStreamAsyncIterator { readableStreamDefaultControllerPull(controller); } - return PromiseResolve({ done: false, value: chunk }); + return PromiseResolve({ value: chunk, done: false }); } } else if (controller[kState].queueTotalSize > 0) { // Byte controller with buffered data: same shape as above via // the queue-filled arm of the byte controller's pull steps. stream[kState].disturbed = true; return PromiseResolve({ - done: false, - value: readableByteStreamControllerDequeueChunk(controller), + + done: false, }); } } @@ -889,11 +889,11 @@ class DefaultReadRequest { } [kChunk](value) { - this[kState].resolve({ done: false, value }); + this[kState].resolve({ value, done: false }); } [kClose]() { - this[kState].resolve({ done: true, value: undefined }); + this[kState].resolve({ value: undefined, done: true }); } [kError](error) { @@ -934,11 +934,11 @@ class ReadIntoRequest { } [kChunk](value) { - this[kState].resolve({ done: false, value }); + this[kState].resolve({ value, done: false }); } [kClose](value) { - this[kState].resolve({ done: true, value }); + this[kState].resolve({ value, done: true }); } [kError](error) { @@ -1007,7 +1007,7 @@ class ReadableStreamDefaultReader { readableStreamDefaultControllerPull(controller); } - return PromiseResolve({ done: false, value: chunk }); + return PromiseResolve({ value: chunk, done: false }); } } else if (controller[kState].queueTotalSize > 0) { // Byte controller with buffered data: mirror the queue-filled arm @@ -1015,9 +1015,9 @@ class ReadableStreamDefaultReader { // the read request. stream[kState].disturbed = true; return PromiseResolve({ - done: false, - value: readableByteStreamControllerDequeueChunk(controller), + + done: false, }); } }