@absolutejs/dispatch

中文文档
0.9.1发布于 2026/10/22BSL-1.1TypeScriptES Module构建来源已验证近 30 天下载 2389

Provider-agnostic outbound dispatcher for email, carrier and rich messaging, and push notifications in the AbsoluteJS ecosystem.

原始使用文档
文档目录17 节
  1. Install
  2. Usage
  3. API
  4. createDispatcher(options)
  5. dispatcher.email(message) / .messaging(message) / .push(mes…
  6. Production push lifecycle
  7. Pre-send authorization policies
  8. dispatcher.metrics()
  9. Substrate pattern
  10. OpenTelemetry
  11. Audit emission
  12. Bundled adapters
  13. memoryEmailAdapter({ max?, idGenerator? })
  14. memoryMessagingAdapter({ max? }) / memoryPushAdapter({ max?…
  15. consoleEmailAdapter({ stream? }) / consoleMessagingAdapter …
  16. What this package does NOT do
  17. License

@absolutejs/dispatch

Provider-agnostic outbound message dispatcher for the AbsoluteJS ecosystem.

Docs: absolutejs.com/documentation/dispatch

What it is. One factory (createDispatcher) returning a Dispatcher with three channels (email / messaging / push). Adapters plug into each channel. The substrate around the adapters — metrics, OTel, audit emission, error handling — is uniform.

What it solves. Every package that needs to send a transactional message currently brings its own SMTP / SMS / push client. @absolutejs/auth's magic-link routes assume the host emails the token. @absolutejs/sync-pack-digest explicitly says "Host-supplied email sender. The pack does NOT own transport". The control plane needs invoice-failed / quota-warning emails. Centralizing through one dispatcher means each consumer plugs into one place.

Install

bun add @absolutejs/dispatch
# Plus one or more adapter siblings:
bun add @absolutejs/dispatch-resend       # Resend (email)
bun add @absolutejs/dispatch-postmark     # Postmark (email)
bun add @absolutejs/dispatch-twilio       # Twilio messaging
bun add @absolutejs/dispatch-telnyx       # Telnyx messaging
# ...etc

Usage

import { createDispatcher } from "@absolutejs/dispatch";
import { createResendAdapter } from "@absolutejs/dispatch-resend";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_KEY!);

const dispatcher = createDispatcher({
  email: createResendAdapter({ client: resend }),
  defaultFrom: { email: "[email protected]" },
});

await dispatcher.email({
  to: "[email protected]",
  subject: "Welcome to Acme",
  text: "Hi Alice, click here to verify: ...",
  tenant: "tenant-A", // optional — propagates to OTel + audit
});

API

createDispatcher(options)

type DispatcherOptions = {
  email?: EmailAdapter;
  messaging?: MessagingAdapter;
  push?: PushAdapter;
  policies?: readonly DispatchPolicy[];
  defaultFrom?: { email?: string; messaging?: MessagingEndpoint };
  onError?: (error: unknown, channel: DispatchChannel, message) => void;
  tracerProvider?: TracerProvider; // OTel
  audit?: AuditLike; // @absolutejs/audit instance
};

Returns { email, messaging, push, metrics }. Calling a channel without an adapter throws DispatchUnsupportedError.

dispatcher.email(message) / .messaging(message) / .push(message)

type EmailMessage = {
  to: string | ReadonlyArray<string>;
  from?: string;
  subject: string;
  text?: string;
  html?: string;
  replyTo?: string;
  cc?: string | ReadonlyArray<string>;
  bcc?: string | ReadonlyArray<string>;
  headers?: Record<string, string>;
  tenant?: string;
  metadata?: Record<string, unknown>; // adapter-specific extras
};

type MessagingMessage = {
  to: { address: string; transport: "sms" | "mms" | "rcs" | "whatsapp" };
  from?: { address: string; transport: "sms" | "mms" | "rcs" | "whatsapp" };
  content:
    | { kind: "text"; text: string }
    | {
        kind: "media";
        mediaUrls: readonly string[];
        text?: string;
        subject?: string;
      }
    | {
        kind: "template";
        id: string;
        variables?: Readonly<Record<string, string>>;
      }
    | {
        kind: "rich";
        text: string;
        title?: string;
        mediaUrl?: string;
        actions?: readonly MessagingAction[];
      };
  fallbacks?: readonly {
    transport: "sms" | "mms" | "whatsapp";
    from?: MessagingEndpoint;
    content?: MessagingContent;
  }[];
  sendAt?: string;
  idempotencyKey?: string;
  privacy?: {
    addressRetention?: "obfuscate" | "retain";
    contentRetention?: "discard" | "retain";
  };
  consent?: {
    programId: string;
    purpose: string;
  };
  extensions?: Readonly<Record<string, unknown>>;
  tenant?: string;
  metadata?: Record<string, unknown>;
};

type PushMessage = {
  to: string; // device token OR topic
  title?: string;
  body: string;
  idempotencyKey?: string;
  actions?: ReadonlyArray<{ id: string; label: string; deepLink?: string }>;
  badge?: number;
  deepLink?: string;
  sound?: string;
  data?: Record<string, unknown>;
  tenant?: string;
  metadata?: Record<string, unknown>;
};

Production push lifecycle

createPushLifecycle() owns the provider-neutral device layer above APNs, FCM, and Web Push: tenant-isolated registration, user/device/topic targeting, bounded concurrency, retries, invalid-token retirement, and idempotent fanout. Resolve the adapter per subscription to support tenant-specific credentials.

import { createPushLifecycle } from "@absolutejs/dispatch";
import {
  createPostgresPushFanoutClaimStore,
  createPostgresPushSubscriptionStore,
} from "@absolutejs/dispatch-push-postgres";

const push = createPushLifecycle({
  adapterFor: ({ platform, tenant }) => adapters.forTenant(tenant, platform),
  claimStore: createPostgresPushFanoutClaimStore(idempotentOperations),
  store: createPostgresPushSubscriptionStore(transactionRunner),
});

await push.register({
  deviceId: "iphone-15",
  platform: "apns",
  tenant: "acme",
  token,
  topics: ["incidents"],
  userId: "user-42",
});

// Authenticated apps use server-issued installation identities. Pass this
// lifecycle directly as Auth's push registrar; ownership is checked on every
// native-token or browser-subscription rotation and removal.
const installation = await push.registerInstallation({
  platform: "apns",
  tenant: "acme",
  token,
  topics: ["incidents"],
  userId: "user-42",
});

await push.registerInstallation({
  platform: "webpush",
  subscription: {
    endpoint: "https://push.example/subscription-id",
    keys: { auth: "browser-auth-key", p256dh: "browser-public-key" },
  },
  tenant: "acme",
  topics: ["incidents"],
  userId: "user-42",
});

await push.send(
  { tenant: "acme", topic: "incidents" },
  {
    body: "Database latency is elevated",
    deepLink: "absolute://incidents/42",
    idempotencyKey: "incident-42:opened",
    sound: "default",
    title: "Production alert",
  },
);

Ambiguous provider failures are recorded as indeterminate instead of being silently retried after a lease expires. This prevents a crashed worker from double-delivering a notification whose provider acknowledgement was lost.

Returns MessagingDispatchResult { id?, provider, at, delivery }. The delivery record carries the requested and actual transports plus normalized primary/fallback attempts. Throws on adapter failure; onError fires before re-throw.

Pre-send authorization policies

Policies run in order before the adapter or vendor receives a message and may perform durable asynchronous lookups. @absolutejs/compliance supplies a messaging-consent policy.

const dispatcher = createDispatcher({
  policies: [messagingConsentPolicy],
  messaging,
});

await dispatcher.messaging({
  content: { kind: "text", text: "Database latency is elevated" },
  consent: { programId: "acme-alerts", purpose: "incident-alerts" },
  to: { address: "+12025550100", transport: "sms" },
});

dispatcher.metrics()

{
  sent: number;
  failed: number;
  byChannel: {
    email: {
      sent, failed;
    }
    messaging: {
      sent, failed;
    }
    push: {
      sent, failed;
    }
  }
}

Substrate pattern

OpenTelemetry

When tracerProvider is set, every send emits a dispatch.<channel>.send span (dispatch.email.send, etc.) with:

  • abs.tenant (when message.tenant is supplied)
  • dispatch.channel (email / messaging / push)
  • dispatch.provider (the adapter name, updated to the result provider on success)
  • dispatch.recipient_count (addresses are intentionally excluded from spans)
  • dispatch.message_id (when the adapter returns one)

Updating the provider from the result keeps broker/delegating adapters observable without leaking their routing implementation.

Status OK on success; ERROR + recordException on failure.

Audit emission

When audit is supplied (any @absolutejs/audit Audit instance), every send emits a dispatch.<channel>.<outcome> event:

  • kind: dispatch.email.sent / dispatch.email.failed (etc.)
  • actor: message.tenant (when set), else 'system'
  • target: the recipient
  • metadata: { channel, provider, messageId?, error? }

Audit emission is fire-and-forget — if your audit sink fails, the dispatch still succeeds. Audit failures are logged to console.warn.

The audit dep is optional — @absolutejs/audit is a peer-dep with peerDependenciesMeta.optional: true, so consumers without it installed just pass audit: undefined.

Bundled adapters

These ship in core for tests + dev. Production deployments use the sibling vendor adapters.

memoryEmailAdapter({ max?, idGenerator? })

In-process FIFO tail. inspect() returns captured messages oldest- first; clear() empties. Useful for asserting "did we email Alice when she signed up?" in tests without a real SMTP server.

memoryMessagingAdapter({ max? }) / memoryPushAdapter({ max? })

Same shape, same usage.

consoleEmailAdapter({ stream? }) / consoleMessagingAdapter / consolePushAdapter

JSON-per-line to stdout (or stderr). Useful for bun --watch dev so you can see exactly what would be sent without burning provider credits.

What this package does NOT do

  • Template rendering. Messages are pre-rendered. Pair with @react-email, mjml, handlebars, or whatever you like.
  • Per-recipient rate limiting. Compose with @absolutejs/rate-limit for "max 5 magic-links per email per hour" gating.
  • Bounce / complaint handling. Each provider's SDK exposes webhooks for these.
  • Scheduling / queueing. For delayed sends use @absolutejs/queue with a handler that calls dispatcher.email(...).

License

BSL-1.1 with a Tier-A carveout: you can't use this to operate a hosted message-dispatch / transactional-email / SMS / push SaaS that competes with hosted offerings (Resend, Postmark, SendGrid, Mailgun, AWS SES managed, Twilio, Vonage, Pushwoosh, OneSignal, FCM managed, Knock). You CAN use it as one piece of your own application (including your own SaaS). Auto-converts to Apache 2.0 on 2030-05-30.

版本与发布记录

@absolutejs/dispatch 共发布 22 个版本,首次发布于 2026/05/31;过去 12 个月发布了 22 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.9.1latest2026/10/2216 天前
0.9.02026/08/272 个月前
0.8.02026/08/272 个月前
0.7.22026/08/073 个月前
0.7.12026/08/023 个月前
0.7.02026/08/023 个月前

关于 @absolutejs/dispatch 的常见问题

@absolutejs/dispatch 是什么?

Provider-agnostic outbound dispatcher for email, carrier and rich messaging, and push notifications in the AbsoluteJS ecosystem.

如何安装 @absolutejs/dispatch?

在项目目录运行 npm install @absolutejs/dispatch,或使用 pnpm add / yarn add / bun add。

@absolutejs/dispatch 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@absolutejs/dispatch 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@absolutejs/dispatch 可以商用吗?

@absolutejs/dispatch 使用 BSL-1.1 许可。声明的许可为 BSL-1.1,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@absolutejs/dispatch 的下载量有多少?

根据 npm 官方下载统计,@absolutejs/dispatch 在 2026/10/05 – 2026/11/03 这 30 天内被下载 2,389 次,日均约 80 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@absolutejs/dispatch 还在维护吗?

最新版本 0.9.1 发布于 2026/10/22(16 天前);至今共发布 22 个版本,其中过去 12 个月发布了 22 个;首次发布于 2026/05/31。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。