nettybun / haptic

Explicit reactive web rendering in TSX with no compiler, no virtual DOM, and no magic. 1.6kb min+gz.
MIT License
79 stars 2 forks source link

Addressing FSM-ambiguity in wires #14

Closed nettybun closed 3 years ago

nettybun commented 3 years ago

Issues with wire state

Wires are driven as a Finite State Machine (FSM). This is a one-dimensional value; internally a integer from 0 to 4.

type Wire<T> = {
  // ...
  /** FSM state: RESET|RUNNING|IDLE|PAUSED|STALE */
  state: WireFSM;
  // ...
};

type WireFSM =
  | FSM_RESET
  | FSM_RUNNING
  | FSM_WIRED_IDLE
  | FSM_WIRED_PAUSED
  | FSM_WIRED_STALE;

Where state is used today

The 0-4 states are used here:

This already has problems, seen right in the FSM names themselves: I'm overloading terms to imply, for instance, that a wire can never be idle and stale, and that a stale wire is a paused wire. This gets really messy with the introduction of computed-signals that also have a stale state (which is stale but never paused; oh no) and needs to be skipped somehow possibly via another state.

Issues with this state model

Note that I'm not interested in trying to prevent people from explicitly doing bad things like wire.state = xyz. I just don't want to introduce unexpected or accidental state transitions.

Here are some of those pitfalls:

Issues with chaining

The issue above about chaining is very concerning. It also came up when I tried fixing computed-signal's to have their saved value updated when a wire is manually run - a small optimization but worth considering.

Why not just wrap the wire function like DOM patching does? The value will be updated via closure...

It'd be impossible to undo. Computed-signals need to be taken down, and there's no way to unwrap a function. It's also not safe to restore to the previously seen function since the wire could have been wrapped again during the computed-signal's lifetime.

Fixes

This is how I'm trying to address all these issues...

Both pause and resume should be dedicated methods

Wires should always run when manually called since it's their expected behaviour. Don't have a magic "unpausing" call. This means a new wireResume next to wirePause.

It'll drop bundle size.

Replace chaining with a Set of post-run calls

It's not clear, explicit, and debuggable to have a dedicated wire.hooks Set that tracks all the post-run actions. During takedown of a computed-signals I can use wire.hooks.delete(...) via function reference.

Might add bundle size.

Split state into a bitmask

I've seen projects like Kairo do that. It could be smaller than adding more individual properties like wire.paused.

It'll add to bundle size.

Here's how I broke it down:

[IS_CW][IS_WIRED][IS_RUNNING][IS_PAUSED][IS_STALE]

I can drop IS_CW and use wire.cs instead. This is a two-way link between a signal and wire. It's there for principles of consistency and debugging, so I'm keeping it. I can drop IS_WIRED and use wire.sigRS as well.

[IS_RUNNING][IS_PAUSED][IS_STALE]

I'm still overloading the term "stale" which mostly means "skip" in _runWires context and "run later" in other contexts.

[IS_RUNNING][SKIP_ME][WAS_SKIPPED]

Lastly I need to convey that a skipped wire is overdue for a run. I'd also like wireReset to be able to indicate that it's uninitialized; wire.run doesn't work since wires can always be manually reset even after they're run.

[RUNNING][SKIP_RUN_QUEUE][NEEDS_RUN]

Here's how this works for the above cases:

Pause/Resume:

Computed-signals:

Reset via wireReset:

Scenarios that are fixed using this model:

Other considerations:

mindplay-dk commented 3 years ago

Might add bundle size. ... It'll add to bundle size.

Just my personal advice: forget about bundle size.

I've fallen into this trap one too many times in the past - obsessing about size/perf before actually reaching a complete, functional design with passing tests. These things are super important, but they really are a distraction at this stage - the problem matter itself is difficult enough without burdening yourself with optimizations for size and speed - concerns that tend to make the code harder to read and change, and often more sensitive to subtle bugs.

  1. Make it work.
  2. Make it fast.
  3. Make it small.

Be kind to yourself. 😄