first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
# `plugins/plugins` — plugin / extension interface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.