diff --git a/spec.bs b/spec.bs index f5bb7d5..bf4fbe3 100644 --- a/spec.bs +++ b/spec.bs @@ -30,6 +30,7 @@ urlPrefix: https://tc39.es/ecma262/#; spec: ECMASCRIPT text: normal completion; url: sec-completion-record-specification-type text: NormalCompletion; url: sec-normalcompletion text: throw completion; url: sec-completion-record-specification-type + text: Iterator Record; url: sec-iterator-records url: sec-returnifabrupt-shorthands text: ? text: ! @@ -490,9 +491,106 @@ An internal observer is a [=struct=] with the following [=struct/item 1. From Observable: If |value|'s [=specific type=] is an {{Observable}}, then return |value|. - 1. Issue: Spec the From async iterable conversion steps which take place before - the iterable conversion steps. See issue #191. + 1. From async iterable: Let + |asyncIteratorMethod| be [=?=] [$GetMethod$](|value|, {{%Symbol.asyncIterator%}}). + + Note: We use [$GetMethod$] instead of [$GetIterator$] because we're only probing for async + iterator protocol support, and we don't want to throw if it's not implemented. + [$GetIterator$] throws errors in BOTH of the following cases: (a) no iterator protocol is + implemented, (b) an iterator protocol is implemented, but isn't callable or its getter + throws. [$GetMethod$] lets us ONLY throw in the latter case. + + 1. If |asyncIteratorMethod|'s is undefined or null, then jump to the step labeled From iterable. + + 1. Let |nextAlgorithm| be the following steps, given a {{Subscriber}} |subscriber| and an + [=Iterator Record=] |iteratorRecord|: + + 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is + [=AbortSignal/aborted=], then return. + + 1. Let |nextPromise| be a {{Promise}}-or-undefined, initially undefined. + + 1. Let |nextCompletion| be [$IteratorNext$](|iteratorRecord|). + + Note: We use [$IteratorNext$] here instead of [$IteratorStepValue$], because + [$IteratorStepValue$] expects the iterator's `next()` method to return an object that + can immediately be inspected for a value, whereas in the async iterator case, `next()` + is expected to return a Promise/thenable (which we wrap in a Promise and react to to get + that value). + + 1. If |nextCompletion| is a [=throw completion=], then: + + 1. [=Assert=]: |iteratorRecord|'s \[[Done]] is true. + + 1. Set |nextPromise| to [=a promise rejected with=] |nextRecord|'s \[[Value]]. + + 1. Otherwise, if |nextRecord| is [=normal completion=], then set |nextPromise| to [=a + promise resolved with=] |nextRecord|'s \[[Value]]. + + Note: This is done in case |nextRecord|'s \[[Value]] is not *itself* already a + {{Promise}}. + + 1. [=React=] to |nextPromise|: + + * If |nextPromise| was fulfilled with value |iteratorResult|, then: + + 1. If [$Type$](|iteratorResult|) is not Object, then run |subscriber|'s + {{Subscriber/error()}} method with a {{TypeError}} and abort these steps. + + 1. Let |done| be [$IteratorComplete$](|iteratorResult|). + + 1. If |done| is a [=throw completion=], then run |subscriber|'s + {{Subscriber/error()}} method with |done|'s \[[Value]] and abort these steps. + + 1. If |done|'s \[[Value]] is true, then run |subscriber|'s {{Subscriber/complete()}} + and abort these steps. + + 1. Run |nextAlgorithm|. + + * If |nextPromise| was rejected with reason |r|, then run |subscriber|'s + {{Subscriber/error()}} method given |r|. + + 1. Return a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an algorithm that + takes a {{Subscriber}} |subscriber| and does the following: + + 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is + [=AbortSignal/aborted=], then return. + + 1. Let |iteratorRecordCompletion| be [$GetIterator$](|value|, async). + + Note: This both re-invokes any {{%Symbol.asyncIterator%}} method getters on |value|—note + that whether this is desirable is an extreme corner case, but it matches test + expectations; see issue#127 for + discussion—and invokes the protocol itself to obtain an [=Iterator Record=]. + + 1. If |iteratorRecordCompletion| is a [=throw completion=], then run |subscriber|'s + {{Subscriber/error()}} method with |iteratorRecordCompletion|'s \[[Value]] and abort these + steps. + + Note: This means we invoke the {{Subscriber/error()}} method synchronously with respect to + subscription, which is the only time this can happen for async iterables that are + converted to {{Observable}}s. In all other cases, errors are propagated to the observer + asynchronously, with microtask timing, by virtue of being wrapped in a rejected + {{Promise}} that |nextAlgorithm| [=reacts=] to. This synchronous-error-propagation + behavior is consistent with language constructs, i.e., **for-await of** loops that invoke + {{%Symbol.asyncIterator%}} and synchronously re-throw exceptions to catch blocks outside + the loop, before any [$Await|Awaiting$] takes place. + + 1. Let |iteratorRecord| be [=!=] |iteratorRecordCompletion|. + + 1. [=Assert=]: |iteratorRecord| is an [=Iterator Record=]. + + 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is + [=AbortSignal/aborted=], then return. + + 1. [=AbortSignal/add|Add the following abort algorithm=] to |subscriber|'s + [=Subscriber/subscription controller=]'s [=AbortController/signal=]: + + 1. Run [$AsyncIteratorClose$](|iteratorRecord|, [=NormalCompletion=](|subscriber|'s + [=Subscriber/subscription controller=]'s [=AbortSignal/abort reason=])). + + 1. Run |nextAlgorithm| given |subscriber| and |iteratorRecord|. 1. From iterable: Let |iteratorMethod| be [=?=] [$GetMethod$](|value|, {{%Symbol.iterator%}}).