@alxia/events

中文文档
0.2.0发布于 2026/11/06MITTypeScriptES Module零依赖近 30 天下载 1810

An event bus for an application on @softistx/mongo and @softistx/redis: events written to a transactional outbox with the domain change, relayed to queues, delivered at least once and applied once through an inbox

原始使用文档
文档目录6 节
  1. In 30 seconds
  2. Delivery
  3. Guarantees
  4. API
  5. Documentation
  6. License

@alxia/events

An event bus for an application on @softistx/mongo and @softistx/redis, built on a transactional outbox. A module writes an event in the same MongoDB transaction as the change that caused it, so the two stand or fall together. A relay hands each event to the modules that subscribed, through queues; each subscriber applies it once, however many times it is delivered.

bun add @alxia/events @alxia/modules @alxia/tenancy @softistx/mongo @softistx/redis mongodb zod
bun add -d typescript

@softistx/mongo and @softistx/redis are private, on GitHub Packages. Map the scope in your project's bunfig.toml and export GITHUB_TOKEN, a token with read:packages:

[install.scopes]
"@softistx" = { url = "https://npm.pkg.github.com/", token = "$GITHUB_TOKEN" }

MongoDB must run as a replica set (transactions need one). See troubleshooting.

In 30 seconds

import { collectionsOf, defineModule } from '@alxia/modules';
import {
    createEvents,
    eventsModule,
    type EventEnvelope,
    type SubscriberContext,
} from '@alxia/events';
import { single } from '@alxia/tenancy';
import { connectMongo, defineMongo, openMongo } from '@softistx/mongo';

// `Tenant` and `Core` are your tenant and core collection wirings
// (see the guide, "Write a subscriber").

// 1. Declare what a module emits and what it subscribes to.
export const shop = defineModule({
    name: 'shop',
    emits: ['shop.order.placed'],
});

export const billing = defineModule({
    name: 'billing',
    subscribers: {
        'shop.order.placed': async (
            event: EventEnvelope,
            context: SubscriberContext<'billing', Tenant, Core>,
        ) => {
            // `context.mongo` is in the transaction that records the inbox row,
            // and narrowed to billing's collections and those of its dependsOn.
            const { mongo } = context;
            await mongo.db.invoices.create({ orderId: event.aggregate.id });
        },
    },
});

// 2. Put the kernel module in the registry you give the tenant store,
//    then build the bus from that store.
const modules = [eventsModule, shop, billing];
const { client } = await connectMongo(process.env.MONGO_URI!);
const mongo = await openMongo(
    defineMongo({ client, database: 'app', collections: collectionsOf(modules) }),
);
const tenancy = single({
    mongo,
    modules,
    tenant: { id: 'main', slug: 'main', name: 'Main' },
}); // or perOrganization(...), see @alxia/tenancy
const events = createEvents({ store: tenancy });
// `request` and `viewer` are the HTTP request and its signed-in account
const scope = await tenancy.scope(request, viewer);

// 3. Emit inside the transaction of the change.
await scope.mongo.transaction(async (tx) => {
    const order = await tx.db.orders.create({ label: 'Ada' });
    await events.emit(
        { tenant: scope.tenant, mongo: tx },
        {
            type: 'shop.order.placed',
            aggregate: { type: 'order', id: order.id },
            payload: { label: 'Ada' },
        },
    );
});

Delivery

In production, a relay and workers run beside the app. Each module that subscribes gets a queue, events.<module>:

import { bindQueue, connectRedis } from '@softistx/redis';
import { createEvents, eventsQueue } from '@alxia/events';

const redis = await connectRedis(process.env.REDIS_URL!);
const queues = { billing: bindQueue(redis.client, eventsQueue('billing')) };
const events = createEvents({ store: tenancy, queues }); // `tenancy` from above

const relay = events.relay({ redis: redis.client }); // one leader per deployment
relay.start();
const workers = events.work();

