cujojs / most

Ultra-high performance reactive programming
MIT License
3.49k stars 231 forks source link

Synchronous subscription #229

Closed AlexGalays closed 8 years ago

AlexGalays commented 8 years ago

Is there a way to create a stream that starts producing values immediately upon subscription? (no setTimeout(0) in the browser) While I can guess why "everything is async" works well when writing apps, I'm trying to observe the first value of a Stream that HAS a starting value synchronously for performance reasons (multiple nested observing; rendering a tree of components with a level of indirection (I need to observe the parent stream before I can render the child) at every node is very slow and may lead to flashing content)

AlexGalays commented 8 years ago

I wrote that quickly; it appears to work but only you would know if it's an abomination :D

import { withScheduler } from 'most/lib/runSource';
import Scheduler from 'most/lib/scheduler/Scheduler';

function syncInitTimer() {
  let first = true;

  return {
    now: Date.now,
    setTimer: function(f, dt) {
      if (first) {
        f();
        first = false;
        return;
      }
        return setTimeout(f, dt);
    },
    clearTimer: function(t) {
        return clearTimeout(t);
    }
  };
}

export function observeSync(stream, f) {
  return withScheduler(f, stream.source, new Scheduler(syncInitTimer()));
}

EDIT: It throws errors if the first value is also the last one (which happens when using startWith) at this line this.disposable = dispose.once(source.run(this, scheduler)); because it's now trying to dispose synchronously but this.disposable is not assigned yet.

AlexGalays commented 8 years ago

I guess what I need is similar to Properties in bacon or kefir. What's your feeling about these?

briancavalier commented 8 years ago

The async guarantee[1] is indeed important. It helps consumers avoid subtle bugs that can occur due to sometimes-sync-sometimes-async. It also allows most.js to avoid weird internal conditions, like the one you experienced: the architecture allows time for the disposer graph to settle into a consistent state before setting the stream graph into motion.

So, opting out of it is unsupported.

It's typical to solve flashing content problems by hiding the DOM until it's in a known good state. Is that something you can do?

I guess what I need is similar to Properties in bacon or kefir. What's your feeling about these?

There's no plan to support Behaviors/Properties right now. If we did decide to do it, I think it'd probably be in a separate companion package.

  1. There’s an important distinction between synchronous subscription and delivering events before observe/forEach/reduce returns. "Subscriptions" are synchronous--that is, a producer finds out about a new consumer synchronously (this is important to avoid consumers missing events). However, producers will never deliver an event to a consumer before observe/forEach/reduce returns.
briancavalier commented 8 years ago

Hmm, have you tried using most-subject? Its holdSubject can be similar to a Property.

AlexGalays commented 8 years ago

I will check most-subject.

By the way, how can event.stopPropagation() work if the EventSource is always consumed asynchronously ? by the time you can tap() it, it already reached the document?

briancavalier commented 8 years ago

Since dom events are already delivered asynchronously by the browser, most.js's dom event producer doesn't introduce additional asynchrony. It propagates the event synchronously. So, most.js introduces async only where absolutely necessary in order to maintain the "don't deliver an event in the same call stack as observer/forEach/reduce was called" guarantee.

AlexGalays commented 8 years ago

nice!

AlexGalays commented 8 years ago

I really like how most is designed and I generally do everything I can to only depend on well crafted , non bloated libraries. That said, while I can appreciate the it's always async, even if it could have been sync invariant and already work with it everyday with Promises, I now truly think there is one other invariant that supersedes it for UI apps/libs: A special kind of stream that has a current value and can produce it synchronously if available; any transformation of that stream keeps the invariant. That is, Bacon's or Kefir property; and I think flyd while not having explicit properties can be considered to have that same behavior too. knockout had that property too, although it was very bad at declaratively transforming its observables.

Explaining my problem a bit more in details: I'm creating a component lib based on virtual dom. The "model" is a Stream. Each component can have its own local "model". To create the children components of a parent components, I must first wait for the parent's "model" to produce a value (which is async in most), only then can I render the parent and find out about its children. Once I know about its children, I need to get a first value for their models, so I can render them. This again is async. If I have a component tree 10 levels deep, I will have to artificially wait 10 times to render the whole tree (mostly annoying during the initial page load); it also means parents are attached to the document before their children which is not great for performances or doing things like measurements/manual DOM modifications. (using snabbdom anyway, where diffing is not decoupled from patching) Libraries like cycle.js / motorcycle.js / Elm don't hit this problem because they only have one global Stream of models/virtual nodes (composed from other streams, but still), but this is precisely what I want to avoid here :)

I think most is not a very good fit for my particular problem, but I understand the choices it made.