Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec the guts of the Observable & Subscriber interfaces #94

Merged
merged 1 commit into from
Jan 2, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 171 additions & 2 deletions spec.bs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ WPT Display: open
<pre class="link-defaults">
</pre>
<pre class="anchors">
urlPrefix: https://tc39.es/ecma262/#; spec: ECMASCRIPT
type: dfn
text: current realm
</pre>

<style>
Expand Down Expand Up @@ -136,7 +139,7 @@ dl, dd {
<xmp class=idl>
[Exposed=*]
interface Subscriber {
undefined next(any result);
undefined next(any value);
undefined error(any error);
undefined complete();
undefined addTeardown(VoidFunction teardown);
Expand All @@ -150,7 +153,123 @@ interface Subscriber {
};
</xmp>

<div>
Each {{Subscriber}} has a <dfn for=Subscriber>next callback</dfn>, which is an
{{ObserverCallback}}-or-null.

Each {{Subscriber}} has a <dfn for=Subscriber>error callback</dfn>, which is an
{{ObserverCallback}}-or-null.

Each {{Subscriber}} has a <dfn for=Subscriber>complete callback</dfn>, which is a
{{VoidFunction}}-or-null.

Each {{Subscriber}} has a <dfn for=Subscriber>complete or error controller</dfn>, which is an
{{AbortController}}.

Each {{Subscriber}} has a <dfn for=Subscriber>signal</dfn>, which is an {{AbortSignal}}.

Note: This is a [=create a dependent abort signal|dependent signal=], derived from both
[=Subscriber/complete or error controller=]'s [=AbortController/signal=], and
{{SubscribeOptions}}'s {{SubscribeOptions/signal}} (if non-null).

Each {{Subscriber}} has a <dfn for=Subscriber>active</dfn> boolean, initially true.

Note: This is a bookkeeping variable to ensure that a {{Subscriber}} never calls any of the
callbacks it owns after it has been [=close a subscription|closed=].

<div algorithm>
The <dfn for=Subscriber method><code>next(|value|)</code></dfn> method steps are:

1. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully
active=], then return.

1. If [=this=]'s [=Subscriber/next callback=] is non-null, [=invoke=] this's [=Subscriber/next
callback=] with |value|.

If <a spec=webidl lt="an exception was thrown">an exception |E| was thrown</a>, then [=report
the exception=] |E|.
</div>

<div algorithm>
The <dfn for=Subscriber method><code>error(|error|)</code></dfn> method steps are:

1. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully
active=], then return.

1. Let |callback| be [=this=]'s [=Subscriber/error callback=].

1. [=close a subscription|Close=] [=this=].

1. If |callback| is not null, [=invoke=] |callback| with |error|.

If <a spec=webidl lt="an exception was thrown">an exception |E| was thrown</a>, then [=report
the exception=] |E|.

1. Otherwise, [=report the exception=] |error|.

1. [=AbortController/Signal abort=] [=this=]'s [=Subscriber/complete or error controller=].
</div>

<div algorithm>
The <dfn for=Subscriber method><code>complete()</code></dfn> method steps are:

1. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully
active=], then return.

1. Let |callback| be [=this=]'s [=Subscriber/complete callback=].

1. [=close a subscription|Close=] [=this=].

1. If |callback| is not null, [=invoke=] |callback|.

If <a spec=webidl lt="an exception was thrown">an exception |E| was thrown</a>, then [=report
the exception=] |E|.

1. [=AbortController/Signal abort=] [=this=]'s [=Subscriber/complete or error controller=].
</div>

<div algorithm>
To <dfn>close a subscription</dfn> given a {{Subscriber}} |subscriber|, run these steps:

1. Set |subscriber|'s [=Subscriber/active=] boolean to false.

1. Set |subscriber|'s [=Subscriber/next callback=], [=Subscriber/error callback=], and
[=Subscriber/complete callback=] all to null.

<div class=note>
<p>This algorithm intentionally does not have script-running side-effects; it just updates the
internal state of a {{Subscriber}}. It's important that this algorithm sets
[=Subscriber/active=] to false and clears all of the callback algorithms *before* running any
script, because running script <span class=allow-2119>may</span> reentrantly invoke one of the
methods that closed the subscription in the first place. And closing the subscription <span
class=allow-2119>must</span> ensure that even if a method gets reentrantly invoked, none of the
{{Observer}} callbacks are ever invoked again. Consider this example:</p>

<div class=example id=reentrant-example>
<pre highlight=js>
let innerSubscriber = null;
const producedValues = [];

const controller = new AbortController();
const observable = new Observable(subscriber =&gt; {
innerSubscriber = subscriber;
subscriber.complete();
});

observable.subscribe({
next: v =&gt; producedValues.push(v),
complete: () =&gt; innerSubscriber.next('from complete'),

}, {signal: controller.signal}
);

// This invokes the complete() callback, and even though it invokes next() from
// within, the given next() callback will never run, because the subscription
// has already been "closed" before the complete() callback actually executes.
controller.abort();
console.assert(producedValues.length === 0);
</pre>
</div>
</div>
</div>

<h3 id=observable-api>The {{Observable}} interface</h3>
Expand Down Expand Up @@ -235,6 +354,56 @@ can be passed in by natively-constructed {{Observable}}s.
Note: This callback will get invoked later when {{Observable/subscribe()}} is called.
</div>

<div algorithm>
The <dfn for=Observable method><code>subscribe(|observer|, |options|)</code></dfn> method steps
are:

1. If [=this=]'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully
active=], then return.

1. Let |nextCallback|, |errorCallback|, and |completeCallback| all be null.

1. If |observer| is an {{ObserverCallback}}, then set |nextCallback| to |observer|.

1. Otherwise:

1. [=Assert=]: |observer| is an {{Observer}}.

1. Set |nextCallback| to |observer|'s {{Observer/next}}.

1. Set |errorCallback| to |observer|'s {{Observer/error}}.

1. Set |completeCallback| to |observer|'s {{Observer/complete}}.

1. Let |subscriber| be a [=new=] {{Subscriber}}, initialized as:

: [=Subscriber/next callback=]
:: |nextCallback|

: [=Subscriber/error callback=]
:: |errorCallback|

: [=Subscriber/complete callback=]
:: |completeCallback|

: [=Subscriber/signal=]
:: The result of [=creating a dependent abort signal=] from the list «|subscriber|'s
[=Subscriber/complete or error controller=]'s [=AbortController/signal=], |options|'s
{{SubscribeOptions/signal}} if it is non-null», using {{AbortSignal}}, and the [=current
realm=].

1. If |subscriber|'s [=Subscriber/signal=] is [=AbortSignal/aborted=], then [=close a
subscription|close=] |subscriber|.

Note: This can happen when {{SubscribeOptions}}'s {{SubscribeOptions/signal}} is already
[=AbortSignal/aborted=].

1. [=Invoke=] [=this=]'s [=Observable/subscribe callback=] with |subscriber|.

If <a spec=webidl lt="an exception was thrown">an exception |E| was thrown</a>, call
|subscriber|'s {{Subscriber/error()}} method with |E|.
</div>

<h3 id=operators>Operators</h3>

For now, see [https://github.com/wicg/observable#operators](https://github.com/wicg/observable#operators).
Expand Down