🐛 Fix signals.is() subscription race (#217)#218
Conversation
|
Warning Review limit reached
More reviews will be available in 34 minutes and 57 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ 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) | ⏱️ ~20 minutes Possibly related issues
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 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.
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
26fc16b to
e6ce60c
Compare
commit: |
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
New Features
Documentation
Tests
Chores