first commit
This commit is contained in:
112
docs/foundation/data/packer/src/plugins/plugins.md
Normal file
112
docs/foundation/data/packer/src/plugins/plugins.md
Normal 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.
|
||||
Reference in New Issue
Block a user