Skip to content

Lazy computed-signals in Haptic Wire#1

Merged
tsugabloom merged 21 commits intohaptic-wfrom
haptic-w-computed-support
Apr 21, 2021
Merged

Lazy computed-signals in Haptic Wire#1
tsugabloom merged 21 commits intohaptic-wfrom
haptic-w-computed-support

Conversation

@tsugabloom
Copy link
Copy Markdown
Owner

@tsugabloom tsugabloom commented Apr 16, 2021

This implements lazy computeds. Similar to the computeds found in Sinuous but these are lazy, explicitly defined, and are true WireSignal functions. They're defined with a WireReactor, but unlike a reactor, they don't evaluate on each signal write.

A computed-signal is a normal signal backed by a reactor (called a computed-reactor). This is a pairing.

It's different than:

// Not a lazy computed-signal!
const data = wS({
  count: 0,
  countPlusOne: null
});
wR($ => {
  console.log("Sending out a new countPlusOne");
  data.countPlusOne(data.count($) + 1);
});

This runs every write, even if no one is reading/observing data.countPlusOne. This is because a wR is an observer, so as far as Haptic can tell, it's worth executing. With computed-signals, the computed-reactor isn't worth executing unless it has observers aka a non-computed reactor.

When a signal inside a computed updates, the signal marks the computed as stale rather than executing it. When it is finally executed, it uses this stale flag to cache the calculation. This leads to the least amount of work performed.

TypeScript #43683 helped me figure out the API design. There are notes directly in the code about the reasoning and why I thought it was worth it to settle on the version that (unfortunately) requires manually specifying the reactor return type - it's the best of many not great options due to limitations in the TS compiler.

const data = wS({
  text: '',
  count: 0,
  // Lazy computed-signals
  countPlusOne: wR(($): number => data.count($) + 1),
  countPlusTwo: wR(($): number => data.countPlusOne($) + 1),
});

If I cut out when() and svg() utilities, and import wR & wS into the main bundle, the size is 1570b min+gz for haptic and 805b min+gz for only haptic/w. This is everything, so hopefully I can keep Haptic around 1.6kb. The down side is this PR adds a lot of complexity. Wire was originally really nicely coupled - two paired functions that share a bit of global state and some helper methods. This shoves a new type of function into the definitions of both signals and reactors: it's not a third type, it's teaching them both to do a new dance. This adds a lot of new code branches and checks. Still, I think it's worth it because I've been chasing lazy computeds for at least a year in Sinuous.

@tsugabloom tsugabloom changed the title Lazy computed-signals support in Haptic Wire Lazy computed-signals in Haptic Wire Apr 16, 2021
Comment thread src/w/index.ts Outdated
Comment thread src/w/index.ts
// Token is set but never deleted since it's a WeakMap
reactorTokenMap.set($, wR);
adopt(wR, () => wR.fn($));
saved = adopt(wR, () => wR.fn($));
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change also has reactors return a value. They're not () => void anymore.

Comment thread src/w/index.ts Outdated
Comment thread src/w/index.ts
wireSignals as wS,
wireReactor,
wireReactor as wR,
reactorRegistry,
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be adding this back into a haptic/w/dev or maybe a haptic/dev bundle

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant