Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add batching and batched effects to solve the interoperability / multi-actor problem with batches #239

Open
justinfagnani opened this issue Sep 6, 2024 · 2 comments

Comments

@justinfagnani
Copy link
Collaborator

One common feature request seems to be not just built-in effects, but some kind of synchronous effect, or at least the ability to build one. The reasons both not including built-in effects and not allowing synchronous access to signals during notification are both well-intentioned and defensible. Userland utilities can generally cover those features and make decisions that might not be appropriate for the core proposal.

And one thing that might not be clear in a lot of the discussion around scheduling synchronous signal access in notifications, is that it is possible to build a userland utility for synchronous effect, as long as there is cooperation between the mutation-side and the effect-side. If mutations are performed in a synchronous batch, then tracked effects can be run before the batch returns.

I added such a batched-effect utility to signal-utils a while.

Normally, I think this would be great to leave to a library, except for the multi-actor problem.

In order for this utility to work, all the signal mutators and effect creators must be using the same library (and the same copy of the library). This is a problem for decoupled use cases where the signal consumers and producers are not in the same library, or maintained by the same team, and do not already use a centralized framework instance.

Example

Usage of the batched-effect utility I wrote looks like this:

const a = new Signal.State(0);
const b = new Signal.State(0);

batchedEffect(() => {
  console.log("a + b =", a.get() + b.get());
});

// Logs: a + b = 0

batch(() => {
  a.set(1);
  b.set(1);
});

// Logs: a + b = 2

The callback passed to batchedEffect() running synchronously when the effect is created, and at the end of any batch() modifies a signal used in the callback.

Mechanism

Batches work by sharing global state: a list of callbacks that were notified. This is the core of the multi-actor problem.

A batchedEffect() creates a watcher that doesn't access any signals, but only adds callbacks into the global list synchronously.

batch() then checks this list after calling its batch callback, then runs any effect callbacks in the global list. There is some handling of exceptions and nested batched. In the case of nested batches, effects are only called at the end of the outermost batch() call.

Possible paths to inclusion

There are a few ways the proposal could address this type of feature:

  1. Not at all, and consider it out of scope. I do think that the motivations for including batching match the motivations for creating a signal standard in the first place, which is largely centered around interoperability.
  2. Include batch() and batchedEffect() very similarly to what's in signal-utils. They could live under .subtle as Signal.subtle.batch() and Signal.subtle.batchedEffect().
  3. Try to include only the global state that causes the interoperability issue. Maybe this looks like a Signal.subtle.notifiedSignals that batching effect utilities should write to and batch utilities should read from. This might be very subtle as there are responsibilities on the batch side, like clearing the list after the signals have been accessed. Maybe it'd be better as Signal.subtle.addNotifiedSignal() and Signal.subtle.runNotifiedSignals().

Related Issues

@transitive-bullshit
Copy link

Just noting that this is related to #73

@tomByrer
Copy link

tomByrer commented Jan 8, 2025

  1. Signal.subtle.notifiedSignals sounds like a 'heartbeat trigger' I used in another no-code platform, where to throttle UI animations, they would all hook up to (AKA 'subscribe') a trigger. The trigger pulse would then be used for other components to update their values, without caring about the value of the source of the trigger-signal, & without needing a timer to keep poling if they need to re-read new values.
    (I hope I understood that correctly.)

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

No branches or pull requests

3 participants