first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user