This is an explanation of the extremely subtle problem in this most.js issue. This solution described isn't necessarily the best, or most rigorous solution. We're investigating other potential solutions, but wanted to record this information in case it's interesting to someone.
One of the fastest ways to schedule a task in ES6 is to use a promise. They tend to use the fastest micro-tick scheduling option the platform provides. So, when a task is scheduled with scheduler.asap it doesn’t use setTimeout 0, it uses a promise to schedule itself, because that’s basically as close to zero-time as you can get in a platform independent way
given this:
const A = just(x).sample(f, fromPromise(Promise.resolve(y)), just(z))
const B = just(x).sample(f, just(z), fromPromise(Promise.resolve(y)))sample() always begins observing the input streams before it observes the sampler (i.e. just(x) in this case)
in A, sample first observes fromPromise(Promise.resolve(y)), then just(z), then just(x). Since Promise.resolve(y) is an already-fulfilled promise, observing it means getting immediately placed onto the platform promise scheduling queue. That promise observation is now the first thing in the platform promise queue. Then, sample observes just(z), which is a zero-time event, i.e. scheduler.asap, which places the scheduler onto the platform promise queue (now behind the observation of the Promise.resolve(y)).
Finally, sample will observe just(z), which will use scheduler.asap. Since the scheduler knows that it has already placed itself (technically, it placed it’s “run all tasks registered for time zero” function) onto the platform promise queue, it knows that it doesn’t have to do that again. So, it just records this new just(z) observation task in its own internal data structures … iow, it puts it in an array.
At some point the call stack clears and control returns to the vm, which begins processing the promise queue.
First, the promise observation happens since it was at the head of the platform promise queue. At this point fromPromise(Promise.resolve(y)) emits an event, and from the perspective of sample(), now has a sample-able value.
Then the next task on the platform promise queue, the scheduler, runs. It process all zero time events, meaning both just(z) and just(x), in that order. just(z) emits an event, and how has a value. Next, just(x), the sampler, emits an event. Since both streams being sampled have emitted at least one event, i.e. they both have a value, this sampler event can indeed sample those two values, apply the sampling function, and finally, emit an event.
Ok, now let’s take B.
First, sample will observe just(z), then fromPromise(Promise.resolve(y)), then just(x). When it observes just(z), that leads to scheduler.asap, which places the scheduler at the head of the platform promise queue.
Then, when fromPromise observes Promise.resolve(y), since the promise is already fulfilled, that observation gets put into the platform promise queue behind the scheduler. Then, when just(x) is observed, since the scheduler knows it is already on the platform promise queue, it simply puts the just(x) observation into its internal array.
Remember: At this point, the scheduler is at the front of the promise queue
The scheduler has two tasks in the zero timeslot of its internal data structure: it has both just(z) and just(x).
At some point the call stack clears and control returns to the vm, and it begins processing the promise queue.
The scheduler is at the head of the queue, so it runs both zero-time tasks. First, it processes just(z), which emits an event, and so it now has a value. then it process just(x), which is the sampler.
At this point, only one of the two streams-to-be-sampled has a value.
Thus, sample cannot emit a value!
In effect, the observation of the sampler, just(x) “jumped over” the Promise.resolve(y)
Why does this change to fromPromise have the effect of making A behave like B? First, more background.
await() maintains strict ordering of events. If you have a stream like [promise1, promise2], and promise2 fulfills before promise1, the resulting event order will still be [valueOfPromise1, valueOfPromise2]. It does this by using the promises themselves as a queue
if you follow that code into self._event, you’ll see that it effectively results in this kind of construct:
queue.then(() => promise.then(emitPromiseValue))IOW, a then which leads sequentially to another then. So, in A, the Promise.resolve(y) leads first to a then on await()’s queue. That will be placed at the head of the platform promise queue, and the scheduler will be behind it.
The vm will start to process the promise queue.
The first thing is await()’s queue.then task, which leads to a then on the actual Promise.resolve(y) promise. That second then is enqueued into the platform promise queue, behind the scheduler!
Now the platform promise queue looks like: scheduler, Promise.resolve(y)
That is exactly what the platform promise queue looks like in B before the change. So, things proceed from that point just as B did: just(z) and just(x) both get processed before Promise.resolve(y)’s then,
and sample can’t emit an event because fromPromise(Promise.resolve(y)) hasn’t emitted an event yet.
Hence, after the change, both A and B behave the same.
Thx @ briancavalier ! 😄