1. Introduction
This section is non-normative.
2. Core infrastructure
2.1. The Subscriber interface
[Exposed=*]interface {Subscriber undefined next (any );value undefined error (any );error undefined complete ();undefined addTeardown (VoidFunction ); // True after the Subscriber is created, up until either // complete()/error() are invoked, or the subscriber unsubscribes. Inside // complete()/error(), this attribute is true.teardown readonly attribute boolean active ;readonly attribute AbortSignal signal ; };
Each Subscriber has a next algorithm, which is a next steps-or-null.
Each Subscriber has a error algorithm, which is an error steps-or-null.
Each Subscriber has a complete algorithm, which is a complete steps-or-null.
Each Subscriber has a teardown callbacks, which is a list of VoidFunctions, initially empty.
Each Subscriber has a complete or error controller, which is an AbortController.
Each Subscriber has a signal, which is an AbortSignal.
Note: This is a dependent signal, derived from both complete or error controller's signal, and SubscribeOptions's signal (if non-null).
Each Subscriber has a active 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 closed.
The active getter steps are to return this's active boolean.
The signal getter steps are to return this's signal.
next(value) method steps are:
-
If this's relevant global object is a
Windowobject, and its associated Document is not fully active, then return. -
If this's next algorithm is not null, then run this's next algorithm given value.
error(error) method steps are:
-
If this's relevant global object is a
Windowobject, and its associated Document is not fully active, then return. -
Let error algorithm be this's error algorithm.
-
If error algorithm is not null, then run error algorithm given error.
-
Otherwise (i.e., when error algorithm is null), report the exception error.
complete() method steps are:
-
If this's relevant global object is a
Windowobject, and its associated Document is not fully active, then return. -
Let complete algorithm be this's complete algorithm.
-
If complete algorithm is not null, then run complete algorithm.
addTeardown(teardown) method steps are:
-
If this's relevant global object is a
Windowobject, and its associated Document is not fully active, then return. -
If this's active is true, then append teardown to this's teardown callbacks list.
-
Otherwise, invoke teardown.
If an exception E was thrown, then report the exception E.
Subscriber subscriber, run these steps:
-
Set subscriber’s active boolean to false.
-
Set subscriber’s next algorithm, error algorithm, and complete algorithm all to null.
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 active to false and clears all of the callback algorithms before running any
script, because running script may reentrantly invoke one of the
methods that closed the subscription in the first place. And closing the subscription must ensure that even if a method gets reentrantly invoked, none of the SubscriptionObserver callbacks are ever invoked again. Consider this example:
let innerSubscriber= null ; const producedValues= []; const controller= new AbortController(); const observable= new Observable( subscriber=> { innerSubscriber= subscriber; subscriber. complete(); }); observable. subscribe({ next: v=> producedValues. push( v), complete: () => 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 );
2.2. The Observable interface
// SubscribeCallback is where the Observable "creator's" code lives. It's // called when subscribe() is called, to set up a new subscription.callback =SubscribeCallback undefined (Subscriber );subscriber callback =SubscriptionObserverCallback undefined (any );value dictionary {SubscriptionObserver SubscriptionObserverCallback ;next SubscriptionObserverCallback ;error VoidFunction ; };complete typedef (SubscriptionObserverCallback or SubscriptionObserver );ObserverUnion dictionary {SubscribeOptions AbortSignal ; };signal callback =Predicate boolean (any );value callback =Reducer any (any ,accumulator any );currentValue callback =Mapper any (any ,element unsigned long long ); // Differs from Mapper only in return type, since this callback is exclusively // used to visit each element in a sequence, not transform it.index callback =Visitor undefined (any ,element unsigned long long ); [Exposed=*]index interface {Observable constructor (SubscribeCallback );callback undefined subscribe (optional ObserverUnion = {},observer optional SubscribeOptions = {}); // Constructs a native Observable from value if it's any of the following: // - Observable // - AsyncIterable // - Iterable // - Promiseoptions static Observable (from any ); // Observable-returning operators. See "Operators" section in the spec. // // takeUntil() can consume promises, iterables, async iterables, and other // observables.value Observable takeUntil (any );notifier Observable map (Mapper );mapper Observable filter (Predicate );predicate Observable take (unsigned long long );amount Observable drop (unsigned long long );amount Observable flatMap (Mapper );mapper Observable finally (VoidFunction ); // Promise-returning operators.callback Promise <sequence <any >>toArray (optional SubscribeOptions = {});options Promise <undefined >forEach (Visitor ,callback optional SubscribeOptions = {});options Promise <boolean >every (Predicate ,predicate optional SubscribeOptions = {}); // Maybe? Promise<any> first(optional SubscribeOptions options = {});options Promise <any >find (Predicate ,predicate optional SubscribeOptions = {});options Promise <boolean >some (Predicate ,predicate optional SubscribeOptions = {});options Promise <any >reduce (Reducer ,reducer optional any ,initialValue optional SubscribeOptions = {}); };options
Each Observable has a subscribe callback, which is a SubscribeCallback or a set of steps that take in a Subscriber.
Note: The "union" of these types is to support both Observables created by JavaScript (that are
always constructed with a SubscribeCallback), and natively-constructed Observable objects
(whose subscribe callback could be an arbitrary set of native steps, not a JavaScript
callback). The return value of on() is an example of the latter.
new
Observable(callback) constructor steps are:
-
Set this's subscribe callback to callback.
Note: This callback will get invoked later when
subscribe()is called.
2.2.1. Supporting concepts
any error, and runs
these steps:
-
Report the exception error.
Note: We pull this default out separately so that every place in this specification that natively subscribes to an Observable (i.e.,
subscribes from spec prose, not going through the subscribe() method) doesn’t have
to redundantly define these steps.
An internal observer is a struct with the following items:
- next steps
-
An algorithm that takes a single parameter of type
any. Initially, these steps do nothing. - error steps
-
An algorithm that takes a single parameter of type
any. Initially, the default error algorithm. - complete steps
-
An algorithm with no parameters. Initially, these steps do nothing.
The internal observer struct is used to mirror the next, error, and complete callback functions. For
any Observable that is subscribed by JavaScript via the subscribe() method,
these algorithm "steps" will just be a wrapper around invoking the corresponding next, error, and complete callback functions provided by script.
But when internal spec prose (not user script) subscribes to an Observable, these "steps" are arbitrary spec algorithms that
are not provided via an ObserverUnion packed with Web IDL callback functions. See the § 2.3.3 Promise-returning operators that make use of this, for example.
Observable given an ObserverUnion-or-internal observer observer, and a SubscribeOptions options, run
these steps:
Note: We split this algorithm out from the Web IDL subscribe() method, so that
spec prose can subscribe to an Observable without going through the Web IDL bindings. See w3c/IntersectionObserver#464 for
similar context, where "internal" prose must not go through Web IDL
bindings on objects whose properties could be mutated by JavaScript. See § 2.3.3 Promise-returning operators for usage of this.
-
If this's relevant global object is a
Windowobject, and its associated Document is not fully active, then return. -
Let internal observer be a new internal observer.
-
Process observer as follows:
-
- If observer is a
SubscriptionObserverCallback -
Set internal observer’s next steps to these steps that take
an
anyvalue:-
Invoke observer with value.
If an exception E was thrown, then report the exception E.
-
- If observer is a
SubscriptionObserver -
-
If observer’s
nextis not null, set internal observer’s next steps to these steps that take ananyvalue:-
Invoke observer’s
nextwith value.If an exception E was thrown, then report the exception E.
-
-
If observer’s
erroris not null, set internal observer’s error steps to these steps that take ananyerror:-
Invoke observer’s
errorwith error.If an exception E was thrown, then report the exception E.
-
-
If observer’s
completeis not null, set internal observer’s complete steps to these steps:-
If an exception E was thrown, then report the exception E.
-
-
- If observer is an internal observer
- Set internal observer to observer.
- If observer is a
-
-
Assert: internal observer’s error steps is either the default error algorithm, or an algorithm that invokes the provided
errorcallback function. -
Let subscriber be a new
Subscriber, initialized as:- next algorithm
-
internal observer’s next steps
- error algorithm
-
internal observer’s error steps
- complete algorithm
-
internal observer’s complete steps
- signal
-
The result of creating a dependent abort signal from the list «subscriber’s complete or error controller's signal, options’s
signalif it is non-null», usingAbortSignal, and the current realm.
-
If subscriber’s signal is aborted, then close subscriber.
Note: This can happen when
SubscribeOptions'ssignalis already aborted. -
Otherwise, add the following abort algorithm to subscriber’s signal:
-
Close subscriber.
-
For each teardown of subscriber’s teardown callbacks sorted in reverse insertion order:
-
If subscriber’s relevant global object is a
Windowobject, and its associated Document is not fully active, then abort these steps.Note: This step runs repeatedly because each teardown could result in the above
Documentbecoming inactive. -
Invoke teardown.
If an exception E was thrown, call subscriber’s
error()method with E.
-
-
-
If this's subscribe callback is a
SubscribeCallback, invoke it with subscriber.If an exception E was thrown, call subscriber’s
error()method with E. -
Otherwise, run the steps given by this's subscribe callback, given subscriber.
2.3. Operators
For now, see https://github.com/wicg/observable#operators.
2.3.1. from()
Spec the exact semantics of from() conversion.
2.3.2. Observable-returning operators
takeUntil(notifier) method steps are:
-
Let sourceObservable be this.
-
Let observable be a new
Observablewhose subscribe callback is an algorithm that takes aSubscribersubscriber and does the following:-
Let controller be a new
AbortController.Note that this method involves Subscribing to twoObservables: (1) notifier, and (2) sourceObservable. This controller is how we "unsubscribe" from both of them. We do this in both of the following situations:-
notifier starts emitting values (either "next" or "error"). In this case, we unsubscribe from notifier since we got all we need from it, and no longer need it to keep producing values. We also unsubscribe from sourceObservable, because it no longer needs to produce values that get plumbed through this method’s returned observable, because we’re manually ending the subscription to observable, since notifier finally produced a value.
-
sourceObservable either
error()s orcomplete()s itself. In this case, we unsubscribe from notifier since we no longer need to listen for values it emits in order to determine when observable can stop mirroring values from sourceObservable (since sourceObservable ran to completion by itself). Unsubscribing from sourceObservable isn’t necessary, since its subscription has exhausted itself.
-
-
Let signal be the result of creating a dependent abort signal from the list «controller’s signal, subscriber’s signal», using
AbortSignal, and the current realm. -
Let notifierObserver be a new internal observer, initialized as follows:
- next steps
-
-
Run subscriber’s
complete()method. -
Signal abort controller.
-
- error steps
-
-
Run subscriber’s
complete()method. -
Signal abort controller.
-
Note: We do not specify complete steps, because if the notifier
Observablecompletes itself, we do not need to complete the subscriber associated with the observable returned from this method. Rather, the observable will continue to mirror sourceObservable uninterrupted. -
Let options be a new
SubscribeOptionswhosesignalis signal. -
Subscribe to notifier given notifierObserver and options.
-
If subscriber’s active is false, then return.
Note: This means that sourceObservable’s subscribe callback will not even get invoked once, if notifier synchronously emits a value. If notifier only "completes" synchronously though (without emitting a "next" or "error" value), then subscriber’s active will still be true, and we proceed to subscribe to sourceObservable, which observable will mirror uninterrupted.
-
Let sourceObserver be a new internal observer, initialized as follows:
- next steps
-
Run subscriber’s
next()method, given the passed in value. - error steps
-
-
Run subscriber’s
error()method, given the passed in error. -
Signal abort controller.
-
- complete steps
-
-
Run subscriber’s
complete()method. -
Signal abort controller.
-
Note: sourceObserver is mostly a pass-through, mirroring everything that sourceObservable emits, with the exception of having the ability to unsubscribe from the notifier
Observablein the case where sourceObservable is exhausted before notifier emits anything. -
Subscribe to sourceObservable given sourceObserver and options.
-
-
Return observable.
map(mapper) method steps are:
-
TODO: Spec this and use mapper.
filter(predicate) method steps are:
-
TODO: Spec this and use predicate.
take(amount) method steps are:
-
TODO: Spec this and use amount.
drop(amount) method steps are:
-
TODO: Spec this and use amount.
flatMap(mapper) method steps are:
-
TODO: Spec this and use mapper.
finally(callback) method steps are:
-
TODO: Spec this and use callback.
2.3.3. Promise-returning operators
toArray(options) method steps are:
-
Let p a new promise.
-
If options’s
signalis not null:-
If options’s
signalis aborted, then:-
Reject p with options’s
signal's abort reason. -
Return p.
-
-
Add the following abort algorithm to options’s
signal:-
Reject p with options’s
signal's abort reason.
Note: All we have to do here is reject p. Note that the subscription to this
Observablewill also be canceled automatically, since the "inner" signal (created during subscription) is a dependent signal of options’ssignal. -
-
-
Let values be a new list.
-
Let observer be a new internal observer, initialized as follows:
- next steps
-
Append the passed in value to values.
- error steps
-
Reject p with the passed in error.
- complete steps
-
Resolve p with values.
-
Return p.
forEach(callback, options) method steps are:
-
Let p a new promise.
-
Let visitor callback controller be a new
AbortController. -
Let internal options be a new
SubscribeOptionswhosesignalis the result of creating a dependent abort signal from the list «visitor callback controller’s signal, options’ssignalif non-null», usingAbortSignal, and the current realm.Many trivial internal observers act as pass-throughs, and do not control the subscription to the
Observablethat they represent; that is, their error steps and complete steps are called when the subscription is terminated, and their next steps simply pass some version of the given value along the chain.For this operator, however, the below observer’s next steps are responsible for actually aborting the underlying subscription to this, in the event that callback throws an exception. In that case, the
SubscribeOptions'ssignalwe pass through to "Subscribe to anObservable", needs to be a dependent signal derived from options’ssignal, and theAbortSignalof anAbortControllerthat the next steps below has access to, and can signal abort when needed. -
If internal options’s
signalis aborted, then:-
Reject p with internal options’s
signal's abort reason. -
Return p.
-
-
Add the following abort algorithm to internal options’s
signal:-
Reject p with internal options’s
signal's abort reason.Note: The fact that rejection of p is tied to internal options’s
signal, and not options’ssignalmeans, that any microtasks queued during the firing of options’ssignal'sabortevent will run before p’s rejection handler runs.
-
-
Let idx be an
unsigned long long, initially 0. -
Let observer be a new internal observer, initialized as follows:
- next steps
-
-
Invoke callback with the passed in value, and idx.
If an exception E was thrown, then reject p with E, and signal abort visitor callback controller with E.
-
Increment idx.
-
- error steps
-
Reject p with the passed in error.
- complete steps
-
Return p.
every(predicate, options) method steps are:
-
TODO: Spec this and use predicate and options.
find(predicate, options) method steps are:
-
TODO: Spec this and use predicate and options.
some(predicate, options) method steps are:
-
TODO: Spec this and use predicate and options.
reduce(reducer, initialValue, options) method steps are:
-
TODO: Spec this and use reducer, initialValue, and options.
3. EventTarget integration
dictionary {ObservableEventListenerOptions boolean =capture false ;boolean ; };passive partial interface EventTarget {Observable on (DOMString ,type optional ObservableEventListenerOptions = {}); };options
on(type, options) method steps are:
-
If this's relevant global object is a
Windowobject, and its associated Document is not fully active, then return. -
Let event target be this.
-
Let observable be a new
Observable, initialized as follows:- subscribe callback
-
An algorithm that takes a
Subscribersubscriber and runs these steps:-
If event target is null, abort these steps.
Note: This is meant to capture the fact that event target can be garbage collected by the time this algorithm runs upon subscription.
-
Add an event listener with event target and an event listener defined as follows:
- type
-
type
- callback
-
The result of creating a new Web IDL
EventListenerinstance representing a reference to a function of one argument of typeEventevent. This function executes the observable event listener invoke algorithm given subscriber and event. - capture
-
options’s
capture - passive
-
options’s
passive - once
-
false
- signal
-
null
Note: The
AbortSignalfor event listeners added byon()is managed by theObservableitself. Seesubscribe()andSubscribeOptions.
-
-
Return observable.
Subscriber subscriber and
an Event event, and runs these steps:
-
Run subscriber’s
next()method with event.
4. Security & Privacy Considerations
This material is being upstreamed from our explainer into this specification, and in the meantime you can consult the following resources:
5. Acknowledgements
A special thanks to Ben Lesh for much of the design
input for the Observable API, and his many years of work maintaining
userland Observable code that made this contribution to the web platform
possible.