Skip to content

Controller model

Everything in msw-panel orbits the controller interface exposed by msw-panel.

interface MswPanelController {
getSnapshot(): MswPanelSnapshot;
setAllEnabled(nextEnabled: boolean): void;
setEnabled(id: string, nextEnabled: boolean): void;
subscribe(listener: () => void): () => void;
sync(): void;
toggle(id: string): void;
}

That small surface area is intentional. The React panel uses it directly, and the bridge client emulates the same shape over a transport boundary.

The controller exposes a derived snapshot rather than leaking internal state. A snapshot includes:

  • the active handler count
  • the disabled handler count
  • one entry per handler with id, enabled, kind, label, method, path, and used, plus optional tags, scenarios, and activeScenario
  • the registered global presets and the currently matching activePreset

Because the snapshot is plain data, UI adapters and transports do not need to know anything about MSW internals.

Interactive example

Controller snapshot playground

Toggle handlers, add a runtime handler, and then call sync() to see how the snapshot catches up.

Snapshot active 0
Snapshot disabled 0
Runtime handlers 0

The snapshot starts aligned with the runtime.

    The host application still owns the runtime and the handler list:

    const controller = createMswPanelController({ handlers, runtime: worker });

    That means msw-panel does not replace your mocking setup. It consumes:

    • the runtime object that can list and reset handlers
    • the handlers your app already defined

    The controller starts from the supplied handler set and can rebuild from the runtime later with sync(). Enabled state and scenario selections are preserved where handler identities still match.

    That design is simple and predictable, but it also explains the current limitation: runtime-added handlers do not appear automatically unless the host calls controller.sync().

    defineScenarios registers a single MSW handler whose resolver dispatches to the currently-active scenario, held in closure-mutable state on the group. setScenario() flips that state and emits a new snapshot. It does not call resetHandlers, so the handler keeps matching and the swap takes effect on the next request. A definePreset simply records a set of scenario selections; applyPreset() applies them all and the snapshot reports which preset (if any) the current selections match. Because all of this lives next to the handlers, scenario and preset commands flow over the bridge to the controller the same way enable/disable does.