proposal-signals
proposal-signals copied to clipboard
Way to temporarily exclude a watcher
So, I ran into a very niche need: I need to read a watched signal, but I need to read it repeatedly while it's dirty (related: #129) and, once it's clean, I need to then re-watch it.
For correctness reasons, I need it to not call Signal.subtle.{un,}watched hooks during this, and as it's in a very hot path, I need this very optimized.
This could be solved enough for my needs very simply: direct watcher.block() and watcher.unblock() methods. (The latter's proposed in #178.) It's just one field set, and so it's O(1) instead of O(tree depth). I do also need watcher.block() to return a boolean, so I can avoid calling watcher.unblock() prematurely (in case of recursive call).
Use case dried up for me, but it'd still be nice to have regardless, since it's such a simple method to implement.
I'm trying to understand this use case but I don't think I quite have my head around what you're (hypothetically/previously) trying to achieve. Can I try to rephrase the pieces and see if I'm making any progress getting towards the page you're on?
-
Reading a dirty signal. Presumably a Computed (since States can't be dirty, and Watchers can't be read). We deliberately didn't expose a way to read dirty or "stale" values from a Computed without rerunning it. But, AIUI, we do want to be in a world where cleaning a Computed has no side effects; this is the underlying motivation behind #77.
- Did you find a use case for reading a stale value out of a Computed?
- Did you find a situation where a Computed gets run, and you get a new value, but it "isn't clean enough" and still needs to be polled until it's clean-for-real?
- (Something else? Neither of these sound quite like what you described.)
-
Blocking a watcher from getting further notifications, without detaching it. As far as I can tell this is already the behavior of watchers -- they get one notification until they're "reset" with
watch(), even if multiple signals they're watching are dirtied. (@littledan this does sound like a case for not making any other Watcher method implicitly re-arm the watcher.)- Are you looking to rearm the watcher but block a particular (set of) edges, so it doesn't
unwatchthose signals, doesn't fire its notify callback if they change, but can still be notified again by other signals it's watching?
- Are you looking to rearm the watcher but block a particular (set of) edges, so it doesn't
(And, maybe more philosophically, but I think it's germane here:)
To me the goal (which I think we haven't yet quite achieved) of the whole Watcher idea is to provide basically a "disembodied Computed": it has no value, it has no function to run, the system doesn't memoize anything for it, it has no readers downstream of it... but upstream it has the same affordances as a Computed does internally, and it tries to expose them to userspace. So it has a notion of "is it dirty" (has its notification fired), of "becoming clean again" (if you re-arm it with watch), of checking which of its dependencies might have changed (getPending / notifiedBy) and perhaps which have changed for real (this one is missing in the current API).
I think we sort have have two direction to go from there:
- Dissolve Watchers more fully, and decompose them into even simpler parts -- perhaps go for per-signal notifications, and no concept of an "aggregate subscriber" in the sense of a Watcher.
- Make a more coherent execution of the "disembodied Computed" analogy, that fills in the rest of the blanks. This probably means at the very least being able to:
- ask "did X signal last change before or after the last time the Watcher was rearmed". (if this doesn't rerun computeds, it's a yes/no/maybe)
- rerun a computed and ask if it changed or not as a result of that execution (a definitive yes/no)
I don't think we have the canonical use cases or correctness criteria nailed down for Watchers yet, but -- if there's going to be such a thing -- this sort of "interface node" exposing all the relevant graph concepts we're already using to implement Computeds seems like one good start for discovering them.
@shaylew My original use case was this: reattempt to render a subtree while it's dirty, and retry synchronously, rather than having it re-schedule against the watcher.
This can be accomplished using something like the following pattern:
const wasBlocked = watcher.isBlocked
// Keeps the watcher callback from being invoked
watcher.isBlocked = true
try {
while (signal.isDirty) {
renderSubtree(signal.get())
}
} finally {
watcher.isBlocked = wasBlocked
}
I found an alternate design not reliant on this, so my use case no longer exists. But the operation is trivial, so I'm still behind it in principle.
Would you want the watcher callback to be invoked immediately when unblocked, or to just not get notifications from any dependencies dirtied while it was blocked until/unless those dependencies are cleaned and then dirtied a second time?
Watchers already have a dirty bit that tracks whether their notify fired since they were last armed, and they already don't fire again unless they were explicitly re-armed. So it really seems like someone should be able to build isBlocked on top of that, by no-oping notifications while blocked and then (if the watcher was armed when it became blocked) re-arming with .watch() when unblocked.
Would you want the watcher callback to be invoked immediately when unblocked, or to just not get notifications from any dependencies dirtied while it was blocked until/unless those dependencies are cleaned and then dirtied a second time?
@shaylew The idea is that the watcher would not get called if dependencies change during that span. And after being unblocked, it would continue to not be called up until a watched dependency is updated, in which it'd be called as normal.
Watchers already have a dirty bit that tracks whether their
notifyfired since they were last armed, and they already don't fire again unless they were explicitly re-armed. So it really seems like someone should be able to buildisBlockedon top of that, by no-oping notifications while blocked and then (if the watcher was armed when it became blocked) re-arming with.watch()when unblocked.
Reusing/exposing the dirty bit for that is the idea, yes. It'd also provide for a clearer story on how to re-arm the watcher after its notify scheduler executes.