sinuous icon indicating copy to clipboard operation
sinuous copied to clipboard

Doing multiple changes inside `transaction()` results multiple invocations of observer subscribed via `subscribe()`.

Open j-sen opened this issue 4 years ago • 3 comments

Doing multiple changes inside transaction() results multiple invocations of observer subscribed via subscribe().

How to reproduce:

import 'https://unpkg.com/[email protected]/dist/observable.js'
const {observable, subscribe, transaction, root}= self.observable
root(() => {
  const a = observable(1)
  const b = observable(2)
  const log = []
  subscribe(() => {
    log.push(a() + b())
  })
  transaction(() => {
    a(10)
    b(20)
  })
  // Expected  3;30
  // Got       3;12;30
  console.assert(log.join(';') === '3;30')
})

Link to https://CodePen.io/j-sen/pen/OJjNKVK

j-sen avatar Oct 19 '21 07:10 j-sen

Unfortunately this is the programmed behaviour... I know it's not intuitive though. It comes down to this phrasing:

https://github.com/luwes/sinuous/blob/master/packages/sinuous/observable/src/observable.js#L51-L71

/**
 * Creates a transaction in which an observable can be set multiple times
 * but only trigger a computation once.
 * @param  {Function} fn
 * @return {*}
 */
...
  q.forEach(data => {
    if (data._pending !== EMPTY_ARR) {
      const pending = data._pending;
      data._pending = EMPTY_ARR;
      data(pending); // This calls all computeds linked to the observable
    }
  });

Each observable can only trigger a computation once, so a(1); a(2); a(3) will be ONE call to the subscribe. However, a(1); a(2); a(3); b(1); b(2) will be TWO calls because it's TWO observables and they each get one trigger.

I didn't like this behaviour so in my own reactive library (which is on indefinite hiatus/cancelled 😞) I implemented transaction() to call each observable/signal and gather all of the computeds/wires that would run into a Set() and then call them after only once.

https://github.com/heyheyhello/haptic/blob/main/src/state/index.ts#L288-L317

/**
 * Batch signal writes so only the last write per signal is applied. Values are
 * committed at the end of the function call. */
...
    signals.forEach((signal) => {
      // Doesn't run any subscribed wires since `transactionCommit` is set
      signal(signal.next); // Does not call computeds.
      delete signal.next;
      signal.wires.forEach((wire) => transactionWires.add(wire));
    });
    transactionCommit = false;
    _runWires(transactionWires) // Call computeds, once each, in the correct order.

tsugabloom avatar Oct 28 '21 18:10 tsugabloom

You could make a PR for @luwes and he might accept it. Just collect computeds instead of running them and then batch call them afterwards. This would be a behavioural change tho...might break some apps.

tsugabloom avatar Oct 28 '21 18:10 tsugabloom

@heyheyhello Thanks for your reply. It seems like a bad idea to make a PR if it's designed to work in this way. And I solved my problem by adding a flag. By the way, Haptic is cool.

j-sen avatar Oct 29 '21 10:10 j-sen