// on shutdown
await relay.stop();
await workers.stop();

Without Redis (dev, tests), run the subscribers in your process, in order:

await events.drain(scope.tenant.id); // omit the tenant to drain every tenant

Guarantees

  • Atomic emit. The event is in _outbox if and only if the transaction that emitted it committed.
  • At least once. The relay enqueues a job per (event, subscriber) with jobId = <eventId>:<module>:<type>, then marks the event dispatched. A crash in between enqueues the same ids again, which the queue ignores.
  • Applied once. A subscriber's effects and its _inbox row commit in one transaction; a second delivery finds the row and does nothing.
  • Order. Per tenant, the relay sends events in _id order. There is no global order, and a subscriber must tolerate reordering.
  • Same on every store. Emit, relay and drain run unchanged on perOrganization and single (one database, one fixed tenant).

API

ExportWhat it does
eventsModulethe kernel module owning _outbox (TTL 7 days after dispatch) and _inbox (30 days); add it to the registry
createEvents({ store, queues?, batchSize? })the bus; checks the registry at once
events.emit(source, event, options?)writes the event through source.mongo, a transaction's handle
events.drain(tenant?)runs pending events through their subscribers in this process
events.relay(options)tick(), start(), stop(): a leader-locked poll that enqueues jobs
events.work(options?)one @softistx/redis worker per subscribing module's queue
events.deadLetters({ limit? })the jobs that used all their attempts
eventsQueue(module, options?), queueName(module), EventsQueueOptions, EventQueue, EventJobthe definition of events.<module>, its options, the bound queue's type and a job's payload
deliver(store, mongo, tenant, event, subscription), Deliveryone subscriber, once: inbox row plus handler in a transaction, handed its module's narrowed scope
emittedTypes(modules), subscriptionsOf(modules), Subscriptionthe registry readers behind the boot checks, and one module's handler for one type
outbox, inbox, OUTBOX_TTL, INBOX_TTLthe collection definitions and TTLs
EventsError, EventsErrorCodethe refusals, with a stable code namespaced events. (events.undeclared-type, events.missing-queue, ...)
EventEnvelope, EventInput, EmitOptions, EventWorkers, EmitSource, EventsMongo, Subscriber, SubscriberContext, DeadLetter, Relay, RelayOptions, Events, EventsOptionsthe types

Peers: @alxia/modules, @alxia/tenancy, @softistx/mongo, @softistx/redis, mongodb, zod, typescript.

Documentation

  • Guide: the envelope, the outbox, subscribers, the relay and workers, drain, dead letters, tenants, testing.
  • Troubleshooting: an error message, and what to do about it.
  • Roadmap: what is coming, and what is not planned.

License

MIT

版本与发布记录

@alxia/events 共发布 11 个版本,首次发布于 2026/11/01;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.2.0latest2026/11/062 天前
0.1.82026/11/017 天前
0.1.72026/11/017 天前
0.1.62026/11/017 天前
0.1.52026/11/017 天前
0.1.42026/11/017 天前

关于 @alxia/events 的常见问题

@alxia/events 是什么?

An event bus for an application on @softistx/mongo and @softistx/redis: events written to a transactional outbox with the domain change, relayed to queues, delivered at least once and applied once through an inbox

如何安装 @alxia/events?

在项目目录运行 npm install @alxia/events,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:zod(^4.6.5)、mongodb(^7.7.0)、typescript(^6.0.3 || ^7.0.0)、@alxia/modules(^0.4.0)、@alxia/tenancy(^0.6.0)、@softistx/mongo(^0.27.0)、@softistx/redis(^0.14.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@alxia/events 支持 TypeScript 吗?

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

@alxia/events 支持 ESM 还是 CommonJS?

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

@alxia/events 可以商用吗?

@alxia/events 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@alxia/events 的下载量有多少?

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

@alxia/events 还在维护吗?

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