@effect-server-utils/cqrs

中文文档
0.1.0-beta.10发布于 2026/10/30MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 4347

Typed CQRS for Effect: command and query buses with per-message success and error types, an event bus whose subscriptions choose their consistency model, and process managers

原始使用文档
文档目录6 节
  1. What you get
  2. Declaring and dispatching a command
  3. Events
  4. Testing helpers
  5. Documentation
  6. License

@effect-server-utils/cqrs

Typed CQRS for Effect: command and query buses with per-message success and error types, an event bus whose subscriptions choose their consistency model, and process managers.

pnpm add @effect-server-utils/cqrs effect

effect is a peer dependency.

What you get

  • Command / Query — declare a message once, with its payload schema, its success type and its failure type. A handler that returns the wrong thing does not compile, and a caller reads both channels off the definition rather than off the bus. The two are separate facades over the same machinery so that a query group is rejected where a command group is expected — the CQRS distinction expressed in types.
  • CommandBus / QueryBus — the application-wide dispatch surface. bus.execute(Command, payload) reads its signature off the definition you pass, so there is no side table a command's declaration could drift from.
  • DispatchTable — folds each module's dispatcher into the one table a bus routes through. The three ways a table can be wrong (DuplicateDispatchTag, UnroutableTags, MissingHandler) travel as tagged defects, so a boot check can match the condition instead of parsing a message string.
  • EventBus — one bus, three delivery contracts, chosen at subscription rather than at dispatch. subscribe runs in the publisher's fiber and can roll it back; subscribeAfterCommit runs once the publisher's boundary has completed, in a boundary of its own, and never can; stream feeds a saga and is never awaited. A producer says only that something happened, so one event can serve consumers that need different things.
  • DeferralSink — the optional seam that decides what subscribeAfterCommit waits for. This package does not know what a transaction is: install @effect-server-utils/unit-of-work and after-commit means after commit; install nothing and those handlers run at the end of each dispatch. Handlers are written the same way either way.
  • Saga — a process manager over deferred events, for when no single event decides.
  • Middleware — behaviour applied once around every dispatch. Middleware.span is installed by default; metrics() and deadline() are opt-in. A middleware may not change a message's success or error channels, which is what lets the bus have a seam without weakening a caller's types.
  • UnhandledFailures — where a failure goes when no caller is left to receive it.

Declaring and dispatching a command

import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import { Command, CommandBus } from "@effect-server-utils/cqrs";

class OrderNotFound extends Schema.TaggedError<OrderNotFound>()("OrderNotFound", {
  orderId: Schema.String,
}) {}

const PlaceOrder = Command.make("PlaceOrder", {
  payload: { orderId: Schema.String },
  success: Schema.String,
  failure: OrderNotFound,
});

// A module groups the tags it owns and implements them in one place. Whatever
// services the handlers need become the Layer's requirements.
const Orders = Command.group(PlaceOrder);

export const OrdersHandlers = Command.handlersOf(Orders, {
  PlaceOrder: (payload) => Effect.succeed(`placed:${payload.orderId}`),
});

// At a call site, the channels come from the definition — not from the bus.
const program = Effect.gen(function* () {
  const bus = yield* CommandBus;
  const receipt = yield* bus.execute(PlaceOrder, { orderId: "ord_1" });
  return receipt;
}).pipe(Effect.catchTag("OrderNotFound", (e) => Effect.succeed(`missing:${e.orderId}`)));

At the composition root, each module's dispatch surface is merged into the table the bus routes through:

import { makeCommandBus, mergeDispatchTables } from "@effect-server-utils/cqrs";

const table = mergeDispatchTables(ordersDispatcher, billingDispatcher);
const bus = makeCommandBus(table, { declaredIn: [Orders, Billing] });

Events

import { Event } from "@effect-server-utils/cqrs";

const OrderPlaced = Event.make("OrderPlaced", { orderId: Schema.String });

const subscriptions = Effect.gen(function* () {
  const bus = yield* EventBus;

  // Part of the same logical operation: its failure rolls the publisher back.
  yield* bus.subscribe(OrderPlaced, (event) => inventory.reserve(event.orderId));

  // A reaction to work that is already durable: isolated, retried on its own.
  yield* bus.subscribeAfterCommit(OrderPlaced, (event) => email.sendReceipt(event.orderId));
});

"Already durable" needs something to be durable in. With no DeferralSink in context the bus runs after-commit handlers at the end of the dispatch — still after every immediate one, still isolated — which is enough for a host with no datastore to coordinate. Add @effect-server-utils/unit-of-work and the same handlers start running after a real commit, each in a transaction of its own, with nothing above rewritten:

import { makeUnitOfWork, withUnitOfWork } from "@effect-server-utils/unit-of-work";

// Installs the sink alongside the boundary — they are one decision.
const runtime = Layer.mergeAll(makeEventBus(), makeUnitOfWork()).pipe(
  Layer.provide(YourTransactionDriver),
);

// The boundary a use case declares once, at the end of its pipe.
const placeOrder = (input: Input) => doTheWork(input).pipe(withUnitOfWork);

Testing helpers

Serializability checks live behind their own entry point, because they derive sample values with a property-testing runtime that has no business in a consumer's production process:

import { checkEventsSerializable, checkSerializable } from "@effect-server-utils/cqrs/testing";

const check = Effect.gen(function* () {
  const problems = yield* checkSerializable(Orders); // empty means every contract is portable
});

Documentation

Full documentation: https://dataquail.github.io/effect-server-utils

License

MIT

版本与发布记录

@effect-server-utils/cqrs 共发布 11 个版本,首次发布于 2026/08/17;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.0-beta.10latest2026/10/3014 天前
0.1.0-beta.92026/10/111 个月前
0.1.0-beta.82026/10/111 个月前
0.1.0-beta.72026/09/221 个月前
0.1.0-beta.62026/09/122 个月前
0.1.0-beta.52026/09/122 个月前

其他发布通道:beta → 0.1.0-beta.0。

关于 @effect-server-utils/cqrs 的常见问题

@effect-server-utils/cqrs 是什么?

Typed CQRS for Effect: command and query buses with per-message success and error types, an event bus whose subscriptions choose their consistency model, and process managers

如何安装 @effect-server-utils/cqrs?

在项目目录运行 npm install @effect-server-utils/cqrs,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(^4.0.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effect-server-utils/cqrs 支持 TypeScript 吗?

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

@effect-server-utils/cqrs 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@effect-server-utils/cqrs 可以商用吗?

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

@effect-server-utils/cqrs 的下载量有多少?

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

@effect-server-utils/cqrs 还在维护吗?

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