Files
skills/docs/communication/messaging/notification-channel/src/hooks/hooks.md
2026-05-29 08:20:56 +01:00

3.5 KiB

hooks — Pre-routing hook pipeline

(no submodules)

Purpose

hooks.ts exposes a small typed hook bus that runs before NotificationRouter commits a notification to a channel. The single key today is notification:routing; handlers receive the original input, the currently resolved channel, and a metadata bag, and may return a partial override that merges into the data flowing forward. The bus is intentionally local to this package — the @suaveplan/events foundation does not yet export a Hooks primitive, and inlining keeps the package universal with no extra workspace dep.

Features

  • Generic Hooks<TMap> class — typed by a hook-map interface; cannot register on an undeclared key.
  • Handlers run in registration order (FIFO).
  • Each handler may return undefined (no-op), a full hook-map value, a partial override, or a Promise of either.
  • Unsubscribe returned from hook(...) — calling twice is safe.
  • clear(key?) drops handlers for a single key or every key.
  • Singleton notificationHooks so cross-cutting integrations (audit, A/B routing) can hook globally.
  • Shorthand onNotificationRouting(handler) for the singleton's primary key.

Usage: Basic

import { onNotificationRouting } from "@suaveplan/notification-channel";

const off = onNotificationRouting((data) => {
    if (data.input.priority === "urgent") {
        return { resolvedChannel: "sms" };
    }
    return undefined;
});

// later
off();

Usage: Advanced

import {
    Hooks,
    type NotificationHookMap,
} from "@suaveplan/notification-channel";

const hooks = new Hooks<NotificationHookMap>();

hooks.hook("notification:routing", async (data) => {
    const enriched = await enrichRecipient(data.input.recipient);
    return {
        metadata: { ...data.metadata, geo: enriched.geo, segment: enriched.segment },
    };
});

const result = await hooks.applyHooksAsync("notification:routing", {
    input: { recipient: "u@x.com", body: "Hi" },
    resolvedChannel: "email",
    metadata: {},
});

API Reference

  • Hooks<TMap> — class.
    • hook(key, fn) → () => void — register, returns unsubscribe.
    • clear(key?) — drop handlers for key, or every key when omitted.
    • count(key) → number — number of registered handlers.
    • applyHooksAsync(key, data) → Promise<data> — run pipeline and return composed data.
  • notificationHooks — singleton Hooks<NotificationHookMap>.
  • onNotificationRouting(handler) — shorthand for notificationHooks.hook("notification:routing", handler).
  • NotificationHookMap — { "notification:routing": NotificationRoutingHookData }.
  • NotificationRoutingHookData — { input, resolvedChannel, metadata }.

Implementation Notes

Handlers compose via Object.assign-style spread, so a handler that returns { resolvedChannel: "sms" } overrides only that field while preserving input and metadata. Returning undefined is the explicit "no change" signal — handlers used for telemetry or audit can side-effect freely without returning anything. The Hooks class accepts a generic TMap so packages with their own hook map can reuse the same implementation; the singleton is pinned to NotificationHookMap so the singleton's only legal key is notification:routing. The bus is intentionally minimal: it has no priority, timeout, or interceptor surface — those concerns belong to @suaveplan/events's richer EventBus interceptor stack, which the router itself uses for lifecycle events.