- Shipped
- August 14, 2026 at 11:20 PM UTC
- Author
- kamo
- Commit
- a2db801
Firing a sound read a context, which made every call site's POSITION IN THE TREE part of its correctness. A component rendered above SoundProvider got a no-op and made no noise — and that is the worst possible failure for this feature, because silence is exactly what a correctly-configured "off" produces. The bug is invisible in the same way the missing chime it causes is. So the resolved stack now lives in lib/sound/soundStore, a module singleton, and the provider's job is to keep it current rather than to hand it out. playEventSound is importable from anywhere and behaves identically wherever it is called — above the provider, below it, or from a socket callback registered before it mounted. With no provider at all it plays the catalogue defaults, which is the honest degradation: an unauthenticated page has no member whose preferences apply. That also deletes an idiom. usePlaySound is gone: a hook that reads no context and no state is not a hook, it is a plain function wearing a hook's shape, implying subscription semantics it does not have. And every socket call site was holding it in a ref with a comment explaining that the callback would otherwise pin the first render's function — true when it came from a context, false the moment it does not. Fifteen files lose the ref, the hook call and the now-wrong comment. Also here: - The unmount flush in useSoundLayerDraft depended on `flush`, whose identity changes with the layer, i.e. on every keystroke. Each re-run cleared the pending timer and wrote immediately — turning the debounce into a PUT per change, the exact thing it exists to prevent, and silently: the settings still saved, just forty times instead of once. Held in a ref, depends on [] now. - Both switches get explicit accessible names. Their visible labels sit in sibling columns, so a screen reader announced fifty unlabelled toggles in a row. - Dead code removed: CATEGORY_ICON, categoryCounts, eventsInCategory, isAudioUnlocked, closeAudioContext, and EventSoundEditor's hideMaster prop, which no caller ever passed. 16 new tests cover what the store decides, because every rule it applies produces SILENCE and silence is indistinguishable from "correctly off" unless something asserts on it: throttle windows (including that a muted event does not spend its own budget), the already-looking rule and its force escape hatch, master and per-call volume, unknown keys, and playing with no settings loaded at all. Gates: tsc clean, 1867 tests pass, next build compiles, i18n guard clean.