Skip to content

Instantly share code, notes, and snippets.

@briancavalier
Last active November 19, 2019 16:47
Show Gist options
  • Select an option

  • Save briancavalier/a27ddc247dd118ae5508 to your computer and use it in GitHub Desktop.

Select an option

Save briancavalier/a27ddc247dd118ae5508 to your computer and use it in GitHub Desktop.
Description of most.js sample+promises issue

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.

Background

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)

Case A

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.

Case B

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)

The change

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.

@davidroman0O
Copy link

Thx @ briancavalier ! 😄

@briancavalier
Copy link
Author

My pleasure! I hope it was interesting and/or helpful 😄. It definitely was fun to come back and read it after almost 3.5 years.

@davidroman0O
Copy link

I'm currently trying and learning MostJs and it's fucking fast! wtf

Thx to you and Tylor for making this awesome piece of software!

You should expose the wiki documentation on a website to help more devs to get into it.

I've being struggling finding examples with code to understand, I found this markdown completely randomly (it's not linked on the main readme.md)!

Today was my learning day and I'm really happy to be able to understand and use stream now 😄 Thx again!

@briancavalier
Copy link
Author

Cool. Since you’re starting out with mostjs, definitely check out most/core if you haven’t already: https://github.com/mostjs/core

It’s the newer implementation of the mostjs architecture. It also has much better docs: https://mostcore.readthedocs.io/en/latest/

Enjoy!

@davidroman0O
Copy link

I've already saw it 😄

Thx a lot!

I found someone talking about most with some benchmarks!

I'm already sold on mostjs but it's was interesting 😃

@briancavalier
Copy link
Author

Thanks for the link. I hadn't seen that article yet (I shared it with the rest of the core team as well). Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment