fix(server): confirm an exact signal count instead of resolving on the first fire - #778
Conversation
…e first fire `COUNT_CONFIRM_MS` keeps an exact-count wait watching after it first reads true, because a count only rises while the window is open and "exactly N" cannot be settled early. `assertsExactCount` decides which predicates get that hold, and its leaf test read `net` alone. `signal` carries `count` too, so a signal count resolved on its first match and `count: 1` silently meant "at least 1" — the assertion `count` exists to avoid. A second fire 59ms later, the gap measured on the net side, arrived after the wait had stopped looking. The channel this was missing on is the one that most needs it. The `signal.count` schema calls the double-fire "the defect no state-only oracle can see": a handler wired twice fires the signal twice, the store ends up in the right shape either way, and a presence check is green on both. A React double-effect fires within one commit, well inside the 300ms hold that `net` already had. `evalSignal` counts correctly, as `evalNet` did — the wait was early, not the count. The combinator recursion above already covered allOf/anyOf/not, so a signal count nested in an allOf was wrong the same way and is fixed by the same line. Presence-only signal predicates are untouched: no `count` still means "at least one", which IS satisfiable early, and holding those would make every ordinary signal wait pay the confirmation delay for nothing. Tests mirror the net block case for case, so the two channels are held to one rule, and assert bounds rather than durations. Signed-off-by: Chirag <chiraghonnyal6722@gmail.com>
…t the clock Signed-off-by: Divyanshu Shekhar <imdshekhar@gmail.com>
|
Taking this over #777, which makes the same one-line change — this one adds the Verified adversarially before merging: reverting the One maintainer edit pushed on top: the |
Closes #775.
One line, and the reason it is the right one
assertsExactCountdecides which predicates get theCOUNT_CONFIRM_MShold. Its combinator recursion is already complete; the leaf test was not:signalcarriescounttoo, so a signal count resolved on its first match:count: 1silently meant "at least 1", which is the assertioncountexists to avoid.The channel it was missing on is the one that most needs it. From the
signal.countschema:A double-wired handler is exactly a React double-effect, which fires within one commit — well inside the 300 ms hold
netalready had. AndevalSignalcounts correctly, asevalNetdid; the wait was early, not the count. The existing"signal count: exactly-once passes on one fire, and a double-fire is caught"test passes today because it evaluates immediately, which is why the gap survived: the counting was never the broken half.Because the recursion above was already right, a signal count nested in an
allOfwas wrong the same way, and the same line fixes it.What is deliberately untouched
Presence-only signal predicates. No
countstill means "at least one", which is satisfiable early, and holding those would make every ordinary signal wait pay the confirmation delay for nothing — the same reasoning the net block states. A test pins it.count: 0. I raised it in #775 and did not take it here. It now gets the hold as a side effect ofcount !== undefined, which I think is right — an absence claim is the one a late arrival can falsify — but it also means every zero-count signal assertion pays 300 ms, and that is a latency call rather than a correctness one. Say the word if you would rather it were excluded and I will special-case it.Tests
Four cases mirroring the net block case for case, so the two channels are visibly held to one rule: the double-fire at the measured 59 ms gap fails and reports what it saw, a genuine single fire still passes without burning the timeout, presence-only still resolves on the first match, and a count nested in
allOfstill holds.Written RED first — before the fix, exactly two of the four failed with
expected true to be false(the direct case and theallOfcase), while the two guard cases passed, which is the split that shows the fix does not over-reach.No duration is asserted as a correctness signal, per the repo's rule on timing assertions: the pass-case bound (
< 3000on a 10 s budget) is the same bound the net block uses to show an honest "exactly one" does not burn the timeout.Gates
format:check,lint17/17,typecheck22/22, and the server suite at 6426 passing (predicate.test.ts87/87).init/format-generated.test.tsfails identically on a cleanmainon this Windows box and is unrelated.This touches the wait path behind
act_and_waitandassert, so the e2e battery is the one that matters here; I started a local run and it was still booting when I opened this, so I am leaving that to CI rather than reporting a result I do not have.