Skip to content

Commit 09ec607

Browse files
Support async sequence types
1 parent 6e847b5 commit 09ec607

5 files changed

Lines changed: 533 additions & 7 deletions

File tree

lib/output/utils.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,103 @@ function isAccessorDescriptor(desc) {
198198
return Object.hasOwn(desc, "get") || Object.hasOwn(desc, "set");
199199
}
200200

201+
function getMethod(value, property, errPrefix = "The provided value") {
202+
const func = value[property];
203+
if (func === undefined || func === null) {
204+
return undefined;
205+
}
206+
if (typeof func !== "function") {
207+
throw new TypeError(`${errPrefix}'s ${property} property is not a function.`);
208+
}
209+
return func;
210+
}
211+
212+
function createAsyncFromSyncIterator(syncIterator) {
213+
// Instead of re-implementing CreateAsyncFromSyncIterator and %AsyncFromSyncIteratorPrototype%,
214+
// we use yield* inside an async generator function to achieve the same result.
215+
216+
// Wrap the sync iterator inside a sync iterable, so we can use it with yield*.
217+
const syncIterable = {
218+
[Symbol.iterator]: () => syncIterator
219+
};
220+
// Create an async generator function and immediately invoke it.
221+
const asyncIterator = (async function* () {
222+
return yield* syncIterable;
223+
})();
224+
// Return as an async iterator record.
225+
return asyncIterator;
226+
}
227+
228+
function convertAsyncSequence(object, itemConverter, errPrefix = "The provided value") {
229+
if (!isObject(object)) {
230+
throw new TypeError(`${errPrefix} is not an object.`);
231+
}
232+
let method = getMethod(object, Symbol.asyncIterator, errPrefix);
233+
let type = "async";
234+
if (method === undefined) {
235+
method = getMethod(object, Symbol.iterator, errPrefix);
236+
if (method === undefined) {
237+
throw new TypeError(`${errPrefix} is not an async iterable object.`);
238+
}
239+
type = "sync";
240+
}
241+
242+
return {
243+
object,
244+
method,
245+
type,
246+
// The wrapperSymbol ensures that if the async sequence is used as a return value,
247+
// that it exposes the original JavaScript value.
248+
// https://webidl.spec.whatwg.org/#js-async-iterable
249+
[wrapperSymbol]: object,
250+
// Implement the async iterator protocol, so users can iterate
251+
// the async sequence directly (e.g. with for await...of)
252+
// instead of needing to call a separate helper function to open the async sequence.
253+
// https://webidl.spec.whatwg.org/#async-sequence-open
254+
[Symbol.asyncIterator]() {
255+
return openAsyncSequence(object, method, type, itemConverter, `${errPrefix}'s iterator`);
256+
}
257+
};
258+
}
259+
260+
function openAsyncSequence(object, method, type, itemConverter, errPrefix = "The provided value") {
261+
let iterator = call(method, object);
262+
if (!isObject(iterator)) {
263+
throw new TypeError(`${errPrefix}'s method must return an object`);
264+
}
265+
if (type === "sync") {
266+
iterator = createAsyncFromSyncIterator(iterator);
267+
}
268+
const nextMethod = iterator.next;
269+
return {
270+
async next() {
271+
const nextResult = await call(nextMethod, iterator);
272+
if (!isObject(nextResult)) {
273+
throw new TypeError(`${errPrefix}'s next method must return an object`);
274+
}
275+
const { done, value } = nextResult;
276+
if (done) {
277+
return { done: true, value: undefined };
278+
}
279+
return { done: false, value: itemConverter(value) };
280+
},
281+
async return(reason) {
282+
const returnMethod = getMethod(iterator, "return", errPrefix);
283+
if (returnMethod === undefined) {
284+
return { done: true, value: undefined };
285+
}
286+
const returnResult = await call(returnMethod, iterator, reason);
287+
if (!isObject(returnResult)) {
288+
throw new TypeError(`${errPrefix}'s return method must return an object`);
289+
}
290+
return { done: true, value: undefined };
291+
},
292+
[Symbol.asyncIterator]() {
293+
return this;
294+
}
295+
};
296+
}
297+
201298
const supportsPropertyIndex = Symbol("supports property index");
202299
const supportedPropertyIndices = Symbol("supported property indices");
203300
const supportsPropertyName = Symbol("supports property name");
@@ -232,6 +329,8 @@ module.exports = exports = {
232329
isArrayBuffer,
233330
isSharedArrayBuffer,
234331
isArrayIndexPropName,
332+
getMethod,
333+
convertAsyncSequence,
235334
supportsPropertyIndex,
236335
supportedPropertyIndices,
237336
supportsPropertyName,

lib/types.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ function generateTypeConversion(
138138
if (idlType.union) {
139139
// union type
140140
generateUnion();
141+
} else if (idlType.generic === "async_sequence") {
142+
// async_sequence type
143+
generateAsyncSequence();
141144
} else if (idlType.generic === "sequence") {
142145
// sequence type
143146
generateSequence();
@@ -278,14 +281,21 @@ function generateTypeConversion(
278281
let code = `if (utils.isObject(${name})) {`;
279282

280283
if (union.sequenceLike) {
281-
code += `if (${name}[Symbol.iterator] !== undefined) {`;
284+
if (union.sequenceLike.generic === "async_sequence") {
285+
code += `if (
286+
utils.getMethod(${name}, Symbol.asyncIterator, ${errPrefix}) !== undefined ||
287+
utils.getMethod(${name}, Symbol.iterator, ${errPrefix}) !== undefined
288+
) {`;
289+
} else {
290+
code += `if (utils.getMethod(${name}, Symbol.iterator, ${errPrefix}) !== undefined) {`;
291+
}
282292
const conv = generateTypeConversion(
283293
ctx,
284294
name,
285295
union.sequenceLike,
286296
[],
287297
parentName,
288-
`${errPrefix} + " sequence"`
298+
`${errPrefix} + " ${union.sequenceLike.generic}"`
289299
);
290300
requires.merge(conv.requires);
291301
code += conv.body;
@@ -362,6 +372,29 @@ function generateTypeConversion(
362372
str += output.join(" else ");
363373
}
364374

375+
function generateAsyncSequence() {
376+
const conv = generateTypeConversion(
377+
ctx,
378+
"item",
379+
idlType.idlType[0],
380+
[],
381+
parentName,
382+
`${errPrefix} + "'s element"`
383+
);
384+
requires.merge(conv.requires);
385+
386+
str += `
387+
${name} = utils.convertAsyncSequence(
388+
${name},
389+
function (item) {
390+
${conv.body};
391+
return item;
392+
},
393+
${errPrefix}
394+
);
395+
`;
396+
}
397+
365398
function generateSequence() {
366399
const conv = generateTypeConversion(
367400
ctx,
@@ -520,7 +553,7 @@ function extractUnionInfo(ctx, idlType, errPrefix) {
520553
unknown: false
521554
};
522555
for (const item of idlType.idlType) {
523-
if (item.generic === "sequence" || item.generic === "FrozenArray") {
556+
if (item.generic === "sequence" || item.generic === "async_sequence" || item.generic === "FrozenArray") {
524557
if (seen.sequenceLike) {
525558
error("There can only be one sequence-like type in a union type");
526559
}
@@ -721,6 +754,12 @@ function sameArray(array1, array2, comparator = (x, y) => x === y) {
721754
return array1.length === array2.length && array1.every((element1, index) => comparator(element1, array2[index]));
722755
}
723756

757+
function isSequenceLike(type) {
758+
return type.generic === "sequence" ||
759+
type.generic === "async_sequence" ||
760+
type.generic === "FrozenArray";
761+
}
762+
724763
function areDistinguishable(ctx, type1, type2) {
725764
const resolved1 = resolveType(ctx, type1);
726765
const resolved2 = resolveType(ctx, type2);
@@ -775,8 +814,8 @@ function areDistinguishable(ctx, type1, type2) {
775814
const isDictionaryLike2 = ctx.dictionaries.has(inner2.idlType) ||
776815
ctx.callbackInterfaces.has(inner2.idlType) ||
777816
inner2.generic === "record";
778-
const isSequenceLike1 = inner1.generic === "sequence" || inner1.generic === "FrozenArray";
779-
const isSequenceLike2 = inner2.generic === "sequence" || inner2.generic === "FrozenArray";
817+
const isSequenceLike1 = isSequenceLike(inner1);
818+
const isSequenceLike2 = isSequenceLike(inner2);
780819

781820
if (inner1.idlType === "object") {
782821
return inner2.idlType !== "object" &&

0 commit comments

Comments
 (0)