4.4 KiB
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 oncauseand a catalog-resolved message keyed by hook name. PluginPipeline.emptylets 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/onEntryDecodedto round-trip custom digests through the bundle'smetadata.custom.
Usage: Basic
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
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<number, Uint8Array>();
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 PackerPluginreadonly 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 PluginPipelineconstructor(plugins, messageResolver)get empty(): booleanasync runHook<K>(name: K, ctx: HookContextMap[K]): Promise<void>
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.