js: make @moq/watch component signals explicit inputs vs outputs#1592
Open
kixelated wants to merge 1 commit into
Open
js: make @moq/watch component signals explicit inputs vs outputs#1592kixelated wants to merge 1 commit into
kixelated wants to merge 1 commit into
Conversation
Adds input/output grouping to every @moq/watch component so callers can tell at a glance which signals they drive and which they read, and so outputs can't be clobbered. See PR description for the full rationale. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A long-standing complaint about
@moq/watch(and@moq/publish) is that you can't tell, from a component's public surface, which signals you're meant to drive (inputs) and which you're meant to read (outputs). Everything was a bareSignal<T>, so nothing stopped a caller from.set()-ing a value the component owns, and wiring one component's output into another's input gave no protection against clobbering it.This reshapes every
@moq/watchcomponent around an explicitinput/outputsplit.@moq/publishis intentionally left for a follow-up.The pattern
Three small helpers added to
@moq/signals:getter(value)— read-only counterpart toSignal.from. Reuses a passedSignal/Getter(so one component's output wires straight into another's input) or wraps a raw value.readonlys(map)— identity-at-runtime cast exposing a record ofSignals as read-onlyGetters.InputProps<I>— derives a component's{ [k]?: value | Signal }constructor props from its input map, so the props contract lives in one place.Each component now looks like:
Getters. Whoever owns the backingSignal(the caller, or another component whose output is wired in) does the writing. This is what stopsrenderer.input.frame.set(...)from clobbering thesource.output.frameit's wired to.Getters over privateSignals the component owns..set()on an input/output, or reassigning one, is a compile error.Owner-at-top
Because inputs are read-only, the mutable source of truth for user controls (
volume,muted,paused,latency, the videotarget, plusname/reload/catalogFormat/catalog) now lives at the top — the<moq-watch>element owns those writableSignals and wires read-only views down intobroadcast/backend. The UI and the element's attribute/property accessors read and write the element'scontrolsgroup.MultiBackendand everything below only ever see read-only inputs.This required relocating a few feedback policies to their owners (these are the behavioral parts — see Testing):
Sourceno longer holdsSync; it producesoutput.jitter, whichMultiBackendwires intoSync'svideo/audioinputs. Sources are constructed beforeSync.syncas a constructor arg instead of reaching throughsource.sync.paused ? !frame : visible) moved fromRenderer(which now just exposesoutput.visible) toMultiBackend, which owns the decoder'senabledinput.output.enabled, proxied into the audio decoder.volume↔mutedcoupling (mute zeroes/stashes volume; volume 0 reports muted) moved from the audioEmitterto the element owner. Note: it now runs in both backends; previously it only ran in the WebCodecs path (the emitter didn't exist under MSE).<video>volumechange→ volume moved from the audio MSE backend (which now only applies volume one-way) to the element.MultiBackendno longer bundles the mute-coupling policy; the owner provides it.Other changes
@moq/hangContainer.Consumer'slatencywidened fromSignal<Time.Milli>toGetter<Time.Milli>(it only ever.peek()s it) so a component's read-onlyoutput.buffercan be passed directly. Backward compatible (aSignalis aGetter).Video.Mse.close()used to close the shared#videoSource(owned byMultiBackend), causing a double-close and a use-after-close if the<canvas>/<video>element was swapped. It no longer does, matchingAudio.Mse.demo/weband@moq/boy(which builds its own pipeline fromWatch.Sync/Decoder/Renderer/Source) — new constructors + read-only inputs (it now owns its owncanvas/targetsignals).Testing
bun run --filter='*' check(tsc + unit tests) passes across all 11 JS packages.biome checkclean.🤖 Generated with Claude Code
(Written by Claude)