Skip to content

Commit

Permalink
Add support for async iterables
Browse files Browse the repository at this point in the history
  • Loading branch information
domenic committed May 18, 2020
1 parent 1d0fcb7 commit 0cfd2df
Show file tree
Hide file tree
Showing 13 changed files with 7,148 additions and 5,287 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ stringifier DOMString operation(); // `toString()` is not needed.

IDL indexed and named properties require multiple things on the implementation class to work properly.

The first is the getters, (optional) setters, and (optional) deleters operations. Much like stringifiers, getters, setters, and deleters can either be standalone or aliased to a named operation (though not an attribute). If an operation is standalone, then the implementation class must implement following symbol-named methods. The `utils` object below refers to the default export from the generated utilities file `utils.js`.
The first is the getters, (optional) setters, and (optional) deleters operations. Much like stringifiers, getters, setters, and deleters can either be standalone or aliased to a named operation (though not an attribute). If an operation is standalone, then the implementation class must implement the following symbol-named methods. The `utils` object below refers to the default export from the generated utilities file `utils.js`.

- Getters: `utils.indexedGet`, `utils.namedGet`
- Setters: `utils.indexedSetNew`, `utils.indexedSetExisting`, `utils.namedSetNew`, `utils.namedSetExisting`
Expand All @@ -395,6 +395,20 @@ The second is the interface's supported property indices/names. By default, the

If the getter function always returns a constant value for unsupported properties, webidl2js also offers a non-standard extended attribute `[WebIDL2JSValueAsUnsupported]` (documented below) that would simply call the getter function to check if a property index/name is supported, so that `supportsPropertyIndex`/`supportsPropertyName` would not need to be implemented separately. However, when using the extended attribute, be very sure that the value specified in the attribute is returned *if and only if* the property is unsupported.

### Iterables

For synchronous value iterable declarations, there is no need to add implementation-class code: they will be automatically generated based on the indexed property getter and `length` property.

For synchronous pair iterable declarations, the implementation class needs to implement the `[Symbol.iterator]()` property, returning an iterable of `[key, value]` pairs. These can be impls; the generated code will convert them into wrappers as necessary.

### Async iterables

