Files
skills/docs/foundation/data/packer/src/plugins/plugins.md
2026-05-29 08:20:56 +01:00

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 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

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 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<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.