3.1 KiB
hooks
(no submodules)
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-safehook/applyHooksAsync/clearmethods. - Handlers run in
order(default 100, lower first) and may be sync or async —applyHooksAsyncawaits each return value. - Errors thrown from any handler short-circuit the pipeline and are
wrapped in an
EmailProcessingErrorwith codeEMAIL_PROCESSING.HOOK_FAILURE. The original throw is preserved on.causewhen it is anError. - Each invocation increments the
email_processing.hook.invocation_totalOTel counter witheventandhandlersattributes. - Returned data is threaded through every handler so transforms compose.
- Module-level
emailHookssingleton +onEmailProcessingshorthand.
Usage: Basic
import { onEmailProcessing } from "@suaveplan/email-processing/hooks";
onEmailProcessing((data) => ({
...data,
parsed: {
...data.parsed,
html: data.parsed.html?.replace(
/<img[^>]*tracking[^>]*>/gi,
""
),
},
}));
Usage: Advanced
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 withhook,applyHooksAsync,clear.emailHooks— singletonHooks<EmailHookMap>instance.onEmailProcessing(handler, options?)— shorthand registration onemailHooks; 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.