| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 9e227ef commit 1d6ec2d
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,39 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const common = require('../common.js'); | ||
| 3 | + const { | ||
| 4 | + ReadableStream, | ||
| 5 | + TextEncoderStream, | ||
| 6 | + TextDecoderStream, | ||
| 7 | + } = require('node:stream/web'); | ||
| 8 | + | ||
| 9 | + const bench = common.createBenchmark(main, { | ||
| 10 | + n: [1e5], | ||
| 11 | + kind: ['encode', 'decode'], | ||
| 12 | + len: [16, 1024], | ||
| 13 | + }); | ||
| 14 | + | ||
| 15 | + async function main({ n, kind, len }) { | ||
| 16 | + const encoded = new TextEncoder().encode('a'.repeat(len)); | ||
| 17 | + const decoded = 'a'.repeat(len); | ||
| 18 | + let i = 0; | ||
| 19 | + const rs = new ReadableStream({ | ||
| 20 | + pull(controller) { | ||
| 21 | + if (i++ < n) { | ||
| 22 | + controller.enqueue(kind === 'encode' ? decoded : encoded); | ||
| 23 | + } else { | ||
| 24 | + controller.close(); | ||
| 25 | + } | ||
| 26 | + }, | ||
| 27 | + }); | ||
| 28 | + const ts = kind === 'encode' ? | ||
| 29 | + new TextEncoderStream() : | ||
| 30 | + new TextDecoderStream(); | ||
| 31 | + | ||
| 32 | + const reader = rs.pipeThrough(ts).getReader(); | ||
| 33 | + bench.start(); | ||
| 34 | + for (;;) { | ||
| 35 | + const { done } = await reader.read(); | ||
| 36 | + if (done) break; | ||
| 37 | + } | ||
| 38 | + bench.end(n); | ||
| 39 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,29 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + const common = require('../common.js'); | ||
| 3 | + const { | ||
| 4 | + ReadableStream, | ||
| 5 | + } = require('node:stream/web'); | ||
| 6 | + | ||
| 7 | + const bench = common.createBenchmark(main, { | ||
| 8 | + n: [1e6], | ||
| 9 | + kind: ['sync', 'async'], | ||
| 10 | + }); | ||
| 11 | + | ||
| 12 | + async function main({ n, kind }) { | ||
| 13 | + function* syncGen() { | ||
| 14 | + for (let i = 0; i < n; i++) yield i; | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + async function* asyncGen() { | ||
| 18 | + for (let i = 0; i < n; i++) yield i; | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + const reader = ReadableStream.from( | ||
| 22 | + kind === 'sync' ? syncGen() : asyncGen()).getReader(); | ||
| 23 | + bench.start(); | ||
| 24 | + for (;;) { | ||
| 25 | + const { done } = await reader.read(); | ||
| 26 | + if (done) break; | ||
| 27 | + } | ||
| 28 | + bench.end(n); | ||
| 29 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ const { | |||
| 4 | 4 | ObjectDefineProperties, | |
| 5 | 5 | String, | |
| 6 | 6 | StringPrototypeCharCodeAt, | |
| 7 | + StringPrototypeSlice, | ||
| 7 | 8 | Uint8Array, | |
| 8 | 9 | } = primordials; | |
| 9 | 10 | ||
@@ -31,6 +32,9 @@ const { | |||
| 31 | 32 | kEnumerableProperty, | |
| 32 | 33 | } = require('internal/util'); | |
| 33 | 34 | ||
| 35 | + // Shared per-chunk decode options; decode() only reads the flag. | ||
| 36 | + const kDecodeStreamingOptions = { __proto__: null, stream: true }; | ||
| 37 | + | ||
| 34 | 38 | /** | |
| 35 | 39 | * @typedef {import('./readablestream').ReadableStream} ReadableStream | |
| 36 | 40 | * @typedef {import('./writablestream').WritableStream} WritableStream | |
@@ -46,34 +50,26 @@ class TextEncoderStream { | |||
| 46 | 50 | this.#transform = new TransformStream({ | |
| 47 | 51 | transform: (chunk, controller) => { | |
| 48 | 52 | // https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk | |
| 53 | + // The only cross-chunk state is a trailing high surrogate; | ||
| 54 | + // encode() replaces interior lone surrogates with U+FFFD exactly | ||
| 55 | + // like the spec's per-code-unit walk. | ||
| 49 | 56 | chunk = String(chunk); | |
| 50 | - let finalChunk = ''; | ||
| 51 | - for (let i = 0; i < chunk.length; i++) { | ||
| 52 | - const item = chunk[i]; | ||
| 53 | - const codeUnit = StringPrototypeCharCodeAt(item, 0); | ||
| 54 | - if (this.#pendingHighSurrogate !== null) { | ||
| 55 | - const highSurrogate = this.#pendingHighSurrogate; | ||
| 56 | - this.#pendingHighSurrogate = null; | ||
| 57 | - if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) { | ||
| 58 | - finalChunk += highSurrogate + item; | ||
| 59 | - continue; | ||
| 60 | - } | ||
| 61 | - finalChunk += '\uFFFD'; | ||
| 62 | - } | ||
| 63 | - if (0xD800 <= codeUnit && codeUnit <= 0xDBFF) { | ||
| 64 | - this.#pendingHighSurrogate = item; | ||
| 65 | - continue; | ||
| 66 | - } | ||
| 67 | - if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) { | ||
| 68 | - finalChunk += '\uFFFD'; | ||
| 69 | - continue; | ||
| 70 | - } | ||
| 71 | - finalChunk += item; | ||
| 57 | + if (chunk.length === 0) | ||
| 58 | + return; | ||
| 59 | + if (this.#pendingHighSurrogate !== null) { | ||
| 60 | + chunk = this.#pendingHighSurrogate + chunk; | ||
| 61 | + this.#pendingHighSurrogate = null; | ||
| 72 | 62 | } | |
| 73 | - if (finalChunk) { | ||
| 74 | - const value = this.#handle.encode(finalChunk); | ||
| 75 | - controller.enqueue(value); | ||
| 63 | + const lastCodeUnit = | ||
| 64 | + StringPrototypeCharCodeAt(chunk, chunk.length - 1); | ||
| 65 | + if (0xD800 <= lastCodeUnit && lastCodeUnit <= 0xDBFF) { | ||
| 66 | + this.#pendingHighSurrogate = | ||
| 67 | + StringPrototypeSlice(chunk, -1); | ||
| 68 | + chunk = StringPrototypeSlice(chunk, 0, -1); | ||
| 69 | + if (chunk.length === 0) | ||
| 70 | + return; | ||
| 76 | 71 | } | |
| 72 | + controller.enqueue(this.#handle.encode(chunk)); | ||
| 77 | 73 | }, | |
| 78 | 74 | flush: (controller) => { | |
| 79 | 75 | // https://encoding.spec.whatwg.org/#encode-and-flush | |
@@ -137,7 +133,7 @@ class TextDecoderStream { | |||
| 137 | 133 | if (chunk === undefined) { | |
| 138 | 134 | throw new ERR_INVALID_ARG_TYPE('chunk', 'string', chunk); | |
| 139 | 135 | } | |
| 140 | - const value = this.#handle.decode(chunk, { stream: true }); | ||
| 136 | + const value = this.#handle.decode(chunk, kDecodeStreamingOptions); | ||
| 141 | 137 | if (value) | |
| 142 | 138 | controller.enqueue(value); | |
| 143 | 139 | }, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -112,6 +112,7 @@ const { | |||
| 112 | 112 | getNonWritablePropertyDescriptor, | |
| 113 | 113 | isBrandCheck, | |
| 114 | 114 | kEmptyQueue, | |
| 115 | + kParkedAlgorithmResult, | ||
| 115 | 116 | kResolvedPromise, | |
| 116 | 117 | kState, | |
| 117 | 118 | kType, | |
@@ -1446,19 +1447,85 @@ function readableStreamFromIterable(iterable) { | |||
| 1446 | 1447 | if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) { | |
| 1447 | 1448 | throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object'); | |
| 1448 | 1449 | } | |
| 1450 | + // Per GetIteratorDirect, the next method is looked up once. | ||
| 1451 | + const nextMethod = iterator.next; | ||
| 1449 | 1452 | const startAlgorithm = nonOpCallback; | |
| 1450 | 1453 | ||
| 1451 | - async function pullAlgorithm() { | ||
| 1452 | - const iterResult = await iterator.next(); | ||
| 1454 | + // Callback-style pull: the reaction steps are reused across chunks and | ||
| 1455 | + // completion is delivered to the controller's cached pull reactions | ||
| 1456 | + // (the kParkedAlgorithmResult contract). One pull runs at a time, so a | ||
| 1457 | + // single slot carries a non-thenable next() result between steps. | ||
| 1458 | + let pendingIterResult; | ||
| 1459 | + | ||
| 1460 | + function rejectPull(error) { | ||
| 1461 | + readableStreamDefaultControllerError(stream[kState].controller, error); | ||
| 1462 | + } | ||
| 1463 | + | ||
| 1464 | + function processIterResult(iterResult) { | ||
| 1465 | + const controller = stream[kState].controller; | ||
| 1453 | 1466 | if (typeof iterResult !== 'object' || iterResult === null) { | |
| 1454 | - throw new ERR_INVALID_STATE.TypeError( | ||
| 1455 | - 'The promise returned by the iterator.next() method must fulfill with an object'); | ||
| 1467 | + rejectPull(new ERR_INVALID_STATE.TypeError( | ||
| 1468 | + 'The promise returned by the iterator.next() method must fulfill with an object')); | ||
| 1469 | + return; | ||
| 1456 | 1470 | } | |
| 1457 | - if (iterResult.done) { | ||
| 1458 | - readableStreamDefaultControllerClose(stream[kState].controller); | ||
| 1459 | - } else { | ||
| 1460 | - readableStreamDefaultControllerEnqueue(stream[kState].controller, await iterResult.value); | ||
| 1471 | + try { | ||
| 1472 | + if (iterResult.done) { | ||
| 1473 | + readableStreamDefaultControllerClose(controller); | ||
| 1474 | + } else { | ||
| 1475 | + const value = iterResult.value; | ||
| 1476 | + if (value !== null && | ||
| 1477 | + (typeof value === 'object' || typeof value === 'function')) { | ||
| 1478 | + // Adopted like `await iterResult.value`, keeping the observable | ||
| 1479 | + // .then lookup on plain objects. | ||
| 1480 | + PromisePrototypeThen(PromiseResolve(value), enqueueValue, rejectPull); | ||
| 1481 | + return; | ||
| 1482 | + } | ||
| 1483 | + readableStreamDefaultControllerEnqueue(controller, value); | ||
| 1484 | + } | ||
| 1485 | + } catch (error) { | ||
| 1486 | + rejectPull(error); | ||
| 1487 | + return; | ||
| 1488 | + } | ||
| 1489 | + // pullFulfilled exists: the controller creates it before the pull. | ||
| 1490 | + controller[kState].pullFulfilled(); | ||
| 1491 | + } | ||
| 1492 | + | ||
| 1493 | + function enqueueValue(value) { | ||
| 1494 | + const controller = stream[kState].controller; | ||
| 1495 | + try { | ||
| 1496 | + readableStreamDefaultControllerEnqueue(controller, value); | ||
| 1497 | + } catch (error) { | ||
| 1498 | + rejectPull(error); | ||
| 1499 | + return; | ||
| 1500 | + } | ||
| 1501 | + controller[kState].pullFulfilled(); | ||
| 1502 | + } | ||
| 1503 | + | ||
| 1504 | + function processPendingIterResult() { | ||
| 1505 | + const iterResult = pendingIterResult; | ||
| 1506 | + pendingIterResult = undefined; | ||
| 1507 | + processIterResult(iterResult); | ||
| 1508 | + } | ||
| 1509 | + | ||
| 1510 | + function pullAlgorithm() { | ||
| 1511 | + let nextResult; | ||
| 1512 | + try { | ||
| 1513 | + nextResult = FunctionPrototypeCall(nextMethod, iterator); | ||
| 1514 | + } catch (error) { | ||
| 1515 | + return PromiseReject(error); | ||
| 1516 | + } | ||
| 1517 | + if (nextResult !== null && | ||
| 1518 | + (typeof nextResult === 'object' || typeof nextResult === 'function')) { | ||
| 1519 | + // Mirrors `await iterator.next()`: processIterResult runs at the | ||
| 1520 | + // microtask position the await resumed. | ||
| 1521 | + PromisePrototypeThen( | ||
| 1522 | + PromiseResolve(nextResult), processIterResult, rejectPull); | ||
| 1523 | + return kParkedAlgorithmResult; | ||
| 1461 | 1524 | } | |
| 1525 | + // A non-thenable next() result fails validation a microtask later. | ||
| 1526 | + pendingIterResult = nextResult; | ||
| 1527 | + PromisePrototypeThen(kResolvedPromise, processPendingIterResult); | ||
| 1528 | + return kParkedAlgorithmResult; | ||
| 1462 | 1529 | } | |
| 1463 | 1530 | ||
| 1464 | 1531 | async function cancelAlgorithm(reason) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -359,7 +359,7 @@ const kResolvedPromise = PromiseResolve(); | |||
| 359 | 359 | // operation and takes responsibility for delivering the fulfilled (or | |
| 360 | 360 | // rejected) continuation itself later, instead of settling a promise | |
| 361 | 361 | // (see the transform stream source pull algorithm). | |
| 362 | - const kParkedAlgorithmResult = { __proto__: null }; | ||
| 362 | + const kParkedAlgorithmResult = Symbol('kParkedAlgorithmResult'); | ||
| 363 | 363 | ||
| 364 | 364 | // Wires the (possibly non-thenable) result of an underlying algorithm | |
| 365 | 365 | // callback to its fulfilled/rejected continuations. A non-thenable result | |
| Back | FazBrowse Home | New Git URL |
0 commit comments