Skip to content

Commit

Permalink
Merge pull request #144 from MattiasBuelens/optimize-rs-from
Browse files Browse the repository at this point in the history
Optimize `ReadableStream.from()`
  • Loading branch information
MattiasBuelens authored Feb 28, 2024
2 parents 0616eb6 + 93d28a1 commit 7ffbb95
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- | --- |
Expand Down
85 changes: 60 additions & 25 deletions src/lib/abstract-ops/ecmascript.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -79,9 +85,11 @@ export function GetMethod<T, K extends MethodName<T>>(receiver: T, prop: K): T[K
return func;
}

export type SyncOrAsync<T> = T | Promise<T>;

export interface SyncIteratorRecord<T> {
iterator: Iterator<T>,
nextMethod: Iterator<T>['next'],
nextMethod: () => SyncOrAsync<IteratorResult<SyncOrAsync<T>>>,
done: boolean;
}

Expand All @@ -93,23 +101,57 @@ export interface AsyncIteratorRecord<T> {

export type SyncOrAsyncIteratorRecord<T> = SyncIteratorRecord<T> | AsyncIteratorRecord<T>;

export function CreateAsyncFromSyncIterator<T>(syncIteratorRecord: SyncIteratorRecord<T>): AsyncIteratorRecord<T> {
// 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<T>(
syncIteratorRecord: SyncIteratorRecord<SyncOrAsync<T>>
): AsyncIteratorRecord<T> {
const asyncIterator: AsyncIterator<T> = {
// 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<T>(result: IteratorResult<SyncOrAsync<T>>): Promise<IteratorResult<T>> {
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 ??
Expand Down Expand Up @@ -160,22 +202,15 @@ function GetIterator<T>(

export { GetIterator };

export function IteratorNext<T>(iteratorRecord: AsyncIteratorRecord<T>): Promise<IteratorResult<T>> {
export function IteratorNext<T>(iteratorRecord: SyncIteratorRecord<T>): IteratorResult<T>;
export function IteratorNext<T>(iteratorRecord: AsyncIteratorRecord<T>): Promise<IteratorResult<T>>;
export function IteratorNext<T>(
iteratorRecord: SyncOrAsyncIteratorRecord<T>
): SyncOrAsync<IteratorResult<SyncOrAsync<T>>> {
const result = reflectCall(iteratorRecord.nextMethod, iteratorRecord.iterator, []);
if (!typeIsObject(result)) {
throw new TypeError('The iterator.next() method must return an object');
}
return result;
}

export function IteratorComplete<TReturn>(
iterResult: IteratorResult<unknown, TReturn>
): iterResult is IteratorReturnResult<TReturn> {
assert(typeIsObject(iterResult));
return Boolean(iterResult.done);
}

export function IteratorValue<T>(iterResult: IteratorYieldResult<T>): T {
assert(typeIsObject(iterResult));
return iterResult.value;
}
3 changes: 3 additions & 0 deletions src/lib/helpers/webidl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(executor: (
resolve: (value: T | PromiseLike<T>) => void,
Expand Down
16 changes: 5 additions & 11 deletions src/lib/readable-stream/from.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
type ReadableStreamLike
} 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 { GetIterator, GetMethod, IteratorNext } from '../abstract-ops/ecmascript';
import { promiseCall, promiseRejectedWith, promiseResolvedWith, transformPromiseWith } from '../helpers/webidl';
import { typeIsObject } from '../helpers/miscellaneous';
import { noop } from '../../utils';

Expand Down Expand Up @@ -37,11 +37,11 @@ export function ReadableStreamFromIterable<R>(asyncIterable: Iterable<R> | 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);
}
});
Expand All @@ -58,13 +58,7 @@ export function ReadableStreamFromIterable<R>(asyncIterable: Iterable<R> | Async
if (returnMethod === undefined) {
return promiseResolvedWith(undefined);
}
let returnResult: IteratorResult<R> | Promise<IteratorResult<R>>;
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');
Expand Down

0 comments on commit 7ffbb95

Please sign in to comment.