🐛 Fix signals.is() subscription race (#217)#218
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThis PR fixes a race condition in the Changesis() helper deterministic synchronization
Sequence DiagramsequenceDiagram
participant Caller
participant Operation as is() Operation
participant Stream as ValueSignal
participant Subscription
Caller->>Operation: call is(stream, predicate)
Operation->>Stream: yield* stream (subscribe)
Stream-->>Subscription: subscription iterator
Operation->>Stream: stream.valueOf() (check current)
alt Current matches predicate
Operation-->>Caller: return immediately
else Current does not match
loop Until match or completion
Operation->>Subscription: subscription.next()
Subscription-->>Operation: next value
Operation->>Operation: check predicate(value)
end
alt Match found
Operation-->>Caller: return
else Stream completed
Operation-->>Caller: return
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The swc-plugin-inline release build failed to link on current Rust: `rust-lld: undefined symbol: __emit_diagnostics` (and the other SWC host-ABI `__*_proxy` imports). Newer wasm-ld no longer leaves these undefined symbols as imports by default, so the link needs `-C link-arg=--allow-undefined`. The cargo config that carries the plugin's rustflags lived at inline/swc/.cargo/config.toml, but `build:bundle` runs cargo from inline/ (`--manifest-path swc/Cargo.toml`) and cargo discovers config relative to the working directory — so those rustflags (including the pre-existing `--cfg=swc_ast_unknown`) were silently ignored in CI. Move the config to inline/.cargo/config.toml so it is actually applied, and add the `--allow-undefined` link arg. Unblocks CI on PR #218.
26fc16b to
e6ce60c
Compare
commit: |
is() checked the current value before subscribing, so a matching update that landed in the gap was sent to zero subscribers and lost, hanging until a second update. Establish the subscription first, then take the valueOf() snapshot, so a change during subscription setup is either reflected in the snapshot or buffered for consumption. is() becomes a stateless function returning an Operation whose [Symbol.iterator] subscribes before checking; the public signature and ValueSignal interface are unchanged and no replay is added. Add behavior and #217 regression coverage (bounded with @effectionx/timebox), align the README and no-sleep policy with the deterministic guarantee, and bump @effectionx/signals to 0.5.4.
stream-helpers and worker depend on @effectionx/signals at runtime via workspace:* (replaced with the exact version on publish), so they need a patch release to ship the is() subscription-race fix to their consumers. - @effectionx/stream-helpers 0.8.2 -> 0.8.3 - @effectionx/worker 0.5.2 -> 0.5.3
e6ce60c to
c03bc7d
Compare
Motivation
is(signal, predicate)from@effectionx/signalscould miss a matching statechange and hang forever. It checked the current value via
valueOf()beforesubscribing:
Between the synchronous check and
each(stream)establishing the subscription,the operation yields. A producer that calls
signal.set()to a matching valuein that gap sends to zero subscribers — the event is lost and
is()waitsforever for a second update. This is a time-of-check/time-of-use race
(issue #217), and it forced producers to
yield* sleep(0)before publishing,contradicting the repo's no-sleep-test-sync policy that recommends
is()as adeterministic state waiter.
Approach
Establish the subscription before the decisive current-state check.
isisnow a stateless function returning an
Operationwhose[Symbol.iterator]subscribes first, then takes the
valueOf()snapshot. With no yield betweensubscribe and snapshot, a matching change during subscription setup is either
reflected in the snapshot or buffered in the subscription and consumed via
subscription.next(). Stream-close-before-match still returns cleanly.signals/helpers.ts— subscribe-firstis(). Publicis()signature and theValueSignalinterface are unchanged; no replay added, so the fix staysindependent of and compatible with the replay-ownership work in ♻️ Move replay semantics from signals to stream-helpers #213.
signals/helpers.test.ts— behavior coverage (already-matching;non-matching-then-matching) plus a is() can hang when publish lands between valueOf() check and each() subscription #217 regression that spawns a producer
mutating the signal with no sleep, bounded by
@effectionx/timeboxpurelyas a diagnostic deadline.
signals/README.mdand.policies/no-sleep-test-sync.md— describeis()asa deterministic state waiter that needs no producer
sleep()/yield.@effectionx/signalsbumped0.5.3→0.5.4;@effectionx/timeboxadded asa test-only dependency.
Verification
pnpm check, biome lint/format,pnpm sync— clean.reverting
helpers.tsfails 3/4 (the race manifests on v4), proving theregression bites.
is()behaviors also pass against Effection 3.0.0 (peer-rangeminimum). Note the race is v4-specific — v3 was already correct — so the fix
corrects v4 without regressing v3, satisfying the declared
^3 || ^4range.Summary by CodeRabbit
ishelper now reliably completes based on the signal’s current value and subsequent matching updates without needing sleep/yield delays.ishelper and examples to reflect the deterministic behavior and remove unnecessary timing notes.