Files
skills/docs/ui/react/react-control-surface/src/runtime/binding-registry/binding-registry.md
2026-05-29 08:20:56 +01:00

3.1 KiB

BindingRegistry

(no submodules)

Purpose

BindingRegistry is the discriminated dispatcher that links every bound control on a surface to the consumer-side logic that actually performs the side-effect. Atomic controls call registry.dispatch(binding, value) whenever a user-initiated onChange fires; the registry resolves the binding's kind and invokes the registered handler.

The constructor accepts a partial handler table. Any kind left unset falls back to the built-in default handler, which is the appropriate consumer hook adapter (for pluginParam, transport, midiOut, midiIn) or a working fan-out (for compound). Every leaf default throws REACT_CONTROL_SURFACE.BINDING_HANDLER_MISSING until a consumer registers production wiring — silently dropping a control update is never acceptable.

Features

  • Closed kind dispatch for all six Binding["kind"] values.
  • Default compound fan-out via defaultCompoundHandler.
  • Per-kind override via the constructor or withHandler(kind, handler) builder.
  • Type-narrowed handler signatures so consumers can't accidentally cross binding kinds.

Usage: Basic

import { BindingRegistry } from "@suaveplan/react-control-surface/runtime/binding-registry";

const registry = new BindingRegistry({
    pluginParam: (binding, value) =>
        chain.get(binding.pluginId)?.setParameter(binding.paramPath, value),
});

registry.dispatch(binding, knob.value);

Usage: Advanced

import { BindingRegistry } from "@suaveplan/react-control-surface/runtime/binding-registry";

const base = new BindingRegistry({
    midiOut: (binding, value) =>
        output.sendMessage({
            kind: "controlChange",
            channel: binding.channel,
            controller: binding.cc,
            value: Math.round(value * 127),
        }),
});

// Layer a recording spy on top of the production handler for an audit run.
const spy = new BindingRegistry();
const audited = base.withHandler("pluginParam", (binding, value) => {
    audit.push({ binding, value });
    spy.dispatch(binding, value);
});

API Reference

  • new BindingRegistry(table?: BindingHandlerTable) — constructor.
  • registry.dispatch(binding: Binding, value: number): void — full dispatch, including compound fan-out.
  • registry.dispatchLeaf(binding: LeafBinding, value: number): void — leaf-only dispatch; called by the default compound handler to avoid re-entering the compound check.
  • registry.has(kind: BindingKind): boolean — true when a handler is registered.
  • registry.withHandler(kind, handler) — returns a new registry with the given handler overridden.

Implementation Notes

The dispatcher is built around an exhaustive switch over the leaf discriminator so adding a new binding kind is a compile-time fail — the switch needs a new branch and BindingHandlerTable needs a new optional slot. The compound default takes the owning registry as a parameter so it can route per-leaf calls through dispatchLeaf, sidestepping a recursive compound check that would otherwise be required by every test path.