[Asynchronous iterable declarations](https://heycam.github.io/webidl/#idl-async-iterable) require the implementation class to implement the following symbol-named methods, corresponding to algorithms from the Web IDL specification. The `utils` object below refers to the default export from the generated utilities file `utils.js`.

- `utils.asyncIteratorNext`: corresponds to the [get the next iteration result](https://heycam.github.io/webidl/#dfn-get-the-next-iteration-result) algorithm, and receives a single argument containing an instance of the generated async iterator. For pair asynchronous iterables, the return value must be a `[key, value]` pair array, or `undefined` to signal the end of the iteration. For value asynchronous iterables, the return value must be the value, or `undefined` to signal the end of the iteration.
- `utils.asyncIteratorInit`: corresponds to the [asynchronous iterator initialization steps](https://heycam.github.io/webidl/#asynchronous-iterator-initialization-steps), and receives two arguments: the instance of the generated async iterator, and an array containing the post-conversion arguments. This method is optional.
- `utils.asyncIteratorReturn`: corresponds to the [asynchronous iterator return](https://heycam.github.io/webidl/#asynchronous-iterator-return) algorithm, and receives two arguments: the instance of the generated async iterator, and the argument passed to the `return()` method. This method is optional.

### Other, non-exposed data and functionality

Your implementation class can contain other properties and methods in support of the wrapped properties and methods that the wrapper class calls into. These can be used to factor out common algorithms, or store private state, or keep caches, or anything of the sort.
Expand Down Expand Up @@ -449,6 +463,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- Stringifiers
- Named and indexed `getter`/`setter`/`deleter` declarations
- `iterable<>` declarations
- `async iterable<>` declarations
- Class strings (with the semantics of [heycam/webidl#357](https://github.com/heycam/webidl/pull/357))
- Dictionary types
- Enumeration types
Expand Down
66 changes: 66 additions & 0 deletions lib/constructs/async-iterable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use strict";

const utils = require("../utils");
const { generateAsyncIteratorArgConversions } = require("../parameters");

class Iterable {
constructor(ctx, I, idl) {
this.ctx = ctx;
this.interface = I;
this.idl = idl;
this.name = idl.type;
}

get isValue() {
return this.idl.idlType.length === 1;
}

get isPair() {
return this.idl.idlType.length === 2;
}

get isAsync() {
return true;
}

generateFunction(key, kind, requires) {
const conv = generateAsyncIteratorArgConversions(
this.ctx, this.idl, this.interface, `Failed to execute '${key}' on '${this.interface.name}': `);
requires.merge(conv.requires);

this.interface.addMethod(this.interface.defaultWhence, key, [], `
if (!exports.is(this)) {
throw new TypeError("'${key}' called on an object that is not a valid instance of ${this.interface.name}.");
}
${conv.body}
const asyncIterator = exports.createDefaultAsyncIterator(this, "${kind}");
if (this[implSymbol][utils.asyncIteratorInit]) {
this[implSymbol][utils.asyncIteratorInit](asyncIterator, args);
}
return asyncIterator;
`);
}

generate() {
const whence = this.interface.defaultWhence;
const requires = new utils.RequiresMap(this.ctx);

// https://heycam.github.io/webidl/#define-the-asynchronous-iteration-methods

if (this.isPair) {
this.generateFunction("keys", "key", requires);
this.generateFunction("values", "value", requires);
this.generateFunction("entries", "key+value", requires);
this.interface.addProperty(whence, Symbol.asyncIterator, `${this.interface.name}.prototype.entries`);
} else {
this.generateFunction("values", "value", requires);
this.interface.addProperty(whence, Symbol.asyncIterator, `${this.interface.name}.prototype.values`);
}

return { requires };
}
}

module.exports = Iterable;
124 changes: 90 additions & 34 deletions lib/constructs/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const utils = require("../utils");
const Attribute = require("./attribute");
const Constant = require("./constant");
const Iterable = require("./iterable");
const AsyncIterable = require("./async-iterable");
const Operation = require("./operation");
const Types = require("../types");
const Overloads = require("../overloads");
Expand Down Expand Up @@ -306,7 +307,9 @@ class Interface {
if (this.iterable) {
throw new Error(`Interface ${this.name} has more than one iterable declaration`);
}
this.iterable = new Iterable(this.ctx, this, member);
this.iterable = member.async ?
new AsyncIterable(this.ctx, this, member) :
new Iterable(this.ctx, this, member);
break;
default:
if (!this.ctx.options.suppressErrors) {
Expand Down Expand Up @@ -368,13 +371,15 @@ class Interface {
// https://heycam.github.io/webidl/#dfn-reserved-identifier
const forbiddenMembers = new Set(["constructor", "toString"]);
if (this.iterable) {
if (this.iterable.isValue) {
if (!this.supportsIndexedProperties) {
throw new Error(`A value iterator cannot be declared on ${this.name} which does not support indexed ` +
"properties");
if (!this.iterable.isAsync) {
if (this.iterable.isValue) {
if (!this.supportsIndexedProperties) {
throw new Error(`A value iterator cannot be declared on ${this.name} which does not support indexed ` +
"properties");
}
} else if (this.iterable.isPair && this.supportsIndexedProperties) {
throw new Error(`A pair iterator cannot be declared on ${this.name} which supports indexed properties`);
}
} else if (this.iterable.isPair && this.supportsIndexedProperties) {
throw new Error(`A pair iterator cannot be declared on ${this.name} which supports indexed properties`);
}
for (const n of ["entries", "forEach", "keys", "values"]) {
forbiddenMembers.add(n);
Expand Down Expand Up @@ -416,7 +421,59 @@ class Interface {
}

generateIterator() {
if (this.iterable && this.iterable.isPair) {
if (!this.iterable) {
return;
}

if (this.iterable.isAsync) {
this.str += `
const AsyncIteratorPrototype = Object.create(utils.AsyncIteratorPrototype, {
next: {
value: function next() {
const internal = this[utils.iterInternalSymbol];
if (!internal) {
return Promise.reject(new TypeError("next() called on an object that is not an async iterator prototype object"));
}
const nextSteps = () => {
if (internal.isFinished) {
return Promise.resolve({ value: undefined, done: true });
}
const nextPromise = internal.target[implSymbol][utils.asyncIteratorNext](this);
return nextPromise.then(next => {
internal.ongoingPromise = null;
if (next === undefined) {
internal.isFinished = true;
return { value: undefined, done: true };
}`;
if (this.iterable.isPair) {
this.str += `
return utils.iteratorResult(next.map(utils.tryWrapperForImpl), kind);
`;
} else {
this.str += `
return { value: utils.tryWrapperForImpl(next), done: false };
`;
}
this.str += `
});
};
internal.ongoingPromise = internal.ongoingPromise ? internal.ongoingPromise.then(nextSteps) : nextSteps();
return internal.ongoingPromise;
},
writable: true,
enumerable: true,
configurable: true
},
[Symbol.toStringTag]: {
value: "${this.name} AsyncIterator",
configurable: true
}
});
`;
} else if (this.iterable.isPair) {
this.str += `
const IteratorPrototype = Object.create(utils.IteratorPrototype, {
next: {
Expand All @@ -431,21 +488,7 @@ class Interface {
const pair = values[index];
internal.index = index + 1;
const [key, value] = pair.map(utils.tryWrapperForImpl);
let result;
switch (kind) {
case "key":
result = key;
break;
case "value":
result = value;
break;
case "key+value":
result = [key, value];
break;
}
return { value: result, done: false };
return utils.iteratorResult(pair.map(utils.tryWrapperForImpl), kind);
},
writable: true,
enumerable: true,
Expand Down Expand Up @@ -532,17 +575,30 @@ class Interface {
};
`;

if (this.iterable && this.iterable.isPair) {
this.str += `
exports.createDefaultIterator = (target, kind) => {
const iterator = Object.create(IteratorPrototype);
Object.defineProperty(iterator, utils.iterInternalSymbol, {
value: { target, kind, index: 0 },
configurable: true
});
return iterator;
};
`;
if (this.iterable) {
if (this.iterable.isAsync) {
this.str += `
exports.createDefaultAsyncIterator = (target, kind) => {
const iterator = Object.create(AsyncIteratorPrototype);
Object.defineProperty(iterator, utils.iterInternalSymbol, {
value: { target, kind, ongoingPromise: null, isFinished: false },
configurable: true
});
return iterator;
};
`;
} else if (this.iterable.isPair) {
this.str += `
exports.createDefaultIterator = (target, kind) => {
const iterator = Object.create(IteratorPrototype);
Object.defineProperty(iterator, utils.iterInternalSymbol, {
value: { target, kind, index: 0 },
configurable: true
});
return iterator;
};
`;
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/constructs/iterable.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class Iterable {
return this.idl.idlType.length === 2;
}

get isAsync() {
return false;
}

generateFunction(key, kind) {
this.interface.addMethod(this.interface.defaultWhence, key, [], `
if (!exports.is(this)) {
Expand Down
28 changes: 27 additions & 1 deletion lib/output/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function tryImplForWrapper(wrapper) {

const iterInternalSymbol = Symbol("internal");
const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype);

function isArrayIndexPropName(P) {
if (typeof P !== "string") {
Expand Down Expand Up @@ -72,6 +73,22 @@ function isArrayBuffer(value) {
}
}

function iteratorResult([key, value], kind) {
let result;
switch (kind) {
case "key":
result = key;
break;
case "value":
result = value;
break;
case "key+value":
result = [key, value];
break;
}
return { value: result, done: false };
}

const supportsPropertyIndex = Symbol("supports property index");
const supportedPropertyIndices = Symbol("supported property indices");
const supportsPropertyName = Symbol("supports property name");
Expand All @@ -84,6 +101,10 @@ const namedSetNew = Symbol("named property set new");
const namedSetExisting = Symbol("named property set existing");
const namedDelete = Symbol("named property delete");

const asyncIteratorNext = Symbol("async iterator get the next iteration result");
const asyncIteratorReturn = Symbol("async iterator return steps");
const asyncIteratorInit = Symbol("async iterator initialization steps");

module.exports = exports = {
isObject,
hasOwn,
Expand All @@ -97,6 +118,7 @@ module.exports = exports = {
tryImplForWrapper,
iterInternalSymbol,
IteratorPrototype,
AsyncIteratorPrototype,
isArrayBuffer,
isArrayIndexPropName,
supportsPropertyIndex,
Expand All @@ -109,5 +131,9 @@ module.exports = exports = {
namedGet,
namedSetNew,
namedSetExisting,
namedDelete
namedDelete,
asyncIteratorNext,
asyncIteratorReturn,
asyncIteratorInit,
iteratorResult
};
Loading

0 comments on commit 0cfd2df

Please sign in to comment.