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,99 @@
# hooks
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the package's `email:processing` hook pipeline: every parsed
email passes through a sequence of handlers that may sanitise, redact,
or enrich the parsed result. The implementation is intentionally local
to this package — the foundation `@suaveplan/events` library
does not (yet) export a shared `Hooks<T>` substrate, so a focused
async pipeline lives here. The public shape matches the upstream
`@suaveplan/email-processing` package so consumer code does not change.
## Features
- Generic `Hooks<TMap>` class with explicit type-safe `hook` /
`applyHooksAsync` / `clear` methods.
- Handlers run in `order` (default 100, lower first) and may be
sync or async — `applyHooksAsync` awaits each return value.
- Errors thrown from any handler short-circuit the pipeline and are
wrapped in an `EmailProcessingError` with code
`EMAIL_PROCESSING.HOOK_FAILURE`. The original throw is preserved
on `.cause` when it is an `Error`.
- Each invocation increments the `email_processing.hook.invocation_total`
OTel counter with `event` and `handlers` attributes.
- Returned data is threaded through every handler so transforms compose.
- Module-level `emailHooks` singleton + `onEmailProcessing` shorthand.
## Usage: Basic
```ts
import { onEmailProcessing } from "@suaveplan/email-processing/hooks";
onEmailProcessing((data) => ({
...data,
parsed: {
...data.parsed,
html: data.parsed.html?.replace(
/<img[^>]*tracking[^>]*>/gi,
""
),
},
}));
```
## Usage: Advanced
```ts
import { Hooks, type EmailHookMap } from "@suaveplan/email-processing/hooks";
const hooks = new Hooks<EmailHookMap>();
const offFirst = hooks.hook(
"email:processing",
async (d) => {
await audit.log({ messageId: d.parsed.messageId });
return d;
},
{ order: 10 }
);
const offRedact = hooks.hook(
"email:processing",
(d) => ({
...d,
parsed: { ...d.parsed, text: redact(d.parsed.text ?? "") },
}),
{ order: 200 }
);
try {
const out = await hooks.applyHooksAsync("email:processing", incoming);
return out;
} finally {
offFirst();
offRedact();
}
```
## API Reference
- `EmailProcessingHookData` — pipeline payload (`parsed`, `rawInput?`,
`metadata`).
- `EmailHookMap` — `{ "email:processing": EmailProcessingHookData }`.
- `HookHandler<T>` — `(data: T) => T | Promise<T>`.
- `HookHandlerOptions` — `{ order?: number }`.
- `Hooks<TMap>` — class with `hook`, `applyHooksAsync`, `clear`.
- `emailHooks` — singleton `Hooks<EmailHookMap>` instance.
- `onEmailProcessing(handler, options?)` — shorthand registration on
`emailHooks`; returns an idempotent unregister.
## Implementation Notes
The unregister function returned from `hook(...)` is idempotent: calling
it twice is a no-op so cleanup code in shared test fixtures can run
without ordering pitfalls. The pipeline does not call the OTel counter
when a handler throws — failures are observed via the
`EMAIL_PROCESSING.HOOK_FAILURE` error path, and a separate consumer
counter is the appropriate place to record those.