Skip to content

Commit

Permalink
Async iterable support + respect signal and abort algorithm to close …
Browse files Browse the repository at this point in the history
…async iterator
  • Loading branch information
domfarolino committed Jan 27, 2025
1 parent 8a8c950 commit 18d1cb0
Showing 1 changed file with 95 additions and 3 deletions.
98 changes: 95 additions & 3 deletions spec.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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: !
Expand Down Expand Up @@ -490,9 +491,100 @@ An <dfn>internal observer</dfn> is a [=struct=] with the following [=struct/item
1. <i id=from-observable-conversion><b>From Observable</b></i>: If |value|'s [=specific type=]
is an {{Observable}}, then return |value|.

1. Issue: Spec the <i><b>From async iterable</b></i> conversion steps which take place before
the iterable conversion steps. See <a
href=https://github.com/WICG/observable/issues/191>issue #191</a>.
1. <i id=from-async-iterable-conversion><b>From async iterable</b></i>: 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 <a
href=#from-iterable-conversion>From iterable</a>.

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|).

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 <a href=https://github.com/WICG/observable/issues/127>issue#127</a> 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. <i id=from-iterable-conversion><b>From iterable</b></i>: Let |iteratorMethod| be [=?=]
[$GetMethod$](|value|, {{%Symbol.iterator%}}).
Expand Down

0 comments on commit 18d1cb0

Please sign in to comment.