-
Notifications
You must be signed in to change notification settings - Fork 24
Description
https://github.com/WICG/observable
I've had a good discussion with @domfarolino at TPAC about how the two proposals should interact.
This is what a basic observable looks like:
const obs = new Observable(s => {
later(() => s.next("foo"));
});
obs.subscribe({
next(v) { console.log(v) }
});
obs.subscribe({
next(v) { alert(v) }
});- When you create a
new Observablenothing happens. The "constructor callback" is only called when somebody subscribes. - The first
obs.subscribecall executes the "constructor callback" - The second
obs.subscribecall does nothing other than registering itself as a subscriber of the observable. - The
s.next("foo")call will iterate through all the subscribers, and call theirnext(v) {}methods
The proposal also has utilities to manipulate observables, which are implemented on top of the above and thus would automatically inherit AsyncContext semantics from the above base case. For example, Observable.prototype.map is effectively
function map(fn) {
return new Observable((s) => {
this.subscribe({ next: val => s.next(fn(val)) })
});
}There are two questions here for AsyncContext, each with two possible answers:
- In what context does the constructor callback run?
- the one from the
new Observablecall - the one from the first
.subscribe()call (which is what would happen by default if Observables are unaware of AsyncContext)
- the one from the
- In what context do the
next()callbacks run?- The one from the corresponding
.subscribe()call - the one from the
s.next("foo")call (which is what would happen by default if Observables are unaware of AsyncContext)
- The one from the corresponding
It is not unlikely that whatever that later(() => …) logic is it will propagate from the constructor callback to s.next("foo"). This means that 1.b + 2.b is not an option, because we wouldn't want the context from a .subscribe() call to be passed to the other subscriber.
We ended up discarding option 1.b completely because the constructor callback running is not because of any specific .subscribe() call. Even though technically it is the first one that causes that code to run, removing any of the .subscribe() call would just move the responsibility to a different one.
2.a and 2.b both seemed similarly ok. We recommend 2.b because:
- it's conceptually simpler, since it's what happens by default if Observables do not manipulate AsyncContext
- when using helpers like
.map, it matches the JS iterator helpers behavior: for iterators the context propagates from the.next()that pulls values into the callbacks that manipulate them, while for observables it propagates from the.next()that pushes values into the callbacks that manipulate them - when tracing, it makes it possible trace how an individual value propagates through a chain of observables
To recap:
- the
new Observableconstructor captures a snapshot and uses it to run the constructor callback - the
next()callbacks of subscribers run in the context that comes from thes.next()call. Note that this is a synchronous process, so it just happens by default
We also noticed that for all currently proposed built-in internal observables (such as the one used by EventTarget.prototype.when, or by Obserable.prototype.map), the constructor callback does not directly call into user code or into async APIs that would capture the AsyncContext. This means that the context it runs it is not actually observable, so in those cases the constructor does not need to capture a snapshot.