# `plugins/plugins` — plugin / extension interface _(no submodules)_ ## Purpose `plugins.ts` exposes a small, opinionated extension surface that lets downstream packages contribute behaviour to `Packer` / `Unpacker` without forking. Plugins are arbitrary objects implementing one or more of six lifecycle hooks; the `PluginPipeline` invokes them in declaration order around the packer's pipeline yield points and wraps any thrown error as `PackerError(PACKER.PLUGIN_FAILED)` with the original error preserved on `cause`. The interface is intentionally narrow — plugins observe and contribute sidecars, they do not replace the gzip / AEAD / sign / integrity pipeline itself. ## Features - Six lifecycle hooks (`onPackStart`, `onEntry`, `onPackEnd`, `onUnpackStart`, `onEntryDecoded`, `onUnpackEnd`). - Every hook is optional — a plugin only declares the hooks it cares about. - Declaration order is the invocation order; deterministic across runs. - Async hooks are awaited; sync hooks return immediately. - Exceptions from any hook surface as `PackerError(PACKER.PLUGIN_FAILED)` with the original error on `cause` and a catalog-resolved message keyed by hook name. - `PluginPipeline.empty` lets hot paths short-circuit when no plugins are registered — zero overhead for callers that don't use plugins. - Plugin-supplied integrity sidecars (e.g. SHA-256 alongside the built-in BLAKE3) can ride on `onEntry`/`onEntryDecoded` to round-trip custom digests through the bundle's `metadata.custom`. ## Usage: Basic ```ts import { Packer, type PackerPlugin } from "@suaveplan/packer"; const logger: PackerPlugin = { name: "noop-logger", onPackStart(ctx) { console.log("pack started", ctx.originalSize); }, onPackEnd(ctx) { console.log("pack done", ctx.result.data.byteLength); }, }; const packer = new Packer({ plugins: [logger] }); await packer.pack(payload); ``` ## Usage: Advanced ```ts import { sha256 } from "@noble/hashes/sha256"; import { Packer, type PackerPlugin, Unpacker } from "@suaveplan/packer"; // A "double integrity" sidecar that contributes SHA-256 alongside the // built-in BLAKE3, then asserts equality on the unpack path. const observed = new Map(); const doubleIntegrity: PackerPlugin = { name: "double-integrity", onEntry(ctx) { observed.set(ctx.entryIndex, new Uint8Array(sha256(ctx.payload))); }, onEntryDecoded(ctx) { // Decoded entry plaintext does NOT match the post-pipeline payload // SHA — sidecars typically stash the digest into custom metadata // and compare round-trips through there. This example just shows // the hook fires symmetrically. if (!observed.has(ctx.entryIndex)) { throw new Error("sidecar missing for entry " + ctx.entryIndex); } }, }; const packer = new Packer({ plugins: [doubleIntegrity] }); const packed = await packer.pack(payload); const unpacker = new Unpacker({ plugins: [doubleIntegrity] }); await unpacker.unpack(packed); ``` ## API Reference - `interface PackerPlugin` - `readonly name: string` — required identifier. - `onPackStart?(ctx: PluginPackStartContext)` — pre-pipeline hook. - `onEntry?(ctx: PluginEntryContext)` — per-entry hook. - `onPackEnd?(ctx: PluginPackEndContext)` — post-result hook. - `onUnpackStart?(ctx: PluginUnpackStartContext)` — pre-unpack hook. - `onEntryDecoded?(ctx: PluginEntryDecodedContext)` — per-decoded-entry hook. - `onUnpackEnd?(ctx: PluginUnpackEndContext)` — post-decode hook. - `class PluginPipeline` - `constructor(plugins, messageResolver)` - `get empty(): boolean` - `async runHook(name: K, ctx: HookContextMap[K]): Promise` ## Implementation Notes The pipeline is intentionally `for-of` + `await` rather than `Promise.all` so plugin ordering is observable and deterministic — a plugin that depends on a prior plugin's side effect can rely on the earlier hook resolving first. Plugin errors are wrapped, not swallowed: `PACKER.PLUGIN_FAILED` carries the original error on `cause` and a catalog message keyed by hook name so observability sinks can attribute the failure to a specific plugin. The `empty` short-circuit means callers who don't register plugins pay nothing — no array iteration, no try/catch entry. Plugins MUST NOT mutate the payload bytes they receive; the contract is observe-and-emit-side-effects.