# `preset` ## Purpose The `preset` module owns the **plugin preset envelope** — `PresetSnapshot` — and the three boundary guards every plugin runs before applying a preset: schema validation (`parsePresetSnapshot`), version-compatibility (`assertPresetVersionSupported`), and plugin-type matching (`assertPresetMatchesPluginType`). Presets are the universal handshake between a plugin's serialised state and a fresh instance. Both per-plugin `getPreset()`/`applyPreset()` and chain-level `snapshot()`/`restore()` use this envelope, so a snapshot persisted from a track preset and a snapshot exported from a single plugin look the same on disk. ## Features - One Zod-validated envelope shape (`schemaVersion`, `pluginType`, `parameters`, optional `data`) that every plugin shares. - `parsePresetSnapshot(input)` validates at the boundary and throws `AUDIO_FX.INVALID_PRESET` with the Zod issue list in `context` on failure. - `assertPresetVersionSupported(preset, supportedSchemaVersion)` throws `AUDIO_FX.PRESET_VERSION_UNSUPPORTED` when the preset is from a newer plugin build. - `assertPresetMatchesPluginType(preset, pluginType)` throws `AUDIO_FX.INVALID_PRESET` (with a `presetPluginType / expected` context) when a chain snapshot tries to load a preset against the wrong plugin. - The schema (`PresetSnapshotSchema`) is exported for callers that want to call `safeParse` themselves. ## Usage: Basic Example ```ts import { parsePresetSnapshot, assertPresetVersionSupported } from "@suaveplan/audio-fx-plugin-base/base"; function applyPresetGuarded(plugin: AudioFxPlugin, raw: unknown, mySchemaVersion = 1) { const preset = parsePresetSnapshot(raw); assertPresetVersionSupported(preset, mySchemaVersion); plugin.applyPreset(preset); } ``` ## Usage: Advanced Example ```ts import { assertPresetMatchesPluginType, assertPresetVersionSupported, parsePresetSnapshot, } from "@suaveplan/audio-fx-plugin-base/base"; class EQPlugin implements AudioFxPlugin { readonly metadata = { id: "eq", name: "8-Band EQ", version: "1.0.0", category: "eq" } as const; readonly schema = EqDataSchema; private readonly supportedSchemaVersion = 2; applyPreset(snapshot: PresetSnapshot): void { // The envelope was already validated by parsePresetSnapshot in the // caller; we re-assert the plugin-type and version locally so direct // calls into the plugin are equally safe. assertPresetMatchesPluginType(snapshot, this.metadata.id); assertPresetVersionSupported(snapshot, this.supportedSchemaVersion); // Validate the plugin-specific data blob against our own schema. const data = EqDataSchema.parse(snapshot.data); for (const [paramId, value] of Object.entries(snapshot.parameters)) { this.setParameter(paramId, value); } this.bands = data.bands; } getPreset(): PresetSnapshot { return { schemaVersion: this.supportedSchemaVersion, pluginType: this.metadata.id, parameters: this.snapshotParameters(), data: { bands: this.bands }, }; } } ``` ## API Reference - `PresetSnapshot` — readonly `{ schemaVersion: number; pluginType: string; parameters: Record; data?: unknown }`. - `PresetSnapshotSchema` — Zod schema for the envelope; exported for callers that want to `safeParse` directly without throwing. - `parsePresetSnapshot(input: unknown): PresetSnapshot` — throws `AUDIO_FX.INVALID_PRESET` on validation failure with the issue list in `context.issues`. - `assertPresetVersionSupported(preset: PresetSnapshot, supportedSchemaVersion: number): void` — throws `AUDIO_FX.PRESET_VERSION_UNSUPPORTED` when `preset.schemaVersion > supportedSchemaVersion`. - `assertPresetMatchesPluginType(preset: PresetSnapshot, pluginType: string): void` — throws `AUDIO_FX.INVALID_PRESET` when `preset.pluginType !== pluginType`. ## Implementation Notes The envelope keeps `parameters` separate from `data` because parameters are universally typed (always `number`) and can be applied generically by the `AbstractAudioFxPlugin` base class without any plugin-specific knowledge. The `data` blob is opaque from the envelope's perspective; each plugin validates it against its own Zod schema (exposed on the plugin's `schema` member). This split lets the chain's snapshot/restore round-trip every plugin's parameters without depending on per-plugin schemas, while still preserving plugin-specific state. `schemaVersion` is a per-plugin integer: a single project may contain presets at different `schemaVersion`s across different plugins. The version is **forwards-only**: a plugin at v2 can read a v1 preset (with a migration path inside `applyPreset`) but a plugin at v1 refuses a v2 preset. This is deliberate — back-versioning is the responsibility of the plugin author, who can write a `migrateV1ToV2(data)` helper inside the plugin and apply it on read. The two assert helpers throw rather than return booleans so plugin authors don't need a separate try/throw pattern. The `context` payload on each error includes both the offending value and the expected value, so the UI's preset-load toast can render a useful message ("Preset is from v3 of EQ; this build supports up to v2"). `assertPresetMatchesPluginType` exists for chain restore: the chain's snapshot lists plugin types it expects to instantiate, but a registry swap (e.g. a fork of the EQ plugin under a different id) could load the wrong factory; the assert catches that at the apply boundary before any state mutation.