Skip to content

Commit 8d7a0bf

Browse files
Add ReadableStream.from(asyncIterable)
This static method takes an async iterable and returns a ReadableStream pulling chunks from that async iterable. Sync iterables (including arrays and generators) are also supported, since GetIterator() already has all the necessary handling to adapt a sync iterator into an async iterator. Closes #1018.
1 parent 058f290 commit 8d7a0bf

File tree

6 files changed

+209
-3
lines changed

6 files changed

+209
-3
lines changed

index.bs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ urlPrefix: https://tc39.es/ecma262/; spec: ECMASCRIPT
4545
text: Number type; url: #sec-ecmascript-language-types-number-type
4646
text: Data Block; url: #sec-data-blocks
4747
type: abstract-op
48+
text: Call; url: #sec-call
4849
text: CloneArrayBuffer; url: #sec-clonearraybuffer
4950
text: CopyDataBlockBytes; url: #sec-copydatablockbytes
5051
text: CreateArrayFromList; url: #sec-createarrayfromlist
@@ -53,9 +54,14 @@ urlPrefix: https://tc39.es/ecma262/; spec: ECMASCRIPT
5354
text: Construct; url: #sec-construct
5455
text: DetachArrayBuffer; url: #sec-detacharraybuffer
5556
text: Get; url: #sec-get-o-p
57+
text: GetIterator; url: #sec-getiterator
58+
text: GetMethod; url: #sec-getmethod
5659
text: GetV; url: #sec-getv
5760
text: IsDetachedBuffer; url: #sec-isdetachedbuffer
5861
text: IsInteger; url: #sec-isinteger
62+
text: IteratorComplete; url: #sec-iteratorcomplete
63+
text: IteratorNext; url: #sec-iteratornext
64+
text: IteratorValue; url: #sec-iteratorvalue
5965
text: OrdinaryObjectCreate; url: #sec-ordinaryobjectcreate
6066
text: SameValue; url: #sec-samevalue
6167
text: Type; url: #sec-ecmascript-data-types-and-values
@@ -478,6 +484,8 @@ The Web IDL definition for the {{ReadableStream}} class is given as follows:
478484
interface ReadableStream {
479485
constructor(optional object underlyingSource, optional QueuingStrategy strategy = {});
480486

487+
static ReadableStream from(any asyncIterable);
488+
481489
readonly attribute boolean locked;
482490

483491
Promise<undefined> cancel(optional any reason);
@@ -808,6 +816,13 @@ option. If {{UnderlyingSource/type}} is set to undefined (including via omission
808816
|underlyingSource|, |underlyingSourceDict|, |highWaterMark|, |sizeAlgorithm|).
809817
</div>
810818

819+
<div algorithm>
820+
The static <dfn id="rs-from" method for="ReadableStream">from(|asyncIterable|)</dfn> method steps
821+
are:
822+
823+
1. Return ? [$ReadableStreamFromIterable$](|asyncIterable|).
824+
</div>
825+
811826
<div algorithm>
812827
The <dfn id="rs-locked" attribute for="ReadableStream">locked</dfn> getter steps are:
813828

@@ -2095,6 +2110,49 @@ The following abstract operations operate on {{ReadableStream}} instances at a h
20952110
1. Return true.
20962111
</div>
20972112

2113+
<div algorithm>
2114+
<dfn abstract-op lt="ReadableStreamFromIterable" id="readable-stream-from-iterable">
2115+
ReadableStreamFromIterable(|asyncIterable|)</dfn> performs the following steps:
2116+
2117+
1. Let |stream| be undefined.
2118+
1. Let |iteratorRecord| be ? [$GetIterator$](|asyncIterable|, async).
2119+
1. Let |startAlgorithm| be an algorithm that returns undefined.
2120+
1. Let |pullAlgorithm| be the following steps:
2121+
1. Let |nextResult| be [$IteratorNext$](|iteratorRecord|).
2122+
1. If |nextResult| is an abrupt completion, return [=a promise rejected with=]
2123+
|nextResult|.\[[Value]].
2124+
1. Let |nextPromise| be [=a promise resolved with=] |nextResult|.\[[Value]].
2125+
1. Return the result of [=reacting=] to |nextPromise| with the following fulfillment steps,
2126+
given |iterResult|:
2127+
1. If [$Type$](|iterResult|) is not Object, throw a {{TypeError}}.
2128+
1. Let |done| be ? [$IteratorComplete$](|iterResult|).
2129+
1. If |done| is true:
2130+
1. Perform ! [$ReadableStreamDefaultControllerClose$](|stream|.[=ReadableStream/[[controller]]=]).
2131+
1. Otherwise:
2132+
1. Let |value| be ? [$IteratorValue$](|iterResult|).
2133+
1. Perform ! [$ReadableStreamDefaultControllerEnqueue$](|stream|.[=ReadableStream/[[controller]]=],
2134+
|value|).
2135+
<!-- TODO (future): If we allow changing the queuing strategy, this Enqueue might throw.
2136+
We'll then need to catch the error and close the async iterator. -->
2137+
1. Let |cancelAlgorithm| be the following steps, given |reason|:
2138+
1. Let |iterator| be |iteratorRecord|.\[[Iterator]].
2139+
1. Let |returnMethod| be [$GetMethod$](|iterator|, "`return`").
2140+
1. If |returnMethod| is an abrupt completion, return [=a promise rejected with=]
2141+
|returnMethod|.\[[Value]].
2142+
1. If |returnMethod|.\[[Value]] is undefined, return [=a promise resolved with=] undefined.
2143+
1. Let |returnResult| be [$Call$](|returnMethod|.\[[Value]], |iterator|, « |reason| »).
2144+
1. If |returnResult| is an abrupt completion, return [=a promise rejected with=]
2145+
|returnResult|.\[[Value]].
2146+
1. Let |returnPromise| be [=a promise resolved with=] |returnResult|.\[[Value]].
2147+
1. Return the result of [=reacting=] to |returnPromise| with the following fulfillment steps,
2148+
given |iterResult|:
2149+
1. If [$Type$](|iterResult|) is not Object, throw a {{TypeError}}.
2150+
1. Return undefined.
2151+
1. Set |stream| to ! [$CreateReadableStream$](|startAlgorithm|, |pullAlgorithm|, |cancelAlgorithm|,
2152+
0).
2153+
1. Return |stream|.
2154+
</div>
2155+
20982156
<div algorithm="ReadableStreamPipeTo">
20992157
<dfn abstract-op lt="ReadableStreamPipeTo"
21002158
id="readable-stream-pipe-to">ReadableStreamPipeTo(|source|, |dest|, |preventClose|, |preventAbort|,

reference-implementation/lib/ReadableStream-impl.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ exports.implementation = class ReadableStreamImpl {
151151
aos.ReadableStreamDefaultReaderRelease(reader);
152152
return promiseResolvedWith(undefined);
153153
}
154+
155+
static from(asyncIterable) {
156+
return aos.ReadableStreamFromIterable(asyncIterable);
157+
}
154158
};
155159

156160
// See pipeTo()/pipeThrough() for why this is needed.

reference-implementation/lib/ReadableStream.webidl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
interface ReadableStream {
33
constructor(optional object underlyingSource, optional QueuingStrategy strategy = {});
44

5+
static ReadableStream from(any asyncIterable);
6+
57
readonly attribute boolean locked;
68

79
Promise<undefined> cancel(optional any reason);

reference-implementation/lib/abstract-ops/ecmascript.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const assert = require('assert');
33

44
const isFakeDetached = Symbol('is "detached" for our purposes');
55

6+
exports.typeIsObject = x => (typeof x === 'object' && x !== null) || typeof x === 'function';
7+
68
exports.CreateArrayFromList = elements => {
79
// We use arrays to represent lists, so this is basically a no-op.
810
// Do a slice though just in case we happen to depend on the unique-ness.
@@ -39,3 +41,84 @@ exports.CanTransferArrayBuffer = O => {
3941
exports.IsDetachedBuffer = O => {
4042
return isFakeDetached in O;
4143
};
44+
45+
exports.Call = (F, V, args = []) => {
46+
if (typeof F !== 'function') {
47+
throw new TypeError('Argument is not a function');
48+
}
49+
50+
return Reflect.apply(F, V, args);
51+
};
52+
53+
exports.GetMethod = (V, P) => {
54+
const func = V[P];
55+
if (func === undefined || func === null) {
56+
return undefined;
57+
}
58+
if (typeof func !== 'function') {
59+
throw new TypeError(`${P} is not a function`);
60+
}
61+
return func;
62+
};
63+
64+
exports.CreateAsyncFromSyncIterator = syncIteratorRecord => {
65+
// Instead of re-implementing CreateAsyncFromSyncIterator and %AsyncFromSyncIteratorPrototype%,
66+
// we use yield* inside an async generator function to achieve the same result.
67+
68+
// Wrap the sync iterator inside a sync iterable, so we can use it with yield*.
69+
const syncIterable = {
70+
[Symbol.iterator]: () => syncIteratorRecord.iterator
71+
};
72+
// Create an async generator function and immediately invoke it.
73+
const asyncIterator = (async function* () {
74+
return yield* syncIterable;
75+
}());
76+
// Return as an async iterator record.
77+
const nextMethod = asyncIterator.next;
78+
return { iterator: asyncIterator, nextMethod, done: false };
79+
};
80+
81+
exports.GetIterator = (obj, hint = 'sync', method) => {
82+
assert(hint === 'sync' || hint === 'async');
83+
if (method === undefined) {
84+
if (hint === 'async') {
85+
method = exports.GetMethod(obj, Symbol.asyncIterator);
86+
if (method === undefined) {
87+
const syncMethod = exports.GetMethod(obj, Symbol.iterator);
88+
const syncIteratorRecord = exports.GetIterator(obj, 'sync', syncMethod);
89+
return exports.CreateAsyncFromSyncIterator(syncIteratorRecord);
90+
}
91+
} else {
92+
method = exports.GetMethod(obj, Symbol.iterator);
93+
}
94+
}
95+
const iterator = exports.Call(method, obj);
96+
if (!exports.typeIsObject(iterator)) {
97+
throw new TypeError('The iterator method must return an object');
98+
}
99+
const nextMethod = iterator.next;
100+
return { iterator, nextMethod, done: false };
101+
};
102+
103+
exports.IteratorNext = (iteratorRecord, value) => {
104+
let result;
105+
if (value === undefined) {
106+
result = exports.Call(iteratorRecord.nextMethod, iteratorRecord.iterator);
107+
} else {
108+
result = exports.Call(iteratorRecord.nextMethod, iteratorRecord.iterator, [value]);
109+
}
110+
if (!exports.typeIsObject(result)) {
111+
throw new TypeError('The iterator.next() method must return an object');
112+
}
113+
return result;
114+
};
115+
116+
exports.IteratorComplete = iterResult => {
117+
assert(exports.typeIsObject(iterResult));
118+
return Boolean(iterResult.done);
119+
};
120+
121+
exports.IteratorValue = iterResult => {
122+
assert(exports.typeIsObject(iterResult));
123+
return iterResult.value;
124+
};

reference-implementation/lib/abstract-ops/readable-streams.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const assert = require('assert');
44
const { promiseResolvedWith, promiseRejectedWith, newPromise, resolvePromise, rejectPromise, uponPromise,
55
setPromiseIsHandledToTrue, waitForAllPromise, transformPromiseWith, uponFulfillment, uponRejection } =
66
require('../helpers/webidl.js');
7-
const { CanTransferArrayBuffer, CopyDataBlockBytes, CreateArrayFromList, IsDetachedBuffer, TransferArrayBuffer } =
8-
require('./ecmascript.js');
7+
const { CanTransferArrayBuffer, Call, CopyDataBlockBytes, CreateArrayFromList, GetIterator, GetMethod, IsDetachedBuffer,
8+
IteratorComplete, IteratorNext, IteratorValue, TransferArrayBuffer, typeIsObject } = require('./ecmascript.js');
99
const { CloneAsUint8Array, IsNonNegativeNumber } = require('./miscellaneous.js');
1010
const { EnqueueValueWithSize, ResetQueue } = require('./queue-with-sizes.js');
1111
const { AcquireWritableStreamDefaultWriter, IsWritableStreamLocked, WritableStreamAbort,
@@ -55,6 +55,7 @@ Object.assign(exports, {
5555
ReadableStreamDefaultControllerHasBackpressure,
5656
ReadableStreamDefaultReaderRead,
5757
ReadableStreamDefaultReaderRelease,
58+
ReadableStreamFromIterable,
5859
ReadableStreamGetNumReadRequests,
5960
ReadableStreamHasDefaultReader,
6061
ReadableStreamPipeTo,
@@ -1879,3 +1880,61 @@ function SetUpReadableByteStreamControllerFromUnderlyingSource(
18791880
stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, autoAllocateChunkSize
18801881
);
18811882
}
1883+
1884+
function ReadableStreamFromIterable(asyncIterable) {
1885+
let stream;
1886+
const iteratorRecord = GetIterator(asyncIterable, 'async');
1887+
1888+
const startAlgorithm = () => undefined;
1889+
1890+
function pullAlgorithm() {
1891+
let nextResult;
1892+
try {
1893+
nextResult = IteratorNext(iteratorRecord);
1894+
} catch (e) {
1895+
return promiseRejectedWith(e);
1896+
}
1897+
const nextPromise = promiseResolvedWith(nextResult);
1898+
return transformPromiseWith(nextPromise, iterResult => {
1899+
if (!typeIsObject(iterResult)) {
1900+
throw new TypeError('The promise returned by the iterator.next() method must fulfill with an object');
1901+
}
1902+
const done = IteratorComplete(iterResult);
1903+
if (done === true) {
1904+
ReadableStreamDefaultControllerClose(stream._controller);
1905+
} else {
1906+
const value = IteratorValue(iterResult);
1907+
ReadableStreamDefaultControllerEnqueue(stream._controller, value);
1908+
}
1909+
});
1910+
}
1911+
1912+
function cancelAlgorithm(reason) {
1913+
const iterator = iteratorRecord.iterator;
1914+
let returnMethod;
1915+
try {
1916+
returnMethod = GetMethod(iterator, 'return');
1917+
} catch (e) {
1918+
return promiseRejectedWith(e);
1919+
}
1920+
if (returnMethod === undefined) {
1921+
return promiseResolvedWith(undefined);
1922+
}
1923+
let returnResult;
1924+
try {
1925+
returnResult = Call(returnMethod, iterator, [reason]);
1926+
} catch (e) {
1927+
return promiseRejectedWith(e);
1928+
}
1929+
const returnPromise = promiseResolvedWith(returnResult);
1930+
return transformPromiseWith(returnPromise, iterResult => {
1931+
if (!typeIsObject(iterResult)) {
1932+
throw new TypeError('The promise returned by the iterator.return() method must fulfill with an object');
1933+
}
1934+
return undefined;
1935+
});
1936+
}
1937+
1938+
stream = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, 0);
1939+
return stream;
1940+
}

0 commit comments

Comments
 (0)