From 52778d48645de4d19e08ccfcc936608d8c477e06 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Sun, 14 Jan 2024 22:20:08 +0100 Subject: [PATCH 1/4] Use promiseCall --- src/lib/readable-stream/from.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/lib/readable-stream/from.ts b/src/lib/readable-stream/from.ts index 1bedb61..2d0f0a6 100644 --- a/src/lib/readable-stream/from.ts +++ b/src/lib/readable-stream/from.ts @@ -6,7 +6,7 @@ import { } from './readable-stream-like'; import { ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue } from './default-controller'; import { GetIterator, GetMethod, IteratorComplete, IteratorNext, IteratorValue } from '../abstract-ops/ecmascript'; -import { promiseRejectedWith, promiseResolvedWith, reflectCall, transformPromiseWith } from '../helpers/webidl'; +import { promiseCall, promiseRejectedWith, promiseResolvedWith, transformPromiseWith } from '../helpers/webidl'; import { typeIsObject } from '../helpers/miscellaneous'; import { noop } from '../../utils'; @@ -58,13 +58,7 @@ export function ReadableStreamFromIterable(asyncIterable: Iterable | Async if (returnMethod === undefined) { return promiseResolvedWith(undefined); } - let returnResult: IteratorResult | Promise>; - try { - returnResult = reflectCall(returnMethod, iterator, [reason]); - } catch (e) { - return promiseRejectedWith(e); - } - const returnPromise = promiseResolvedWith(returnResult); + const returnPromise = promiseCall(returnMethod, iterator, [reason]); return transformPromiseWith(returnPromise, iterResult => { if (!typeIsObject(iterResult)) { throw new TypeError('The promise returned by the iterator.return() method must fulfill with an object'); From a5d18304f1c84d9a7de2975ed15c71ccf1f3dee8 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Sun, 14 Jan 2024 22:45:50 +0100 Subject: [PATCH 2/4] Remove IteratorComplete and IteratorValue --- src/lib/abstract-ops/ecmascript.ts | 11 ----------- src/lib/readable-stream/from.ts | 6 +++--- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/lib/abstract-ops/ecmascript.ts b/src/lib/abstract-ops/ecmascript.ts index 6a07db4..1416fbf 100644 --- a/src/lib/abstract-ops/ecmascript.ts +++ b/src/lib/abstract-ops/ecmascript.ts @@ -168,14 +168,3 @@ export function IteratorNext(iteratorRecord: AsyncIteratorRecord): Promise return result; } -export function IteratorComplete( - iterResult: IteratorResult -): iterResult is IteratorReturnResult { - assert(typeIsObject(iterResult)); - return Boolean(iterResult.done); -} - -export function IteratorValue(iterResult: IteratorYieldResult): T { - assert(typeIsObject(iterResult)); - return iterResult.value; -} diff --git a/src/lib/readable-stream/from.ts b/src/lib/readable-stream/from.ts index 2d0f0a6..53d1194 100644 --- a/src/lib/readable-stream/from.ts +++ b/src/lib/readable-stream/from.ts @@ -5,7 +5,7 @@ import { type ReadableStreamLike } from './readable-stream-like'; import { ReadableStreamDefaultControllerClose, ReadableStreamDefaultControllerEnqueue } from './default-controller'; -import { GetIterator, GetMethod, IteratorComplete, IteratorNext, IteratorValue } from '../abstract-ops/ecmascript'; +import { GetIterator, GetMethod, IteratorNext } from '../abstract-ops/ecmascript'; import { promiseCall, promiseRejectedWith, promiseResolvedWith, transformPromiseWith } from '../helpers/webidl'; import { typeIsObject } from '../helpers/miscellaneous'; import { noop } from '../../utils'; @@ -37,11 +37,11 @@ export function ReadableStreamFromIterable(asyncIterable: Iterable | Async if (!typeIsObject(iterResult)) { throw new TypeError('The promise returned by the iterator.next() method must fulfill with an object'); } - const done = IteratorComplete(iterResult); + const done = iterResult.done; if (done) { ReadableStreamDefaultControllerClose(stream._readableStreamController); } else { - const value = IteratorValue(iterResult); + const value = iterResult.value; ReadableStreamDefaultControllerEnqueue(stream._readableStreamController, value); } }); From fc67d1626e9e020b36236a0b63702d64b4eee1b9 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Sun, 14 Jan 2024 22:51:26 +0100 Subject: [PATCH 3/4] Implement CreateAsyncFromSyncIterator manually --- src/lib/abstract-ops/ecmascript.ts | 74 ++++++++++++++++++++++++------ src/lib/helpers/webidl.ts | 3 ++ 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/lib/abstract-ops/ecmascript.ts b/src/lib/abstract-ops/ecmascript.ts index 1416fbf..3bac163 100644 --- a/src/lib/abstract-ops/ecmascript.ts +++ b/src/lib/abstract-ops/ecmascript.ts @@ -1,4 +1,10 @@ -import { reflectCall } from 'lib/helpers/webidl'; +import { + PerformPromiseThen, + promiseRejectedWith, + promiseResolve, + promiseResolvedWith, + reflectCall +} from 'lib/helpers/webidl'; import { typeIsObject } from '../helpers/miscellaneous'; import assert from '../../stub/assert'; @@ -79,9 +85,11 @@ export function GetMethod>(receiver: T, prop: K): T[K return func; } +export type SyncOrAsync = T | Promise; + export interface SyncIteratorRecord { iterator: Iterator, - nextMethod: Iterator['next'], + nextMethod: () => SyncOrAsync>>, done: boolean; } @@ -93,23 +101,57 @@ export interface AsyncIteratorRecord { export type SyncOrAsyncIteratorRecord = SyncIteratorRecord | AsyncIteratorRecord; -export function CreateAsyncFromSyncIterator(syncIteratorRecord: SyncIteratorRecord): AsyncIteratorRecord { - // Instead of re-implementing CreateAsyncFromSyncIterator and %AsyncFromSyncIteratorPrototype%, - // we use yield* inside an async generator function to achieve the same result. - - // Wrap the sync iterator inside a sync iterable, so we can use it with yield*. - const syncIterable = { - [Symbol.iterator]: () => syncIteratorRecord.iterator +export function CreateAsyncFromSyncIterator( + syncIteratorRecord: SyncIteratorRecord> +): AsyncIteratorRecord { + const asyncIterator: AsyncIterator = { + // https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%.next + next() { + let result; + try { + result = IteratorNext(syncIteratorRecord); + } catch (e) { + return promiseRejectedWith(e); + } + return AsyncFromSyncIteratorContinuation(result); + }, + // https://tc39.es/ecma262/#sec-%asyncfromsynciteratorprototype%.return + return(value: any) { + let result; + try { + const returnMethod = GetMethod(syncIteratorRecord.iterator, 'return'); + if (returnMethod === undefined) { + return promiseResolvedWith({ done: true, value }); + } + // Note: ReadableStream.from() always calls return() with a value. + result = reflectCall(returnMethod, syncIteratorRecord.iterator, [value]); + } catch (e) { + return promiseRejectedWith(e); + } + if (!typeIsObject(result)) { + return promiseRejectedWith(new TypeError('The iterator.return() method must return an object')); + } + return AsyncFromSyncIteratorContinuation(result); + } + // Note: throw() is never used by the Streams spec. }; - // Create an async generator function and immediately invoke it. - const asyncIterator = (async function* () { - return yield* syncIterable; - }()); // Return as an async iterator record. const nextMethod = asyncIterator.next; return { iterator: asyncIterator, nextMethod, done: false }; } +// https://tc39.es/ecma262/#sec-asyncfromsynciteratorcontinuation +function AsyncFromSyncIteratorContinuation(result: IteratorResult>): Promise> { + try { + const done = result.done; + const value = result.value; + const valueWrapper = promiseResolve(value); + return PerformPromiseThen(valueWrapper, v => ({ done, value: v })); + } catch (e) { + return promiseRejectedWith(e); + } +} + // Aligns with core-js/modules/es.symbol.async-iterator.js export const SymbolAsyncIterator: (typeof Symbol)['asyncIterator'] = Symbol.asyncIterator ?? @@ -160,7 +202,11 @@ function GetIterator( export { GetIterator }; -export function IteratorNext(iteratorRecord: AsyncIteratorRecord): Promise> { +export function IteratorNext(iteratorRecord: SyncIteratorRecord): IteratorResult; +export function IteratorNext(iteratorRecord: AsyncIteratorRecord): Promise>; +export function IteratorNext( + iteratorRecord: SyncOrAsyncIteratorRecord +): SyncOrAsync>> { const result = reflectCall(iteratorRecord.nextMethod, iteratorRecord.iterator, []); if (!typeIsObject(result)) { throw new TypeError('The iterator.next() method must return an object'); diff --git a/src/lib/helpers/webidl.ts b/src/lib/helpers/webidl.ts index 425e837..4bc106c 100644 --- a/src/lib/helpers/webidl.ts +++ b/src/lib/helpers/webidl.ts @@ -2,9 +2,12 @@ import { rethrowAssertionErrorRejection } from './miscellaneous'; import assert from '../../stub/assert'; const originalPromise = Promise; +const originalPromiseResolve = Promise.resolve.bind(originalPromise); const originalPromiseThen = Promise.prototype.then; const originalPromiseReject = Promise.reject.bind(originalPromise); +export const promiseResolve = originalPromiseResolve; + // https://webidl.spec.whatwg.org/#a-new-promise export function newPromise(executor: ( resolve: (value: T | PromiseLike) => void, From 93d28a18611eeebe7488ce14dbc35b3f0da76750 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 28 Feb 2024 22:28:45 +0100 Subject: [PATCH 4/4] Update changelog [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c859b..812c543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * When using TypeScript, version 4.7 or higher is now required. Additionally, [`moduleResolution`](https://www.typescriptlang.org/tsconfig#moduleResolution) must be set to `"node16"`, `"nodenext"` or `"bundler"`. * 🚀 Support [importing as ESM in Node](https://nodejs.org/api/esm.html). * 💅 Minify all code in the published package, to reduce the download size. +* 💅 Rework `ReadableStream.from()` implementation to avoid depending on `async function*` down-leveling for ES5. ([#144](https://github.com/MattiasBuelens/web-streams-polyfill/pull/144)) | v3 import | v4 import | description | | --- | --- | --- |