This is the full developer documentation for effect-cqrs # effect-cqrs > Event sourcing and CQRS for Effect v4. Commands decide on events, events are the only source of truth, and read models are projections you can throw away and rebuild. ## How a command flows 1. ### Command A typed call, or JSON from a transport through `dispatch`. The payload is checked against its schema first. [Commands guide](/guides/commands/) 2. ### Aggregate The service rebuilds the aggregate's state from its events, and the handler decides what happened, or fails with a typed error. [Aggregates guide](/guides/aggregates/) 3. ### Event store The events are appended, but only if nobody wrote to that aggregate since its state was loaded. [Storage guide](/guides/storage/) 4. ### View models Each one reads the new events from its own checkpoint. `caughtUp` waits until your own write is there. [View models guide](/guides/view-models/) ## Beyond one command The path above is the core. These modules cover what a system needs around it, and each is opt-in. * Saga ### Sagas across aggregates A process that touches several aggregates, like a transfer between two accounts, runs as a durable workflow that an event starts. The engine keeps the outcome of every step, so a restart skips the commands it already ran, and compensations undo the steps that succeeded. [Sagas guide](/guides/sagas/) * PersonalData ### GDPR, by crypto-shredding Forget a person without rewriting the log. Personal fields are encrypted with a key that belongs to their subject, and `forget` destroys it. The events stay, with their amounts and dates, and streams still replay. [Personal data guide](/guides/personal-data/) * CommandApi ### An HTTP API from an aggregate A `POST` endpoint per command, with history and state, as an `HttpApiGroup` with OpenAPI. `Idempotency-Key` and `If-Match` reach the command, and failures map to status codes. [HTTP](/guides/integration/#http) * Outbox · EventCatalog ### Events for other systems An outbox publishes events to Kafka, NATS, SQS and the like, at least once and in order. The catalog describes them as JSON Schema and AsyncAPI, and its guard fails when a change can no longer decode what is stored. [Outbox](/guides/integration/#outbox)[Event catalog](/guides/integration/#event-catalog) * EventVersions ### Events that change shape The log is never rewritten. Declare the older shapes of an event and how each becomes the next, and old events are upcast as they are read. Handlers and projections only see the current one. [Evolving events](/guides/aggregates/#evolving-events) * ViewModel · Leases ### Made to keep running Command ids make a retried command run once. View models retry a failing handler, park an event that never passes, and run on one replica at a time. Every event records its cause, and the services report metrics. [Command ids](/guides/commands/#retries-and-command-ids)[Failing handlers](/guides/view-models/#failing-handlers)[Several processes](/guides/view-models/#several-processes) ## A bank, running in your browser Accounts are aggregates, three view models project them, and a saga moves money between them. The customer app on the left only shows read models, so you can watch them trail the store. The devtools on the right slow every stage down, pause and step commands, crash a projection, and replay the log. [Open the demo](/demo/)[Read how it's built](/tutorial/bank/) ******effect-cqrs.dambox.ch/demo/ [The bank demo](/demo/) ## Tests read like moleculer-cqrs `Fixture` runs commands through the real service on an in-memory store, so validation and encoding are tested too. The chain is an Effect, which `it.effect` runs. ```ts it.effect("deletes a todo", () => Fixture.make(Todo) .givenEvents([created]) .when("deleteTodo", {}) .expectEvent("TodoDeleted") .inspectState((state) => assert.isTrue(state.deleted))) ``` [Testing guide](/guides/testing/) ## Keep the log where it fits Every store passes the same tests. The numbers come from one laptop, so read them as orders of magnitude. * In memoryFor tests, examples and the browser.60,000 commands/s * SQLiteOne process, one file, no server.0.1 ms a command * PostgreSQLSeveral processes writing, positions in commit order.1 ms a command [Storage guide](/guides/storage/)[Performance](/reference/performance/) # Getting started > Install effect-cqrs, or clone the repository to run its examples. ## Install * pnpm ```bash pnpm add effect-cqrs effect ``` * npm ```bash npm install effect-cqrs effect ``` * yarn ```bash yarn add effect-cqrs effect ``` * bun ```bash bun add effect-cqrs effect ``` It needs Node 24 or later and Effect 4. To store events in SQLite or PostgreSQL, add that database’s `effect/sql` driver too: * pnpm ```bash pnpm add @effect/sql-sqlite-node pnpm add @effect/sql-pg ``` * npm ```bash npm install @effect/sql-sqlite-node npm install @effect/sql-pg ``` * yarn ```bash yarn add @effect/sql-sqlite-node yarn add @effect/sql-pg ``` * bun ```bash bun add @effect/sql-sqlite-node bun add @effect/sql-pg ``` Everything is exported from `effect-cqrs`, and each module also has its own path, such as `effect-cqrs/ViewModel`. ## Try the examples The examples live in the repository, where they import the TypeScript sources directly. 1. Clone the repository and install the dependencies. It needs pnpm. ```bash git clone https://github.com/davidnussio/effect-cqrs.git cd effect-cqrs pnpm install ``` 2. Run the tests. ```bash pnpm test ``` 3. Run the [tutorial](/tutorial/shop/)’s shop, which uses every feature in one file. ```bash pnpm tutorial ``` 4. Run the todo example. It is the moleculer-cqrs README walkthrough as a program, on an in-memory SQLite database. ```bash pnpm example ``` 5. Serve the bank demo at . ```bash pnpm ui ``` 6. Serve the tutorial’s shop, with its traces, saga and stored rows. ```bash pnpm ui:shop ``` ## What’s where * src/ * Aggregate.ts defines aggregates * AggregateService.ts runs commands and reads state and history * ViewModel.ts projects events into read models * Checkpoints.ts remembers how far each view model has read * EventStore.ts the log, and the store in memory * SqliteEventStore.ts * PostgresEventStore.ts * Fixture.ts tests an aggregate in isolation * examples/ * tutorial/ the shop of the tutorial * … * todo/ the moleculer-cqrs walkthrough * … * bank-ui/ the bank demo, in the browser * … * shop-ui/ the tutorial’s shop, in the browser * … * workshop/ a bank and a shop on a server, for a room * … * bench/ performance and stress tests, and their report * … * test/ * … ## Next steps [Follow the tutorial](/tutorial/shop/) [Define an aggregate](/guides/aggregates/) [Walk through the bank](/tutorial/bank/) [Use it with a coding agent](/using-with-ai/) # The bank > The second example: a bank whose accounts are aggregates, with three view models and a transfer saga, running in your browser. [Open the bank demo](/demo/)effect-cqrs in your browser, on the in-memory event store. Nothing leaves the page. The second example. [The shop](/tutorial/shop/) builds each piece in turn; the bank puts them in an app you can poke at. It is the classic event sourcing example, built on the framework as it is: the same sources the tests run, bundled for the browser. Bank accounts are aggregates, three view models project them, and a saga moves money between accounts. On the left, a customer app renders only the read models, so their lag behind the event store is visible. On the right, devtools slow each stage down, pause the command bus and step it, crash a projection, and travel back through the log. To run it locally: ```bash pnpm ui ``` The code quoted below comes from the demo’s sources when this site is built, so it can’t drift from what runs. ## The account aggregate An account’s balance is never stored. It is folded from the money that went in and out, so the events are the whole truth: examples/bank-ui/src/domain/account.ts ```ts export const AccountEvent = Schema.TaggedUnion({ // Encrypted with a key of the account's own, which "forget" destroys. AccountOpened: { owner: PersonalData.field(Schema.NonEmptyString, { redacted: "[forgotten]" }), color: Schema.String, openedAt: Schema.DateTimeUtc }, MoneyDeposited: { amount: Amount }, MoneyWithdrawn: { amount: Amount }, TransferSent: { transferId: Schema.String, to: Schema.String, amount: Amount }, TransferReceived: { transferId: Schema.String, from: Schema.String, amount: Amount }, TransferRefunded: { transferId: Schema.String, to: Schema.String, amount: Amount, reason: Schema.String }, AccountClosed: { closedAt: Schema.DateTimeUtc } }) ``` The aggregate folds them into a state with a status, an owner and a balance, and its commands check that state before they decide: examples/bank-ui/src/domain/account.ts ```ts export const Account = Aggregate.make()({ name: "account", events: AccountEvent, initial: initialAccount, projection: { AccountOpened: (state, { color, owner }) => ({ ...state, status: "open", owner, color }), MoneyDeposited: (state, { amount }) => ({ ...state, balance: state.balance + amount }), MoneyWithdrawn: (state, { amount }) => ({ ...state, balance: state.balance - amount }), TransferSent: (state, { amount }) => ({ ...state, balance: state.balance - amount }), TransferReceived: (state, { amount, transferId }) => ({ ...state, balance: state.balance + amount, received: [...state.received, transferId] }), TransferRefunded: (state, { amount }) => ({ ...state, balance: state.balance + amount }), AccountClosed: (state) => ({ ...state, status: "closed" }) } }) .command("openAccount", { payload: Schema.Struct({ owner: Schema.NonEmptyString, color: Schema.String }), handler: Effect.fnUntraced(function*(state, { color, owner }, { aggregateId }) { if (state.status !== "none") return yield* new AccountAlreadyOpened({ aggregateId }) return AccountEvent.cases.AccountOpened.make({ owner, color, openedAt: yield* DateTime.now }) }) }) .command("deposit", { payload: Schema.Struct({ amount: Amount }), handler: Effect.fnUntraced(function*(state, { amount }, { aggregateId }) { if (state.status !== "open") return yield* new AccountNotOpen({ aggregateId }) return AccountEvent.cases.MoneyDeposited.make({ amount }) }) }) ``` A command that would break a rule, such as withdrawing more than the balance, fails with a typed error and stores nothing. The customer app shows those rejections as they happen. ## Transfers, between two aggregates A command changes one aggregate, and a transfer touches two. So it is split in two: `sendTransfer` debits the sender, and `receiveTransfer` credits the receiver once it hears about it. examples/bank-ui/src/domain/account.ts ```ts .command("sendTransfer", { payload: Schema.Struct({ transferId: Schema.NonEmptyString, to: Schema.NonEmptyString, amount: Amount }), handler: Effect.fnUntraced(function*(state, { amount, to, transferId }, { aggregateId }) { if (state.status !== "open") return yield* new AccountNotOpen({ aggregateId }) if (to === aggregateId) return yield* new SameAccountTransfer() if (state.balance < amount) return yield* new InsufficientFunds({ balance: state.balance, requested: amount }) return AccountEvent.cases.TransferSent.make({ transferId, to, amount }) }) }) .command("receiveTransfer", { payload: Schema.Struct({ transferId: Schema.NonEmptyString, from: Schema.NonEmptyString, amount: Amount }), handler: Effect.fnUntraced(function*(state, { amount, from, transferId }, { aggregateId }) { if (state.status !== "open") return yield* new AccountNotOpen({ aggregateId }) if (state.received.includes(transferId)) return [] return AccountEvent.cases.TransferReceived.make({ transferId, from, amount }) }) }) ``` `receiveTransfer` records the transfers it has credited. If it runs twice for the same transfer, the second time decides on nothing, so the saga below can safely retry. The [saga](/guides/sagas/) starts a workflow for each `TransferSent` event. The workflow sends `receiveTransfer` to the other account, and when that is refused, because the account is closed for instance, it refunds the sender. Each command is an activity, so the engine keeps its outcome: examples/bank-ui/src/engine.ts ```ts /** * A transfer touches two aggregates, so no single command can do it. For each * `TransferSent` the trigger starts a workflow that credits the other account, * and refunds the sender when that is refused because the account isn't open. * Each command is an activity whose outcome the engine keeps, and starting the * same transfer again joins the run already under way. */ const Transfer = Saga.workflow("Transfer", { payload: { transferId: Schema.String, from: Schema.String, to: Schema.String, amount: Schema.Int }, idempotencyKey: ({ transferId }) => transferId }) const TransferWorkflow = Saga.toLayer(Transfer, ({ amount, from, to, transferId }) => Effect.gen(function*() { const bus = yield* Bus yield* Saga.command("credit", { error: AccountNotOpen, execute: bus.run({ aggregateId: to, type: "receiveTransfer", payload: { transferId, from, amount } }, "saga") }).pipe( Effect.catchTag("AccountNotOpen", ({ _tag }) => Saga.command("refund", { execute: bus.run({ aggregateId: from, type: "refundTransfer", payload: { transferId, to, amount, reason: _tag } }, "saga") })) ) // Both commands record `TransferSent` as their cause, in the correlation it started. })) const Transfers = Saga.make({ name: "transfers", aggregates: [Account], handlers: { TransferSent: ({ amount, to, transferId }, stored) => Saga.start(Transfer, { transferId, from: stored.aggregateId, to, amount }) } }) const TransferSaga = Saga.layer([Transfers]).pipe( Layer.provideMerge(TransferWorkflow), Layer.provide(WorkflowEngine.layerMemory) ) ``` Between the two steps the money is in flight. The bank vault, a view model, shows that amount. ## Three view models Each view model folds the same events into a different shape, keeps its own checkpoint, and can be thrown away and rebuilt: * **accounts**: one row per account, with its balance, for the customer app. * **bank-totals**: money in the bank, money in flight, and counts by event type. * **activity-feed**: the latest 40 events, as sentences. Their storage is a plain object outside React and Effect, so the screen shows exactly what they hold. Delivery is at least once, so the accounts view ignores an event whose position it has already applied: examples/bank-ui/src/domain/views.ts ```ts export const AccountsView = ViewModel.make({ name: "accounts", aggregates: [Account], dispose: Effect.sync(() => devstore.dispose("accounts")), handlers: everyEvent( "accounts", (event, { aggregateId, position }) => Effect.sync(() => devstore.apply("accounts", position, (accounts) => { const current = accounts[aggregateId] if (event._tag === "AccountOpened") { return { ...accounts, [aggregateId]: { id: aggregateId, owner: event.owner, color: event.color, balance: 0, status: "open", txCount: 0, lastPosition: position, lastDelta: 0 } } } // Idempotent: an event seen again leaves the row as it is. if (current === undefined || current.lastPosition >= position) return accounts const delta = balanceDelta(event) return { ...accounts, [aggregateId]: { ...current, balance: current.balance + delta, status: event._tag === "AccountClosed" ? "closed" : current.status, txCount: current.txCount + (delta === 0 ? 0 : 1), lastPosition: position, lastDelta: delta } } }) ) ), // The owner's name is personal data: once they are forgotten, the row keeps the // balance and loses the name. onForget: (subject, { position }) => Effect.sync(() => devstore.apply("accounts", position, (accounts) => { const current = accounts[subject] return current === undefined ? accounts : { ...accounts, [subject]: { ...current, owner: forgotten } } }) ) }) ``` `bank-totals` has a switch in the devtools that makes its handler fail. Turn it on and the view model stalls on that event and retries with backoff, while the other two carry on. Turn it off and it catches up, without skipping anything. ## Commands, through `dispatch` Every command in the demo, from the customer app, the saga, the autopilot or the raw JSON console, goes through `dispatch`, the entry point for untyped input: examples/bank-ui/src/engine.ts ```ts // `dispatch` is the real entry point: it validates the JSON envelope and // payload, rehydrates the aggregate, runs the handler and appends. return yield* bank.dispatch(input).pipe( // Recorded on the events beside the app version the service was built with. Metadata.withMetadata({ origin }), Effect.tap((result) => Effect.sync(() => devstore.finish(id, toOutcome(result)))), Effect.tapError((error) => Effect.sync(() => devstore.finish(id, describeError(error)))), Effect.tapDefect((defect) => Effect.sync(() => devstore.finish(id, describeError(defect)))) ) ``` ## Wiring it together The whole engine is a layer: the in-memory store, checkpoints and snapshots, the service, the view models, and the background fibers that mirror events to the devtools, run the saga, poll the view models’ status and the service’s metrics, and send random traffic. The service keeps only four accounts in memory and saves a snapshot every five events, so with more accounts open the **Metrics** tab shows commands starting from snapshots and replays. examples/bank-ui/src/engine.ts ```ts // The keys that encrypt owners' names. A random master key is enough for a store that // lives as long as the page. const Keys = PersonalData.layerMemory().pipe(Layer.provide(PersonalData.layerMasterKeyEphemeral)) const Storage = Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, KeyValueStore.layerMemory, Keys) const AccountState = Schema.Struct({ status: Schema.Literals(["none", "open", "closed"]), owner: Schema.String, color: Schema.String, balance: Schema.Number, received: Schema.Array(Schema.String) }) // Only four accounts stay in memory, so the metrics tab shows loads from snapshots too. const BankService = Layer.effect( Bank, AggregateService.make(Account, { stateCacheSize: 4, snapshots: { every: 5, schema: AccountState } }) ) const Core = Layer.merge(BankService, ViewModel.layer(allViews)).pipe(Layer.provideMerge(Storage)) const WithBus = BusLayer.pipe(Layer.provideMerge(Core)) const Main = Layer.mergeAll(Bridge, TransferSaga, StatusPoll, MetricsPoll, Autopilot).pipe( Layer.provideMerge(WithBus), // Every command records the version of the software that ran it. Layer.provide(Metadata.layer({ appVersion })), // A registry of its own, so a reset counts from zero. Layer.provideMerge(Layer.sync(Metric.MetricRegistry, () => new Map())), // Every span reports to the devtools' trace tab as it starts, changes and ends. Layer.provideMerge(Layer.succeed(Tracer.Tracer, makeRecordingTracer(devstore.span))) ) ``` A `ManagedRuntime` runs it, and React reaches it through plain functions. **Reset store** disposes the runtime and starts a new one. ## Forgetting an owner The owner’s name is [personal data](/guides/personal-data/): `AccountOpened` stores it encrypted with a key of the account’s own, which the **Event store** tab shows as a `$personal` block. **forget** on a card destroys that key: examples/bank-ui/src/engine.ts ```ts /** * Forgets the owner of an account: the key that encrypts their name is * destroyed. Stored events stay as they were, view models erase the name in * `onForget`, and the events tab decodes the log again to show the replacement. */ forget: (subject: string) => runtime.runPromise(Effect.gen(function*() { yield* PersonalData.forget(subject).pipe(Metadata.withMetadata({ origin: "app" })) const decode = AggregateService.makeEventDecoder(Account, yield* PersonalData.Keys) const entries = devstore.get().events.filter((entry) => entry.aggregateId === subject) const decoded = yield* Effect.forEach(entries, (entry) => decode(entry.stored)) devstore.redecoded(new Map(entries.map((entry, index) => [entry.position, decoded[index]!]))) })), ``` The log doesn’t change and the totals still add up, while the card, the feed and every replay read `[forgotten]`. The accounts view erases the name in `onForget`, and new commands on the account still work, since deposits hold no personal data. ## The devtools | Tab | What you can do | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Commands | Watch each command through validating, loading, deciding and appending. Slow those stages down, pause the bus and step it, run the autopilot, and send raw JSON to `dispatch` | | Event store | Read the log as it grows | | Projections | See each view model’s checkpoint and lag, crash `bank-totals`, and rebuild one | | Time travel | Scrub through the log: the customer app becomes a preview, where each account is its events up to the cursor folded with the aggregate’s projection. Replay view models up to a chosen event, or entirely | | Trace | Follow every span Effect opens, recorded by a `Tracer` of the demo’s own: each command on the bus with the stages it waited in, the aggregate’s span and `EventStore.append`, and for a transfer the saga’s workflow and its activities | | Metrics | Watch the service’s [metrics](/guides/commands/#metrics): where each load found its state, the events it read, the states in memory and those dropped, the snapshots saved, and how long each command took by outcome | Note Replaying up to an event uses the `finishTime` of `viewModels.replay`, since the replay API takes a time window rather than a position. ## Source [examples/bank-ui on GitHub](https://github.com/davidnussio/effect-cqrs/blob/main/examples/bank-ui) # The shop > The first example, built step by step: a shop with orders, stock and a saga with a timeout, then metadata, metrics and event versions. [Open the shop demo](/shop-demo/)The same shop in your browser, with its traces, its saga's state, the stored rows and the metrics. The first of three examples, and the one to start from: it builds each piece of the framework in turn. [The bank](/tutorial/bank/) and [the workshop](/tutorial/workshop/) then put them together in bigger apps. We build a shop: customers place **orders**, and a **saga** holds the **stock** for each one and gives the customer 15 seconds to pay. It confirms a paid order, and rejects one that is out of stock or not paid in time. Every snippet comes from [the full example](#the-full-example), which you can run with `pnpm tutorial`. ## 1. Events Events are what happened. Each aggregate declares its own as a tagged union: examples/tutorial/shop.ts ```ts const Quantity = Schema.Int.check(Schema.isGreaterThan(0)) export const StockEvent = Schema.TaggedUnion({ StockAdded: { quantity: Quantity }, StockReserved: { orderId: Schema.String, quantity: Quantity }, StockReleased: { orderId: Schema.String, quantity: Quantity } }) export const OrderEvent = Schema.TaggedUnion({ OrderPlaced: { sku: Schema.String, quantity: Quantity }, // The saga asks for the payment. The token names the saga's run that waits for it. PaymentRequested: { token: DurableDeferred.Token }, OrderPaid: { token: DurableDeferred.Token }, OrderConfirmed: {}, OrderRejected: { reason: Schema.String } }) ``` ## 2. Aggregates and commands An aggregate folds its events into a state with `projection`, and each command looks at that state and returns new events, or fails: examples/tutorial/shop.ts ```ts export class OutOfStock extends Schema.TaggedError()("OutOfStock", { sku: Schema.String, available: Schema.Int }) {} export class OrderAlreadyPlaced extends Schema.TaggedError()("OrderAlreadyPlaced", { orderId: Schema.String }) {} export class OrderNotPayable extends Schema.TaggedError()("OrderNotPayable", { orderId: Schema.String, status: Schema.String }) {} export class OrderAlreadyPaid extends Schema.TaggedError()("OrderAlreadyPaid", { orderId: Schema.String }) {} ``` examples/tutorial/shop.ts ```ts export interface StockState { readonly available: number /** What each order holds, by order id. */ readonly reserved: Readonly> } export const Stock = Aggregate.make()({ name: "stock", events: StockEvent, initial: { available: 0, reserved: {} }, projection: { StockAdded: (state, { quantity }) => ({ ...state, available: state.available + quantity }), StockReserved: (state, { orderId, quantity }) => ({ available: state.available - quantity, reserved: { ...state.reserved, [orderId]: quantity } }), StockReleased: (state, { orderId, quantity }) => { const { [orderId]: _, ...reserved } = state.reserved return { available: state.available + quantity, reserved } } } }) .command("add", { payload: Schema.Struct({ quantity: Quantity }), handler: (_, { quantity }) => Effect.succeed(StockEvent.cases.StockAdded.make({ quantity })) }) .command("reserve", { payload: Schema.Struct({ orderId: Schema.String, quantity: Quantity }), handler: Effect.fnUntraced(function*(state, { orderId, quantity }, { aggregateId }) { // A saga may run this twice: the second time records nothing. if (state.reserved[orderId] !== undefined) return [] if (state.available < quantity) return yield* new OutOfStock({ sku: aggregateId, available: state.available }) return StockEvent.cases.StockReserved.make({ orderId, quantity }) }) }) .command("release", { payload: Schema.Struct({ orderId: Schema.String }), handler: (state, { orderId }) => { const quantity = state.reserved[orderId] return Effect.succeed(quantity === undefined ? [] : StockEvent.cases.StockReleased.make({ orderId, quantity })) } }) ``` examples/tutorial/shop.ts ```ts export interface OrderState { readonly status: "none" | "placed" | "awaiting-payment" | "paid" | "confirmed" | "rejected" readonly sku: string readonly quantity: number readonly payment: DurableDeferred.Token | null } export const Order = Aggregate.make()({ name: "order", events: OrderEvent, initial: { status: "none", sku: "", quantity: 0, payment: null }, projection: { OrderPlaced: (_, { quantity, sku }) => ({ status: "placed", sku, quantity, payment: null }), PaymentRequested: (state, { token }) => ({ ...state, status: "awaiting-payment", payment: token }), OrderPaid: (state) => ({ ...state, status: "paid" }), OrderConfirmed: (state) => ({ ...state, status: "confirmed" }), OrderRejected: (state) => ({ ...state, status: "rejected" }) }, versions: orderVersions }) .command("place", { payload: Schema.Struct({ sku: Schema.String, quantity: Quantity }), handler: Effect.fnUntraced(function*(state, { quantity, sku }, { aggregateId }) { if (state.status !== "none") return yield* new OrderAlreadyPlaced({ orderId: aggregateId }) return OrderEvent.cases.OrderPlaced.make({ sku, quantity }) }) }) .command("requestPayment", { payload: Schema.Struct({ token: DurableDeferred.Token }), handler: (state, { token }) => Effect.succeed(state.status === "placed" ? OrderEvent.cases.PaymentRequested.make({ token }) : []) }) // The customer pays, and only while the saga waits for it. .command("pay", { payload: Schema.Struct({}), handler: Effect.fnUntraced(function*(state, _, { aggregateId }) { if (state.status === "paid" || state.status === "confirmed") return [] if (state.status !== "awaiting-payment" || state.payment === null) { return yield* new OrderNotPayable({ orderId: aggregateId, status: state.status }) } return OrderEvent.cases.OrderPaid.make({ token: state.payment }) }) }) .command("confirm", { payload: Schema.Struct({}), handler: (state) => Effect.succeed(state.status === "paid" ? OrderEvent.cases.OrderConfirmed.make({}) : []) }) .command("reject", { payload: Schema.Struct({ reason: Schema.String }), handler: Effect.fnUntraced(function*(state, { reason }, { aggregateId }) { if (state.status === "paid") return yield* new OrderAlreadyPaid({ orderId: aggregateId }) if (state.status !== "placed" && state.status !== "awaiting-payment") return [] return OrderEvent.cases.OrderRejected.make({ reason }) }) }) ``` Returning `[]` records nothing, which makes the commands a saga runs safe to repeat. `requestPayment` and `pay` take part in the saga’s payment step in [step 5](#5-a-saga), and `versions` comes in [step 8](#8-event-versioning). ## 3. Running commands `AggregateService.make` turns an aggregate into a service with one typed function per command: examples/tutorial/shop.ts ```ts export class Orders extends Context.Service>()("shop/Orders") { static readonly layer = Layer.effect(Orders, AggregateService.make(Order)) } export class Stocks extends Context.Service>()("shop/Stocks") { static readonly layer = Layer.effect(Stocks, AggregateService.make(Stock)) } ``` examples/tutorial/shop.ts ```ts yield* stocks.commands.add("apple", { quantity: 5 }) yield* orders.commands.place("order-1", { sku: "apple", quantity: 3 }) // The saga holds the stock and asks for the payment: the customer has 15 seconds to pay. yield* reached("order-1", "PaymentRequested", "OrderRejected") yield* orders.commands.pay("order-1", {}) yield* settled("order-1") const rejected = yield* Effect.flip(orders.commands.place("order-1", { sku: "apple", quantity: 1 })) yield* Effect.log("placing order-1 again fails with", rejected._tag) ``` A refused command fails with its typed error and stores nothing. `pay` is the customer’s side of the saga’s payment step: the order only takes it while the saga is waiting for it. ## 4. A view model A view model reads events into whatever a query needs, here a list of orders: examples/tutorial/shop.ts ```ts export class OrderList extends Context.Service>>()("shop/OrderList") { static readonly layer = Layer.effect(OrderList, Ref.make>(new Map())) } const setStatus = (orderId: string, status: string) => OrderList.use((list) => Ref.update(list, (orders) => new Map(orders).set(orderId, status))) export const OrderListView = ViewModel.make({ name: "order-list", aggregates: [Order], dispose: OrderList.use((list) => Ref.set(list, new Map())), handlers: { OrderPlaced: ({ quantity, sku }, { aggregateId }) => setStatus(aggregateId, `placed ${quantity} ${sku}`), PaymentRequested: (_, { aggregateId }) => setStatus(aggregateId, "awaiting payment"), OrderPaid: (_, { aggregateId }) => setStatus(aggregateId, "paid"), OrderConfirmed: (_, { aggregateId }) => setStatus(aggregateId, "confirmed"), OrderRejected: ({ reason }, { aggregateId }) => setStatus(aggregateId, `rejected: ${reason}`) } }) ``` ## 5. A saga A saga reacts to an event and runs commands on other aggregates. The trigger starts a workflow for each `OrderPlaced`, and each command in it is a step the engine remembers: examples/tutorial/shop.ts ```ts /** How long a customer has to pay once the stock is held. */ export const paymentTimeout = Duration.seconds(15) // The customer's payment, which a run of the saga waits for. export const Payment = DurableDeferred.make("Payment") // One run per order: hold the stock, ask for the payment and wait for it, then // confirm the order, or reject it and put the stock back. export const Fulfil = Saga.workflow("Fulfil", { payload: { orderId: Schema.String, sku: Schema.String, quantity: Schema.Int }, idempotencyKey: ({ orderId }) => orderId }) export const FulfilWorkflow = Saga.toLayer(Fulfil, ({ orderId, quantity, sku }) => Effect.gen(function*() { const orders = yield* Orders const stocks = yield* Stocks // 1. Hold the stock, or turn the order down. const reserved = yield* Saga.command("reserve", { error: OutOfStock, execute: stocks.commands.reserve(sku, { orderId, quantity }) }).pipe( Effect.as(true), Effect.catchTag("OutOfStock", ({ available }) => Saga.command("reject", { execute: orders.commands.reject(orderId, { reason: `only ${available} left` }) }).pipe(Effect.as(false))) ) if (!reserved) return // 2. Ask for the payment, then wait for whichever comes first: the payment or the deadline. const token = yield* DurableDeferred.token(Payment) yield* Saga.command("request-payment", { execute: orders.commands.requestPayment(orderId, { token }) }) const paid = yield* DurableDeferred.raceAll({ name: "payment", success: Schema.Boolean, error: Schema.Never, effects: [ DurableDeferred.await(Payment).pipe(Effect.as(true)), DurableClock.sleep({ name: "payment-timeout", duration: paymentTimeout }).pipe(Effect.as(false)) ] }) // 3. Too late: reject the order and give the stock back. The order refuses if the // customer paid just as the time ran out, and the saga confirms it instead. if (!paid) { const expired = yield* Saga.command("expire", { error: OrderAlreadyPaid, execute: orders.commands.reject(orderId, { reason: "not paid in time" }) }).pipe( Effect.as(true), Effect.catchTag("OrderAlreadyPaid", () => Effect.succeed(false)) ) if (expired) { yield* Saga.command("release", { execute: stocks.commands.release(sku, { orderId }) }) return } } yield* Saga.command("confirm", { execute: orders.commands.confirm(orderId, {}) }) })) // The trigger: starts a run for every order placed, and wakes it when the order is paid. export const Fulfilment = Saga.make({ name: "fulfilment", aggregates: [Order], handlers: { OrderPlaced: ({ quantity, sku }, { aggregateId }) => Saga.start(Fulfil, { orderId: aggregateId, sku, quantity }), OrderPaid: ({ token }) => DurableDeferred.succeed(Payment, { token, value: undefined }) } }) ``` The run waits for the payment in the middle of the process: * `DurableDeferred.token` names this run’s `Payment`. The saga stores it in `PaymentRequested`, the order copies it into `OrderPaid`, and the trigger hands it to `DurableDeferred.succeed`, which wakes the run. * `DurableDeferred.raceAll` races the payment against `DurableClock.sleep` and saves the winner, so a replay takes the same branch. * When the time runs out, the saga rejects the order and releases the stock. If the customer paid just before, the order refuses with `OrderAlreadyPaid`, and the saga confirms it instead. Each aggregate decides alone, so the order settles any race between the two. `DurableClock.sleep` keeps a minute or less in memory. A longer timeout becomes a clock the engine schedules, which a durable engine keeps across restarts. ## 6. Wiring Everything is a layer. Here all of it runs in memory: examples/tutorial/shop.ts ```ts const Storage = Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, OrderList.layer) const Sagas = Saga.layer([Fulfilment]).pipe( Layer.provideMerge(FulfilWorkflow), Layer.provide(WorkflowEngine.layerMemory) ) export const Shop = Layer.mergeAll(Sagas, ViewModel.layer([OrderListView])).pipe( Layer.provideMerge(Layer.merge(Orders.layer, Stocks.layer)), Layer.provideMerge(Storage), // Every command records the version of the software that ran it. Layer.provide(Metadata.layer({ appVersion: "1.0.0" })) ) ``` ## 7. Metadata Metadata is what a command carries beside its payload: the app version, the user, the channel. `Metadata.layer` above sets the version for every command, and `Metadata.withMetadata` adds to it for one call: examples/tutorial/shop.ts ```ts yield* orders.commands.place("order-2", { sku: "apple", quantity: 3 }).pipe( Metadata.withMetadata({ userId: "bob", channel: "mobile" }) ) yield* settled("order-2") const history = yield* orders.history("order-2") yield* Effect.log("order-2 history", history.map(({ metadata, type }) => ({ type, metadata }))) ``` ```txt order-2 history [ { type: 'OrderPlaced', metadata: { appVersion: '1.0.0', userId: 'bob', channel: 'mobile' } }, { type: 'OrderRejected', metadata: { appVersion: '1.0.0' } } ] ``` The saga’s `OrderRejected` doesn’t inherit `userId`: a reaction records its own metadata. ## 8. Event versioning Stored events never change. When an event’s schema does, declare the old shape and how to upcast it: examples/tutorial/shop.ts ```ts // Before quantities existed, an order was always for one item. const OrderPlacedV1 = Schema.Struct({ sku: Schema.String }) const orderVersions = { OrderPlaced: EventVersions.from(OrderPlacedV1).next( OrderEvent.cases.OrderPlaced, (v1) => ({ ...v1, quantity: 1 }) ) } ``` An order stored before quantities existed is read as an order for one item, by the projection, the view model and the saga alike: examples/tutorial/shop.ts ```ts // An order the previous version of the shop stored, before quantities existed. yield* EventStore.EventStore.use((store) => store.append({ aggregateName: "order", aggregateId: "order-0", expectedVersion: 0, events: [{ type: "OrderPlaced", schemaVersion: 1, payload: { sku: "pear" } }] }) ) yield* settled("order-0") const legacy = yield* orders.state("order-0") yield* Effect.log("order-0 is read as", legacy) ``` ```txt order-0 is read as { status: 'rejected', sku: 'pear', quantity: 1, payment: null } ``` ## 9. Metrics Every aggregate service records metrics with Effect’s `Metric` module. Read them with `Metric.snapshot`, or send them to an exporter: examples/tutorial/shop.ts ```ts // How many `place` commands ran, and how long they took, by outcome. const timings = (yield* Metric.snapshot).flatMap((metric) => metric.id === AggregateService.MetricNames.commandDuration && metric.type === "Histogram" && metric.attributes?.["aggregate"] === "order" && metric.attributes["command"] === "place" ? [{ outcome: metric.attributes["outcome"], count: metric.state.count, totalMs: metric.state.sum }] : [] ) yield* Effect.log("order.place timings", timings) ``` ```txt order.place timings [ { outcome: 'success', count: 2, totalMs: 0.806 }, { outcome: 'OrderAlreadyPlaced', count: 1, totalMs: 0.273 } ] ``` ## The full example examples/tutorial/shop.ts ```ts /** * The tutorial's shop: a saga holds the stock for each order and gives the * customer 15 seconds to pay. It uses commands, events, a view model, a saga with * a timeout, metadata, metrics and an event version. Run it with `pnpm tutorial`. */ import { Context, Duration, Effect, Layer, Metric, Option, Ref, Schema, Stream } from "effect" import { Aggregate, AggregateService, Checkpoints, EventStore, EventVersions, Metadata, Saga, ViewModel } from "effect-cqrs" import { DurableClock, DurableDeferred, WorkflowEngine } from "effect/workflow" const Quantity = Schema.Int.check(Schema.isGreaterThan(0)) export const StockEvent = Schema.TaggedUnion({ StockAdded: { quantity: Quantity }, StockReserved: { orderId: Schema.String, quantity: Quantity }, StockReleased: { orderId: Schema.String, quantity: Quantity } }) export const OrderEvent = Schema.TaggedUnion({ OrderPlaced: { sku: Schema.String, quantity: Quantity }, // The saga asks for the payment. The token names the saga's run that waits for it. PaymentRequested: { token: DurableDeferred.Token }, OrderPaid: { token: DurableDeferred.Token }, OrderConfirmed: {}, OrderRejected: { reason: Schema.String } }) export class OutOfStock extends Schema.TaggedError()("OutOfStock", { sku: Schema.String, available: Schema.Int }) {} export class OrderAlreadyPlaced extends Schema.TaggedError()("OrderAlreadyPlaced", { orderId: Schema.String }) {} export class OrderNotPayable extends Schema.TaggedError()("OrderNotPayable", { orderId: Schema.String, status: Schema.String }) {} export class OrderAlreadyPaid extends Schema.TaggedError()("OrderAlreadyPaid", { orderId: Schema.String }) {} export interface StockState { readonly available: number /** What each order holds, by order id. */ readonly reserved: Readonly> } export const Stock = Aggregate.make()({ name: "stock", events: StockEvent, initial: { available: 0, reserved: {} }, projection: { StockAdded: (state, { quantity }) => ({ ...state, available: state.available + quantity }), StockReserved: (state, { orderId, quantity }) => ({ available: state.available - quantity, reserved: { ...state.reserved, [orderId]: quantity } }), StockReleased: (state, { orderId, quantity }) => { const { [orderId]: _, ...reserved } = state.reserved return { available: state.available + quantity, reserved } } } }) .command("add", { payload: Schema.Struct({ quantity: Quantity }), handler: (_, { quantity }) => Effect.succeed(StockEvent.cases.StockAdded.make({ quantity })) }) .command("reserve", { payload: Schema.Struct({ orderId: Schema.String, quantity: Quantity }), handler: Effect.fnUntraced(function*(state, { orderId, quantity }, { aggregateId }) { // A saga may run this twice: the second time records nothing. if (state.reserved[orderId] !== undefined) return [] if (state.available < quantity) return yield* new OutOfStock({ sku: aggregateId, available: state.available }) return StockEvent.cases.StockReserved.make({ orderId, quantity }) }) }) .command("release", { payload: Schema.Struct({ orderId: Schema.String }), handler: (state, { orderId }) => { const quantity = state.reserved[orderId] return Effect.succeed(quantity === undefined ? [] : StockEvent.cases.StockReleased.make({ orderId, quantity })) } }) // Before quantities existed, an order was always for one item. const OrderPlacedV1 = Schema.Struct({ sku: Schema.String }) const orderVersions = { OrderPlaced: EventVersions.from(OrderPlacedV1).next( OrderEvent.cases.OrderPlaced, (v1) => ({ ...v1, quantity: 1 }) ) } export interface OrderState { readonly status: "none" | "placed" | "awaiting-payment" | "paid" | "confirmed" | "rejected" readonly sku: string readonly quantity: number readonly payment: DurableDeferred.Token | null } export const Order = Aggregate.make()({ name: "order", events: OrderEvent, initial: { status: "none", sku: "", quantity: 0, payment: null }, projection: { OrderPlaced: (_, { quantity, sku }) => ({ status: "placed", sku, quantity, payment: null }), PaymentRequested: (state, { token }) => ({ ...state, status: "awaiting-payment", payment: token }), OrderPaid: (state) => ({ ...state, status: "paid" }), OrderConfirmed: (state) => ({ ...state, status: "confirmed" }), OrderRejected: (state) => ({ ...state, status: "rejected" }) }, versions: orderVersions }) .command("place", { payload: Schema.Struct({ sku: Schema.String, quantity: Quantity }), handler: Effect.fnUntraced(function*(state, { quantity, sku }, { aggregateId }) { if (state.status !== "none") return yield* new OrderAlreadyPlaced({ orderId: aggregateId }) return OrderEvent.cases.OrderPlaced.make({ sku, quantity }) }) }) .command("requestPayment", { payload: Schema.Struct({ token: DurableDeferred.Token }), handler: (state, { token }) => Effect.succeed(state.status === "placed" ? OrderEvent.cases.PaymentRequested.make({ token }) : []) }) // The customer pays, and only while the saga waits for it. .command("pay", { payload: Schema.Struct({}), handler: Effect.fnUntraced(function*(state, _, { aggregateId }) { if (state.status === "paid" || state.status === "confirmed") return [] if (state.status !== "awaiting-payment" || state.payment === null) { return yield* new OrderNotPayable({ orderId: aggregateId, status: state.status }) } return OrderEvent.cases.OrderPaid.make({ token: state.payment }) }) }) .command("confirm", { payload: Schema.Struct({}), handler: (state) => Effect.succeed(state.status === "paid" ? OrderEvent.cases.OrderConfirmed.make({}) : []) }) .command("reject", { payload: Schema.Struct({ reason: Schema.String }), handler: Effect.fnUntraced(function*(state, { reason }, { aggregateId }) { if (state.status === "paid") return yield* new OrderAlreadyPaid({ orderId: aggregateId }) if (state.status !== "placed" && state.status !== "awaiting-payment") return [] return OrderEvent.cases.OrderRejected.make({ reason }) }) }) export class Orders extends Context.Service>()("shop/Orders") { static readonly layer = Layer.effect(Orders, AggregateService.make(Order)) } export class Stocks extends Context.Service>()("shop/Stocks") { static readonly layer = Layer.effect(Stocks, AggregateService.make(Stock)) } export class OrderList extends Context.Service>>()("shop/OrderList") { static readonly layer = Layer.effect(OrderList, Ref.make>(new Map())) } const setStatus = (orderId: string, status: string) => OrderList.use((list) => Ref.update(list, (orders) => new Map(orders).set(orderId, status))) export const OrderListView = ViewModel.make({ name: "order-list", aggregates: [Order], dispose: OrderList.use((list) => Ref.set(list, new Map())), handlers: { OrderPlaced: ({ quantity, sku }, { aggregateId }) => setStatus(aggregateId, `placed ${quantity} ${sku}`), PaymentRequested: (_, { aggregateId }) => setStatus(aggregateId, "awaiting payment"), OrderPaid: (_, { aggregateId }) => setStatus(aggregateId, "paid"), OrderConfirmed: (_, { aggregateId }) => setStatus(aggregateId, "confirmed"), OrderRejected: ({ reason }, { aggregateId }) => setStatus(aggregateId, `rejected: ${reason}`) } }) /** How long a customer has to pay once the stock is held. */ export const paymentTimeout = Duration.seconds(15) // The customer's payment, which a run of the saga waits for. export const Payment = DurableDeferred.make("Payment") // One run per order: hold the stock, ask for the payment and wait for it, then // confirm the order, or reject it and put the stock back. export const Fulfil = Saga.workflow("Fulfil", { payload: { orderId: Schema.String, sku: Schema.String, quantity: Schema.Int }, idempotencyKey: ({ orderId }) => orderId }) export const FulfilWorkflow = Saga.toLayer(Fulfil, ({ orderId, quantity, sku }) => Effect.gen(function*() { const orders = yield* Orders const stocks = yield* Stocks // 1. Hold the stock, or turn the order down. const reserved = yield* Saga.command("reserve", { error: OutOfStock, execute: stocks.commands.reserve(sku, { orderId, quantity }) }).pipe( Effect.as(true), Effect.catchTag("OutOfStock", ({ available }) => Saga.command("reject", { execute: orders.commands.reject(orderId, { reason: `only ${available} left` }) }).pipe(Effect.as(false))) ) if (!reserved) return // 2. Ask for the payment, then wait for whichever comes first: the payment or the deadline. const token = yield* DurableDeferred.token(Payment) yield* Saga.command("request-payment", { execute: orders.commands.requestPayment(orderId, { token }) }) const paid = yield* DurableDeferred.raceAll({ name: "payment", success: Schema.Boolean, error: Schema.Never, effects: [ DurableDeferred.await(Payment).pipe(Effect.as(true)), DurableClock.sleep({ name: "payment-timeout", duration: paymentTimeout }).pipe(Effect.as(false)) ] }) // 3. Too late: reject the order and give the stock back. The order refuses if the // customer paid just as the time ran out, and the saga confirms it instead. if (!paid) { const expired = yield* Saga.command("expire", { error: OrderAlreadyPaid, execute: orders.commands.reject(orderId, { reason: "not paid in time" }) }).pipe( Effect.as(true), Effect.catchTag("OrderAlreadyPaid", () => Effect.succeed(false)) ) if (expired) { yield* Saga.command("release", { execute: stocks.commands.release(sku, { orderId }) }) return } } yield* Saga.command("confirm", { execute: orders.commands.confirm(orderId, {}) }) })) // The trigger: starts a run for every order placed, and wakes it when the order is paid. export const Fulfilment = Saga.make({ name: "fulfilment", aggregates: [Order], handlers: { OrderPlaced: ({ quantity, sku }, { aggregateId }) => Saga.start(Fulfil, { orderId: aggregateId, sku, quantity }), OrderPaid: ({ token }) => DurableDeferred.succeed(Payment, { token, value: undefined }) } }) const Storage = Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, OrderList.layer) const Sagas = Saga.layer([Fulfilment]).pipe( Layer.provideMerge(FulfilWorkflow), Layer.provide(WorkflowEngine.layerMemory) ) export const Shop = Layer.mergeAll(Sagas, ViewModel.layer([OrderListView])).pipe( Layer.provideMerge(Layer.merge(Orders.layer, Stocks.layer)), Layer.provideMerge(Storage), // Every command records the version of the software that ran it. Layer.provide(Metadata.layer({ appVersion: "1.0.0" })) ) /** Waits until one of these events is stored for an order, and the view model has seen it. */ export const reached = (orderId: string, ...eventTypes: ReadonlyArray) => Effect.gen(function*() { const store = yield* EventStore.EventStore const viewModels = yield* ViewModel.ViewModels const done = yield* store.subscribe({ aggregateIds: [orderId], eventTypes }).pipe(Stream.runHead) if (Option.isSome(done)) yield* viewModels.caughtUp(done.value.position) }) /** Waits until the saga has confirmed or rejected an order. */ export const settled = (orderId: string) => reached(orderId, "OrderConfirmed", "OrderRejected") export const program = Effect.gen(function*() { const orders = yield* Orders const stocks = yield* Stocks // An order the previous version of the shop stored, before quantities existed. yield* EventStore.EventStore.use((store) => store.append({ aggregateName: "order", aggregateId: "order-0", expectedVersion: 0, events: [{ type: "OrderPlaced", schemaVersion: 1, payload: { sku: "pear" } }] }) ) yield* settled("order-0") const legacy = yield* orders.state("order-0") yield* Effect.log("order-0 is read as", legacy) yield* stocks.commands.add("apple", { quantity: 5 }) yield* orders.commands.place("order-1", { sku: "apple", quantity: 3 }) // The saga holds the stock and asks for the payment: the customer has 15 seconds to pay. yield* reached("order-1", "PaymentRequested", "OrderRejected") yield* orders.commands.pay("order-1", {}) yield* settled("order-1") const rejected = yield* Effect.flip(orders.commands.place("order-1", { sku: "apple", quantity: 1 })) yield* Effect.log("placing order-1 again fails with", rejected._tag) yield* orders.commands.place("order-2", { sku: "apple", quantity: 3 }).pipe( Metadata.withMetadata({ userId: "bob", channel: "mobile" }) ) yield* settled("order-2") const history = yield* orders.history("order-2") yield* Effect.log("order-2 history", history.map(({ metadata, type }) => ({ type, metadata }))) const list = yield* OrderList.use(Ref.get) yield* Effect.log("order list", Object.fromEntries(list)) const stock = yield* stocks.state("apple") yield* Effect.log("apple stock", stock) // How many `place` commands ran, and how long they took, by outcome. const timings = (yield* Metric.snapshot).flatMap((metric) => metric.id === AggregateService.MetricNames.commandDuration && metric.type === "Histogram" && metric.attributes?.["aggregate"] === "order" && metric.attributes["command"] === "place" ? [{ outcome: metric.attributes["outcome"], count: metric.state.count, totalMs: metric.state.sum }] : [] ) yield* Effect.log("order.place timings", timings) return { orders: Object.fromEntries(list), stock, legacy, rejected: rejected._tag, history, timings } }) ``` examples/tutorial/main.ts ```ts /** Runs the tutorial's shop on the in-memory store: `pnpm tutorial`. */ import { NodeRuntime } from "@effect/platform-node" import { Effect } from "effect" import { program, Shop } from "./shop.ts" program.pipe(Effect.provide(Shop), NodeRuntime.runMain) ``` ```bash pnpm tutorial ``` Next, [the bank](/tutorial/bank/) puts the same pieces in an app you can poke at, and the guides go deeper into [aggregates](/guides/aggregates/), [commands](/guides/commands/), [view models](/guides/view-models/) and [sagas](/guides/sagas/). # The workshop > The third example: a bank and a shop on a real server, for a room full of phones and a projector. [Run the workshop](https://github.com/davidnussio/effect-cqrs/blob/main/examples/workshop/README.md)It needs a server, so there is no copy on this site. Its README says how to run it and put it online on free plans. The third example. [The bank](/tutorial/bank/) runs in the browser, for one person. The workshop runs the framework on a server, for a room: participants join from their phones, and a control room on the projector shows what the system does while they use it. Its code is in [`examples/workshop`](https://github.com/davidnussio/effect-cqrs/tree/main/examples/workshop). ## What the participants do * They join with a nickname, which is personal data: the framework encrypts it with a key of their own. * They add money to their account and send it to each other. A transfer touches two accounts, so a saga completes it, and refunds the sender when the other account has closed. * They buy from a shared catalog with real stock, paying from their account. When a product is sold out they can wait in its queue, and cancel for a refund until the goods arrive. * They close their account: the balance is paid out, and the key of their nickname is destroyed. The events stay, and the nickname reads `[forgotten]`. A bot restocks the shop, and simulated shoppers keep it busy when the room is quiet. Both go through the same aggregate services as the HTTP API and sign their events. ## Two orders, one piece The queue lives in the product aggregate. Restocking hands goods to the waiting orders in the same command, in order of arrival, so two orders can’t get the same piece, and while a queue exists newcomers join it rather than take goods ahead of it: examples/workshop/domain/shop.ts ```ts /** * Hands the available goods to the queue in order. The first order that doesn't * fit stops it: a small order behind it doesn't jump the queue. */ const allocate = (available: number, queue: ReadonlyArray): ReadonlyArray => { const events: Array = [] for (const waiting of queue) { if (waiting.quantity > available) break available -= waiting.quantity events.push(ProductEvent.cases.BackorderAllocated.make(waiting)) } return events } ``` An order touches the account, the product and itself, so a saga carries it. Each step does nothing the second time, so the saga can run again after a restart. The presenter can make the next order break after it is paid, and the room watches the saga give the money back: examples/workshop/domain/shop.ts ```ts const CheckoutWorkflow = Saga.toLayer( Checkout, ({ backorder, customer, orderId, productId, quantity, total }) => Effect.gen(function*() { const accounts = yield* Accounts const products = yield* Products const orders = yield* Orders const chaos = yield* Chaos const reject = (reason: string) => Saga.command("reject", { execute: orders.commands.reject(orderId, { reason }) }) const refund = (reason: string) => Saga.command("refund", { execute: accounts.commands.refundOrder(customer, { orderId, amount: total, reason }) }) const paid = yield* Saga.command("pay", { error: Schema.Union([InsufficientFunds, AccountNotOpen]), execute: accounts.commands.payOrder(customer, { orderId, amount: total }) }).pipe( Effect.as(true), Effect.catch((error) => reject(error._tag === "InsufficientFunds" ? reasons.insufficientFunds : reasons.accountClosed).pipe( Effect.as(false) ) ) ) if (!paid) return yield* Saga.command("mark-paid", { execute: orders.commands.markPaid(orderId, {}) }) // An activity, so a saga that runs again after a restart decides the same way. const broken = yield* Activity.make({ name: "warehouse", success: Schema.Boolean, execute: chaos.consume }) if (broken) { yield* refund(reasons.warehouse) return yield* reject(reasons.warehouse) } const taken = yield* Saga.command("take", { error: OutOfStock, execute: products.commands.take(productId, { orderId, quantity }) }).pipe(Effect.as(true), Effect.catchTag("OutOfStock", () => Effect.succeed(false))) if (taken) { yield* Saga.command("fulfil", { execute: orders.commands.fulfil(orderId, {}) }) } else if (backorder) { yield* Saga.command("enqueue", { execute: products.commands.enqueue(productId, { orderId, quantity }) }) // If goods were there already, the order is fulfilled by now and this does nothing. yield* Saga.command("mark-backordered", { execute: orders.commands.markBackordered(orderId, {}) }) } else { yield* refund(reasons.outOfStock) yield* reject(reasons.outOfStock) } }) ) ``` Note The tests in [`examples/workshop/workshop.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/workshop/workshop.test.ts) send twenty buyers after the last piece at once: one gets it, nineteen get their money back. ## Reading your own writes The screens read view models, which run a moment behind the commands. Every command the UI sends answers with the position of its last event, and the reads that follow it, the movements, the orders, the participants and the catalog, send it back in a `Read-After` header. [`CommandApi.ReadAfter`](/guides/integration/#reading-your-own-writes) holds each of those reads until the view models have handled that position, so a participant who buys something sees the order at once, while the other phones catch up on their own a moment later. When the view models are more than five seconds behind, because the room is busy or a handler keeps failing, the server answers `503` with `NotCaughtUp`. The UI then reads again without the header, and shows a screen that is a little old rather than an error. ## The control room The server follows the log once and sends each event on two Server-Sent Events streams. The participants’ stream hides personal fields; the presenter’s stream shows them decrypted, next to the payload as the store keeps it, encrypted. Each event carries who caused it and the correlation of the request it follows from, so a saga’s steps line up behind the click that started them. The control room also checks that the money adds up. The participants’ balances come from one view model, the books from another, both folded from the same events. When the two are at the same position their totals must agree, and while one is behind the room says so instead of guessing: eventual consistency on the projector. Everything the streams send is described in an AsyncAPI 3 document, built from the aggregates with `EventCatalog.document`, and the REST API in OpenAPI, built from the same `HttpApi` the UI calls through a typed client. See the [source](https://github.com/davidnussio/effect-cqrs/tree/main/examples/workshop) and its [`README`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/workshop/README.md). # Using with AI > What to give a coding agent or a chat so it writes effect-cqrs code that compiles. effect-cqrs is young and built on Effect 4, so a model’s training data knows little about either. Give it the documentation instead. ## A guide for coding agents The package ships `LLMS.md`, a short guide to the modules, the patterns to follow, the mistakes to avoid and how to test. Point your agent at it from `AGENTS.md` or `CLAUDE.md`: ```md Before writing code that uses effect-cqrs, read node_modules/effect-cqrs/LLMS.md. ``` It is also [on GitHub](https://github.com/davidnussio/effect-cqrs/blob/main/LLMS.md). Effect has [its own guide](https://github.com/Effect-TS/effect/blob/main/LLMS.md) for the code around it. ## The documentation as text The site follows the [llms.txt](https://llmstxt.org/) convention: | File | What it holds | | ----------------------------------- | --------------------------------------------------------------------------- | | [`llms.txt`](/llms.txt) | An index of the files below | | [`llms-full.txt`](/llms-full.txt) | Every page, the API reference included, as one Markdown file | | [`llms-small.txt`](/llms-small.txt) | The guides without asides or the API reference, for smaller context windows | Each page is also served as Markdown: replace the trailing slash of its URL with `.md`, as in [`guides/aggregates.md`](/guides/aggregates.md). The button under each page title copies that Markdown, and its menu opens the page in Claude or ChatGPT. # Aggregates > Events, a projection that folds them into state, and commands that decide on new events. An aggregate is the unit that commands change. It has three parts: a schema of the events that can happen to it, a projection that folds those events into its current state, and commands that look at the state and decide which events happen next. ```ts import { DateTime, Effect, Schema } from "effect" import { Aggregate } from "effect-cqrs" const TodoEvent = Schema.TaggedUnion({ TodoCreated: { title: Schema.NonEmptyString, createdAt: Schema.DateTimeUtc }, TodoDeleted: { deletedAt: Schema.DateTimeUtc } }) class TodoAlreadyDeleted extends Schema.TaggedError()("TodoAlreadyDeleted", {}) {} type TodoState = { readonly title: string; readonly deleted: boolean } const Todo = Aggregate.make()({ name: "todo", events: TodoEvent, initial: { title: "", deleted: false }, projection: { TodoCreated: (state, { title }) => ({ ...state, title }), TodoDeleted: (state) => ({ ...state, deleted: true }) } }) .command("createTodo", { payload: Schema.Struct({ title: Schema.NonEmptyString }), handler: Effect.fnUntraced(function*(_, { title }) { return TodoEvent.cases.TodoCreated.make({ title, createdAt: yield* DateTime.now }) }) }) .command("deleteTodo", { payload: Schema.Struct({}), handler: Effect.fnUntraced(function*(state) { if (state.deleted) return yield* new TodoAlreadyDeleted() return TodoEvent.cases.TodoDeleted.make({ deletedAt: yield* DateTime.now }) }) }) ``` ## Events Events are a `Schema.TaggedUnion`. The store keeps them as JSON through the schema’s JSON codec, so a field like `Schema.DateTimeUtc` round-trips. `TodoEvent.cases.TodoCreated.make` builds an event and fills in its `_tag`. ## The projection The projection has one function per event tag, and it must cover every tag or the aggregate doesn’t compile. Each function takes the state, the event and an `EventMeta` (`aggregateName`, `aggregateId`, `version`, `position`, `timestamp`, the [`correlationId` and `causationId`](/guides/commands/#causation) and the command’s [`metadata`](/guides/commands/#metadata)), and returns the new state. `Aggregate.make()` takes the state type explicitly, in a curried call like `Schema.TaggedError()`. Inferred from `initial`, a literal such as `status: "open"` would widen to `string`. ## Commands `.command(name, { payload, handler })` adds a command. The payload is a schema, and the service validates every call against it before the handler runs. The handler receives the current state, the decoded payload, and the aggregate’s id and version. It returns an `Effect` with one event, an array of events, or `[]` to record nothing. It can fail with typed errors, which reach the caller unchanged, and it can use services, which the service picks up when it is built. A handler should only decide, for three reasons: * **It may run more than once.** A command that loses a race with another process reloads the state and runs again. * **The state is shared.** The service keeps recent states between commands, so a handler or projection that mutates one corrupts the next command. * **Commands on one aggregate take turns.** A handler that runs a command on its own aggregate waits for itself. ## Evolving events The log is never rewritten, so events stored months ago keep the shape they had then. To change an event’s schema, declare its older shapes with `EventVersions` and say how each one becomes the next. Projections, view models and handlers only ever see the current shape. ```ts import { EventVersions } from "effect-cqrs" // What TodoCreated looked like before it had a due date const TodoCreatedV1 = Schema.Struct({ title: Schema.String, createdAt: Schema.DateTimeUtc }) const Todo = Aggregate.make()({ name: "todo", events: TodoEvent, initial, projection, versions: { TodoCreated: EventVersions.from(TodoCreatedV1).next( TodoEvent.cases.TodoCreated, (v1) => ({ ...v1, dueDate: null }) ) } }) ``` * **Versions count from 1.** The first schema is the shape events had before the history was declared, which is what every event already stored was written with. Each `next` adds a version, and new events are written in the last one. * **The last version is the current schema.** A history that stops short of it doesn’t compile, and neither does an upcast that returns the wrong shape. * **Each upcast returns the next version without its `_tag`.** It is a plain function, and its result is checked against that version’s schema. * **Old events are upcast as they are read.** The steps are composed into one decoder per old version with `Schema.decodeTo` when the service starts, so an event five versions behind costs one decode. The store records each event’s `schemaVersion`. An event in a version the aggregate doesn’t declare, for instance one written by newer code, fails with `EventDecodeError`. ## Types from an aggregate The `Aggregate` module derives types from the definition, for code around it: | Type | What it is | | --------------------------------------------------- | ------------------------------ | | `Aggregate.State` | The state | | `Aggregate.Event` | The union of its events | | `Aggregate.CommandName` | `"createTodo" \| "deleteTodo"` | | `Aggregate.Payload` | `{ readonly title: string }` | | `Aggregate.CommandError` | `TodoAlreadyDeleted` | | `Aggregate.Context` | The services its handlers need | # Commands > Run commands, read state and history, and know how concurrent writers behave. `AggregateService.make(aggregate)` runs an aggregate against the event store. It is the `command`, `read-model` and `history` actions of moleculer-cqrs, as a typed service. ```ts import { AggregateService, EventStore } from "effect-cqrs" const program = Effect.gen(function*() { const todos = yield* AggregateService.make(Todo) // Typed call yield* todos.commands.createTodo("todo-1", { title: "Buy Milk" }) // Untyped JSON from a transport, validated before running yield* todos.dispatch({ aggregateId: "todo-2", type: "createTodo", payload: { title: "Buy Eggs" } }) yield* todos.state("todo-1") yield* todos.state("todo-1", { finishTime }) // as it was before finishTime yield* todos.history("todo-1", { payload: true }) }).pipe(Effect.provide(EventStore.layerMemory)) ``` ## Running a command `todos.commands` has one function per command, typed from the aggregate. Each takes the aggregate’s id, the payload and optionally a `commandId` and an `expectedVersion`, see [retries](#retries-and-command-ids) and [expected version](#expected-version). It returns a `CommandResult`: | Field | What it is | | ------------------------------ | ---------------------------------------------------------------------------------------------------- | | `aggregateName`, `aggregateId` | The aggregate it ran on | | `events` | The events it recorded, possibly none | | `version` | The aggregate’s version afterwards | | `position` | The store position of the last event, for [`caughtUp`](/guides/view-models/#reading-your-own-writes) | | `correlationId` | The correlation its events joined, see [causation](#causation) | The result is a receipt for what was stored, not an answer to a question. A screen that shows the change reads it as any other screen does, from [`state`](#reading) or a [view model](/guides/view-models/), and `position` lets that read wait for the command. `dispatch` takes the envelope a transport hands over, `{ aggregateId, type, payload }`, as `unknown`. It checks the envelope and decodes the payload with the command’s JSON codec, so an ISO string becomes a `DateTime`. This is the entry point to wire into an HTTP or RPC layer. The envelope can also carry a `correlationId` and a `causationId`, to continue a correlation the caller started, a `metadata` object, see [metadata](#metadata), and a `commandId` and an `expectedVersion`: ```ts yield* todos.dispatch({ aggregateId: "todo-1", type: "completeTodo", payload: {}, commandId: "6f1c…", // the same on every retry of this request expectedVersion: 1 // the version the caller saw }) ``` ## Errors A command fails with the errors its handler declares, plus: | Error | When | | ---------------------- | ------------------------------------------------------------------------------------------------------- | | `InvalidCommand` | The payload, or for `dispatch` the envelope, doesn’t match its schema | | `UnknownCommand` | `dispatch` names a command the aggregate doesn’t have | | `WrongExpectedVersion` | The aggregate isn’t at the caller’s `expectedVersion`. It carries `expectedVersion` and `actualVersion` | | `ConcurrencyConflict` | Another process kept winning the race for this aggregate | | `EventStoreError` | The store failed | | `EventDecodeError` | A stored event doesn’t match the schema of its [version](/guides/aggregates/#evolving-events) | ## Reading * `state(id)` folds the aggregate’s events into its state. With `finishTime` it gives the state as it was before that moment. `readModel` is its former name, deprecated and still there. * `history(id)` lists the aggregate’s events as `{ version, timestamp, type, correlationId, causationId, metadata }`, with the decoded event as `payload` when you ask for it. `startTime` and `finishTime` narrow the window. ## Concurrent writers Inside a service, commands on one aggregate take turns, so they can’t race. Commands on different aggregates don’t wait for each other. Writers in different processes use optimistic concurrency. A command appends its events with the version its decision was based on, and if another process got there first, it backs off, reloads the state and decides again. After `maxRetries` attempts, 5 by default, it fails with `ConcurrencyConflict`. ## Retries and command ids A client that times out doesn’t know whether its command ran, and sending it again could record the same deposit twice. Give the command a `commandId`, the same on every attempt, and the service runs it only once: ```ts const first = yield* accounts.commands.deposit("alice", { amount: 100 }, { commandId: requestId }) const again = yield* accounts.commands.deposit("alice", { amount: 100 }, { commandId: requestId }) // `again` equals `first`: same version, position, events and correlationId, and nothing new is stored ``` The id is recorded in the metadata of the events the command writes, under the reserved key `cqrs.commandId` (`AggregateService.commandIdKey`), with the command’s name under `cqrs.command` (`AggregateService.commandTypeKey`). Each aggregate remembers the ids of its latest commands, rebuilt from those events when its stream is replayed and kept in [snapshots](#snapshots). When an id comes again, the service reads the events it wrote back from the store and returns them as the first result, without running the handler. * **Ids are per aggregate.** The same id on another aggregate is another command. * **Only commands that write events are remembered.** One that writes none leaves nothing to find, so it runs again, which is harmless as long as it still decides to write nothing. * **The window is bounded.** Each aggregate remembers its last `commandIdsPerStream` commands, 100 by default. An older id runs again. * **Another command under a known id fails** with `CommandIdReused`, rather than getting a result it didn’t ask for. The payload isn’t compared: the same command with another payload under a known id gets the first result. Make ids unique per request, such as a UUID the client makes once. Ids recorded before the command’s name was have nothing to compare. Comparing payloads would mean keeping a fingerprint of each one beside the id, in plain text, where it would outlive [forgetting](/guides/personal-data/) a subject whose personal data it was made from. * **Metadata given under `cqrs.commandId` or `cqrs.command` is dropped**, so the keys only hold what the service recorded. Two processes running the same command at once are covered too: the one that loses the race reloads, finds the id and returns the winner’s result. ## Expected version A user who edits what they read a minute ago can overwrite a change made in between. Pass the version they read as `expectedVersion`, like HTTP’s `If-Match`, and the command fails with `WrongExpectedVersion` if the aggregate has moved since: ```ts yield* docs.commands.rename("doc-1", { title }, { expectedVersion: versionShown }).pipe( Effect.catchTag("WrongExpectedVersion", ({ actualVersion }) => reloadAndAskAgain(actualVersion)) ) ``` It is checked before the handler runs, and it isn’t retried: unlike a `ConcurrencyConflict` between two processes, deciding again would act on a state the user hasn’t seen. A command that comes again with its `commandId` returns its first result rather than failing, although its own events moved the version on. ## Remembered state The service keeps the state of the aggregates it used recently, including the events its own commands wrote, so a command only reads the events it hasn’t seen yet. Those can come from another process too: a state plus what came after it is always the current state. After a restart, the first command on an aggregate replays its whole stream, unless [snapshots](#snapshots) are on. ```ts const todos = yield* AggregateService.make(Todo, { maxRetries: 5, // re-decisions after losing to another process stateCacheSize: 10_000, // aggregates kept in memory, 0 to turn it off commandIdsPerStream: 100 // command ids each aggregate remembers }) ``` ## Snapshots A long aggregate takes a while to replay after a restart. With `snapshots` the service saves the state every so many events, and a cold start replays only what came after the last one: ```ts import { KeyValueStore } from "effect/persistence" const todos = yield* AggregateService.make(Todo, { snapshots: { every: 100, // events folded between two snapshots schema: Schema.Struct({ title: Schema.String, deleted: Schema.Boolean }), // the state, as JSON version: 1 // change it when the state or the projection changes } }).pipe(Effect.provide(KeyValueStore.layerSql({ table: "todo_snapshots" }))) ``` Snapshots go to a `KeyValueStore` from `effect/persistence`, which has layers for memory, files, any `effect/sql` database and the browser’s `localStorage`. * **A snapshot is only a shortcut.** One that can’t be read or saved is logged and skipped, and the state comes from the log as before. A command never fails because of one. * **The version is part of every key.** Snapshots of a state the code no longer produces would be wrong rather than unreadable, so change `version` whenever the state or the projection changes. The old ones are then ignored and rebuilt from the log. * **They are saved in the background.** Once `every` events have been folded since the last snapshot, the service starts saving one and the command carries on without waiting for it. The service needs a `Scope`, which `Layer.effect` provides, and closing it waits for the saves still running. * `state` with a `finishTime` always replays from the start. * They keep the aggregate’s latest [command ids](#retries-and-command-ids) beside the state, unsealed, as the events hold them in plain text too. ## Metrics The service records how it works through Effect’s `Metric` module, so you can see whether the remembered states and snapshots are paying off. Every series carries an `aggregate` attribute, and `AggregateService.MetricNames` has their names: | Metric | Type | What it counts | | ----------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------- | | `cqrs_state_loads_total` | counter | Where a load found the state to start from: `source` is `memory`, `snapshot` or `replay` | | `cqrs_events_replayed` | histogram | Events a load read from the store on top of that state | | `cqrs_state_cache_size` | gauge | States remembered in memory | | `cqrs_state_cache_evictions_total` | counter | States dropped to make room, a sign `stateCacheSize` is too small | | `cqrs_snapshot_saves_total` | counter | Snapshots saved, by `result`: `success` or `failure` | | `cqrs_snapshot_read_failures_total` | counter | Snapshots that couldn’t be read, so the stream was replayed instead | | `cqrs_concurrency_conflicts_total` | counter | Appends that lost a race with another process | | `cqrs_command_duration` | timer | How long commands took, by `command` and `outcome`: `success`, the tag of the error, `defect` or `interrupted` | A command is timed from when it asks for its turn on the stream, so time spent behind an earlier command on the same aggregate counts. A payload that doesn’t decode, or a type the aggregate doesn’t know, fails before that and isn’t recorded. Send the metrics on with an exporter, such as `OtlpMetrics` or `PrometheusMetrics` from `effect/observability`, or read them yourself: ```ts import { Metric } from "effect" const loads = (yield* Metric.snapshot).filter( (metric) => metric.id === AggregateService.MetricNames.stateLoads && metric.attributes?.aggregate === "todo" ) ``` Metrics go to the `Metric.MetricRegistry` in context, which by default is one registry for the whole process. Provide a fresh `Map` to keep one test’s counts apart from another’s. The [bank demo](/tutorial/bank/) shows them in its **Metrics** tab. ## Causation Every stored event records why it happened, so a chain of reactions can be traced back to the request that started it: | Field | What it is | | --------------- | ----------------------------------------------------------- | | `correlationId` | Shared by everything that follows from one original request | | `causationId` | What directly caused the command that recorded the event | They travel in the fiber’s context, not in payloads, and nothing has to pass them along: * A command outside any causation starts a correlation with a fresh id, which is both ids of its events. * A view model handler runs with the event it handles as the cause. A command it runs records that event’s id, like `account/alice@4`, and keeps its correlation. * A [saga](/guides/sagas/) carries the event that started it in its workflow’s payload, so the commands it runs record that event as their cause, even after a restart. * `Causation.causedBy(stored)` does the same for other subscribers. * `Causation.withCausation({ correlationId, causationId })` sets them by hand, for instance from an incoming request’s id. ```ts import { Causation } from "effect-cqrs" yield* todos.commands.createTodo("todo-1", { title: "Buy Milk" }).pipe( Causation.withCausation({ correlationId: requestId, causationId: requestId }) ) ``` The command’s span carries the correlation as `cqrs.correlation_id`, and its `commandId`, if any, as `cqrs.command_id`. New correlations get a random UUID from `Ids.IdGenerator`, a reference a test can replace to get ids it can compare: ```ts import { Ids } from "effect-cqrs" program.pipe(Effect.provideService(Ids.IdGenerator, Ids.sequential("id"))) // id-1, id-2, … ``` ## Metadata A command can carry what the domain doesn’t decide on but you want to keep: the version of the software that ran it, the user who sent it, the address it came from. Metadata is a JSON object, recorded on every event the command writes as `metadata`, and `{}` when there is none. Projections and view model handlers find it in their `meta` argument, and `history` lists it. Like causation it travels in the fiber’s context. A command records, key by key, from the lowest precedence: 1. the metadata where its service was built, with `Metadata.layer`; 2. the metadata where it runs, added with `Metadata.withMetadata`; 3. the `metadata` of a `dispatch` envelope. ```ts import { Metadata } from "effect-cqrs" // Once for the whole application: every command records it. const AppLayer = Layer.mergeAll(Todos.layer, ViewModelsLayer).pipe( Layer.provide(Metadata.layer({ appVersion: "1.4.0" })) ) // Per request, on top of it. yield* todos.commands.createTodo("todo-1", { title: "Buy Milk" }).pipe( Metadata.withMetadata({ userId: session.userId, ip: request.remoteAddress }) ) ``` The service remembers the metadata it was built with, so the software version is recorded even by commands run where the layer that provided it isn’t visible, such as an HTTP handler on a `ManagedRuntime`. A view model or saga reacting to an event doesn’t inherit that event’s metadata: the commands it runs record their own, including the version of the process that ran them. Their `causationId` leads back to the event, and from there to its metadata. Command handlers don’t see metadata: it isn’t meant to change a decision. If it should, it belongs in the payload. ## As a service To use one service from several places, put it in a layer, as the todo example does: ```ts class Todos extends Context.Service>()("example/Todos") { static readonly layer = Layer.effect(Todos, AggregateService.make(Todo)) } ``` ## Several aggregates `CommandGateway` is one entry point for the commands of several aggregates. The caller hands over a command and the gateway finds the aggregate that takes it: ```ts import { CommandGateway } from "effect-cqrs" const gateway = yield* CommandGateway.make([Todo, Account]) yield* gateway.send({ aggregateName: "todo", aggregateId: "todo-1", type: "createTodo", payload: { title: "Buy Milk" } }) // JSON from a transport: `aggregateName` can be left out when a single aggregate has the command. yield* gateway.dispatch({ aggregateId: "todo-1", type: "completeTodo", payload: {} }) ``` `send` is typed: the aggregate’s name picks its commands, the type picks the payload, and the errors are the handler’s own plus `CommandFailure`. It also takes `commandId` and `expectedVersion`. `dispatch` takes the same envelope as `AggregateService.dispatch`, with `aggregateName` on top. When it can’t tell where a command goes, because the envelope names an aggregate the gateway doesn’t have, or names none and no aggregate or more than one has the command, it fails with `UnroutableCommand`, whose `candidates` lists the aggregates that have it. The gateway builds the service of each aggregate with the default options. To choose them, or to share a service and its state cache with code that uses it directly, give the gateway the service instead: ```ts const todos = yield* AggregateService.make(Todo, { snapshots }) const gateway = yield* CommandGateway.make([todos, Account]) ``` `gateway.services.todo` is the service of `todo`, for `state` and `history`. ### Interceptors Interceptors run around every command the gateway routes: to authorize it, log it, or record metadata. Each gets the command’s `aggregateName`, `aggregateId` and `type`, and the command to run. It can fail instead of running it, or run it with more context: ```ts const authorize: CommandGateway.Interceptor = (command, next) => Effect.gen(function*() { const user = yield* CurrentUser if (!user.may(command.type)) return yield* new Unauthorized({ type: command.type }) return yield* Metadata.withMetadata(next, { userId: user.id }) }) const gateway = yield* CommandGateway.make([Todo, Account], { interceptors: [authorize, log] }) ``` The first interceptor runs outermost. An interceptor’s errors and requirements are added to every command of the gateway, and they are read from its type, so declare it as an `Interceptor`: an interceptor written inline in the options doesn’t compile. Interceptors run in the caller’s fiber, so `CurrentUser` here comes from the caller, such as an HTTP handler, and not from the layer that built the gateway. Commands run through `gateway.services`, or through `CommandApi`, skip them. To use the gateway from several places, put it in a layer like a service: ```ts const makeGateway = CommandGateway.make([Todo, Account], { interceptors: [authorize] }) class Gateway extends Context.Service>()("app/Gateway") { static readonly layer = Layer.effect(Gateway, makeGateway) } ``` # Integration > Serve aggregates over HTTP, describe their events, publish them to other systems and keep tenants apart. An aggregate service runs in one process. These modules connect it to the rest of a system: | Module | For | | -------------- | ------------------------------------------------------------------------------ | | `CommandApi` | Commands and reads over HTTP, with OpenAPI | | `EventCatalog` | JSON Schema of events and commands, and a guard against breaking stored events | | `Outbox` | Publishing events to Kafka, NATS, SQS and the like | | `LayerMap` | One store per tenant, a pattern rather than a module | `examples/server` puts the first one together with a view model, SQLite or PostgreSQL and OpenTelemetry. Run it with `pnpm example:server`. ## HTTP `CommandApi.group` builds an `HttpApiGroup` from an aggregate, and `CommandApi.layer` implements it with an aggregate service: ```ts import { HttpApi, HttpApiBuilder, HttpApiScalar } from "effect/http-api" import { AggregateService, CommandApi } from "effect-cqrs" const TodoApi = CommandApi.group(Todo, { state: TodoState, // a schema of the state, for GET /todo/:aggregateId errors: [TodoAlreadyCreated, TodoNotFound, TodoAlreadyDeleted] }) class Api extends HttpApi.make("api").add(TodoApi) {} const Routes = Layer.mergeAll( HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe( Layer.provide(CommandApi.layer(Api, "todo", AggregateService.make(Todo))) ), HttpApiScalar.layer(Api, { path: "/docs" }) ) ``` [effect-cqrs-http-example](https://github.com/davidnussio/effect-cqrs-http-example) is this example as a repository of its own, with a server and SQLite storage, to clone and run. | Endpoint | Does | | ----------------------------------- | ------------------------------------------------------------------------------------------------------ | | `POST /todo/:aggregateId/` | Runs the command with the body as payload, and answers with the `CommandResult` | | `GET /todo/:aggregateId/history` | `history`, with optional `payload`, `startTime`, `finishTime`, `afterVersion` and `limit` in the query | | `GET /todo/:aggregateId` | `state`, with an optional `finishTime`. Only with the `state` option | The group is an ordinary `HttpApiGroup`: `name` and `prefix` change its identifier and path, and middleware, OpenAPI annotations and `HttpApiClient` work as with any other. A request without a body runs a command whose payload has no required fields. `history` answers with at most 1,000 events, the `historyLimit` option of `CommandApi.layer`, and a client reads the next page with `afterVersion` set to the version of the last entry it got. Two headers reach the command. `Idempotency-Key` becomes its [`commandId`](/guides/commands/#retries-and-command-ids), so a client that retries after a timeout gets the first result back instead of a second change. The body sent with a known key isn’t compared, so a key belongs to one request. `If-Match` is the version the client based the command on, as `3` or `"3"`, and a stream that has moved on answers `409`. `If-Match: *` asks for no version in particular. Failures map to status codes: | Status | When | Body | | ------ | ------------------------------------------------------- | --------------------------------------------------- | | `400` | The body isn’t JSON or doesn’t match the payload schema | `InvalidCommand` | | `409` | Another writer kept winning the race for the stream | `ConcurrencyConflict` | | `409` | The stream is no longer at the version in `If-Match` | `WrongExpectedVersion` | | `422` | The handler failed with an error in `errors` | The error, with its fields | | `422` | The handler failed with another error | `CommandRejected`, with the error’s tag and message | | `500` | The store, decoding or the keys failed | Empty | A command’s errors are Effect types rather than schemas, so the ones a client should see in full are listed in `errors`. They are answered with `422`, or with their own `httpApiStatus`: `Schema.TaggedError()("X", { ... }, { httpApiStatus: 404 })`. `HttpApiTest.groups(Api, ["todo"])` gives a typed client that calls the handlers without a server, which is how `test/CommandApi.test.ts` tests them. ### Reading your own writes `GET /todo/:aggregateId` folds the aggregate’s events on every request, so it always shows the latest command. Your own query endpoints read view models, which run a moment behind. A client that runs a command and then reads a list can miss its own change. Every command answers with the store position of its last event, as `position` in the body and in an `Event-Position` header. Add `CommandApi.ReadAfter` to the query endpoints, and a request that sends the position back as `Read-After` waits until the view models of this process have handled it: ```ts const Catalog = HttpApiGroup.make("catalog") .add(HttpApiEndpoint.get("available", "/books/available", { success: Books })) .middleware(CommandApi.ReadAfter) class Api extends HttpApi.make("api").add(BookApi).add(Catalog) {} const Server = HttpApiBuilder.layer(Api).pipe( Layer.provide([BookHandlers, CatalogHandlers]), Layer.provide(CommandApi.layerReadAfter({ timeout: "2 seconds" })), Layer.provide(ViewModel.layer([CatalogViewModel])) ) ``` ```ts const response = await fetch("/book/book-1/lend", { method: "POST", body }) const position = response.headers.get("event-position")! const books = await fetch("/books/available", { headers: { "read-after": position } }) ``` A request without the header runs at once. One that waits longer than `timeout`, 5 seconds by default, is answered with `503` and `NotCaughtUp`: a view model is far behind, or stalled on a failing handler, and `viewModels.status` says which. A header that isn’t a position is answered with `400`. In a browser on another origin, list `Event-Position` in `Access-Control-Expose-Headers` so the page can read it. `layerReadAfter` waits for the view models that run in the same process, as [`caughtUp`](/guides/view-models/#reading-your-own-writes) does. With several replicas, send the read to a replica that runs the view models it needs. ## Event catalog `EventCatalog` describes events and commands as JSON Schema, for teams that consume them: ```ts import { EventCatalog } from "effect-cqrs" EventCatalog.events(Account) // every event, with every version from EventVersions EventCatalog.commands(Account) // every command payload EventCatalog.document([Account, Transfer], { title: "Bank", version: "1.0.0" }) // AsyncAPI 3.0 ``` The schemas describe the JSON form of each event, as `Schema.toCodecJson` encodes it. In the AsyncAPI document each aggregate has a channel of events it sends and one of commands it receives. A message keeps its older versions under `x-versions` and the paths of its personal fields under `x-personal-fields`. ### Compatibility guard A change to an event schema can make events already in the store unreadable. The guard keeps a few stored payloads of every event type and version in a file, and a test checks that the current aggregate still decodes them, the way a store read does: ```ts // Once in a while, against a copy of production data: const samples = yield* EventCatalog.captureSamples([Account]) yield* EventCatalog.saveSamples("test/samples/account.json", samples) // In a test: it.effect("still reads stored events", () => Effect.gen(function*() { const samples = yield* EventCatalog.readSamples("test/samples/account.json") yield* EventCatalog.checkCompatibility(Account, samples) }).pipe(Effect.provide(NodeFileSystem.layer))) ``` `checkCompatibility` fails with `IncompatibleSamples`, which lists each sample that no longer decodes and why. It returns how many samples it checked. `saveSamples` adds to the file and leaves the samples already there, so it keeps the shapes the store held over time. Note In the store, [personal fields](/guides/personal-data/) are encrypted. `captureSamples` puts each field’s `redacted` value in their place, so samples hold no personal data, can be committed, and are checked without keys. What the guard can’t see is a personal field whose plaintext type changed: the redacted value is checked, not real data. ## Outbox An outbox publishes events to another system as integration events: ```ts import { Outbox } from "effect-cqrs" const TodoOutbox = Outbox.make({ name: "todo-outbox", aggregates: [Todo], messages: { TodoCreated: ({ title }, { aggregateId }) => ({ type: "todo.created.v1", payload: { id: aggregateId, title } }), TodoDeleted: (_, { aggregateId }) => ({ type: "todo.deleted.v1", payload: { id: aggregateId } }) }, publish: (messages) => producer.send(messages), // an Effect batchSize: 100, linger: "10 millis" }) const Publishing = Outbox.layer([TodoOutbox]).pipe( Layer.provide(Layer.merge(SqliteEventStore.layer(), SqliteEventStore.layerCheckpoints())) ) ``` `messages` maps events by tag, and only those are published. A mapping that returns `undefined` skips the event. Without `messages`, every event is published as `.` with its JSON form as payload. Each `Message` carries the payload with the event’s stream, version, position, timestamp, `correlationId` and `causationId`. An outbox is a [view model](/guides/view-models/): it reads the store from its checkpoint, so events stored while it was down are published when it starts, and nothing is lost between storing an event and publishing it. Delivery is at least once: * Messages go out in batches of up to `batchSize`, when a batch is full or `linger` after its first message. The checkpoint never moves past a message that wasn’t published. * A failed batch is retried in order, with growing pauses, until it succeeds. Once `batchSize` messages wait, the outbox stops reading the store, and the `status` of `Outbox.Outboxes` shows the failure. * After a crash, messages published since the last checkpoint are published again. `Message.id` is the event’s id, such as `todo/a@1`, and stays the same, so consumers deduplicate on it. Checkpoints should be durable. With `Checkpoints.layerMemory` an outbox publishes everything again after every restart. In tests, `Outboxes`’ `published(position)` waits until everything up to a position is published. Caution Events reach the mappings decrypted. Leave personal fields out of integration events, or other systems keep data that `PersonalData.forget` can no longer erase. ## Tenants Every service here works on whatever `EventStore` it is given, so tenants are kept apart by giving each one its own store. Effect’s `LayerMap` builds the layers of a tenant the first time it is asked for, keeps them while they are used, and releases them after `idleTimeToLive`: ```ts import { LayerMap } from "effect" class Tenants extends LayerMap.Service()("app/Tenants", { lookup: (tenant: string) => { const tables = { eventsTable: `${tenant}_events`, checkpointsTable: `${tenant}_checkpoints` } return Layer.mergeAll(Todos.layer, ViewModel.layer([TodoList])).pipe( Layer.provideMerge(Layer.mergeAll( SqliteEventStore.layer(tables), SqliteEventStore.layerCheckpoints(tables), Layer.fresh(TodoListStore.layer) )) ) }, dependencies: [SqliteClient.layer({ filename: "events.sqlite" })], idleTimeToLive: "5 minutes" }) {} const listForTenant = (tenant: string) => listTodos.pipe(Effect.provide(Tenants.get(tenant))) ``` * The tenants share the database client of `dependencies`, and each has its own tables, so the same aggregate id in two tenants is two streams. PostgreSQL works the same way, with `PostgresEventStore`. Another database per tenant also works: put the client in `lookup`. * A layer value is built once per `LayerMap`, so in-memory state of a tenant’s own, like the list above, needs `Layer.fresh`. * Table names come from the tenant id. `sql(name)` quotes identifiers, but check the id against a known alphabet before it gets there. * View models and outboxes of a tenant run while its layers are alive. A tenant evicted for being idle catches up from its checkpoints the next time it is used. * Personal data keys go per tenant too, with `PersonalData.layerSql({ table })`. `test/MultiTenancy.test.ts` runs this on SQLite and PostgreSQL. # Personal data > Forget a person without rewriting the log, by encrypting their data with a key you can destroy. The log is append-only, so an event can’t be edited or deleted when someone asks to be forgotten, as the GDPR allows. Crypto-shredding gets around that: the personal fields of an event are encrypted with a key that belongs to the person they describe, the *subject*, and forgetting the subject destroys the key. The event stays where it is with its amounts, dates and ids, so streams still replay and balances still add up, while its personal fields read as a replacement. ## Marking fields `PersonalData.field` marks a field of an event and says what it reads as once its subject is forgotten: ```ts import { PersonalData } from "effect-cqrs" const AccountEvent = Schema.TaggedUnion({ AccountOpened: { owner: PersonalData.field(Schema.String, { redacted: "[forgotten]" }), email: Schema.optional(PersonalData.field(Schema.String, { redacted: "forgotten@example.invalid" })), currency: Schema.Literals(["CHF", "EUR"]) }, TransferSent: { to: Schema.String, beneficiary: PersonalData.field(Schema.String, { redacted: "[forgotten]", subject: "to" }), amount: Schema.Number } }) ``` * **The subject** is the aggregate id of the stream, unless `subject` names a property next to the field. Above, `beneficiary` belongs to the account in `to`, so forgetting the receiver of a transfer leaves the sender’s events readable. * **The replacement** has to be a valid value of the field. It is checked when the field is defined. * **Where a field can be:** a property of the event, of a nested struct, optional or nullable. Building an aggregate fails when a personal field sits in an array, a record or an untagged union, is transformed after being marked, or names a subject that isn’t next to it or isn’t always a string. Mark the transformed schema instead: `PersonalData.field(Schema.String.pipe(Schema.decodeTo(...)), ...)`. Schemas stay plain: a personal field is an ordinary string to projections, handlers and tests. Encryption happens where `AggregateService` writes to and reads from the store, so nothing else can write a personal field unencrypted. ## Keys An aggregate with personal fields needs `PersonalData.Keys`, and building its service fails without it. Each subject gets its own AES-256 key the first time it is written. The key store only holds that key wrapped by a `MasterKey`: ```ts const Keys = PersonalData.layerSql().pipe( Layer.provide(PersonalData.layerMasterKeyConfig()), Layer.provide(SqliteClient.layer({ filename: "./data/keys.sqlite" })) ) ``` | Key store | For | | ---------------------------------- | ---------------------------------------------------------------------------------------------- | | `PersonalData.layerMemory()` | Tests and the browser. Keys are gone after a restart, and with them everything they encrypted. | | `PersonalData.layerSql({ table })` | SQLite or PostgreSQL, through any `effect/sql` client | | Master key | For | | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `layerMasterKeyConfig()` | 32 bytes in base64 from `PERSONAL_DATA_MASTER_KEY`, with its id in `PERSONAL_DATA_MASTER_KEY_ID`, and earlier keys in `PERSONAL_DATA_MASTER_KEY_PREVIOUS` | | `layerMasterKey({ keyId, key, previous })` | A key you load yourself. Keys wrapped by older master keys stay readable while they are in `previous`. | | `layerMasterKeyEphemeral` | A random key for tests and demos | | Your own `MasterKey` | A KMS such as AWS KMS, [Google Cloud KMS](#on-google-cloud-kms) or HashiCorp Vault, which keeps the master key out of the process | A `MasterKey` is two functions, `wrap` and `unwrap`, so implementing one on a KMS is a call to its encrypt and decrypt endpoints. Keep the key store apart from the events: another database or at least another client, with its own permissions and backups. A copy of the keys outlives a forget for as long as the copy is kept, so let backups of the key table expire. `layerSql` creates its table on startup. To create it with your own migrations instead, turn that off and take the migrations from `PersonalData.migrations`, the same on SQLite and PostgreSQL and in the shape of the stores’ `migrations`: ```ts const migrations = PersonalData.migrations({ table: "personal_data_keys" }) const Keys = PersonalData.layerSql({ table: "personal_data_keys", migrate: false }) ``` Unwrapped keys stay in memory for `cacheTtl`, up to `cacheSize` subjects, 10,000 by default. Requests for a subject that isn’t in memory share one read of the store and one call to the master key, and writers racing for a new subject create a single key, so a KMS is called once rather than once per writer. ### Rotating the master key A new master key needs a new `keyId`, and wraps the keys of subjects created from then on. Keys of earlier subjects stay wrapped by the master key that created them, so that one goes in `previous` until they are moved: 1. Deploy the new master key as the current one, with the old one in `previous`. With `layerMasterKeyConfig()`, that is `PERSONAL_DATA_MASTER_KEY_PREVIOUS`, as `id:base64` pairs separated by commas. 2. Run `PersonalData.rewrap()` once, from any process. It wraps every subject key with the current master key, a page at a time, and returns how many it moved. The keys themselves don’t change, so no event is rewritten, and running it again only moves what is left. 3. Drop the old key from `previous`. A [KMS](#on-google-cloud-kms) that rotates versions of one key behind the same name needs none of this. ### On Google Cloud KMS With [`@google-cloud/kms`](https://www.npmjs.com/package/@google-cloud/kms), the master key is a symmetric crypto key that never leaves Cloud KMS. `wrap` and `unwrap` call its `encrypt` and `decrypt`. [effect-cqrs-kms](https://github.com/davidnussio/effect-cqrs-kms) runs this code against your own Cloud KMS, with the steps to set it up, rotate the key and forget a subject: ```ts import { KeyManagementServiceClient } from "@google-cloud/kms" import { Config, Effect, Layer } from "effect" import { PersonalData } from "effect-cqrs" /** * Wraps subject keys with a symmetric Cloud KMS key, named like * `projects/acme/locations/europe-west6/keyRings/app/cryptoKeys/personal-data`. */ const layerGoogleKms = (keyName: string) => Layer.effect( PersonalData.MasterKey, Effect.gen(function*() { const client = yield* Effect.acquireRelease( Effect.sync(() => new KeyManagementServiceClient()), (client) => Effect.promise(() => client.close()) ) // Binds a wrapped key to its subject and id, so it can't be swapped for another. const context = (subject: string, keyId: string) => Buffer.from(JSON.stringify([subject, keyId])) const call = (operation: "wrap" | "unwrap", subject: string, request: () => Promise) => Effect.tryPromise({ try: request, catch: (cause) => new PersonalData.PersonalDataError({ operation, subject, message: "Cloud KMS call failed", cause }) }) return PersonalData.MasterKey.of({ keyId: keyName, wrap: (dataKey, { keyId, subject }) => call("wrap", subject, () => client.encrypt({ name: keyName, plaintext: dataKey, additionalAuthenticatedData: context(subject, keyId) })).pipe(Effect.map(([{ ciphertext }]) => new Uint8Array(ciphertext as Uint8Array))), // Keys wrapped by an earlier crypto key name it as `masterKeyId`. unwrap: (wrapped, { keyId, masterKeyId, subject }) => call("unwrap", subject, () => client.decrypt({ name: masterKeyId, ciphertext: wrapped, additionalAuthenticatedData: context(subject, keyId) })).pipe(Effect.map(([{ plaintext }]) => new Uint8Array(plaintext as Uint8Array))) }) }) ) const Keys = PersonalData.layerSql().pipe( Layer.provide(Layer.unwrap(Effect.map(Config.String("PERSONAL_DATA_KMS_KEY"), layerGoogleKms))), Layer.provide(SqliteClient.layer({ filename: "./data/keys.sqlite" })) ) ``` * **Access.** The client signs in with Application Default Credentials. The service account needs `roles/cloudkms.cryptoKeyEncrypterDecrypter` on the key, and nothing else in Cloud KMS. * **Rotation.** Rotate the key in Cloud KMS as often as you like: `decrypt` uses the version a key was wrapped with, so nothing changes here. Moving to another crypto key only takes a new `PERSONAL_DATA_KMS_KEY`. Keys wrapped by the old one record its name as `masterKeyId`, and stay readable while the service account may still use it. * **Calls.** Cloud KMS bills each operation: one `encrypt` for each new subject, and one `decrypt` each time a subject’s key isn’t in memory, see `cacheTtl` above. * **Forgetting** deletes the subject’s wrapped key from the key store, so Cloud KMS has nothing left to decrypt. Disabling or destroying the crypto key does the opposite: every subject becomes unreadable at once. Google also recommends checking each request’s CRC32C, with `plaintextCrc32c` and `ciphertextCrc32c`, to catch data corrupted on the way. Add those fields to the requests if you want that check. ### On Node Encryption runs on WebCrypto, which works in Node and in browsers but is asynchronous. On Node, `PersonalDataNode.layerCipher` encrypts with `node:crypto` instead, synchronously, which makes a personal field about half as expensive to read. Both write the same format, so data written with one reads with the other. It is a module of its own, so that code for browsers never imports `node:crypto`: ```ts import * as PersonalDataNode from "effect-cqrs/PersonalDataNode" const Keys = PersonalData.layerSql().pipe( Layer.provide(PersonalData.layerMasterKeyConfig()), Layer.provide(PersonalDataNode.layerCipher), Layer.provide(SqliteClient.layer({ filename: "./data/keys.sqlite" })) ) ``` The cipher is a `PersonalData.Cipher`, provided to the keys and the master key when they are built. ## Forgetting ```ts yield* PersonalData.forget("alice") ``` * **The key is destroyed** and the subject is marked as forgotten for good. Commands that would record new personal data of the subject fail with `SubjectForgotten`. * **The request is recorded** as a `SubjectForgotten` event in the stream `PersonalData.streamName`, with the current causation and metadata, and no personal data. Running it again records nothing new. * **States and snapshots follow.** A state in memory that holds the subject’s data is rebuilt the next time it is used. Snapshots are encrypted with the key of every subject they hold data of, so the snapshot becomes unreadable and is deleted on the next read. Other processes keep using a key they have in memory until it expires, a minute by default. Set `cacheTtl` on the key store to change that. ## View models View models receive events with their personal fields decrypted, so what they store is in plaintext. `onForget` lets one erase it: ```ts const Owners = ViewModel.make({ name: "owners", aggregates: [Account], handlers: { AccountOpened: ({ owner }, { aggregateId }) => OwnerStore.set(aggregateId, owner) }, onForget: (subject) => OwnerStore.delete(subject), dispose: OwnerStore.clear }) ``` `onForget` runs when the `SubjectForgotten` event comes up, in order with the other events and again on every replay. Events after it already carry the replacements. A view model without `onForget` only loses the data when it is replayed. ## What it doesn’t cover * **Fields marked later.** Events stored before a field became personal keep it in plaintext, because the log is never rewritten. They still decode. * **Copies outside effect-cqrs.** Logs, traces, caches and exports that saw a decrypted value keep it. Don’t log payloads of events with personal fields. * **Identifiers.** The subject id is stored in plaintext next to each encrypted field, so use an opaque id rather than an email address. * **Deleted bytes.** Databases keep overwritten rows for a while in free pages, dead tuples or their write-ahead log, until they are vacuumed. * **Copies within a stream.** An encrypted field is bound to its stream, event type, place in the event and subject, so it doesn’t decrypt anywhere else. It isn’t bound to one event: someone who can write to the store can copy it into another event of the same type in the same stream. The cost is one AES-GCM operation per personal field on every write and read, see [performance](/reference/performance/). # Sagas > Run processes that span several aggregates as durable workflows, started by events. A command changes one aggregate. A process that touches several, like a transfer between two accounts, is a saga: it reacts to an event and runs more commands, possibly over a long time, and undoes what it did when a later step is refused. `Saga` runs each saga as a workflow from `effect/workflow`. The workflow engine keeps the outcome of every step, so a workflow that is replayed after a restart skips the commands it already ran, and starting the same saga twice joins the first run. ```ts import { Effect, Layer, Schema } from "effect" import { WorkflowEngine } from "effect/workflow" import { Saga } from "effect-cqrs" // What one run of the saga is about, and what identifies it. const Transfer = Saga.workflow("Transfer", { payload: { transferId: Schema.String, from: Schema.String, to: Schema.String, amount: Schema.Int }, idempotencyKey: ({ transferId }) => transferId }) // The process: each command is an activity whose outcome the engine keeps. const TransferWorkflow = Saga.toLayer(Transfer, ({ amount, from, to, transferId }) => Effect.gen(function*() { const bank = yield* Bank yield* Saga.command("credit", { error: AccountNotOpen, execute: bank.commands.receiveTransfer(to, { transferId, from, amount }) }).pipe( Effect.catchTag("AccountNotOpen", () => Saga.command("refund", { execute: bank.commands.refundTransfer(from, { transferId, to, amount, reason: "AccountNotOpen" }) })) ) })) // The trigger: starts a run for each event. const Transfers = Saga.make({ name: "transfers", aggregates: [Account], handlers: { TransferSent: ({ amount, to, transferId }, { aggregateId }) => Saga.start(Transfer, { transferId, from: aggregateId, to, amount }) } }) const TransferSaga = Saga.layer([Transfers]).pipe( Layer.provideMerge(TransferWorkflow), Layer.provide(WorkflowEngine.layerMemory) ) ``` ## The trigger `Saga.make` takes the same handlers as a view model, and is one: it reads the event store from its own checkpoint, retries a failing handler with backoff, and sees every event at least once. Its handlers only start workflows with `Saga.start`, which returns as soon as the engine has the run, so the trigger never waits for a saga to finish. `Saga.layer` runs triggers in the background. Workflows run commands, so they sit on top of the command side; keeping triggers out of `ViewModel.layer` avoids providing the view models to the commands they depend on. It provides `Saga.Sagas`, which manages the triggers as `ViewModel.ViewModels` manages view models: `status`, `caughtUp`, and, for a trigger made with `onFailure: ViewModel.park(...)`, the events it parked and `retryParked`. A trigger can still go in `ViewModel.layer`, where `replay` starts every saga again and `caughtUp` waits until the sagas have started. ## The workflow `Saga.workflow` is `Workflow.make` with a `causation` field added to the payload. A workflow runs in the engine’s fibers, not in the trigger’s, so that field is how its commands learn their [cause](/guides/commands/#causation): `Saga.start` fills it with the event being handled, and `Saga.toLayer` applies it to the body. Every command the saga runs then records the trigger event as its cause, in the correlation it started. `Saga.command` wraps a command in an `Activity`: * On success it saves the command’s aggregate, version and store position. * A failure that matches `error`, such as a domain error the saga reacts to, is saved too, so a replay takes the same branch. * A lost race or a store error is retried a few times. * Any other failure ends the workflow as a defect. When the workflow fails, `withCompensation` runs the compensations of the steps that had succeeded, last first: ```ts yield* Saga.command("reserve", { execute: stock.commands.reserve(sku, { orderId, quantity }) }).pipe( Order.withCompensation(() => Saga.command("release", { execute: stock.commands.release(sku, { orderId }) }).pipe(Effect.asVoid) ) ) yield* Saga.command("charge", { error: CardDeclined, execute: payments.commands.charge(orderId, { amount }) }) ``` Compensations are only registered on steps at the top of the workflow body. A refusal that is part of the normal flow, like the refund above, reads more plainly as a `catchTag`. ## Delivery and durability Caution An activity that is cut off before its outcome is saved runs again, and a trigger can start a saga again after a crash. Make the commands a saga runs idempotent: give each a [`commandId`](/guides/commands/#retries-and-command-ids) built from the execution id and the step, or record the transfer ids an account has already credited. A second start joins the first run only while the engine remembers it. Pair the engine with checkpoints that live as long: * `WorkflowEngine.layerMemory` with `Checkpoints.layerMemory`, for tests and the browser. After a restart the trigger reads the log from the start and runs every saga again. * `ClusterWorkflowEngine.layer` from `effect/cluster`, which keeps workflows in SQL through the cluster’s message storage, with durable checkpoints such as `SqliteEventStore.layerCheckpoints` or `PostgresEventStore.layerCheckpoints`. Inside a workflow, `effect/workflow` also offers `DurableClock.sleep` for timeouts, and `DurableDeferred` for waiting on a signal, such as a later event that a trigger turns into `DurableDeferred.succeed`. `DurableClock.sleep` keeps a minute or less in memory, and schedules a clock with the engine beyond that, so it survives restarts on a durable engine. The [tutorial’s saga](/tutorial/shop/#5-a-saga) races the two to give a customer 15 seconds to pay. # Storage > Keep events in memory, in SQLite or in PostgreSQL, or write your own backend. The event store is an append-only log. Every event belongs to a stream, one aggregate of one type, and has two coordinates: a `version` that orders it in its stream, and a `position` that orders it across the whole store. Writers use the version for optimistic concurrency, and readers use the position as a cursor. | Store | For | | ------------------------ | ------------------------------------------------- | | `EventStore.layerMemory` | Tests, examples, the browser | | `SqliteEventStore` | One machine, on a local file | | `PostgresEventStore` | A server, possibly with several processes writing | ## In memory `EventStore.layerMemory` keeps events in process memory. They don’t survive a restart. ## SQLite Provide any `effect/sql` SQLite client: ```ts import { SqliteClient } from "@effect/sql-sqlite-node" import { SqliteEventStore } from "effect-cqrs" const Storage = Layer.merge(SqliteEventStore.layer(), SqliteEventStore.layerCheckpoints()).pipe( Layer.provide(SqliteClient.layer({ filename: "./data/events.sqlite" })) ) ``` Positions rely on SQLite having a single writer, so the schema doesn’t carry over to other databases. An append is one statement, which checks the stream’s version and inserts the events only if it still matches. On a file, each commit waits for the disk, so once writers overlap, appends that become ready together are written in one transaction and share that wait. An append whose stream moved on still fails alone. A database in memory writes each append on its own. Several processes can share one file. A store sees its own appends at once, and with `pollInterval` it also reads the head that often to notice the others’: ```ts SqliteEventStore.layer({ pollInterval: "200 millis" }) ``` ## PostgreSQL Provide an `effect/sql` PostgreSQL client: ```ts import { PgClient } from "@effect/sql-pg" import { PostgresEventStore } from "effect-cqrs" const Storage = Layer.merge(PostgresEventStore.layer(), PostgresEventStore.layerCheckpoints()).pipe( Layer.provide(PgClient.layer({ url: Redacted.make("postgres://localhost:5432/cqrs") })) ) ``` A sequence can’t number events: transactions commit in a different order from the one they drew their numbers in, so a view model that has read up to 6 would never come back for a 5 that commits later. Positions come from a counter row instead, which an append holds until it commits. Writers wait for each other at that one step, and positions follow commit order with no gaps. So that writers don’t wait in line, a store writes the appends of its process in groups. Appends that arrive while one is being written go together into the next statement, which checks each stream’s version, numbers the events of the appends that pass and inserts them, in one round trip and one commit. An append whose stream moved on fails alone. When a writer in another process takes a stream first, the group is written again one append at a time. Every append also sends a notification with the new head, on a channel named after the events table. On a `PgClient` from `@effect/sql-pg` the store listens to it, on a connection of its own, so view models and subscribers in every process wake up on every append. * Behind a pooler that doesn’t keep sessions, such as PgBouncer in transaction mode, `LISTEN` doesn’t work. Set `listen: false` and a `pollInterval`. A client other than `PgClient` needs a `pollInterval` too. * A `pollInterval` next to listening is a safety net. The store already reads the head again whenever it reconnects, so a notification sent while it was away isn’t lost. * An append inside a transaction of your own, with `sql.withTransaction`, is written there and then, so it commits or rolls back with your other writes. It holds the counter row until that transaction ends, and every other writer of the store waits for it meanwhile, in this process and in others. Keep such transactions short, and do the slow work before them. * After a bulk load, run `ANALYZE`. Until autovacuum gets to a new table, reads for view models are slow. To run the tests on PostgreSQL, start the database of `docker-compose.yml` and say where it is. `pnpm test` then covers it too. ```bash docker compose up -d --wait POSTGRES_URL=postgres://postgres:postgres@localhost:5432/cqrs pnpm test ``` ## Migrations By default, `layer` and `layerCheckpoints` create their tables on startup, and bring tables from an earlier version of effect-cqrs up to date: existing events get schema version 1, no causation and `{}` as metadata. The statements can run again without harm, and on PostgreSQL an advisory lock keeps processes that start together apart. Where the application may not change the schema, set `migrate: false` and apply the migrations with your own tooling. `migrations(options)` lists them in order, as SQL with the table names filled in: ```ts for (const { id, name, statements } of PostgresEventStore.migrations({ eventsTable: "orders_events" })) { writeFileSync(`migrations/${id}_${name}.sql`, statements.map((statement) => `${statement};\n`).join("")) } ``` | Migration | What it does | | ---------------------- | ----------------------------------------------------------------- | | 1 `create_events` | The events table and its index, and on PostgreSQL the counter row | | 2 `add_causation` | `schema_version`, `correlation_id` and `causation_id` | | 3 `add_metadata` | `metadata` | | 4 `create_checkpoints` | The checkpoints table | A database that effect-cqrs 0.2 created already has all four. One from 0.1 lacks 2 and 3. On PostgreSQL every statement checks first, but SQLite can’t add a column only if it is missing, so there its `ALTER TABLE` statements must run exactly once. ## Options | Option | Stores | Default | What it does | | --------------------------------- | ---------- | --------------------------------- | ----------------------------------------------------------- | | `eventsTable`, `checkpointsTable` | Both | `cqrs_events`, `cqrs_checkpoints` | Table names, so several stores can share one database | | `migrate` | Both | `true` | Creates and updates the tables on startup | | `pollInterval` | Both | Off | Reads the head this often, for appends from other processes | | `listen` | PostgreSQL | `true` | Listens for appends from other processes, on a `PgClient` | ```ts SqliteEventStore.layer({ eventsTable: "orders_events" }) SqliteEventStore.layerCheckpoints({ checkpointsTable: "orders_checkpoints" }) ``` ## Your own backend Another database needs an `EventStore.Backend` with three operations, and `EventStore.make(backend, options)` builds the store on top: | Operation | What it does | | ----------------------------------------- | ------------------------------------------------------------------------------------------------- | | `append(input)` | Appends atomically, failing with `ConcurrencyConflict` if the stream moved past `expectedVersion` | | `readBatch(filter, afterPosition, limit)` | Up to `limit` events past `afterPosition` that match the filter, by position | | `head` | The position of the latest event, `0` when the store is empty | | `changes`, optional | The head positions other processes write, as the database announces them | A backend stores each event’s `schemaVersion`, `correlationId`, `causationId` and `metadata` along with it. `EventStore.toStored` fills in the defaults: version 1, no causation and `{}`. `make` runs `changes` and the `pollInterval` of its options in fibers that need a `Scope`, which `Layer.effect` provides. If `changes` fails, the store logs it and runs it again a second later, so it should start with the current head. Positions must follow commit order, so a backend on a database with concurrent writers has to guarantee that itself. `EventStore.groupCommit(appendGroup)` gives a backend group commit: it calls `appendGroup` with appends to different streams, to write in one transaction, and expects an exit for each. The store tests in `test/EventStore.test.ts` run against every backend and show what each must do. # Testing > Test an aggregate with givenEvents, when and expectEvent, like CQRSFixture. `Fixture` tests an aggregate in isolation: given these events, when this command runs, what happens? It runs commands through the real service on an in-memory store, so payload validation and event encoding are tested too. It reads like the `CQRSFixture` of moleculer-cqrs. The chain is an `Effect`, which runs when you yield it or return it from `it.effect`: ```ts import { assert, it } from "@effect/vitest" import { Fixture } from "effect-cqrs" it.effect("creates a todo", () => Fixture.make(Todo) .givenEvents() .when("createTodo", { title: "Buy Milk" }) .expectEvent(TodoEvent.cases.TodoCreated.make({ title: "Buy Milk", createdAt: DateTime.makeUnsafe(0) })) .inspectState((state) => assert.strictEqual(state.title, "Buy Milk"))) it.effect("runs commands one after another", () => Fixture.make(Todo) .givenEvents([created]) .when("deleteTodo", {}) .expectEvent("TodoDeleted") .when("createTodo", { title: "Buy Eggs" }) .expectEvents(["TodoCreated"])) it.effect("rejects deleting twice", () => Effect.gen(function*() { const error = yield* Fixture.make(Todo) .givenEvents([created, deleted]) .whenFails("deleteTodo", {}) assert.strictEqual(error._tag, "TodoAlreadyDeleted") })) ``` ## The chain | Step | What it does | | ------------------------------------ | ------------------------------------------------------------------------------------------------ | | `givenEvents(events?, aggregateId?)` | The history the commands run against, empty by default. It can include [old events](#old-events) | | `when(command, payload)` | Runs a command, typed against the aggregate. A command that fails stops the chain with its error | | `expectEvent(event)` | The last command emitted exactly this event | | `expectEvents([...])` | The last command emitted exactly these events, in order. `[]` means it recorded nothing | | `inspectState(check)` | Passes the state to a callback that throws to fail the test, so any assertion library works | | `whenFails(command, payload)` | Runs a command that should fail, and returns its error. It dies if the command succeeds | An expected event is either a whole event, compared with the aggregate’s schema, or only its tag. A mismatch dies with `ExpectationFailed`, which carries what was expected and what happened, and Vitest shows the difference. Yielding the chain gives `{ events, state, version, fixture }` for the last command, if you’d rather write plain assertions. Caution The chain is lazy. Built and never yielded or returned, it checks nothing and the test passes. ## Old events `givenEvents` also takes events in an older version of their schema, built with `Aggregate.historical`. They are stored as code from that time would have stored them, so the history goes through the [upcasting](/guides/aggregates/#evolving-events) a real store does: ```ts it.effect("upcasts todos created before due dates", () => Fixture.make(Todo) .givenEvents([Aggregate.historical(Todo, "TodoCreated", 1, { title: "Buy Milk", createdAt })]) .inspectState((state) => assert.strictEqual(state.dueDate, null))) ``` The value is checked against that version’s schema, at compile time and when it runs, so a test can’t seed a shape that never existed. For JSON taken from a database, `new Aggregate.Historical({ type, schemaVersion, payload })` skips the check. ## Invariants `checkInvariants` runs random sequences of commands, with payloads generated from their schemas by Effect’s `Arbitrary`, and checks a predicate on the state after every command that succeeds. Commands that fail are part of the game. A violation dies with `ExpectationFailed`, holding the shortest sequence it found that breaks the predicate. ```ts it.effect("never overdraws", () => Fixture.make(Account) .givenEvents([opened]) .checkInvariants({ invariant: (state) => state.balance >= 0, commands: { closeAccount: false, // left out deposit: Arbitrary.schema(SmallAmount).pipe(Arbitrary.map((amount) => ({ amount }))) }, maxCommands: 20, // the longest sequence, 20 by default check: { runs: 200, seed: 1 } // passed to Arbitrary.checkEffect })) ``` Each run starts from the given events on a fresh store. ## Time `it.effect` runs on a test clock stopped at zero, so timestamps need no mocking: an event created with `DateTime.now` has `DateTime.makeUnsafe(0)` until `TestClock.adjust` moves time. # View models > Project events into read models that catch up after downtime and can be rebuilt. A view model folds events into whatever shape a query needs: a list, a total, a table. Each one reads the event store from its own checkpoint, so it catches up after downtime and can’t miss an event. It runs after the command has stored its events, not in the same transaction, so it is a moment behind the write side: see [reading your own writes](#reading-your-own-writes). ```ts import { Checkpoints, ViewModel } from "effect-cqrs" const TodoList = ViewModel.make({ name: "todo-list", aggregates: [Todo], dispose: clearList, // runs before a replay handlers: { TodoCreated: ({ title }, { aggregateId }) => addToList(aggregateId, title), TodoDeleted: (_, { aggregateId }) => removeFromList(aggregateId) } }) const program = Effect.gen(function*() { const todos = yield* AggregateService.make(Todo) const viewModels = yield* ViewModel.ViewModels const { position } = yield* todos.commands.createTodo("todo-1", { title: "Buy Milk" }) yield* viewModels.caughtUp(position) // read your own write yield* viewModels.replay({ viewModels: ["todo-list"] }) yield* viewModels.status }).pipe( Effect.provide(ViewModel.layer([TodoList]).pipe( Layer.provideMerge(Layer.merge(EventStore.layerMemory, Checkpoints.layerMemory)) )) ) ``` ## Defining one `handlers` has one function per event tag the view model cares about, and only accepts tags of its `aggregates`. Each gets the decoded event and its `EventMeta`, the same as a [projection](/guides/aggregates/#the-projection), with the command’s `metadata`. `dispose` clears everything the view model stored, before a replay rebuilds it. `ViewModel.layer([...])` starts the view models in the background for as long as the layer lives, and provides the `ViewModels` service. It needs an `EventStore`, `Checkpoints` and whatever services the handlers use. It also uses `ParkedEvents` and `Leases` when they are provided, see [failing handlers](#failing-handlers) and [several processes](#several-processes). ## At least once A view model pulls events from the store, rather than waiting for them to be pushed, so an event can’t be lost while it is down. It saves its checkpoint every 500 events and when it stops, so after a crash it can see up to two batches of events again. Note Handlers must be idempotent. The bank demo’s accounts view, for instance, ignores an event whose position it has already applied. ## Checkpoints in a transaction A view model that keeps its data in the same database as its checkpoints can commit both together. With `transaction`, each batch of handlers runs in one transaction with the checkpoint it reaches, so the checkpoint is saved after every batch. A batch that fails leaves nothing behind, and a crash repeats nothing, so handlers needn’t be idempotent. ```ts const TodoRows = ViewModel.make({ name: "todo-rows", aggregates: [Todo], dispose: SqlClient.SqlClient.use((sql) => sql`DELETE FROM todo_rows`), transaction: ViewModel.sqlTransaction, handlers: { TodoCreated: ({ title }, { aggregateId }) => SqlClient.SqlClient.use((sql) => sql`INSERT INTO todo_rows (id, title) VALUES (${aggregateId}, ${title})`) } }) ``` `ViewModel.sqlTransaction` uses the `SqlClient` in context. The handlers and the checkpoints, `SqliteEventStore.layerCheckpoints()` or `PostgresEventStore.layerCheckpoints()`, have to get that same client: a transaction belongs to one client, and another client’s writes don’t join it. Any other `(effect) => Effect` works as `transaction` too, for a database of its own. Only the database is rolled back. A handler that runs a command or calls a service still does that once per attempt. ## Reading your own writes A command returns the position of its last event. `viewModels.caughtUp(position)` waits until every view model has processed it, so a client can read what it just wrote. Without a position it waits for the latest event in the store. Over HTTP, [`CommandApi.ReadAfter`](/guides/integration/#reading-your-own-writes) does the same for a request that sends the position back in a `Read-After` header. There are three ways to read, from the most current to the most flexible: | Read | Up to date | For | | ---------------------------------------------------------------------------- | ---------------------------------- | -------------------------- | | [`state(id)`](/guides/commands/#reading), which folds one aggregate’s events | Always | One aggregate, by id | | A view model after `caughtUp(position)` | Includes the caller’s own commands | The screen after a command | | A view model | A moment behind | Lists, searches, totals | effect-cqrs has no query handlers of its own. A view model writes to whatever storage you choose, and a query is your own code that reads it: a `Ref`, a SQL `SELECT`, a search index. ## Failing handlers A handler that fails stalls its view model on that event, which then retries: after 100 ms, then twice as long each time it fails at the same place, up to 30 seconds. Once it moves on, the next failure starts again from 100 ms. Events are never skipped or reordered. `viewModels.status` lists each view model’s position and the failure it is retrying, if any. A stalled view model doesn’t catch up, so put a timeout on `caughtUp` where that matters. ### Parking events An event that can never be handled, a bug or data the handler doesn’t expect, stalls its view model for good. `onFailure: ViewModel.park({ attempts })` gives up on an event after it has failed that many times: the view model records it in `ParkedEvents`, skips it from then on and carries on with the next one. ```ts const TodoList = ViewModel.make({ name: "todo-list", aggregates: [Todo], dispose: clearList, onFailure: ViewModel.park({ attempts: 5 }), handlers: { /* ... */ } }) const viewModels = yield* ViewModel.ViewModels yield* viewModels.parked("todo-list") // view model, position, event id, error, attempts, time yield* viewModels.retryParked({ viewModel: "todo-list" }) // after fixing the handler ``` `retryParked` handles the parked events again, in order of position. Those that succeed leave `ParkedEvents`, the others stay with the new failure, and `positions` narrows it to some of them. A retried event comes after the events that followed it, so park only where order doesn’t matter between them, and a replay keeps skipping parked events until they are retried. Stalling stays the default. A view model that skips an event is wrong from then on, in a way nothing shows, while a stalled one is visibly behind and right once it moves: a balance that missed a deposit is worse than one that is late. Parking is a choice per view model, for those where a gap is better than a stop. Parked events are kept in memory unless `ParkedEvents` is provided. `ParkedEvents.layerSql()` keeps them in a table, `cqrs_parked_events` by default, on SQLite or PostgreSQL. It creates the table on startup unless `migrate` is `false`, and `ParkedEvents.migrations()` gives its migrations to run another way. `viewModels.status` counts each view model’s parked events, and the counter `ViewModel.MetricNames.parkedEvents`, with a `view_model` attribute, counts every event parked. ## Replay `viewModels.replay({ viewModels: ["todo-list"] })` clears the checkpoint, runs `dispose` and rebuilds the view model from the log. `startTime` and `finishTime` limit which events go in: the view model then resumes from the present, and events outside the window stay out of it. Moleculer-cqrs needed replay to recover events lost on the broker. Here view models can’t lose events, so replay is for rebuilding a view model that changed, or one that got damaged. ## Personal data Handlers receive [personal fields](/guides/personal-data/) decrypted. A view model that stores them handles `onForget(subject, meta)` to erase them when a subject is forgotten, see [view models and personal data](/guides/personal-data/#view-models). ## Checkpoints A checkpoint has to live as long as its view model: * A view model kept in memory pairs with `Checkpoints.layerMemory`, and rebuilds itself on every start. * A view model stored in a database pairs with that database’s checkpoints, `SqliteEventStore.layerCheckpoints()` or `PostgresEventStore.layerCheckpoints()`. ## Several processes Replicas that share a database would each run every view model, and race on its checkpoint. `ViewModel.layer` takes a lease on each view model before running it, from the `Leases` in context. Without one, every lease is held, which suits a single process. `Leases.layerPostgres()` holds leases as PostgreSQL advisory locks, on a hash of the view model’s name, so only one process runs each view model. The others are standbys: they try to take over every 5 seconds, `interval`, and the one that does resumes from the checkpoint the holder saved. The locks live on one connection reserved from the client’s pool, so they go when the process dies or its connection does. ```ts const Live = ViewModel.layer([TodoList]).pipe( Layer.provideMerge(Layer.mergeAll( PostgresEventStore.layer(), PostgresEventStore.layerCheckpoints(), Leases.layerPostgres() )), Layer.provide(PgClient.layer({ url: Redacted.make("postgres://localhost:5432/cqrs") })) ) ``` In a standby: * `viewModels.status` shows `holdsLease: false` for the view models another process runs. * `caughtUp` doesn’t wait for those view models, since nothing in this process moves them. It returns once the view models this process runs have caught up, so read your own writes where the view model runs, or wait on the read model itself. * `replay` and `retryParked` fail with a `ViewModelError`: run them where the view model runs. A holder notices that its connection is gone at its next check, up to `interval` later, and a standby may already have started by then. For that long, both can handle the same events, even in a `transaction`: a lease doesn’t fence off the old holder’s writes. In an Effect cluster, `Singleton` from `effect/cluster` does the same job with the cluster’s own shard ownership, and needs no lease: ```ts import { Singleton } from "effect/cluster" const TodoListSingleton = Singleton.make("todo-list", Layer.launch(ViewModel.layer([TodoList]))) ``` The view models then run on whichever runner owns the singleton, and `ViewModels` is only available inside it. ## Keeping up View models within 500 events of the end of the log share one read of it, and decode each event once. Commands give other fibers a turn every millisecond, so a burst of commands doesn’t starve the view models. A slow handler still falls behind without slowing the writers down, and catches up when they stop. ## Other subscribers Anything else that reacts to events can use `store.subscribe(filter)`: a stream of past events, followed by live ones. A process that runs commands in reaction to events is better written as a [saga](/guides/sagas/), whose trigger is a view model. A handler runs with the event it handles as the [cause](/guides/commands/#causation) of the commands it runs. A subscriber gets the same with `Causation.causedBy(stored)`. # Blog > Posts about CQRS and event sourcing, with examples that run on effect-cqrs. ## CQRS with effect-cqrs A series in five parts, November 2026. We build a small lending library, where members borrow books and bring them back, from the first event to a saga, tests and a database. Every snippet comes from [`examples/blog`](https://github.com/davidnussio/effect-cqrs/tree/main/examples/blog), whose tests run it. [1. Why CQRS and event sourcing](/blog/why-cqrs-and-event-sourcing/)What goes missing when you store only the current state, event sourcing by hand in forty lines, and what CQRS adds on top. [2. Your first aggregate](/blog/your-first-aggregate/)A book as an aggregate: events as schemas, commands that decide, typed errors, and a store that handles concurrent writers. [3. Read models you can throw away](/blog/read-models/)View models that follow the log, catch up after a restart, read your own writes, and rebuild from scratch. [4. Sagas](/blog/sagas/)Borrowing a book touches the member and the book, so a saga coordinates them as a durable workflow. [5. Tests, schema changes and production](/blog/tests-schema-changes-and-production/)Given, when, then and random commands, an event that changes shape, SQLite and PostgreSQL, and forgetting a member. ## When things go wrong Bugs and incidents every team meets, on the same library: what they do to a database that keeps only the current state, and what the log changes. Each post stands alone, and its example runs in a test of its own. [Fix the code, not the data](/blog/fix-the-code-not-the-data/)A loans table with a bug that loses loans, fixed in a database, where the lost rows stay lost, and in a read model, where a replay brings them back. [The wrong event](/blog/the-wrong-event/)A bug in a command stores wrong due dates for a week. Why not to edit the log, hide it in an upcaster or patch the read models, and how a correction event fixes it. [The double write](/blog/the-double-write/)Store the loan and tell the broker, in either order, and see what each breaks on a bad day. With event sourcing the log is already the outbox. [I clicked twice](/blog/i-clicked-twice/)A payment request times out and is sent again. Why disabling the button and a unique column fall short, and how a command id makes a retry return the first answer. [The form that overwrites](/blog/the-form-that-overwrites/)Two librarians edit the same record and the second save undoes the first. Commands that say what changed avoid most collisions, and an expected version catches the rest. [The message that blocks the queue](/blog/the-message-that-blocks-the-queue/)One event a handler can't read stops everything behind it. Stop and wait, or park it and carry on: when each is right, and how a parked event comes back after the fix. [What it looked like last Tuesday](/blog/what-it-looked-like-last-tuesday/)A member was refused a book that is on the shelf today. The log shows the book as it was at that moment, and every event the request caused, across aggregates. [The bug that won't reproduce](/blog/the-bug-that-wont-reproduce/)A member can't borrow anything and every test passes. Copy her stream from production into a fixture, and the bug report becomes a failing test. [Two pods, two emails](/blog/two-pods-two-emails/)Scaled to two replicas, every member gets every email twice. Background work that reads the log must run in one place, and move when that place goes away. ## Design choices Decisions that shape an event-sourced system before anything breaks: where a process lives, how to read, what to keep, and how to get there from the tables you already have. [The saga you skipped](/blog/the-saga-you-skipped/)Borrowing a book without a saga, in the two ways that look right, what breaks when a write fails between them, and what the saga does instead. [Three ways to read](/blog/three-ways-to-read/)A lent book read from its aggregate, from a read model as it is, and from a read model that waits for the command, in one process and over HTTP. [Forget me, from the backups too](/blog/forget-me-from-the-backups-too/)A member asks to be erased, and her name is still in every backup, ready to come back with the next restore. Crypto-shredding erases her from the backups you already have. [From a table to a log](/blog/from-a-table-to-a-log/)Every project that adopts event sourcing already has data in tables. Import each row as the first event of its stream, invent no history, and switch writes over one aggregate at a time. [Change the rules, keep the history](/blog/change-the-rules-keep-the-history/)The loan period changes and every open loan's due date moves with it. Store the decisions rules made, run rules only when deciding, and never in a projection. # Change the rules, keep the history > The loan period goes from four weeks to three, and every open loan's due date moves with it. Rules change all the time: store the decisions they made, run them only when deciding, and never in a projection. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian on a ladder hangs a new rules board next to the old one, while at the desk below a colleague stamps a new card in a different colour beside a row of cards stamped before](/_astro/change-the-rules-keep-the-history.DAEIopJ9_Z29eLcH.webp) Every summer the library relaxes its rules: five books instead of three, four weeks instead of three. On the first of September the old rules come back. In the CRUD version of the library, the loan period is a setting, and due dates are worked out when they are shown: ```sql SELECT title, lent_on + (SELECT loan_days FROM settings) * interval '1 day' AS due_on FROM loans WHERE returned = false; ``` On September 1st someone changes `loan_days` from 28 to 21. Every loan made in August is now a week shorter, and the reminder job emails a few hundred members that their books are overdue. They were told four weeks, and they were right. Business rules change: prices, limits, periods, fees, eligibility. This post shows where they belong in an event-sourced system so that a change applies to new decisions and leaves the old ones alone. Every snippet runs in [`examples/blog/change-the-rules.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/change-the-rules.test.ts), which moves the clock from August to September. ## Rules with a date The library’s rules are a list, each with the day it starts: examples/blog/change-the-rules.ts ```ts export interface Policy { readonly name: string /** From when it applies, inclusive. */ readonly from: DateTime.Utc readonly limit: number readonly days: number } /** The library's lending rules over time, newest last. */ export const policies: ReadonlyArray = [ { name: "standard-2025", from: DateTime.makeUnsafe("2025-01-01T00:00:00Z"), limit: 3, days: 21 }, { name: "summer-2026", from: DateTime.makeUnsafe("2026-06-01T00:00:00Z"), limit: 5, days: 28 }, { name: "standard-2026", from: DateTime.makeUnsafe("2026-09-01T00:00:00Z"), limit: 3, days: 21 } ] /** The policy in force at a given time. */ const policyAt = (at: DateTime.Utc): Policy => policies.findLast(({ from }) => DateTime.isLessThanOrEqualTo(from, at)) ?? policies[0]! ``` A rule change is a new entry, deployed before its date. The old entries stay, because they say what the rules *were*, and that will be asked. ## Decide with the rules, record the decision The member aggregate runs the rules in one place, the command handler, on the day it decides: examples/blog/change-the-rules.ts ```ts export const MemberEvent = Schema.TaggedUnion({ // The decision, with the rule that made it: the due date the member was given, and why. BookBorrowed: { bookId: Schema.String, dueOn: Schema.DateTimeUtc, policy: Schema.String }, BookReturned: { bookId: Schema.String } }) export interface MemberState { /** The books the member has, with the due date each was given. */ readonly books: Readonly> } export const Member = Aggregate.make()({ name: "member", events: MemberEvent, initial: { books: {} }, // The projection records what was decided. It checks no rule, so a member who kept five // books under the summer rules still loads in September. projection: { BookBorrowed: ({ books }, { bookId, dueOn }) => ({ books: { ...books, [bookId]: dueOn } }), BookReturned: ({ books }, { bookId }) => { const { [bookId]: _, ...rest } = books return { books: rest } } } }) .command("borrow", { payload: Schema.Struct({ bookId: Schema.String }), // The rules live here, and run on the day of the decision. handler: Effect.fnUntraced(function*({ books }, { bookId }, { aggregateId }) { const now = yield* DateTime.now const policy = policyAt(now) if (Object.keys(books).length >= policy.limit) { return yield* new LoanLimitReached({ memberId: aggregateId, limit: policy.limit }) } const dueOn = DateTime.add(now, { days: policy.days }) return MemberEvent.cases.BookBorrowed.make({ bookId, dueOn, policy: policy.name }) }) }) .command("giveBack", { payload: Schema.Struct({ bookId: Schema.String }), handler: ({ books }, { bookId }) => Effect.succeed(books[bookId] === undefined ? [] : MemberEvent.cases.BookReturned.make({ bookId })) }) ``` Two choices in it carry the whole post: * **`BookBorrowed` stores the due date**, not the date it was lent plus a period to add later. The due date is what the member was told. It is a decision, made once, under the rules of that day, and it is a fact from then on. * **It also stores which policy decided it.** A member who asks why her book is due on the 17th gets an answer: the summer rules. So does an auditor. ## August examples/blog/change-the-rules.ts ```ts for (const bookId of ["dune", "emma", "ulysses", "solaris"]) { yield* members.commands.borrow("alice", { bookId }) } // Four BookBorrowed, each due in 28 days, under "summer-2026" ``` Alice borrows four books under the summer rules, each due in four weeks. ## September The standard rules are back. Alice’s state, and a few commands: examples/blog/change-the-rules.ts ```ts const alice = yield* members.state("alice") // Four books, each with the due date she was given in August, four weeks after she took it const refused = yield* Effect.flip(members.commands.borrow("alice", { bookId: "emma-2" })) // refused: LoanLimitReached { limit: 3 } yield* members.commands.giveBack("alice", { bookId: "dune" }) yield* members.commands.giveBack("alice", { bookId: "emma" }) const borrowed = yield* members.commands.borrow("alice", { bookId: "neuromancer" }) // borrowed.events: [BookBorrowed { bookId: "neuromancer", dueOn: in 21 days, policy: "standard-2026" }] ``` * **Her four books load**, with the due dates from August. Nothing moved, because nothing was worked out again. * **Her fifth request is refused** under the new limit of three, although she has four. The rule runs on the decision being made now, with today’s rules. * **After she brings two back, she borrows again**, and the new loan is due in three weeks, under `standard-2026`. ## Never check rules in a projection Look at the projection again: it checks nothing. A member with four books when the limit is three is a state it folds without complaint. That is on purpose. A projection runs on every load, over every event ever recorded, with today’s code. If it checked today’s limit, Alice’s stream would fail to load the moment the rules changed, and she could neither borrow nor return a book. Worse, the projection would be judging decisions made under rules it doesn’t know about. Events are what happened. A projection that disagrees with them is wrong, not the events. So rules go in command handlers, where they decide about the future, and projections only record what was decided. Note The same goes for read models. A report of “members over the limit” in September lists Alice, correctly: she has four books and the limit is three. Whether that is a problem is a question for the report, which can say she got them under the summer rules, because the events say so. ## Facts and conclusions [Fix the code, not the data](/blog/fix-the-code-not-the-data/) recommended working out conclusions in read models, so that a replay can fix them. This post stores the due date in the event. Both hold, because they are different things: * **A decision someone was told is a fact.** The due date on Alice’s slip, the price on an invoice, the fee in a confirmation email. Store it in the event, with what decided it. * **A judgment you may revise is a conclusion.** Whether a member is a frequent borrower, which books are popular, whether a return was late by a newer definition. Work it out in a read model, from the facts. If you are unsure which one you have, ask whether a customer would be upset if it changed after the fact. If yes, it is a fact. ## Rules of thumb * **Rules run in command handlers, on the day of the decision.** Never in projections. * **Store the outcome, not the inputs and a formula.** `dueOn`, not `lentOn` plus a setting. * **Record which rule decided.** A name or a version in the event answers “why” for years. * **Keep old rules around with their dates.** They explain the history, and a rule that takes effect later is deployed before its date like any other entry. * **Load whatever the log says.** A state that today’s rules wouldn’t allow is a state that older rules did. ## Next [The wrong event](/blog/the-wrong-event/) covers the other kind of change: a decision that was wrong when it was made, and how to correct it. # Fix the code, not the data > A loans table with a bug that loses loans, fixed twice: in a database, where the fix stops the damage and the lost rows stay lost, and in a read model, where a replay brings them back. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![One librarian frowns at a single card from a card drawer, while another rebuilds a board of loans from a long ribbon of stamped loan cards, a wrench on the desk beside her](/_astro/fix-the-code-not-the-data.e6YY328X_1sL7ui.webp) Every team has fixed this bug. Some code writes the wrong thing to a table, for weeks, until someone notices. The fix is a few lines and the tests pass. Then someone asks about the rows the bug already wrote, and the few lines turn into a migration, a script to run on production, a dig through the backups, or an apology. With event sourcing the same bug ends with the code fix. The read model is rebuilt from the log by the fixed code, and nothing in the database has to be edited by hand. This post shows both, on the library of the series. Every snippet runs in [`examples/blog/fix-the-code.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/fix-the-code.test.ts). ## The bug, in a table The library wants a history of its loans, so it gets a `loans` table. Someone writes it with one row per book, the way you’d store where a book is now: ```sql INSERT INTO loans (book_id, loan_id, member_id, returned) VALUES ('book-1', 'loan-1', 'alice', false) ON CONFLICT (book_id) DO UPDATE SET loan_id = excluded.loan_id, member_id = excluded.member_id, returned = false; ``` Alice borrows *Dune* and brings it back. Bob borrows it. Bob’s loan lands on Alice’s row, and Alice’s loan is gone. A month later a member of staff asks why Alice has never borrowed anything, and the bug is found. The fix is to key the table by loan. From the next loan on, every row is right. Alice’s loan is not coming back, though: the `UPDATE` wrote over the only copy of it, and nothing else in the database remembers it. If you’re lucky, it’s in last night’s backup, or in an application log, and someone pieces it back together by hand. A gentler bug, one that works out a value wrong without destroying anything, still costs twice. The code fix covers new rows, and a migration has to repair the old ones: the same logic written again in SQL, run once, on production, with no test that the two agree. ## The same bug, in a read model Now the same history as a read model, kept by a view model like the catalog of [part 3](/blog/read-models/). It lives in memory here, and it would be the same with a table: examples/blog/fix-the-code.ts ```ts export interface LoanRecord { readonly loanId: string readonly bookId: string readonly memberId: string readonly returned: boolean } /** Every loan the library has made: the `loans` table, as a read model. */ export class LoanHistory extends Context.Service>>()( "library/LoanHistory" ) { static readonly layer = Layer.effect(LoanHistory, Ref.make>(new Map())) } const save = (key: string, record: LoanRecord) => LoanHistory.use((history) => Ref.update(history, (loans) => new Map(loans).set(key, record))) const markReturned = (key: string) => LoanHistory.use((history) => Ref.update(history, (loans) => { const record = loans.get(key) return record === undefined ? loans : new Map(loans).set(key, { ...record, returned: true }) }) ) /** The loans of one member, as the front desk asks for them. */ export const loansOf = (memberId: string) => LoanHistory.use((history) => Ref.get(history).pipe(Effect.map((loans) => [...loans.values()].filter((loan) => loan.memberId === memberId))) ) ``` And the same bug, with the record kept under the book’s id: examples/blog/fix-the-code.ts ```ts export const BuggyLoanHistoryView = ViewModel.make({ name: "loan-history", aggregates: [Book], dispose: LoanHistory.use((history) => Ref.set(history, new Map())), handlers: { // The bug: the record is kept under the book's id, so the next loan of the same // book replaces it. BookLent: ({ loanId, memberId }, { aggregateId }) => save(aggregateId, { loanId, bookId: aggregateId, memberId, returned: false }), BookReturned: (_, { aggregateId }) => markReturned(aggregateId) } }) ``` The day goes the same way: examples/blog/fix-the-code.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) yield* books.commands.returnBook("book-1", {}) const { position } = yield* books.commands.lend("book-1", { loanId: "loan-2", memberId: "bob" }) yield* viewModels.caughtUp(position) const alice = yield* loansOf("alice") // []: Bob's loan took the place of hers ``` Alice’s loan is gone from the read model, exactly as it was gone from the table. The difference is in what the bug could reach. The view model only writes its own read model. The log, where `BookLent` for `loan-1` was appended by the book’s command, is written by the commands alone, and the bug never touched it. ## Fix the code The fix is the same few lines: examples/blog/fix-the-code.ts ```ts export const LoanHistoryView = ViewModel.make({ name: "loan-history", aggregates: [Book], dispose: LoanHistory.use((history) => Ref.set(history, new Map())), handlers: { // The fix: one record per loan. BookLent: ({ loanId, memberId }, { aggregateId }) => save(loanId, { loanId, bookId: aggregateId, memberId, returned: false }), BookReturned: ({ loanId }) => markReturned(loanId) } }) ``` To see what a deploy does, the example keeps the storage apart from the code that runs on it. The log, the checkpoints and the read model live through the deploy. The view model is replaced: examples/blog/fix-the-code.ts ```ts // What outlives a deploy: the log, the checkpoints and the loans table itself. export const Storage = Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, LoanHistory.layer) // What a deploy starts: the books and one version of the view model. const deploy = (view: typeof LoanHistoryView) => ViewModel.layer([view]).pipe(Layer.provideMerge(Books.layer)) ``` Then the new version starts: examples/blog/fix-the-code.ts ```ts yield* viewModels.caughtUp() const beforeReplay = yield* loansOf("alice") // []: the view model starts from its checkpoint, after every event the bug handled const { eventCount } = yield* viewModels.replay({ viewModels: ["loan-history"] }) const afterReplay = yield* loansOf("alice") // [{ loanId: "loan-1", bookId: "book-1", memberId: "alice", returned: true }], from the 3 loan events ``` Deploying alone changes nothing for the past. The view model picks up from its checkpoint, after every event the bug handled, so new loans are right and Alice’s is still missing. `replay` is the step that repairs it: it resets the checkpoint, clears the read model with `dispose`, and runs the fixed handlers over the whole log. Alice’s loan is back, worked out from the same `BookLent` the bug read wrong. There was no migration, no script and no `UPDATE`. The log didn’t change, through the bug or through the fix. The handler that will record tomorrow’s loans is the one that rebuilt yesterday’s, so there is one piece of code to test, not a fix and a migration that have to agree. ## Why it works * **The truth and the copy are apart.** Commands write the log, and view models only read it. A bug in a view model can damage the copy it keeps, never the facts it was made from. * **The past and the future go through the same code.** A replay is the view model running as it always does, from the first event instead of from its checkpoint. * **Nothing is lost while it’s wrong.** Every event the bug mishandled is still in the log, waiting for a handler that reads it right. This is the same reason [part 1](/blog/why-cqrs-and-event-sourcing/) could count loans with a `timesLent` written after them. A wrong read model and a missing one are fixed the same way. ## What a replay can’t fix A replay rebuilds read models. It doesn’t change what the commands decided. * **A bug on the write side stays in the log.** If `lend` had stored the wrong member, the event says so for good, and replays reproduce it faithfully. That is fixed forward: a command that records the correction as a new event, which read models handle like any other, as [The wrong event](/blog/the-wrong-event/) shows. An event whose *shape* is wrong is read through an upcaster, as in [part 5](/blog/tests-schema-changes-and-production/). * **What an event concludes is fixed with it.** The library’s `BookReturned` carries `late`, worked out by `returnBook` when the book came back. If the rule for lateness had a bug, every return it judged keeps the wrong answer. A read model that worked out lateness from `dueOn` and the time of the return would be fixed with a replay. Deciding what an event states and what read models work out is the main choice in designing one. * **A replay takes time.** It reads the whole log, and the read model is cleared while it runs. For a large one, deploy the fix under a new name, say `loan-history-2`, with its own storage: it starts from the first event next to the old one, and queries move over once it has caught up. * **Handlers should only write their read model.** A handler that sends an email sends it again on every replay. Work that must happen once belongs in a [saga](/blog/sagas/), which records what it has done. Note A view model whose read model is in SQL can keep its checkpoint in the same database and commit both in one transaction, with `transaction: ViewModel.sqlTransaction`. A replay then clears and rebuilds the table the same way. See [checkpoints in a transaction](/guides/view-models/#checkpoints-in-a-transaction). ## Rules of thumb * **Keep facts in events, and conclusions in read models.** What happened can’t be wrong later. What it means can, and a read model can work it out again. * **When a read model is wrong, fix the handler and replay.** Don’t patch its data by hand: the next replay rebuilds it from the handlers and throws the patch away. * **When an event is wrong, add one.** The log records the mistake and the correction, which is what an auditor wants to see anyway. ## Next The [view models guide](/guides/view-models/) covers replays, checkpoints and failing handlers in full, and [part 3](/blog/read-models/) builds the library’s read models from the start. # Forget me, from the backups too > A member asks to be erased. Deleting her row is easy, and her name is still in every backup for months, ready to come back with the next restore. Crypto-shredding erases her from the backups you already have. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian drops a small key into a shredder, next to shelves of archive boxes that each stay closed with a padlock](/_astro/forget-me-from-the-backups-too.C1BFkExM_Z1JUyeu.webp) Alice moves away and asks the library to erase her personal data, as the GDPR allows. In a CRUD application it looks like one statement: ```sql DELETE FROM members WHERE id = 'alice'; ``` Then the questions start. The nightly backups of the last 30 days still have her row. So do the weekly ones kept for a year, the copy the analytics team loaded last month, and the dump a developer took to debug something. Six weeks later a disk fails, the database is restored from a backup, and Alice is back, with her name and address, as if she had never asked. Erasure in a system with backups is a known hard problem. This post shows how crypto-shredding solves it, and why the way [part 5](/blog/tests-schema-changes-and-production/#forgetting-a-member) forgets a member was about backups all along. Every snippet runs in [`examples/blog/forget-me.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/forget-me.test.ts). ## Why deleting isn’t enough The usual answers each fall short somewhere: * **Delete from the backups too.** Most backups can’t be edited: they are compressed snapshots, often on write-once storage on purpose, so ransomware can’t touch them. * **Re-apply deletions after a restore.** Keep a list of erased ids and delete them again after every restore. It works if nobody forgets, and the list of who asked to be erased is itself personal data. * **Keep backups short.** Thirty days of backups means thirty days during which a restore brings Alice back. Regulators tend to accept it as long as it is documented. Users don’t always. And with event sourcing it looks worse: the log is never edited, so there is no row to delete in the first place. ## Encrypt each person with their own key Crypto-shredding turns the problem around. Each personal field is stored encrypted, with a key that belongs to the person it describes. Erasing the person means destroying one key. Every copy of the data, in the store, in every backup, in every dump, becomes unreadable at once, without touching any of them. In the library, the member’s name is marked as personal data in its event schema, as in [part 4](/blog/sagas/): examples/blog/library.ts ```ts MemberJoined: { name: PersonalData.field(Schema.NonEmptyString, { redacted: "[forgotten]" }) }, ``` The keys live in their own store, apart from the events: examples/blog/forget-me.ts ```ts // The keys live apart from the events: another database, other permissions, other backups. export const Keys = PersonalData.layerMemory().pipe(Layer.provide(PersonalData.layerMasterKeyEphemeral)) ``` Here it is in memory with a random master key, for the test. In production it is `PersonalData.layerSql()` on another database, with the master key from the environment or a KMS. What the event store holds is not Alice’s name: examples/blog/forget-me.ts ```ts yield* members.commands.join("alice", { name: "Alice Rossi" }) yield* members.commands.join("bob", { name: "Bob Bianchi" }) const [stored] = yield* store.read({ aggregateIds: ["alice"] }).pipe(Stream.runCollect) // stored.payload: { _tag: "MemberJoined", name: { $personal: { subject: "alice", data: "X4DnRIZB…" } } } ``` The subject and the ciphertext, and nothing that reads as Alice. Projections, handlers and read models never see this: `AggregateService` decrypts on the way in and encrypts on the way out, so to the code, `name` is a string. ## Back up, then forget Last night, the backup ran: examples/blog/forget-me.ts ```ts // Last night's backup: every event, as stored. const backup = yield* store.read().pipe(Stream.runCollect) ``` This morning, Alice asked to be forgotten: examples/blog/forget-me.ts ```ts yield* PersonalData.forget("alice") const alice = yield* members.state("alice") // { joined: true, name: "[forgotten]", loans: {} } ``` Her key is destroyed, and her name reads as the replacement given in the schema. Her events are all still there, and with them everything else they record: loans, dates, late returns. They still add up, and so does everything read models built from them. ## Restore Then the bad day: the event store has to be restored from last night’s backup, from before Alice asked: examples/blog/forget-me.ts ```ts // Something goes wrong, and the events are restored from last night's backup. const restored = yield* Effect.gen(function*() { yield* restore(backup) const members = yield* AggregateService.make(Member) return { alice: yield* members.state("alice"), bob: yield* members.state("bob") } }).pipe(Effect.provide(Layer.fresh(EventStore.layerMemory))) // restored.alice.name: "[forgotten]", restored.bob.name: "Bob Bianchi" ``` Alice doesn’t come back. The backup holds her name exactly as the store did, encrypted with a key that no longer exists. Bob, who never asked, is restored as he was. Nobody had to remember a list of erased ids or edit a backup. Every backup of the events ever taken lost Alice’s name at the moment her key was destroyed. ## The keys have backups too The problem moves to the key store, and it is a much smaller problem: * **Keep the key store’s backups short.** A copy of the keys outlives a forget for as long as it is kept, so the key store’s backups should expire in days, not months. The events, which are most of the data, can be kept as long as you need. * **Keep the keys apart from the events.** Another database or at least another client, with its own permissions and its own backups. A dump of the events then holds no names, and a dump of the keys holds no events. * **Wrap the keys with a master key** from the environment or, better, a KMS, so that even the key store’s backups are useless without it. Caution Crypto-shredding covers what goes through `AggregateService`. Read models store decrypted values, so a view model that kept a name erases it with `onForget`, as part 5 shows. Application logs, traces, exports and caches that saw a decrypted value keep it: don’t log event payloads with personal fields. ## Rules of thumb * **Mark personal fields from the first version of an event.** Events stored before a field was marked keep it in plaintext, because the log is never rewritten. * **Use opaque ids as subjects.** The subject id is stored next to each encrypted field, so `alice` or a UUID, never an email address. * **Separate what you keep from who it was about.** Loans, amounts and dates are the library’s records. Names and addresses are Alice’s. Crypto-shredding lets the first outlive the second. * **Make backups of keys expire fast.** That is the one place where a forget can be undone. ## Next The [personal data guide](/guides/personal-data/) covers marking fields, key stores, master keys on a KMS, key rotation and what crypto-shredding doesn’t cover. # From a table to a log > Every real project that adopts event sourcing already has data in tables. Import each row as the first event of its stream, say where it came from, don't invent the history you never had, and switch writes over one aggregate at a time. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian copies the rows of an old ledger, one card each, onto the start of a long ribbon of stamped loan cards that continues across the desk](/_astro/from-a-table-to-a-log.DBwaNY81_Z1XTqi2.webp) The series built its library from the first event. Nobody’s real project starts there. The library already runs on a database, with a `books` table that has years of data in it, and the question after reading about event sourcing is not *how do I start* but *how do I get there from here*. This post moves the library’s books from a table to a log, without stopping it and without pretending to know more than the table did. Every snippet runs in [`examples/blog/table-to-log.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/table-to-log.test.ts). ## What the table knows examples/blog/table-to-log.ts ```ts /** A row of the old `books` table, as `SELECT * FROM books` returns it. */ export interface BookRow { readonly id: string readonly title: string readonly author: string readonly lent_to: string | null readonly loan_id: string | null readonly due_on: string | null } export const rows: ReadonlyArray = [ { id: "book-1", title: "Dune", author: "Frank Herbert", lent_to: "alice", loan_id: "L-1042", due_on: "2026-12-01T00:00:00Z" }, { id: "book-2", title: "Emma", author: "Jane Austen", lent_to: null, loan_id: null, due_on: null } ] ``` The table knows each book’s current state: its title, its author, who has it and until when. It doesn’t know when the book was added, how many times it has been lent, or who had it before Alice. That history was thrown away one `UPDATE` at a time, as [part 1](/blog/why-cqrs-and-event-sourcing/) put it. No migration can bring it back. ## Don’t invent the past The tempting import writes the history the events *would* have had: a `BookAdded`, then a `BookLent` for Alice. Every read model would work at once, since they already handle those events. It is also false. The `BookAdded` would carry the date of the import, not the date the book was added. The `BookLent` would claim Alice borrowed the book at the moment of the import. Any read model that counts loans per month, or measures how long books stay out, would now count a spike of fake loans on import day. A log is valuable because it says what happened, and an import that makes things up spends that on day one. ## Import what the table says Instead, the import is a fact of its own: on this date, the old system said this about the book. It gets an event that says exactly that: examples/blog/table-to-log.ts ```ts const Loan = Schema.Struct({ loanId: Schema.String, memberId: Schema.String, dueOn: Schema.DateTimeUtc }) export const BookEvent = Schema.TaggedUnion({ // What the old system knew about the book on the day of the import, and nothing more. BookImported: { title: Schema.String, author: Schema.String, loan: Schema.NullOr(Loan) }, BookAdded: { title: Schema.NonEmptyString, author: Schema.NonEmptyString }, BookLent: { loanId: Schema.String, memberId: Schema.String, dueOn: Schema.DateTimeUtc }, BookReturned: { loanId: Schema.String, memberId: Schema.String, late: Schema.Boolean } }) ``` `BookImported` carries the row’s state, the loan included if there is one, and nothing more. The book’s projection starts from it like from any other first event, and the `importBook` command records it only for a book that has no stream yet: examples/blog/table-to-log.ts ```ts export const Book = Aggregate.make()({ name: "book", events: BookEvent, initial: { status: "unknown", loan: null }, projection: { BookImported: (_, { loan }) => ({ status: loan === null ? "available" : "lent", loan }), BookAdded: () => ({ status: "available", loan: null }), BookLent: (_, loan) => ({ status: "lent", loan }), BookReturned: () => ({ status: "available", loan: null }) } }) .command("importBook", { payload: Schema.Struct({ title: Schema.String, author: Schema.String, loan: Schema.NullOr(Loan) }), // A book that already has a stream was imported already: running the import again is harmless. handler: (state, payload) => Effect.succeed(state.status === "unknown" ? BookEvent.cases.BookImported.make(payload) : []) }) ``` The import script reads the rows and sends one command per row, with metadata naming where the data came from: examples/blog/table-to-log.ts ```ts /** Writes one `BookImported` per row. Safe to run again, for instance after a failure halfway. */ export const importBooks = Effect.fnUntraced(function*(rows: ReadonlyArray) { const books = yield* AggregateService.make(Book) for (const row of rows) { const loan = row.lent_to === null || row.loan_id === null || row.due_on === null ? null : { loanId: row.loan_id, memberId: row.lent_to, dueOn: DateTime.makeUnsafe(row.due_on) } yield* books.commands.importBook(row.id, { title: row.title, author: row.author, loan }) } }, Metadata.withMetadata({ importedFrom: "books table", script: "scripts/import-books.ts" })) ``` It can be run as often as needed. A book that has a stream is skipped, so a script that dies halfway through ten thousand rows is simply started again. ## Carry on Once a book has a stream, it only changes through commands: examples/blog/table-to-log.ts ```ts yield* importBooks(rows) yield* importBooks(rows) // the second run records nothing // From here on, the books only change through commands. yield* books.commands.returnBook("book-1", {}) yield* books.commands.lend("book-2", { loanId: "loan-1", memberId: "bob" }) yield* viewModels.caughtUp() const history = yield* books.history("book-1") // BookImported, with metadata { importedFrom: "books table", … }, then BookReturned const catalog = yield* Catalog.use(Ref.get) // Map { "book-1" => { title: "Dune", …, available: true }, "book-2" => { title: "Emma", …, available: false } } ``` Alice brings *Dune* back, and the book decides on it with the due date from the old table. *Emma* is lent to Bob. The book’s history says where it came from: the first event is the import, with the script in its metadata, and from then on, what really happened. Read models need one more handler, for `BookImported`. Here is the catalog of the series with it: examples/blog/table-to-log.ts ```ts // The catalog of the series, which now also reads imported books. export const CatalogView = ViewModel.make({ name: "catalog", aggregates: [Book], dispose: Catalog.use((catalog) => Ref.set(catalog, new Map())), handlers: { BookImported: ({ author, loan, title }, { aggregateId }) => Catalog.use((catalog) => Ref.update(catalog, (entries) => new Map(entries).set(aggregateId, { title, author, available: loan === null })) ), BookAdded: ({ author, title }, { aggregateId }) => Catalog.use((catalog) => Ref.update(catalog, (entries) => new Map(entries).set(aggregateId, { title, author, available: true })) ), BookLent: (_, { aggregateId }) => setAvailable(aggregateId, false), BookReturned: (_, { aggregateId }) => setAvailable(aggregateId, true) } }) ``` A read model like “loans per month” simply ignores it: an imported loan didn’t start that month, and the event says so by not being a `BookLent`. ## Switching over The import is the easy part. The hard part is the day the old code stops writing the table. * **One aggregate type at a time.** Move books first, members later. Commands on books go through the new code, and the rest of the application keeps its tables until its turn. * **Stop writes to the table, then import.** Anything written to the table after the import is lost to the log. Make the old code read-only for books, run the import, switch the commands over, and turn writes back on through the new path. If that pause is too long, import first, then import again for the rows that changed in between, as `BookImported`s for new books and corrections for the others, then switch. * **Keep the old queries working** with a read model in the old table’s shape, written by a view model. Reports and other teams that read `books` don’t notice the move, and can be moved to better read models one by one. * **Keep the table** read-only for a while after the switch, to compare with the read models. Drop it once they agree. Note If the old database has an audit or history table, it can be imported too, as events of their own, such as `LoanRecordedBeforeImport`, with the dates it recorded. Read models then decide whether to trust them. The same rule holds: import what was recorded, and invent nothing. ## Rules of thumb * **An import is an event.** `BookImported` says what the old system knew, on which day, from where. It doesn’t pretend to be the history the table never kept. * **Make the import idempotent.** One event per row, skipped if the stream exists, so the script can run again after any failure. * **Put the source in the metadata.** Every imported stream then says where it came from, and an import with a bug can be found and [corrected](/blog/the-wrong-event/). * **Move one aggregate type at a time**, and keep the old shape alive as a read model until nobody reads it. ## Next [Why CQRS and event sourcing](/blog/why-cqrs-and-event-sourcing/) is where the series starts, and [the storage guide](/guides/storage/) covers moving to SQLite or PostgreSQL. # I clicked twice > A payment request times out and the client sends it again. Disabling the button doesn't help, a unique column helps half way. Commands that carry their own id, or an id per request, run once however often they arrive. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A member at the front desk presses a desk bell twice while the librarian holds up a single receipt, a second identical slip set aside on a spike](/_astro/i-clicked-twice.urlmxC17_gMJut.webp) Alice owes the library two fines and pays one on the website. The spinner turns, the page times out, and she presses the button again. Behind the page, the first request had gone through: only its answer was lost. She has now paid twice. Everyone has seen this bug, on one side of it or the other. It isn’t caused by impatient users. It is what every retry looks like from the server, and retries are everywhere: the browser, the mobile app that reconnects, the load balancer, the HTTP client library, the queue that redelivers. Every snippet runs in [`examples/blog/clicked-twice.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/clicked-twice.test.ts). ## A member’s account The library keeps what each member owes as an aggregate: fines charged, payments received, and a balance in cents. One rule: there must be something to pay. examples/blog/clicked-twice.ts ```ts export const AccountEvent = Schema.TaggedUnion({ FineCharged: { amount: Schema.Int, reason: Schema.String }, PaymentReceived: { amount: Schema.Int } }) export class NothingToPay extends Schema.TaggedError()("NothingToPay", { memberId: Schema.String }) {} /** What a member owes the library, in cents. Negative is credit. */ export const Account = Aggregate.make<{ readonly balance: number }>()({ name: "account", events: AccountEvent, initial: { balance: 0 }, projection: { FineCharged: ({ balance }, { amount }) => ({ balance: balance + amount }), PaymentReceived: ({ balance }, { amount }) => ({ balance: balance - amount }) } }) .command("chargeFine", { payload: Schema.Struct({ amount: Schema.Int, reason: Schema.String }), handler: (_, payload) => Effect.succeed(AccountEvent.cases.FineCharged.make(payload)) }) .command("pay", { payload: Schema.Struct({ amount: Schema.Int }), handler: Effect.fnUntraced(function*({ balance }, { amount }, { aggregateId }) { if (balance <= 0) return yield* new NothingToPay({ memberId: aggregateId }) return AccountEvent.cases.PaymentReceived.make({ amount }) }) }) export class Accounts extends Context.Service>()( "library/Accounts" ) { static readonly layer = Layer.effect(Accounts, AggregateService.make(Account)) } ``` ## The same request, twice examples/blog/clicked-twice.ts ```ts yield* accounts.commands.chargeFine("alice", { amount: 300, reason: "Dune, 10 days late" }) yield* accounts.commands.chargeFine("alice", { amount: 300, reason: "Neuromancer, 10 days late" }) // The payment page sends 3 euros. The response is lost, and the page sends it again. yield* accounts.commands.pay("alice", { amount: 300 }) yield* accounts.commands.pay("alice", { amount: 300 }) const alice = yield* accounts.state("alice") // { balance: 0 }: it looks paid, and Alice was charged 6 euros for one click ``` Two `PaymentReceived` events for one click. The balance even looks right, which is the worst part: Alice owed 6 euros and paid 6, and only she knows she meant to pay 3. Nobody notices until she reads her bank statement. Notice the rule didn’t help. With one fine, the second payment would have been refused, because there was nothing left to pay. That makes the bug rarer, not gone: it only shows when a member owes more than one fine, which is when it costs the most to explain. Business rules catch duplicates by luck. ## What the usual fixes miss **Disabling the button** stops the double click and nothing else. The retry that matters is the one the user never sees: the HTTP client that tries again after a timeout, the phone that resends when it gets its signal back, the message that a queue delivers twice. **A unique constraint** is the right instinct. Add a `request_id` column to the payments table, unique, and the second insert fails. But then: * the second request gets an error, while the first succeeded, so the client shows “payment failed” for a payment that went through. To answer it properly, the server has to find the first payment and return what it returned then, which means keeping the response too. * every table that can be written by a retried request needs the column, the index and the code that looks it up. A fine, a loan, a reservation, a renewal. * someone has to decide how long ids are kept, and clean them up. ## Give the request an id The client makes an id once, when Alice clicks, and sends it with every attempt. effect-cqrs calls it a `commandId`: examples/blog/clicked-twice.ts ```ts yield* accounts.commands.chargeFine("alice", { amount: 300, reason: "Dune, 10 days late" }) yield* accounts.commands.chargeFine("alice", { amount: 300, reason: "Neuromancer, 10 days late" }) // The page makes one id when Alice clicks, and sends it with every attempt. const requestId = "7b6f0e1c-pay-dune" const first = yield* accounts.commands.pay("alice", { amount: 300 }, { commandId: requestId }) const again = yield* accounts.commands.pay("alice", { amount: 300 }, { commandId: requestId }) // again equals first: the same version, position and events, and nothing new is stored const alice = yield* accounts.state("alice") // { balance: 300 }: one payment, 3 euros still owed ``` The second attempt didn’t run the handler. The service recognized the id, read back the events the first attempt wrote, and returned the same result: same version, same position, same events. The client can’t tell a retry from the first try, which is the point. It gets the answer it lost. Where are the ids kept? In the log. The id is recorded in the metadata of the events the command wrote, so there is no extra table, and nothing to keep in step: the payment and the proof that it was paid by this request are written in the same append. Each aggregate remembers the ids of its last 100 commands, rebuilt from its events when its state is loaded. An id belongs to one request. Sent again with another command, it is refused rather than answered with a result that isn’t the caller’s: examples/blog/clicked-twice.ts ```ts const reused = yield* Effect.flip( accounts.commands.chargeFine("alice", { amount: 100, reason: "Lost card" }, { commandId: requestId }) ) // reused._tag: "CommandIdReused", with previous: "pay" and command: "chargeFine" ``` Note Over HTTP, `CommandApi` reads the `Idempotency-Key` header as the command id, so a browser or another service only has to send a UUID it made once per request. See [Three ways to read](/blog/three-ways-to-read/) for `CommandApi`. ## Or make the command idempotent itself Sometimes the domain already has an id that makes a repeat recognizable. A loan in this library has one, chosen by the client, and the book’s `lend` command records nothing if the book is already lent for that loan: examples/blog/clicked-twice.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) const first = yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) const again = yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) // first.events: [BookLent], again.events: []: the book already has that loan ``` That is the best kind of idempotency, because it doesn’t depend on remembering anything: a retry next month is still harmless. It is why the [saga](/blog/sagas/) in the series can repeat any of its steps. Not every command has such an id. “Pay 3 euros” twice is two payments, unless something says they are the same request. ## Rules of thumb * **Every command that can be retried needs a way to be recognized.** Prefer an id in the domain, like `loanId`. Where there isn’t one, send a `commandId`. * **The client makes the id, once per intention.** A UUID made when the user clicks, kept across retries. An id the server makes can’t help, because the retry doesn’t know it. * **A retry gets the first answer, not an error.** The caller needs the result it lost. * **Don’t count on business rules to catch duplicates.** They catch some, by luck. * **The window is bounded.** An aggregate remembers its last 100 command ids by default, `commandIdsPerStream`. Retries come within seconds or minutes, so that’s plenty for a member’s account, and an aggregate with heavy traffic can keep more. ## Next The [commands guide](/guides/commands/#retries-and-command-ids) covers command ids in full, including two processes running the same command at once. # Read models you can throw away > Part 3 of the series: view models that follow the log, catch up after a restart, read your own writes, and rebuild from scratch. *Part 3 of 5 of [CQRS with effect-cqrs](/blog/). November 2026.* ![One shelf of loan cards feeds three boards on the wall, a catalog, a board of members and a chart, while a librarian takes one board down to rebuild it](/_astro/read-models.Dl-HwW4__18Fpgx.webp) In [part 2](/blog/your-first-aggregate/) the book became an aggregate: commands decide on events, and `state("book-1")` folds one book’s events into its state. That is the write side, and it is good at one question: what is the state of *this* book, so I can decide what happens to it? The front desk asks other questions. Which books can be borrowed right now? Which books does Alice have? Folding every book on every request would answer them, slowly. The read side of CQRS answers them from read models: data shaped for one query, kept up to date by following the log. effect-cqrs calls the code that keeps one a *view model*. ## A catalog Here is the catalog, one entry per book with a flag for whether it is on the shelf. It lives in memory in a `Ref`, wrapped in a service so that handlers and queries can share it: examples/blog/library.ts ```ts export interface CatalogEntry { readonly title: string readonly author: string readonly available: boolean } /** The catalog: one entry per book, kept in memory. */ export class Catalog extends Context.Service>>()( "library/Catalog" ) { static readonly layer = Layer.effect(Catalog, Ref.make>(new Map())) } const updateEntry = (bookId: string, update: (entry: CatalogEntry) => CatalogEntry) => Catalog.use((catalog) => Ref.update(catalog, (entries) => { const entry = entries.get(bookId) return entry === undefined ? entries : new Map(entries).set(bookId, update(entry)) }) ) export const CatalogView = ViewModel.make({ name: "catalog", aggregates: [Book], dispose: Catalog.use((catalog) => Ref.set(catalog, new Map())), handlers: { BookAdded: ({ author, title }, { aggregateId }) => Catalog.use((catalog) => Ref.update(catalog, (entries) => new Map(entries).set(aggregateId, { title, author, available: true })) ), BookLent: (_, { aggregateId }) => updateEntry(aggregateId, (entry) => ({ ...entry, available: false })), BookReturned: (_, { aggregateId }) => updateEntry(aggregateId, (entry) => ({ ...entry, available: true })) } }) ``` A view model has a name, the aggregates whose events it reads, and a handler for each event it cares about. The handlers get the event and its metadata, which includes the `aggregateId`, the `version`, the `position` in the store, the `timestamp`, and the correlation and causation ids we’ll meet in part 4. `dispose` clears what the view model stored, for when it has to be rebuilt. Notice what is *not* here. There is no `if (book.status === ...)`, no rule, no refusal. The events have already been decided and stored. A handler only records what happened, in the shape a query wants. ## Another shape, same events The second question, which books each member has, wants another shape. It is another view model over the same events: examples/blog/library.ts ```ts /** The books each member has, by member id. */ export class Loans extends Context.Service>>>()( "library/Loans" ) { static readonly layer = Layer.effect(Loans, Ref.make>>(new Map())) } const setOnLoan = (memberId: string, bookId: string, onLoan: boolean) => Loans.use((loans) => Ref.update(loans, (members) => { const books = new Set(members.get(memberId)) if (onLoan) books.add(bookId) else books.delete(bookId) return new Map(members).set(memberId, books) }) ) export const LoansView = ViewModel.make({ name: "loans", aggregates: [Book], dispose: Loans.use((loans) => Ref.set(loans, new Map())), handlers: { BookLent: ({ memberId }, { aggregateId }) => setOnLoan(memberId, aggregateId, true), BookReturned: ({ memberId }, { aggregateId }) => setOnLoan(memberId, aggregateId, false) } }) ``` Read models are cheap. Each one serves its queries and nothing else, so there is no compromise between the catalog page and the member page, and no join at read time. They also don’t share code: changing how the catalog stores a book can’t break the loans, and a read model that needs a search index or raw SQL can have one without the others noticing. Both view models are written so that handling an event twice changes nothing: setting a map entry or adding to a set again leaves them as they were. That matters, because **delivery is at least once**. A view model saves how far it has read, its *checkpoint*, every 500 events and when it stops. After a crash it starts again from the last checkpoint and sees a few events a second time. Note A view model that keeps its data in SQL can skip the idempotency: with `transaction: ViewModel.sqlTransaction`, each batch of handlers commits in one transaction with its checkpoint. See [checkpoints in a transaction](/guides/view-models/#checkpoints-in-a-transaction). ## Running them `ViewModel.layer` starts the view models in the background, as long as the layer lives. They need the event store and somewhere to keep their checkpoints: examples/blog/library.ts ```ts export const ReadSide = ViewModel.layer([CatalogView, LoansView]).pipe( Layer.provideMerge(Books.layer), Layer.provideMerge(Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, Catalog.layer, Loans.layer)) ) ``` Each view model *pulls* events from the store, from its own checkpoint, rather than waiting for them to be pushed. A view model that was down, or that is new, reads what it missed and then waits for more. Nothing is lost while it isn’t running, and there is no message broker to configure. The checkpoints here are in memory, which suits read models in memory: both are gone after a restart, and the view models rebuild from the start of the log. A read model kept in a database pairs with checkpoints in the same database. **A checkpoint must live exactly as long as what its view model stores**, or after a restart it says “done” for data that isn’t there. ## Reading your own writes The read side runs a moment behind the write side. A command returns when its events are stored, and the view models see them a little later. A user who lends a book and then looks at the catalog could see it still on the shelf. Every command returns the `position` of its last event in the store, and `caughtUp` waits until every view model has handled it: examples/blog/library.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* books.commands.add("book-2", { title: "Neuromancer", author: "William Gibson" }) const { position } = yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) // The view models run in the background: wait until they have seen the loan. yield* viewModels.caughtUp(position) const catalog = yield* Catalog.use(Ref.get) // Map { "book-1" => { title: "Dune", …, available: false }, "book-2" => { …, available: true } } const alice = (yield* Loans.use(Ref.get)).get("alice") // Set { "book-1" } ``` Without a position, `caughtUp()` waits for the latest event in the store. Over HTTP, `CommandApi` answers every command with the position in an `Event-Position` header, and `CommandApi.ReadAfter` makes a query wait for the one a client sends back. [Three ways to read](/blog/three-ways-to-read/) puts it together with the other two ways of reading a book. ## Throw it away A read model is not the truth, the log is. So you can throw one away and build it again: examples/blog/library.ts ```ts // Throw the catalog away and build it again from the log. const { eventCount } = yield* viewModels.replay({ viewModels: ["catalog"] }) const rebuilt = yield* Catalog.use(Ref.get) // The same entries as before, from 3 events ``` `replay` resets the checkpoint, runs `dispose`, reads the log from the start and returns once the view model has caught up. This is how you fix a read model that a bug damaged, or one whose shape changed: deploy the new handlers and replay. [Fix the code, not the data](/blog/fix-the-code-not-the-data/) walks through one. A brand new view model needs no replay at all. Its checkpoint starts at zero, so it begins with the first event ever stored. In part 1, `timesLent` answered a question nobody had asked when the events were written. In a running system, that is a new view model: write the handlers, deploy, and it fills itself from the whole history. ## When a handler fails A handler that fails, say because a database is down, stalls its view model on that event and retries with backoff, up to every 30 seconds. Events are never skipped and never reordered, so a read model is either right or visibly behind. `viewModels.status` shows each view model’s position and the failure it is retrying. For read models where a gap is better than a stop, `onFailure: ViewModel.park({ attempts })` sets an event aside after a few failures, and `retryParked` handles it again once the bug is fixed. The [view models guide](/guides/view-models/#failing-handlers) has the details. ## What we have The library now has two sides. The write side keeps the rules, one aggregate at a time. The read side follows the log, in as many shapes as the queries need, and can be rebuilt whenever we like. What it can’t do yet is anything that involves *two* aggregates. A member may only have three books at a time, and a book can only be with one member. Borrowing has to respect both rules, and a command only changes one aggregate. [Part 4](/blog/sagas/) solves this with a saga. # Sagas > Part 4 of the series: borrowing a book touches the member and the book, so a saga coordinates them as a durable workflow. *Part 4 of 5 of [CQRS with effect-cqrs](/blog/). November 2026.* ![A courier carries a loan card along a dotted path from the member's desk to the book's desk, under a row of steps with the first one checked](/_astro/sagas.Cp9vVQy5_Z1uSj5e.webp) So far a member was just a string on a loan. The library has a rule about members too: **a member may have at most three books at a time.** Together with the book’s own rule, one member per copy, borrowing now has to satisfy two aggregates at once. Why not one aggregate that holds both? Because an aggregate is the unit that commands take turns on. A “library” aggregate with every book and every member would make every loan in the building wait for every other one, and fold the whole library’s history to decide anything. Aggregates stay small, and a process that spans several of them is a *saga*: it reacts to an event and runs commands on other aggregates, possibly over a long time. ## The member The member is an aggregate like the book, with its own events: examples/blog/library.ts ```ts export const MemberEvent = Schema.TaggedUnion({ MemberJoined: { name: PersonalData.field(Schema.NonEmptyString, { redacted: "[forgotten]" }) }, LoanRequested: { loanId: Schema.String, bookId: Schema.String }, LoanConfirmed: { loanId: Schema.String }, LoanCancelled: { loanId: Schema.String, reason: Schema.String }, LoanClosed: { loanId: Schema.String } }) ``` `PersonalData.field` marks the name as personal data, so that a member can be forgotten. Everywhere in the code it is a plain string. [Part 5](/blog/tests-schema-changes-and-production/) explains how it works. examples/blog/library.ts ```ts export class MemberAlreadyJoined extends Schema.TaggedError()("MemberAlreadyJoined", { memberId: Schema.String }) {} export class MemberNotFound extends Schema.TaggedError()("MemberNotFound", { memberId: Schema.String }) {} export class LoanLimitReached extends Schema.TaggedError()("LoanLimitReached", { memberId: Schema.String, limit: Schema.Int }) {} ``` examples/blog/library.ts ```ts /** How many books a member may have at once, counting the loans still being arranged. */ export const loanLimit = 3 export interface MemberState { readonly joined: boolean readonly name: string /** Open loans by id: requested until the book is lent, then active. */ readonly loans: Readonly> } const withoutLoan = (state: MemberState, loanId: string): MemberState => { const { [loanId]: _, ...loans } = state.loans return { ...state, loans } } export const Member = Aggregate.make()({ name: "member", events: MemberEvent, initial: { joined: false, name: "", loans: {} }, projection: { MemberJoined: (state, { name }) => ({ ...state, joined: true, name }), LoanRequested: (state, { bookId, loanId }) => ({ ...state, loans: { ...state.loans, [loanId]: { bookId, status: "requested" } } }), LoanConfirmed: (state, { loanId }) => { const loan = state.loans[loanId] return loan === undefined ? state : { ...state, loans: { ...state.loans, [loanId]: { ...loan, status: "active" } } } }, LoanCancelled: (state, { loanId }) => withoutLoan(state, loanId), LoanClosed: (state, { loanId }) => withoutLoan(state, loanId) } }) .command("join", { payload: Schema.Struct({ name: Schema.NonEmptyString }), handler: Effect.fnUntraced(function*(state, { name }, { aggregateId }) { if (state.joined) return yield* new MemberAlreadyJoined({ memberId: aggregateId }) return MemberEvent.cases.MemberJoined.make({ name }) }) }) .command("requestLoan", { payload: Schema.Struct({ loanId: Schema.String, bookId: Schema.String }), handler: Effect.fnUntraced(function*(state, { bookId, loanId }, { aggregateId }) { if (!state.joined) return yield* new MemberNotFound({ memberId: aggregateId }) if (state.loans[loanId] !== undefined) return [] if (Object.keys(state.loans).length >= loanLimit) { return yield* new LoanLimitReached({ memberId: aggregateId, limit: loanLimit }) } return MemberEvent.cases.LoanRequested.make({ loanId, bookId }) }) }) // The saga runs the next three. Each records nothing when there is nothing to do, so // running one twice is harmless. .command("confirmLoan", { payload: Schema.Struct({ loanId: Schema.String }), handler: (state, { loanId }) => Effect.succeed( state.loans[loanId]?.status === "requested" ? MemberEvent.cases.LoanConfirmed.make({ loanId }) : [] ) }) .command("cancelLoan", { payload: Schema.Struct({ loanId: Schema.String, reason: Schema.String }), handler: (state, { loanId, reason }) => Effect.succeed( state.loans[loanId]?.status === "requested" ? MemberEvent.cases.LoanCancelled.make({ loanId, reason }) : [] ) }) .command("closeLoan", { payload: Schema.Struct({ loanId: Schema.String }), handler: (state, { loanId }) => Effect.succeed(state.loans[loanId]?.status === "active" ? MemberEvent.cases.LoanClosed.make({ loanId }) : []) }) ``` A loan goes through two steps on the member. `requestLoan` takes one of the three places right away, as `requested`. The member can’t know yet whether the book is free, so the place is held until the book answers: `confirmLoan` makes it `active`, `cancelLoan` gives it back. Counting requested loans against the limit means three requests in a row can’t borrow four books. The three commands at the end are the ones the saga runs, and each records nothing when there is nothing to do. Confirming a loan twice confirms it once. We’ll see why that matters below. ## The process Borrowing goes like this: 1. The member requests a loan: `LoanRequested`. 2. The saga sees the event and lends the book. 3. If the book was free, the saga confirms the loan. If it was out, it cancels the loan, and the member has the place back. Here it is as a workflow: examples/blog/library.ts ```ts // One run per loan: lend the book and confirm the loan, or cancel it if the book is out. export const Borrow = Saga.workflow("Borrow", { payload: { loanId: Schema.String, memberId: Schema.String, bookId: Schema.String }, idempotencyKey: ({ loanId }) => loanId }) export const BorrowWorkflow = Saga.toLayer(Borrow, ({ bookId, loanId, memberId }) => Effect.gen(function*() { const books = yield* Books const members = yield* Members const lent = yield* Saga.command("lend", { error: Schema.Union([BookAlreadyLent, BookNotFound]), execute: books.commands.lend(bookId, { loanId, memberId }) }).pipe( Effect.as(true), Effect.catchTag(["BookAlreadyLent", "BookNotFound"], (error) => Saga.command("cancel", { execute: members.commands.cancelLoan(memberId, { loanId, reason: error._tag }) }).pipe(Effect.as(false))) ) if (lent) { yield* Saga.command("confirm", { execute: members.commands.confirmLoan(memberId, { loanId }) }) } })) ``` `Saga.workflow` declares a workflow from `effect/workflow`, Effect’s durable workflow engine, with what one run is about, and `idempotencyKey` says what identifies a run. Starting the saga twice for the same loan joins the first run rather than lending the book twice. Every command in the body goes through `Saga.command`, which makes it an *activity*: a step whose outcome the engine records. If the process restarts in the middle of a run, the engine replays the body and skips the steps it already has an outcome for. The `error` option lists the domain errors the saga reacts to. Their outcome is recorded too, so a replay takes the same branch. Any other failure ends the run as a defect. Refusing to lend is part of the normal flow here, not an accident, so the saga handles it with `catchTag` and cancels the loan. For steps that have to be *undone* when a later one fails, the workflow also has `withCompensation`, see the [sagas guide](/guides/sagas/#the-workflow). Bringing a book back is a second, shorter process: the book records `BookReturned`, and the member’s loan has to be closed so the place is free again. examples/blog/library.ts ```ts // When a book comes back, the loan is closed and the member has a place free again. export const CloseLoan = Saga.workflow("CloseLoan", { payload: { loanId: Schema.String, memberId: Schema.String }, idempotencyKey: ({ loanId }) => loanId }) export const CloseLoanWorkflow = Saga.toLayer(CloseLoan, ({ loanId, memberId }) => Effect.gen(function*() { const members = yield* Members yield* Saga.command("close", { execute: members.commands.closeLoan(memberId, { loanId }) }) })) ``` ## The trigger What starts the workflows is a trigger, made with `Saga.make`. It is a view model, the same as in [part 3](/blog/read-models/), whose handlers only start runs: examples/blog/library.ts ```ts export const Lending = Saga.make({ name: "lending", aggregates: [Member, Book], handlers: { LoanRequested: ({ bookId, loanId }, { aggregateId }) => Saga.start(Borrow, { loanId, memberId: aggregateId, bookId }), BookReturned: ({ loanId, memberId }) => Saga.start(CloseLoan, { loanId, memberId }) } }) ``` Being a view model, it follows the log from its own checkpoint, catches up after downtime, and sees every event at least once. Its checkpoint is saved every 500 events, so after a crash the trigger can handle a few hundred events again and start their sagas a second time. `Saga.start` returns as soon as the engine has the run, so a slow saga never holds up the trigger. Caution At least once means a run can start again after a crash, and an activity cut off before its outcome was saved runs again. That is why `lend`, `confirmLoan`, `cancelLoan` and `closeLoan` record nothing when the work is already done: **the commands a saga runs must be idempotent.** ## Wiring it together Everything is a layer. The sagas sit on top of the command side, which sits on top of the storage: examples/blog/library.ts ```ts const Sagas = Saga.layer([Lending]).pipe( Layer.provideMerge(Layer.merge(BorrowWorkflow, CloseLoanWorkflow)), Layer.provide(WorkflowEngine.layerMemory) ) // Everything but the storage, which the last post swaps for a database. export const Application = Layer.mergeAll(Sagas, ViewModel.layer([CatalogView, LoansView, DirectoryView])).pipe( Layer.provideMerge(Layer.merge(Books.layer, Members.layer)), Layer.provideMerge(Layer.mergeAll(Catalog.layer, Loans.layer, Directory.layer)) ) // Events, checkpoints and the keys of personal data, all in memory. export const InMemory = Layer.mergeAll( EventStore.layerMemory, Checkpoints.layerMemory, PersonalData.layerMemory().pipe(Layer.provide(PersonalData.layerMasterKeyEphemeral)) ) export const Library = Application.pipe(Layer.provideMerge(InMemory)) ``` `Application` is everything but the storage, which part 5 swaps for a database. `WorkflowEngine.layerMemory` keeps runs in memory, which pairs with in-memory checkpoints: after a restart the trigger reads the log from the start and starts every saga again, and the idempotent commands make that harmless. In production, `ClusterWorkflowEngine` from `effect/cluster` keeps runs in SQL, and pairs with durable checkpoints. ## Two members, one book A saga finishes a little after the command that started it. To wait for the outcome, we subscribe to the member’s events and stop at the one that settles the loan: examples/blog/library.ts ```ts /** Waits until one of these events is stored for a member's loan, and the view models have seen it. */ export const loanReached = (memberId: string, loanId: string, ...eventTypes: ReadonlyArray) => Effect.gen(function*() { const store = yield* EventStore.EventStore const viewModels = yield* ViewModel.ViewModels const reached = yield* store.subscribe({ aggregateNames: ["member"], aggregateIds: [memberId], eventTypes }).pipe( Stream.filter(({ payload }) => Predicate.hasProperty(payload, "loanId") && payload.loanId === loanId), Stream.runHead ) if (Option.isSome(reached)) yield* viewModels.caughtUp(reached.value.position) return Option.map(reached, ({ type }) => type) }) /** Waits until the saga has confirmed or cancelled a loan. */ export const settled = (memberId: string, loanId: string) => loanReached(memberId, loanId, "LoanConfirmed", "LoanCancelled") ``` `store.subscribe` is a stream of past events followed by live ones, so the outcome is found whether it was stored before or after the call. Now Alice and Bob both want *Dune*: examples/blog/library.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* members.commands.join("alice", { name: "Alice Rossi" }) yield* members.commands.join("bob", { name: "Bob Bianchi" }) yield* members.commands.requestLoan("alice", { loanId: "loan-1", bookId: "book-1" }) const forAlice = yield* settled("alice", "loan-1") // Option.some("LoanConfirmed") yield* members.commands.requestLoan("bob", { loanId: "loan-2", bookId: "book-1" }) const forBob = yield* settled("bob", "loan-2") // Option.some("LoanCancelled"): Alice has the book ``` Alice’s loan is confirmed. Bob’s request takes a place, the saga finds the book out, and cancels his loan, which gives the place back. Neither aggregate ever broke its own rule, and neither had to know about the other. Why not let the member’s command lend the book itself, and skip the saga? [The saga you skipped](/blog/the-saga-you-skipped/) tries that, in two ways, and shows what happens to the data when a write fails in the middle. When Alice brings the book back, the second workflow closes her loan: examples/blog/library.ts ```ts yield* books.commands.returnBook("book-1", {}) yield* loanReached("alice", "loan-1", "LoanClosed") const alice = yield* members.state("alice") // { joined: true, name: "Alice Rossi", loans: {} } ``` ## Following the thread A saga turns one request into commands on several aggregates. effect-cqrs records how they are linked, on every event: examples/blog/library.ts ```ts const bobsHistory = yield* members.history("bob") // LoanRequested and LoanCancelled share a correlationId, and LoanCancelled's // causationId names the event that started the saga: "member/bob@2" ``` * The **`correlationId`** is shared by everything that follows from one original request. Bob’s request and its cancellation carry the same one, and so does the book’s `BookLent` that Alice’s request led to. * The **`causationId`** names what directly caused a command. The cancellation was caused by the event `member/bob@2`, version 2 of Bob’s stream, his `LoanRequested`. When you need to know why a loan was cancelled at 3 a.m., you follow the causation ids back to the request that started it. To join a request from outside, an HTTP request id for instance, wrap the command in `Causation.withCausation`. ## Waiting inside a saga A saga can also wait. A reminder when a loan is overdue would be a run that sleeps until the due date with `DurableClock.sleep`, then sends a reminder unless the book came back. A durable engine keeps the clock across restarts, even for three weeks. `DurableDeferred` waits for a signal instead, such as a later event. The [tutorial’s shop](/tutorial/shop/#5-a-saga) races the two, to give a customer fifteen seconds to pay. ## What we have The library now has every piece of CQRS: aggregates that decide, read models that follow, and a saga that coordinates. What’s left is making sure it is right and keeping it right as it changes and grows. [Part 5](/blog/tests-schema-changes-and-production/) covers testing, changing an event’s shape, moving to a database, and forgetting a member. # Tests, schema changes and production > Part 5 of the series: test aggregates with given, when, then and random commands, change an event's shape without rewriting the log, move to SQLite or PostgreSQL, and forget a member. *Part 5 of 5 of [CQRS with effect-cqrs](/blog/). November 2026.* ![A brass machine turns an old yellowed loan card into a new one, beside databases, a checklist of passing tests and a drawer where one card is erased](/_astro/tests-schema-changes-and-production.CEx_FDPA_1WQXog.webp) Over the last four posts the library grew books, members, read models and a saga, all in memory. This last post is about living with it: testing the rules, changing an event that is already stored, moving to a real database, and forgetting a member who asks for it. ## Given, when, then An aggregate is a pure decision: given these past events, when this command comes, these events happen, or this error. `Fixture` tests exactly that: examples/blog/library.test.ts ```ts const added = BookEvent.cases.BookAdded.make({ title: "Dune", author: "Frank Herbert" }) const lent = BookEvent.cases.BookLent.make({ loanId: "loan-1", memberId: "alice", dueOn: DateTime.makeUnsafe(0).pipe(DateTime.add({ days: 21 })) }) it.effect("lends a book for three weeks", () => Fixture.make(Book) .givenEvents([added]) .when("lend", { loanId: "loan-1", memberId: "alice" }) .expectEvent(lent) .inspectState((state) => assert.strictEqual(state.status, "lent"))) it.effect("records nothing when the same loan comes again", () => Fixture.make(Book) .givenEvents([added, lent]) .when("lend", { loanId: "loan-1", memberId: "alice" }) .expectEvents([])) it.effect("refuses a book that is out", () => Effect.gen(function*() { const error = yield* Fixture.make(Book) .givenEvents([added, lent]) .whenFails("lend", { loanId: "loan-2", memberId: "bob" }) assert.strictEqual(error._tag, "BookAlreadyLent") })) it.effect("notes a book that comes back late", () => Effect.gen(function*() { yield* TestClock.adjust("22 days") yield* Fixture.make(Book) .givenEvents([added, lent]) .when("returnBook", {}) .expectEvent(BookEvent.cases.BookReturned.make({ loanId: "loan-1", memberId: "alice", late: true })) })) ``` The chain runs the command through the real `AggregateService` on an in-memory store, so payload validation and the JSON round trip of every event are tested too. An expected event is compared through the aggregate’s schema, and a mismatch shows the difference. Time needs no mocking. `it.effect` runs on a test clock stopped at zero, so `DateTime.now` in the `lend` handler gives the epoch, and the due date is exactly 21 days later. To test a late return, `TestClock.adjust` moves time on by 22 days before the book comes back. Caution The chain is lazy, like every effect. Built and never returned from `it.effect` or yielded, it checks nothing and the test passes. ## Rules that hold for any sequence Examples test the cases you think of. The rule “never more than three books” should hold for *any* sequence of commands, including the ones you don’t think of. `checkInvariants` runs random sequences, with payloads generated from the command schemas, and checks a predicate after every command that succeeds: examples/blog/library.test.ts ```ts // A few loan ids, so that random commands often talk about the same loan. const LoanId = Schema.Literals(["loan-1", "loan-2", "loan-3", "loan-4", "loan-5", "loan-6", "loan-7", "loan-8"]) it.effect("never has more than three books out for a member", () => Fixture.make(Member) .givenEvents([MemberEvent.cases.MemberJoined.make({ name: "Alice Rossi" })]) .checkInvariants({ invariant: (state) => Object.keys(state.loans).length <= loanLimit, commands: { join: false, requestLoan: Arbitrary.schema(Schema.Struct({ loanId: LoanId, bookId: Schema.String })), confirmLoan: Arbitrary.schema(Schema.Struct({ loanId: LoanId })), cancelLoan: Arbitrary.schema(Schema.Struct({ loanId: LoanId, reason: Schema.String })), closeLoan: Arbitrary.schema(Schema.Struct({ loanId: LoanId })) }, check: { runs: 1000, seed: 1 } })) ``` Loan ids come from a small set, so that random commands keep talking about the same loans: a confirmation of a loan that was requested, a cancellation of one that was already confirmed, the same request twice. Commands that fail are part of the game. When the invariant breaks, the test fails with the shortest sequence of commands it found that breaks it, which reads like a bug report. Try it: change `>=` to `>` in `requestLoan`, and after a few hundred runs the test fails with four `requestLoan` commands for four different loans. A thousand runs take a fraction of a second, and the fixed `seed` makes every run of the test try the same sequences. ## Changing an event’s shape Events written today will be read by code written years from now, and the log is never rewritten. Suppose the first version of the library recorded `BookAdded` with a title only, and authors came later. The events already stored don’t have one. effect-cqrs records the schema version of every event. To change an event, declare its older shapes and how each one becomes the next: examples/blog/library.ts ```ts // The first version of the library didn't record authors. const BookAddedV1 = Schema.Struct({ title: Schema.NonEmptyString }) const bookVersions = { BookAdded: EventVersions.from(BookAddedV1).next( BookEvent.cases.BookAdded, (v1) => ({ ...v1, author: "Unknown" }) ) } ``` That is the `versions: bookVersions` line we skipped in [part 2](/blog/your-first-aggregate/). An old `BookAdded` is *upcast* as it is read: projections, view models and sagas only ever see the current shape, with `author: "Unknown"`. New events are written in the latest version. The types check every step: an upcast that returns the wrong shape, or a history that doesn’t end at the current schema, doesn’t compile. A test can seed events as old code would have stored them, with `Aggregate.historical`: examples/blog/library.test.ts ```ts it.effect("reads books added before authors were recorded", () => Fixture.make(Book) .givenEvents([Aggregate.historical(Book, "BookAdded", 1, { title: "Dune" })]) .inspectState((state) => assert.strictEqual(state.author, "Unknown"))) ``` Upcasting covers adding a field, renaming one or splitting one. A change of meaning is better as a new event type, next to the old one. ## Moving to a database Everything so far ran on `EventStore.layerMemory`. Because the application is a layer that *requires* a store, moving to a database is a change in one place. Here are the events on SQLite: examples/blog/storage.ts ```ts // The events in one file and the keys in another. The read models and the workflow // engine live in memory, so their checkpoints do too: on every start, they read the // log again from the beginning. export const onSqlite = (files: { readonly events: string; readonly keys: string }) => Layer.mergeAll( SqliteEventStore.layer().pipe(Layer.provide(SqliteClient.layer({ filename: files.events }))), Checkpoints.layerMemory, PersonalData.layerSql().pipe(Layer.provide(SqliteClient.layer({ filename: files.keys }))) ) // The master key comes from PERSONAL_DATA_MASTER_KEY and PERSONAL_DATA_MASTER_KEY_ID. export const LibraryOnSqlite = Application.pipe( Layer.provideMerge(onSqlite({ events: "./data/library.sqlite", keys: "./data/keys.sqlite" })), Layer.provide(PersonalData.layerMasterKeyConfig()) ) ``` Tables are created on startup. Only the events moved: the read models and the workflow engine are still in memory, so their checkpoints are too, and they rebuild from the log on every start. That is a fine setup for one process, and the rule from [part 3](/blog/read-models/) holds: a checkpoint lives exactly as long as what it describes. PostgreSQL works the same way: examples/blog/storage.ts ```ts // The same on PostgreSQL, where several processes can share the events. export const onPostgres = (urls: { readonly events: Redacted.Redacted; readonly keys: Redacted.Redacted }) => Layer.mergeAll( PostgresEventStore.layer().pipe(Layer.provide(PgClient.layer({ url: urls.events }))), Checkpoints.layerMemory, PersonalData.layerSql().pipe(Layer.provide(PgClient.layer({ url: urls.keys }))) ) ``` On PostgreSQL several processes can write to the same events. Each process still runs every view model and saga trigger in this setup, which the idempotent handlers and commands make safe, but wasteful. To run each one in one process only, add `Leases.layerPostgres()` for the view models, and a durable workflow engine with durable checkpoints for the sagas, see [several processes](/guides/view-models/#several-processes) and [delivery and durability](/guides/sagas/#delivery-and-durability). The tests of this series run the whole library on SQLite in memory, with `onSqlite({ events: ":memory:", keys: ":memory:" })`. ## Forgetting a member The GDPR lets Alice ask to be forgotten, and the log can’t be edited. effect-cqrs gets around that with *crypto-shredding*: in [part 4](/blog/sagas/), the member’s name was marked with `PersonalData.field`. Every personal field is encrypted in the store with a key of its own subject, here the member, and forgetting the member destroys the key. Her events stay where they are, with their loans and dates, so streams still replay and the saga’s history still adds up, while her name reads as `"[forgotten]"`. That is why `onSqlite` keeps the keys in a database of their own, wrapped by a master key from the environment. The keys must be kept apart from the events, with their own permissions and backups. A read model that stored the name has to erase it too. The front desk’s directory handles `onForget`: examples/blog/library.ts ```ts /** Members' names, for the front desk. */ export class Directory extends Context.Service>>()( "library/Directory" ) { static readonly layer = Layer.effect(Directory, Ref.make>(new Map())) } export const DirectoryView = ViewModel.make({ name: "directory", aggregates: [Member], dispose: Directory.use((directory) => Ref.set(directory, new Map())), handlers: { MemberJoined: ({ name }, { aggregateId }) => Directory.use((directory) => Ref.update(directory, (names) => new Map(names).set(aggregateId, name))) }, // The names are personal data: erase a member's when they are forgotten. onForget: (subject) => Directory.use((directory) => Ref.update(directory, (names) => { const rest = new Map(names) rest.delete(subject) return rest }) ) }) ``` And the whole thing, as a test: examples/blog/library.test.ts ```ts it.effect("forgets a member", () => Effect.gen(function*() { const members = yield* Members const viewModels = yield* ViewModel.ViewModels yield* members.commands.join("alice", { name: "Alice Rossi" }) yield* PersonalData.forget("alice") yield* viewModels.caughtUp() const alice = yield* members.state("alice") assert.strictEqual(alice.name, "[forgotten]") const directory = yield* Directory.use(Ref.get) assert.isFalse(directory.has("alice")) }).pipe(Effect.provide(Library), quiet)) ``` After `forget`, Alice’s stream still holds every loan, her name reads as the replacement, and the directory no longer has it. The forget itself is recorded as an event, with no personal data in it, and from then on a command that would record new personal data about her fails with `SubjectForgotten`. The [personal data guide](/guides/personal-data/) covers master key rotation, keys in a KMS, and what crypto-shredding doesn’t cover. ## Where to go from here That is the whole library, in about six hundred lines of [`examples/blog`](https://github.com/davidnussio/effect-cqrs/tree/main/examples/blog): * **Events** as schemas, the only source of truth, versioned as they change. * **Aggregates** that decide on events, with typed errors and optimistic concurrency. * **View models** that follow the log, catch up, and can be thrown away and rebuilt. * **A saga** that coordinates two aggregates as a durable workflow. * **Tests** that read like the rules, and find the sequences you didn’t think of. From here: * The [bank demo](/tutorial/bank/) runs all of this in your browser, and shows each command travelling through the pipeline. * The [workshop](/tutorial/workshop/) runs it on a server, for a room full of phones. * The [guides](/guides/aggregates/) go deeper into every piece, and the [comparison with moleculer-cqrs](/reference/moleculer-cqrs/) shows where the framework comes from. Thanks for reading the series. # The bug that won't reproduce > A member can't borrow anything, and every test passes. The data that triggers the bug is in production, as a history nobody can rebuild by hand. With events, that history is a file: copy the stream into a fixture, and the bug report is a failing test. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian cuts a short strip from a long roll of loan cards and drops it into a small glass box on the workbench, next to a magnifying glass](/_astro/the-bug-that-wont-reproduce.Bq4JK0UI_15yKxf.webp) The ticket says: *Carla can’t borrow anything. The website says she has reached her limit of three books. She has none.* The developer tries it: a new member, a loan, it works. Three loans, a fourth refused, correct. Every test is green. *Cannot reproduce*, and the ticket waits for more information that never comes. Bugs like this depend on history, a sequence of things that happened to one record over weeks. Nobody can guess the sequence, and a database row doesn’t keep it. Every snippet here runs in [`examples/blog/wont-reproduce.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/wont-reproduce.test.ts). ## The member This member aggregate is a variant of the one in [the series](/blog/sagas/). At some point someone wanted members to see their cancelled loans on their page, so a cancelled loan is now kept, marked `cancelled`, instead of removed: examples/blog/wont-reproduce.ts ```ts const makeMember = (countsAgainstLimit: (loan: Loan) => boolean) => Aggregate.make()({ name: "member", events: MemberEvent, initial: { joined: false, loans: {} }, projection: { MemberJoined: (state) => ({ ...state, joined: true }), LoanRequested: (state, { bookId, loanId }) => ({ ...state, loans: { ...state.loans, [loanId]: { bookId, status: "requested" } } }), LoanConfirmed: (state, { loanId }) => setStatus(state, loanId, "active"), // Cancelled loans are kept, so the member's page can list them. LoanCancelled: (state, { loanId }) => setStatus(state, loanId, "cancelled") } }) .command("join", { payload: Schema.Struct({}), handler: Effect.fnUntraced(function*(state, _, { aggregateId }) { if (state.joined) return yield* new MemberAlreadyJoined({ memberId: aggregateId }) return MemberEvent.cases.MemberJoined.make({}) }) }) .command("requestLoan", { payload: Schema.Struct({ loanId: Schema.String, bookId: Schema.String }), handler: Effect.fnUntraced(function*(state, { bookId, loanId }, { aggregateId }) { if (!state.joined) return yield* new MemberNotFound({ memberId: aggregateId }) if (Object.values(state.loans).filter(countsAgainstLimit).length >= loanLimit) { return yield* new LoanLimitReached({ memberId: aggregateId, limit: loanLimit }) } return MemberEvent.cases.LoanRequested.make({ loanId, bookId }) }) }) // What the saga runs when the book is out. .command("cancelLoan", { payload: Schema.Struct({ loanId: Schema.String, reason: Schema.String }), handler: (state, payload) => Effect.succeed( state.loans[payload.loanId]?.status === "requested" ? MemberEvent.cases.LoanCancelled.make(payload) : [] ) }) const setStatus = (state: MemberState, loanId: string, status: Loan["status"]): MemberState => { const loan = state.loans[loanId] return loan === undefined ? state : { ...state, loans: { ...state.loans, [loanId]: { ...loan, status } } } } // The bug: every loan the member ever had counts against the limit, cancelled ones too. export const Member = makeMember(() => true) ``` The change looked harmless and the tests passed: none of them cancels a loan and then asks for another. ## Works for me The developer’s test is the obvious one, and it passes: examples/blog/wont-reproduce.test.ts ```ts it.effect("lends to a member who just joined", () => Fixture.make(Member) .givenEvents([MemberEvent.cases.MemberJoined.make({})]) .when("requestLoan", { loanId: "loan-1", bookId: "dune" }) .expectEvent("LoanRequested")) ``` What happened to Carla is not obvious. Over a few weeks she asked for three books that were out, and the saga cancelled each request. Then she asked for a fourth: examples/blog/wont-reproduce.ts ```ts yield* members.commands.join("carla", {}) for (const [loanId, bookId] of [["loan-1", "dune"], ["loan-2", "emma"], ["loan-3", "ulysses"]]) { yield* members.commands.requestLoan("carla", { loanId, bookId }) yield* members.commands.cancelLoan("carla", { loanId, reason: "BookAlreadyLent" }) } const refused = yield* Effect.flip(members.commands.requestLoan("carla", { loanId: "loan-4", bookId: "solaris" })) // refused: LoanLimitReached { memberId: "carla", limit: 3 }, and Carla has no books ``` With a table, all the developer would find is a row with `open_loans = 3`, or three rows in a `loans` table with a status. How did they get there? In which order? Was the counter ever right? The row is the end of a story, and the story is gone. ## Copy the stream With events, the story is the data. Carla’s stream can be exported as it is, as JSON, by a support script with read access: examples/blog/wont-reproduce.ts ```ts // The support script: Carla's stream, as JSON, for the bug report. const history = yield* members.history("carla", { payload: true }) const encode = Schema.encodeSync(Schema.toCodecJson(MemberEvent)) const exported = JSON.stringify(history.map(({ payload }) => encode(payload!)), null, 2) ``` The result is short and readable, which is half the diagnosis already: examples/blog/carla.json ```json [ { "_tag": "MemberJoined" }, { "_tag": "LoanRequested", "loanId": "loan-1", "bookId": "dune" }, { "_tag": "LoanCancelled", "loanId": "loan-1", "reason": "BookAlreadyLent" }, { "_tag": "LoanRequested", "loanId": "loan-2", "bookId": "emma" }, { "_tag": "LoanCancelled", "loanId": "loan-2", "reason": "BookAlreadyLent" }, { "_tag": "LoanRequested", "loanId": "loan-3", "bookId": "ulysses" }, { "_tag": "LoanCancelled", "loanId": "loan-3", "reason": "BookAlreadyLent" } ] ``` The file goes into the repository next to the test, and the test reads it: examples/blog/wont-reproduce.test.ts ```ts // Carla's stream, as the support script exported it. const carla = decodeStream(readFileSync(join(import.meta.dirname, "carla.json"), "utf8")) ``` Caution A stream can hold personal data: names, addresses, notes. Leave it out or replace it before it leaves production. With [personal data fields](/guides/personal-data/), `history` returns decrypted values, so the export script is the place to redact them. ## A failing test A fixture runs a command against a given history, without a database, in milliseconds. Given Carla’s history: examples/blog/wont-reproduce.test.ts ```ts it.effect("refuses Carla, who has no books", () => Effect.gen(function*() { const error = yield* Fixture.make(Member).givenEvents(carla).whenFails("requestLoan", { loanId: "loan-4", bookId: "solaris" }) assert.strictEqual(error._tag, "LoanLimitReached") })) ``` It fails the same way production did. The bug report is now a test, and the cause is one `inspectState` away: three loans in `state.loans`, all `cancelled`, all counted by the limit check. The fix counts only the loans that are open: examples/blog/wont-reproduce.ts ```ts // The fix: only loans that are open count. export const FixedMember = makeMember((loan) => loan.status !== "cancelled") ``` And Carla’s history becomes a regression test that stays in the suite: examples/blog/wont-reproduce.test.ts ```ts it.effect("lends to Carla, whose earlier loans were all cancelled", () => Fixture.make(FixedMember) .givenEvents(carla) .when("requestLoan", { loanId: "loan-4", bookId: "solaris" }) .expectEvent("LoanRequested")) ``` ## Carla’s account fixes itself There is no data to repair. Carla’s stream says what happened, and it was right all along: she asked, the books were out, the requests were cancelled. The member’s state, the one the limit check reads, is folded from those events every time, so the moment the fix is deployed, her state counts zero open loans and her next request goes through. The same goes for every other member who hit it, whether or not they filed a ticket. With an `open_loans` counter in a table, the fix would also need a migration to recompute the counter for everyone, from data that may not say what it should be. ## Rules of thumb * **Ask for the aggregate id, not for steps to reproduce.** The stream *is* the steps. * **Keep a support script that exports a stream as JSON**, encoded with the events’ own schema, and redacting personal data. * **Turn every such bug into a fixture with the real history.** It’s the shortest reproduction there is, and it keeps the bug from coming back. * **When a change adds a state, test the commands that read it.** Here a new status, `cancelled`, changed what the limit check counted, and no test asked for a loan after a cancellation. ## Next [Tests, schema changes and production](/blog/tests-schema-changes-and-production/) in the series covers fixtures and invariant tests, and the [testing guide](/guides/testing/) covers `Fixture` in full. # The double write > Store the loan, then tell the other systems, or the other way round: each order breaks on a bad day, and no transaction spans a database and a broker. With event sourcing the log is already the outbox. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian files a loan card in a ledger at the desk while a courier at the open door puts copies of the cards into his satchel, a mail cart waiting outside in the rain](/_astro/the-double-write.BTkb4_32_1OvPFL.webp) Sooner or later the library has to tell someone else. The notifications service emails members their due dates, the statistics team counts loans, the city’s network of libraries wants to know which books are out. They live in other processes, and they hear about loans through a message broker: Kafka, NATS, SQS, RabbitMQ. So a loan is two writes: one to our database, one to the broker. This post is about the moment between them, which is where a whole class of production bugs lives. Every snippet runs in [`examples/blog/double-write.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/double-write.test.ts). ## A broker that is sometimes down The example’s broker keeps messages in memory and can be told to fail the next few sends, the way a real one does when the network drops or it is restarting: examples/blog/double-write.ts ```ts export class BrokerDown extends Schema.TaggedError()("BrokerDown", {}) {} export interface Sent { readonly id: string | null readonly type: string readonly payload: unknown } /** A message broker in memory, which can be told to fail the next few sends. */ export class Broker extends Context.Service): Effect.Effect readonly received: Effect.Effect> failNext(times: number): Effect.Effect }>()("library/Broker") { static readonly layer = Layer.effect( Broker, Effect.gen(function*() { const received = yield* Ref.make>([]) const failures = yield* Ref.make(0) return Broker.of({ send: (messages) => Ref.modify(failures, (left) => [left > 0, Math.max(0, left - 1)]).pipe( Effect.flatMap((down) => down ? Effect.fail(new BrokerDown()) : Ref.update(received, (all) => [...all, ...messages]) ) ), received: Ref.get(received), failNext: (times) => Ref.set(failures, times) }) }) ) } ``` ## Store, then send The obvious version stores the loan, then publishes it: examples/blog/double-write.ts ```ts export const storeThenSend = Effect.fnUntraced(function*(bookId: string, loanId: string, memberId: string) { const books = yield* Books const broker = yield* Broker yield* books.commands.lend(bookId, { loanId, memberId }) yield* broker.send([{ id: null, type: "library.book-lent.v1", payload: { bookId, loanId, memberId } }]) }) ``` On a day the broker hiccups for one send: examples/blog/double-write.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* broker.failNext(1) const failed = yield* Effect.flip(storeThenSend("book-1", "loan-1", "alice")) // failed._tag: "BrokerDown", so the front desk tells Alice it didn't work const book = yield* books.state("book-1") // { status: "lent", loan: { loanId: "loan-1", memberId: "alice", … }, … } const afterStoring = yield* broker.received // []: nobody else will ever hear of loan-1 ``` The loan is stored. The message is lost. And the request failed, so the front desk tells Alice that the loan didn’t work while the library’s own records say she has the book. If the desk sends the same request again, `lend` records nothing for a loan it already has, and the send gets through this time. If nobody does, because Alice walked off with the book or tried another one, the notifications service never emails her a due date, and the statistics are one loan short, forever. A crash between the two lines does the same: a deploy, an out-of-memory kill, a pod moved to another node. There is no line of code that can run “after the commit, guaranteed”. A retry around the send helps with a short hiccup and does nothing for a process that is gone. ## Send, then store So swap them. If the message must not be lost, send it first: examples/blog/double-write.ts ```ts export const sendThenStore = Effect.fnUntraced(function*(bookId: string, loanId: string, memberId: string) { const books = yield* Books const broker = yield* Broker yield* broker.send([{ id: null, type: "library.book-lent.v1", payload: { bookId, loanId, memberId } }]) yield* books.commands.lend(bookId, { loanId, memberId }) }) ``` Now the failure goes the other way: examples/blog/double-write.ts ```ts const refused = yield* Effect.flip(sendThenStore("book-1", "loan-2", "bob")) // refused._tag: "BookAlreadyLent" const afterSending = yield* broker.received // [{ type: "library.book-lent.v1", payload: { bookId: "book-1", loanId: "loan-2", memberId: "bob" } }]: // everybody heard of a loan that never happened ``` The broker accepted the message. Then the book refused, because Alice has it. Every consumer now believes Bob borrowed *Dune*: Bob gets an email with a due date for a book he doesn’t have, and the statistics count a loan that never happened. The command could also fail for duller reasons, a lost connection or a version conflict, with the same result. Sending first means announcing a decision before it is made, and the aggregate is the one that makes it. Note What about a transaction around both? A database transaction covers the database. The broker is another system, with its own idea of committed, and the protocols that span the two, such as two-phase commit, are slow, fragile and mostly unsupported by brokers. In practice there is no transaction to put them in. ## The outbox The usual fix in a CRUD application is the *transactional outbox*: in the same transaction as the change, insert a row into an `outbox` table, and let a separate process read that table and publish what it finds, marking rows as sent. One transaction, one database, and the broker is only ever written from a table that already committed. With event sourcing you already have that table. The events are written in one append, they are never changed, and they have a position. A process that reads the log from where it last stopped and publishes what it finds is an outbox, without the extra table and without anyone having to remember to insert into it. effect-cqrs calls it `Outbox`: examples/blog/double-write.ts ```ts export const LoansOutbox = Outbox.make({ name: "loans-outbox", aggregates: [Book], messages: { BookLent: ({ loanId, memberId }, { aggregateId }) => ({ type: "library.book-lent.v1", payload: { bookId: aggregateId, loanId, memberId } }), BookReturned: ({ late, loanId }, { aggregateId }) => ({ type: "library.book-returned.v1", payload: { bookId: aggregateId, loanId, late } }) }, publish: (messages) => Broker.use((broker) => broker.send(messages.map(({ id, payload, type }) => ({ id, type, payload })))) }) export const Library = Outbox.layer([LoansOutbox]).pipe( Layer.provideMerge(Books.layer), Layer.provideMerge(Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, Broker.layer)) ) ``` `messages` turns domain events into integration events: a type with a version in it, and a payload that is a contract with other teams, which is not the same as the event’s shape. Events without a mapping, like `BookAdded`, are not published. The same bad day, with the outbox: examples/blog/double-write.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* broker.failNext(2) // The command only stores. The broker isn't on its path, so it can't fail it. const { position } = yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) // The outbox tries, fails, waits a little longer each time, and gets through. // `published` fails while the broker is down, so the example asks again. yield* outboxes.published(position).pipe(Effect.retry(Schedule.spaced("50 millis"))) const received = yield* broker.received // [{ id: "book/book-1@2", type: "library.book-lent.v1", payload: { bookId: "book-1", loanId: "loan-1", … } }] ``` The command succeeded although the broker was down, because the broker was never on its path: a command only appends. The outbox runs in the background, like a view model. It read the `BookLent`, failed to publish it, waited, tried again with growing pauses, and got through when the broker came back. Alice was told the truth at the desk, and everybody else heard about her loan a little later. ## What it costs * **Delivery is at least once.** The outbox saves its checkpoint after a batch is published. If the process dies in between, the batch is published again after the restart. Each message has an `id`, the event’s id, such as `book/book-1@2`, which never changes, so consumers keep the ids they have handled and skip repeats. Exactly once is not on offer from any broker either: at least once plus deduplication is how it’s done. * **The others hear a little later.** Usually milliseconds: a batch goes out when it is full or after `linger`, ten milliseconds by default. While the broker is down, it is longer, and `status` shows the outbox retrying. * **Order is per log.** Messages leave in the order the events were stored, and a failed batch is retried before anything after it. A consumer that reads in parallel has to keep order per aggregate itself, as Kafka does with partition keys. * **The checkpoint must be durable.** With checkpoints in memory, an outbox publishes the whole log again after every restart. Keep them in the database, next to the events. ## Rules of thumb * **Never write to two systems in one request.** One of the writes has to come from the other, after it has committed. * **A command only stores.** Whatever has to happen because of a decision reads it from the log: read models, sagas and outboxes. * **Publish integration events, not your events.** Map them in the outbox, give them a versioned type, and leave out what other teams shouldn’t depend on, or keep: personal data in particular. * **Consumers deduplicate on the message id.** It is the same every time a message is published. ## Next The [integration guide](/guides/integration/#outbox) covers `Outbox` in full, and [The saga you skipped](/blog/the-saga-you-skipped/) shows the same two-writes problem between two aggregates, where a saga plays the outbox’s part. # The form that overwrites > Two people edit the same record, and the second save quietly undoes the first. Event sourcing doesn't fix it by itself: commands that say what changed avoid most collisions, and an expected version catches the rest. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![Two librarians at separate desks each fill in their own copy of the same catalog card, while the large card on the wall between them shows one change and a smudge where the other was erased](/_astro/the-form-that-overwrites.BFw1XHBO_gqPUa.webp) Bianca notices that the catalog spells Frank Herbert without its second R, and fixes it. A minute later Carlo, who opened the same record before her fix, moves the book to another shelf and saves. The typo is back. Nobody did anything wrong, nobody got an error, and the fix is gone until someone notices it again. This is the *lost update*, and it is as old as forms. It lives in admin panels, CMSs, CRMs and every screen with a Save button. This post shows that event sourcing doesn’t fix it by itself, and what does. Every snippet runs in [`examples/blog/form-overwrites.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/form-overwrites.test.ts). ## The record and the screens A book’s record in the catalog is an aggregate, `listing`, with a title, an author and a shelf. It has two kinds of commands on purpose: one that saves the whole form, as a CRUD screen does, and some that say what changed: examples/blog/form-overwrites.ts ```ts const Fields = { title: Schema.NonEmptyString, author: Schema.NonEmptyString, shelf: Schema.NonEmptyString } export const ListingEvent = Schema.TaggedUnion({ ListingCreated: Fields, // What a form that saves every field records. ListingSaved: Fields, // What commands that say what changed record. AuthorCorrected: { author: Schema.NonEmptyString }, MovedToShelf: { shelf: Schema.NonEmptyString }, Retitled: { title: Schema.NonEmptyString } }) export interface ListingState { readonly title: string readonly author: string readonly shelf: string } /** A book's record in the catalog: what the librarians edit. */ export const Listing = Aggregate.make()({ name: "listing", events: ListingEvent, initial: { title: "", author: "", shelf: "" }, projection: { ListingCreated: (_, { author, shelf, title }) => ({ title, author, shelf }), ListingSaved: (_, { author, shelf, title }) => ({ title, author, shelf }), AuthorCorrected: (state, { author }) => ({ ...state, author }), MovedToShelf: (state, { shelf }) => ({ ...state, shelf }), Retitled: (state, { title }) => ({ ...state, title }) } }) .command("create", { payload: Schema.Struct(Fields), handler: (_, fields) => Effect.succeed(ListingEvent.cases.ListingCreated.make(fields)) }) .command("saveForm", { payload: Schema.Struct(Fields), handler: (_, fields) => Effect.succeed(ListingEvent.cases.ListingSaved.make(fields)) }) .command("correctAuthor", { payload: Schema.Struct({ author: Schema.NonEmptyString }), handler: (state, { author }) => Effect.succeed(state.author === author ? [] : ListingEvent.cases.AuthorCorrected.make({ author })) }) .command("moveToShelf", { payload: Schema.Struct({ shelf: Schema.NonEmptyString }), handler: (state, { shelf }) => Effect.succeed(state.shelf === shelf ? [] : ListingEvent.cases.MovedToShelf.make({ shelf })) }) .command("retitle", { payload: Schema.Struct({ title: Schema.NonEmptyString }), handler: (state, { title }) => Effect.succeed(state.title === title ? [] : ListingEvent.cases.Retitled.make({ title })) }) ``` The librarians’ screens come from a read model. Next to the fields, it keeps the `version` of the record it shows, from the metadata of the last event it handled: examples/blog/form-overwrites.ts ```ts export interface Screen extends ListingState { /** The version of the record this screen shows, which an edit sends back. */ readonly version: number } /** What the librarians' screens show: every record, with the version it was read at. */ export class Screens extends Context.Service>>()("library/Screens") { static readonly layer = Layer.effect(Screens, Ref.make>(new Map())) } const show = (listingId: string, version: number, update: (fields: ListingState) => ListingState) => Screens.use((screens) => Ref.update(screens, (all) => { const fields = all.get(listingId) ?? { title: "", author: "", shelf: "" } return new Map(all).set(listingId, { ...update(fields), version }) }) ) export const ScreensView = ViewModel.make({ name: "screens", aggregates: [Listing], dispose: Screens.use((screens) => Ref.set(screens, new Map())), handlers: { ListingCreated: ({ author, shelf, title }, { aggregateId, version }) => show(aggregateId, version, () => ({ title, author, shelf })), ListingSaved: ({ author, shelf, title }, { aggregateId, version }) => show(aggregateId, version, () => ({ title, author, shelf })), AuthorCorrected: ({ author }, { aggregateId, version }) => show(aggregateId, version, (f) => ({ ...f, author })), MovedToShelf: ({ shelf }, { aggregateId, version }) => show(aggregateId, version, (f) => ({ ...f, shelf })), Retitled: ({ title }, { aggregateId, version }) => show(aggregateId, version, (f) => ({ ...f, title })) } }) ``` ## The whole form The CRUD version of a save is `UPDATE listings SET title = ?, author = ?, shelf = ? WHERE id = ?`, every field, from what the screen showed. The same thing as a command: examples/blog/form-overwrites.ts ```ts // Bianca and Carlo open the same record, author misspelled, on shelf A3. const bianca = yield* openDune const carlo = bianca // Bianca fixes the author. Carlo, a minute later, moves the book. yield* listings.commands.saveForm("dune", { title: bianca.title, author: "Frank Herbert", shelf: bianca.shelf }) yield* listings.commands.saveForm("dune", { title: carlo.title, author: carlo.author, shelf: "B1" }) const dune = yield* listings.state("dune") // { title: "Dune", author: "Frank Herbet", shelf: "B1" }: Carlo's form put the typo back ``` Event sourcing didn’t save us. The log has both `ListingSaved` events, so at least someone looking at the history can see what happened, which a table can’t offer. But the state is wrong, because Carlo’s command said “the author is Frank Herbet”: his form sent back every field it showed, including the one he never touched. The problem isn’t storage. It’s that the command says more than the user meant. ## Say what changed Bianca meant “correct the author”. Carlo meant “move it to B1”. Those are the commands: examples/blog/form-overwrites.ts ```ts yield* openDune yield* listings.commands.correctAuthor("dune", { author: "Frank Herbert" }) yield* listings.commands.moveToShelf("dune", { shelf: "B1" }) const dune = yield* listings.state("dune") // { title: "Dune", author: "Frank Herbert", shelf: "B1" }: both edits kept ``` Both edits survive, in either order, because neither command carries a field it doesn’t change. Most lost updates disappear this way, without locks, versions or error messages: two people working on the same record rarely change the same thing. This is what people mean by *task-based UI*: screens built around what users do, “correct author”, “move to shelf”, “withdraw”, rather than around a table’s columns. It costs more than one generic form. It also gives you events that say what happened, `AuthorCorrected` rather than `ListingSaved`, which every read model, report and audit after it will appreciate. ## When two edits really collide Sometimes two people do change the same thing. Bianca retitles the book “Dune (Deluxe Edition)”, and Carlo, from the screen he opened before, retitles it “Dune: 60th Anniversary”. The last one would win, and Carlo would never know he overwrote a decision he hadn’t seen. His screen knows which version it showed. Sending it back as the `expectedVersion` turns the silent overwrite into a question: examples/blog/form-overwrites.ts ```ts const bianca = yield* openDune const carlo = bianca // Both screens show version 1. yield* listings.commands.retitle("dune", { title: "Dune (Deluxe Edition)" }, { expectedVersion: bianca.version }) const refused = yield* Effect.flip( listings.commands.retitle("dune", { title: "Dune: 60th Anniversary" }, { expectedVersion: carlo.version }) ) // refused: WrongExpectedVersion { expectedVersion: 1, actualVersion: 2 } // Carlo's screen reloads, shows Bianca's title, and he decides what to do. ``` Bianca’s command matched: the record was at version 1, as her screen said. Carlo’s didn’t: the record had moved to version 2. The command failed before its handler ran, with both versions in the error, and the screen can reload and show Carlo what changed. Then he decides, knowing. Note Over HTTP, `CommandApi` reads the `If-Match` header as the expected version, as in `If-Match: 1`. A screen that read the record over HTTP can send it back from the version it was given. `WrongExpectedVersion` is never retried. That is the difference from the other version check in effect-cqrs: when two *processes* append to the same aggregate at once, the loser reloads the state and runs the handler again, and the rules decide on the new state. That’s safe because the handler sees what changed. A user doesn’t, so deciding again for them would be the same silent overwrite. ## When to send a version Not always. An expected version refuses any change in between, even one that didn’t touch what the user is editing. With it on every command, Carlo’s shelf move would have been refused because Bianca corrected the author, and he would have had to reload for nothing. * **Leave it out** for commands whose result doesn’t depend on what the user saw: “move to shelf B1” means the same whatever the title is. The aggregate’s rules still run on the current state. * **Send it** when the user’s decision is based on what they read: a new title chosen while looking at the old one, a price set while looking at the cost, an approval of a document someone may have edited since. ## Rules of thumb * **A command carries what the user changed, not what the screen showed.** Most lost updates are a form that sends back fields nobody touched. * **Read models keep the version they show.** The `version` in an event’s metadata is free, and the screen will need it. * **Send `expectedVersion` when the decision depends on what was read.** It turns an overwrite into a conflict the user can see. * **Never retry a conflict for the user.** Reload, show the change, and let them decide. ## Next The [commands guide](/guides/commands/#expected-version) covers expected versions and how they combine with command ids, and [I clicked twice](/blog/i-clicked-twice/) covers the other kind of repeat: the same request arriving twice. # The message that blocks the queue > One event a handler can't read stops everything behind it, at three in the morning. Stop and wait, or set it aside and carry on: when each is right, and how a parked event comes back once the bug is fixed. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A line of books waits on a conveyor at a spine-labelling machine, while a librarian lifts the one book it jammed on onto a side cart](/_astro/the-message-that-blocks-the-queue.C0Q4wEgk_1GNdbn.webp) Every queue consumer meets it eventually: the *poison message*. One message the code can’t handle, because of a bug, or data nobody expected. The consumer fails, retries, fails again, and everything behind it waits. In the morning the dashboard shows a lag of six hours and one error, repeated forty thousand times. View models are consumers of the log, so they meet it too. This post shows what effect-cqrs does by default, why, and the other choice. Every snippet runs in [`examples/blog/blocked-queue.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/blocked-queue.test.ts). ## Shelf marks The library prints a shelf mark on every spine: the first three letters of the author’s surname. A view model works them out as books are added, and it has a bug, an assumption every developer has made about names: examples/blog/blocked-queue.ts ```ts /** The shelf mark printed on each book's spine: the first letters of the author's surname. */ export class ShelfMarks extends Context.Service>>()( "library/ShelfMarks" ) { static readonly layer = Layer.effect(ShelfMarks, Ref.make>(new Map())) } // The bug: an author is assumed to have a first name and a surname. const surnameOf = (author: string) => author.split(" ")[1]! const shelfMarks = (surname: (author: string) => string, onFailure?: ViewModel.FailurePolicy) => ViewModel.make({ name: "shelf-marks", aggregates: [Book], dispose: ShelfMarks.use((marks) => Ref.set(marks, new Map())), onFailure, handlers: { BookAdded: ({ author }, { aggregateId }) => ShelfMarks.use((marks) => Ref.update(marks, (all) => new Map(all).set(aggregateId, surname(author).slice(0, 3).toUpperCase())) ) } }) export const ShelfMarksView = shelfMarks(surnameOf) ``` It works for Frank Herbert and William Gibson. Then a librarian adds *The Odyssey*: examples/blog/blocked-queue.ts ```ts yield* books.commands.add("dune", { title: "Dune", author: "Frank Herbert" }) yield* books.commands.add("odyssey", { title: "The Odyssey", author: "Homer" }) yield* books.commands.add("neuromancer", { title: "Neuromancer", author: "William Gibson" }) ``` Homer has one name. `split(" ")[1]` is `undefined`, and `.slice` throws. ## Stop and wait By default, a view model that fails on an event stops there and tries again: after 100 milliseconds, then twice as long each time, up to every 30 seconds. It never skips the event and never handles a later one first: examples/blog/blocked-queue.ts ```ts const status = yield* waitFor(statusOf("shelf-marks"), (status) => Option.isSome(status?.lastError ?? Option.none())) // { name: "shelf-marks", position: 1, lastError: Some(ViewModelError { position: 2, cause: TypeError }) } const marks = yield* ShelfMarks.use(Ref.get) // Map { "dune" => "HER" }: nothing for Homer, and nothing for Gibson either const catalog = yield* Catalog.use(Ref.get) // all three books: the catalog has no bug, and doesn't wait for the shelf marks ``` The shelf marks are stuck at position 1, after *Dune*. *Neuromancer* has no shelf mark either, although nothing is wrong with it: it is behind Homer. The catalog, a different view model over the same events, has all three books. Each view model reads the log on its own, from its own checkpoint, so one that is stuck holds up no one else. That’s the default because it is the safe one. A view model that is stalled is *visibly* wrong: `status` shows where it stopped and why, a health check can alert on it, and the moment the bug is fixed and deployed it carries on from Homer, in order, and is right again. A view model that skipped an event would be wrong in a way nothing shows, for as long as it lives. For an account balance, a stock level, or anything where every event changes a total, missing one is worse than being late. Note `caughtUp` doesn’t return while a view model is stalled, since it waits for every view model to reach a position. Put a timeout on it where a request is waiting. ## Or set it aside Shelf marks are not a balance. A book without a label for a day is a small problem. Every book after it without a label is a big one. For a read model like this, a gap is better than a stop, and `park` says so: examples/blog/blocked-queue.ts ```ts export const ParkingShelfMarksView = shelfMarks(surnameOf, ViewModel.park({ attempts: 3 })) ``` After three failed attempts, the view model records the event in `ParkedEvents` and moves on: examples/blog/blocked-queue.ts ```ts yield* waitFor(viewModels.parked("shelf-marks"), (parked) => parked.length > 0) yield* viewModels.caughtUp() const marks = yield* ShelfMarks.use(Ref.get) // Map { "dune" => "HER", "neuromancer" => "GIB" } const parked = yield* viewModels.parked("shelf-marks") // [{ viewModel: "shelf-marks", position: 2, eventId: "book/odyssey@1", error: "…TypeError…", attempts: 3, … }] ``` Gibson has his shelf mark. Homer is parked: the event’s position and id, the error, and how many times it was tried. That list is the to-do list for the morning, and it’s in one place, not spread over forty thousand log lines. ## Bring it back The fix takes the last word as the surname, so a single name is all surname. Deploy it, and hand the parked event to the fixed handler: examples/blog/blocked-queue.ts ```ts // The fix: the surname is the last word, and a single name is all surname. const lastWord = (author: string) => author.split(" ").at(-1) ?? author export const FixedShelfMarksView = shelfMarks(lastWord, ViewModel.park({ attempts: 3 })) ``` examples/blog/blocked-queue.ts ```ts const result = yield* viewModels.retryParked({ viewModel: "shelf-marks" }) // { succeeded: [2], failed: [] } const marks = yield* ShelfMarks.use(Ref.get) // Map { "dune" => "HER", "neuromancer" => "GIB", "odyssey" => "HOM" } ``` `retryParked` handles the parked events again, in order of position. Those that succeed leave the list. Those that fail again stay, with the new error, so a fix that didn’t work is as visible as the bug was. ## What parking costs * **Order.** Homer’s shelf mark was written after Gibson’s. For shelf marks that doesn’t matter: each book has its own. For a read model where events of the same thing must be applied in order, a later event can be handled before the parked one, and the result can be wrong in a way the retry won’t undo. Park only where events don’t depend on each other. * **Replays skip parked events** until they are retried, since the handler would fail on them again. * **Someone has to look.** A parked event is a bug report the system filed for you. Keep `ParkedEvents` in the database with `ParkedEvents.layerSql()` rather than in memory, watch the `parked` count in `status`, and alert on it. ## Rules of thumb * **Stall by default.** A view model that is visibly behind is better than one that is silently wrong. * **Park where a gap is better than a stop:** labels, search indexes, notifications, statistics. Never where events add up to a total. * **Read models are independent.** One stuck view model holds up nothing but itself, which is a reason to split read models by purpose rather than build one that does everything. * **Fix, deploy, retry.** The parked event is still in the log, unchanged, waiting for a handler that can read it. ## Next The [view models guide](/guides/view-models/#failing-handlers) covers failures, parking and retries in full, and [Fix the code, not the data](/blog/fix-the-code-not-the-data/) covers the bug that doesn’t fail: a handler that writes the wrong thing. # The saga you skipped > Borrowing a book without a saga, in the two ways that look right: one aggregate that records another's events, and one that calls another's commands. What breaks, and what the saga does instead. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![Two members hold loan cards for the same single copy of a book, their lines tangled, while a courier waits by a dotted path in the background](/_astro/the-saga-you-skipped.2dF_e-d7_Z1kxY5r.webp) Sagas are the part of CQRS that teams skip most often. Aggregates and read models make sense on day one, while a saga looks like ceremony: a workflow, a trigger, an engine, just to lend a book? It is so much shorter to let the member’s command do everything at once. This post takes that shortcut, twice, on the library of the series. Both versions compile, pass a happy-path test, and look fine in a demo. Then we look at what they do to the data, and at what the saga of [part 4](/blog/sagas/) does differently. Every snippet runs in [`examples/blog/without-saga.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/without-saga.test.ts). The rules are the ones from the series: **a member may have at most three books at a time, and a copy can only be with one member.** The first rule is about the member, the second about the book. ## Mistake 1: one aggregate records everything Borrowing changes the member and the book, so the member’s command records both changes. The member’s events get a `BookLent`, and `borrow` returns two events in one go: examples/blog/without-saga.ts ```ts const GreedyMemberEvent = Schema.TaggedUnion({ MemberJoined: { name: Schema.NonEmptyString }, LoanStarted: { loanId: Schema.String, bookId: Schema.String }, // The book's event, recorded in the member's stream. BookLent: { loanId: Schema.String, bookId: Schema.String, dueOn: Schema.DateTimeUtc } }) export const GreedyMember = Aggregate.make()({ name: "member", events: GreedyMemberEvent, initial: { joined: false, loans: [] }, projection: { MemberJoined: (state) => ({ ...state, joined: true }), LoanStarted: (state, { loanId }) => ({ ...state, loans: [...state.loans, loanId] }), BookLent: (state) => state } }) .command("join", { payload: Schema.Struct({ name: Schema.NonEmptyString }), handler: Effect.fnUntraced(function*(state, { name }, { aggregateId }) { if (state.joined) return yield* new MemberAlreadyJoined({ memberId: aggregateId }) return GreedyMemberEvent.cases.MemberJoined.make({ name }) }) }) .command("borrow", { payload: Schema.Struct({ loanId: Schema.String, bookId: Schema.String }), handler: Effect.fnUntraced(function*(state, { bookId, loanId }, { aggregateId }) { if (!state.joined) return yield* new MemberNotFound({ memberId: aggregateId }) if (state.loans.length >= loanLimit) { return yield* new LoanLimitReached({ memberId: aggregateId, limit: loanLimit }) } const dueOn = DateTime.add(yield* DateTime.now, loanPeriod) // Two events in one go: the member's, and the one the book should have decided on. return [ GreedyMemberEvent.cases.LoanStarted.make({ loanId, bookId }), GreedyMemberEvent.cases.BookLent.make({ loanId, bookId, dueOn }) ] }) }) ``` It reads well. One command, one atomic append, both facts recorded together. Here is a morning at the library: examples/blog/without-saga.ts ```ts const members = yield* AggregateService.make(GreedyMember) yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* members.commands.join("alice", { name: "Alice Rossi" }) yield* members.commands.join("bob", { name: "Bob Bianchi" }) yield* members.commands.borrow("alice", { loanId: "loan-1", bookId: "book-1" }) yield* members.commands.borrow("bob", { loanId: "loan-2", bookId: "book-1" }) // Both succeed: two loans of one copy. const book = yield* books.state("book-1") // { status: "available", loan: null, … }: the book never heard of either ``` Alice and Bob both have the only copy of *Dune*. Nothing failed, so nothing told us. Look at where the rule “one member per copy” is checked in `borrow`: nowhere, and it can’t be. The handler decides from the *member’s* state, which holds the member’s loans and nothing about the book. The member wrote a fact about the book, `BookLent`, without asking the book. And the event is in the wrong place. `BookLent` sits in Alice’s stream and in Bob’s, not in `book-1`’s, so the book’s own state says it is on the shelf. Every read model that follows the book’s events agrees with it. The library now has two versions of the truth, and the one in the book’s stream is wrong. The usual patch is to read the book first, with `books.state(bookId)`, and refuse if it is lent. That only moves the problem: the book’s stream still never gets the `BookLent`, so the read always says available. Even if it did, reading and then writing elsewhere is check-then-act: between the read and the member’s append, another member can borrow the same copy, and no version check stops it, because the append goes to a different stream. Note An aggregate is a consistency boundary. It can guarantee its rules because every change to its data goes through its own commands, in turn, checked against its own version. **A rule can only be enforced by the aggregate that owns the data the rule is about.** ## Mistake 2: one aggregate calls the other Fine, the book must decide for itself. So the member’s command calls the book’s command, and records its own event once the book has said yes: examples/blog/without-saga.ts ```ts const CallingMemberEvent = Schema.TaggedUnion({ MemberJoined: { name: Schema.NonEmptyString }, LoanStarted: { loanId: Schema.String, bookId: Schema.String } }) export const CallingMember = Aggregate.make()({ name: "member", events: CallingMemberEvent, initial: { joined: false, loans: [] }, projection: { MemberJoined: (state) => ({ ...state, joined: true }), LoanStarted: (state, { loanId }) => ({ ...state, loans: [...state.loans, loanId] }) } }) .command("join", { payload: Schema.Struct({ name: Schema.NonEmptyString }), handler: Effect.fnUntraced(function*(state, { name }, { aggregateId }) { if (state.joined) return yield* new MemberAlreadyJoined({ memberId: aggregateId }) return CallingMemberEvent.cases.MemberJoined.make({ name }) }) }) .command("borrow", { payload: Schema.Struct({ loanId: Schema.String, bookId: Schema.String }), handler: Effect.fnUntraced(function*(state, { bookId, loanId }, { aggregateId }) { if (!state.joined) return yield* new MemberNotFound({ memberId: aggregateId }) if (state.loans.length >= loanLimit) { return yield* new LoanLimitReached({ memberId: aggregateId, limit: loanLimit }) } // Now the book decides, through its own command, from inside the member's decision. const books = yield* Books yield* books.commands.lend(bookId, { loanId, memberId: aggregateId }) return CallingMemberEvent.cases.LoanStarted.make({ loanId, bookId }) }) }) ``` The book’s events are back in the book’s stream, and its rule runs: examples/blog/without-saga.ts ```ts const members = yield* AggregateService.make(CallingMember) yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* members.commands.join("alice", { name: "Alice Rossi" }) yield* members.commands.join("bob", { name: "Bob Bianchi" }) yield* members.commands.borrow("alice", { loanId: "loan-1", bookId: "book-1" }) const refused = yield* Effect.flip(members.commands.borrow("bob", { loanId: "loan-2", bookId: "book-1" })) // refused._tag: "BookAlreadyLent". So far, so good. ``` This version passes every test you’d think of writing first. The problem is in a test you wouldn’t: one where something goes wrong *between* the two writes. `borrow` now writes to two streams: the book’s, inside `lend`, then the member’s, when the handler returns. Those are two appends. No transaction spans them, and in general none can: the two streams may live in different tables, databases or services. So let’s lose the connection to the database at the worst moment, with a store that fails one append: examples/blog/without-saga.ts ```ts /** * The store in memory, except that the first append with an event of this type fails, * as it would if the connection to the database dropped at that moment. */ export const failingOnce = (eventType: string) => Layer.effect( EventStore.EventStore, Effect.gen(function*() { const store = yield* EventStore.EventStore const failed = yield* Ref.make(false) return EventStore.EventStore.of({ ...store, append: (input) => Effect.gen(function*() { if (input.events.some(({ type }) => type === eventType) && !(yield* Ref.getAndSet(failed, true))) { return yield* new EventStore.EventStoreError({ operation: "append", cause: new Error("Connection lost") }) } return yield* store.append(input) }) }) }) ).pipe(Layer.provide(EventStore.layerMemory)) ``` Same code, same request, on that store: examples/blog/without-saga.ts ```ts yield* books.commands.add("book-2", { title: "Neuromancer", author: "William Gibson" }) yield* members.commands.join("carol", { name: "Carol Verdi" }) // The member's append fails, after the book's went through. const failure = yield* Effect.flip(members.commands.borrow("carol", { loanId: "loan-3", bookId: "book-2" })) // failure._tag: "EventStoreError" const book = yield* books.state("book-2") // { status: "lent", loan: { loanId: "loan-3", memberId: "carol", … }, … } const carol = yield* members.state("carol") // { joined: true, loans: [] } ``` The book is lent to Carol, for loan `loan-3`. Carol has no loan `loan-3`. The command failed, so the client tells Carol to try again later, and if she doesn’t, the book stays lent to a loan that doesn’t exist. Nothing in the system knows that a loan was half done, so nothing will come back to finish it or undo it. A lost connection is only one way to get there. Any of these does the same: * **The process stops** between the two appends: a crash, a deploy, a scaled-down pod. * **The member’s append loses a race.** Another request for Carol got there first, so the service reloads her state and runs the handler again, which calls `lend` again. Here `lend` happens to be idempotent, so the second call records nothing. If the handler had instead picked “the first free copy”, the retry could lend a second one. * **The member’s rules change their mind on the retry.** After the reload Carol has three books, so the retry fails with `LoanLimitReached`, after the book was lent. There are quieter costs too. A handler is meant to only decide, because it can run more than once. The member module now depends on the books service, and an aggregate whose handler calls another aggregate’s command can deadlock if that one ever calls back, since commands on one aggregate take turns. And when someone asks why a book is lent, the member’s stream has no answer. ## The same loan, with a saga The saga starts from a different question. Not “how do I write both changes at once?”, which has no good answer, but “how do I make sure the process *finishes*?” The member only ever records its own decision. `requestLoan` checks the member’s rule and appends `LoanRequested` to the member’s stream, one atomic write to one aggregate. From that moment, the intention to borrow is a fact in the log, and the rest of the process can always be finished from it. Then the saga carries the data from one aggregate to the next. This is the workflow of [part 4](/blog/sagas/): examples/blog/library.ts ```ts // One run per loan: lend the book and confirm the loan, or cancel it if the book is out. export const Borrow = Saga.workflow("Borrow", { payload: { loanId: Schema.String, memberId: Schema.String, bookId: Schema.String }, idempotencyKey: ({ loanId }) => loanId }) export const BorrowWorkflow = Saga.toLayer(Borrow, ({ bookId, loanId, memberId }) => Effect.gen(function*() { const books = yield* Books const members = yield* Members const lent = yield* Saga.command("lend", { error: Schema.Union([BookAlreadyLent, BookNotFound]), execute: books.commands.lend(bookId, { loanId, memberId }) }).pipe( Effect.as(true), Effect.catchTag(["BookAlreadyLent", "BookNotFound"], (error) => Saga.command("cancel", { execute: members.commands.cancelLoan(memberId, { loanId, reason: error._tag }) }).pipe(Effect.as(false))) ) if (lent) { yield* Saga.command("confirm", { execute: members.commands.confirmLoan(memberId, { loanId }) }) } })) ``` Look at what it does with the data. It reads the member id, the book id and the loan id from the member’s event, and hands them to the *book’s* command. The book decides with its own state and its own rule. The answer goes back to the *member*, as one of the member’s own commands, `confirmLoan` or `cancelLoan`. Every change is still made by a command on the aggregate that owns the data. The saga owns no data, and changes none directly: it puts together what several aggregates know, and asks each one to do its part. Now the same failure, the same store, on the library of the series. This time the append that fails is the member’s `LoanConfirmed`, the saga’s last step: examples/blog/without-saga.ts ```ts yield* books.commands.add("book-2", { title: "Neuromancer", author: "William Gibson" }) yield* members.commands.join("carol", { name: "Carol Verdi" }) // One write, to one aggregate: the member records what she asked for. yield* members.commands.requestLoan("carol", { loanId: "loan-3", bookId: "book-2" }) // The saga lends the book, fails to confirm the loan, tries again, and confirms it. const outcome = yield* settled("carol", "loan-3") // Option.some("LoanConfirmed") const book = yield* books.state("book-2") // { status: "lent", loan: { loanId: "loan-3", memberId: "carol", … }, … } const carol = yield* members.state("carol") // { …, loans: { "loan-3": { bookId: "book-2", status: "active" } } } ``` The loan settles: the book is lent to Carol, and Carol has the loan. Here is why: * **Each step is one command on one aggregate**, with one atomic append. A step either happened or it didn’t. There is no half of a step. * **A step that fails is tried again.** `Saga.command` retries a lost race or a store error a few times, with a backoff that starts at 50 milliseconds, so a dropped connection costs a short wait. * **Finished steps are not done again.** The workflow engine records the outcome of every step. A run that restarts after a crash skips the book’s `lend`, which already happened, and goes on with the confirmation. With a durable engine, that holds across restarts of the whole process. * **The commands are idempotent**, so a step that ran but whose outcome wasn’t recorded can run again harmlessly. * **The state in between is explicit.** Until the saga confirms or cancels it, Carol’s loan is `requested`. It holds one of her three places, so she can’t over-borrow while the book decides, and it shows that something is in progress. If a failure outlasts the retries, the run stops there, and the loan stays visibly `requested` instead of silently missing. Here are the three versions side by side: | | One aggregate records both | One aggregate calls the other | Saga | | ----------------------------------- | -------------------------- | ----------------------------- | ---------------------------------- | | The book’s rule is checked | No | Yes | Yes | | The book’s events are in its stream | No | Yes | Yes | | A failure between two writes | — | Leaves them disagreeing | Retried, then resumed | | Where the process is recorded | Nowhere | Nowhere | The member’s events and the engine | ## Rules of thumb * **One command, one aggregate, one append.** When a command needs to change a second aggregate, the first one records what it decided, and something else, a saga, asks the second. * **An event names its own aggregate’s facts.** A member’s event that says “the book was lent” is a fact the member had no right to decide. * **A decision that needs another aggregate’s state is a process, not a command.** Model the steps in between, such as `requested`, in the state, because they are real: the book really hasn’t answered yet. * **If you can’t live with the step in between, check the boundary.** A rule that must hold instantly across two aggregates may mean they are one aggregate. That is a design decision, and sometimes the right one. But “every loan in the library” is too big to be one aggregate, so here the saga wins. What about a view model whose handler runs the book’s command? It gets closer: the member records its intention, and the handler sees every event at least once. But it keeps no record of which steps ran or which branch it took, so it can’t resume a half-done process or replay one the same way. The saga’s trigger *is* a view model, plus the workflow engine that keeps that record. ## Next The series shows the whole saga, its trigger and its wiring in [part 4](/blog/sagas/), and the [sagas guide](/guides/sagas/) covers compensations, timeouts and durable engines. # The wrong event > A bug in a command stores wrong due dates for a week. A replay can't fix it, and editing the log would hide it, so the fix records a correction: one more event, with its reason. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian pins a new, clearly marked correction card next to an older loan card on a ribbon of cards, without removing the old one](/_astro/the-wrong-event.CsAC0XVC_1Nxjpt.webp) [Fix the code, not the data](/blog/fix-the-code-not-the-data/) ended on a warning: a replay rebuilds read models, and it can’t change what the commands decided. A bug in a view model is fixed by its own handlers. A bug in a command writes wrong facts into the log, and the log is never edited. This post is about that bug: what you do when the log itself says something false. Every snippet runs in [`examples/blog/wrong-event.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/wrong-event.test.ts). ## A week of short loans A release changes how the loan period is written, and the new line says hours where it meant days. The library’s `lend` command is the one from the series, with the period made a parameter so the example can run both versions: examples/blog/wrong-event.ts ```ts /** The book of the series, with the loan period as a parameter, so two deploys can differ. */ const makeBook = (loanPeriod: Partial) => Aggregate.make()({ name: "book", events: BookEvent, initial: { status: "unknown", loan: null }, projection: { BookAdded: (state) => ({ ...state, status: "available" }), BookLent: (_, { dueOn, loanId, memberId }) => ({ status: "lent", loan: { loanId, memberId, dueOn } }), BookReturned: () => ({ status: "available", loan: null }), DueDateCorrected: (state, { dueOn }) => state.loan === null ? state : { ...state, loan: { ...state.loan, dueOn } } } }) .command("add", { payload: Schema.Struct({ title: Schema.NonEmptyString, author: Schema.NonEmptyString }), handler: Effect.fnUntraced(function*(state, payload, { aggregateId }) { if (state.status !== "unknown") return yield* new BookAlreadyAdded({ bookId: aggregateId }) return BookEvent.cases.BookAdded.make(payload) }) }) .command("lend", { payload: Schema.Struct({ loanId: Schema.String, memberId: Schema.String }), handler: Effect.fnUntraced(function*(state, { loanId, memberId }, { aggregateId }) { if (state.status === "unknown") return yield* new BookNotFound({ bookId: aggregateId }) if (state.loan !== null) return yield* new BookAlreadyLent({ bookId: aggregateId }) const dueOn = DateTime.add(yield* DateTime.now, loanPeriod) return BookEvent.cases.BookLent.make({ loanId, memberId, dueOn }) }) }) .command("returnBook", { payload: Schema.Struct({}), handler: Effect.fnUntraced(function*(state, _, { aggregateId }) { if (state.loan === null) return yield* new BookNotLent({ bookId: aggregateId }) const { dueOn, loanId, memberId } = state.loan const late = DateTime.isGreaterThan(yield* DateTime.now, dueOn) return BookEvent.cases.BookReturned.make({ loanId, memberId, late }) }) }) .command("correctDueDate", { payload: Schema.Struct({ loanId: Schema.String, dueOn: Schema.DateTimeUtc, reason: Schema.String }), handler: Effect.fnUntraced(function*(state, { dueOn, loanId, reason }, { aggregateId }) { if (state.loan?.loanId !== loanId) return yield* new LoanNotFound({ bookId: aggregateId, loanId }) // Correcting it again to the same date records nothing, so the script can run twice. if (DateTime.Equivalence(state.loan.dueOn, dueOn)) return [] return BookEvent.cases.DueDateCorrected.make({ loanId, dueOn, reason }) }) }) // The bug: hours where days were meant. export const BookWithTheBug = makeBook({ hours: 21 }) // The fix. export const Book = makeBook({ days: 21 }) ``` For a week, every `BookLent` says the book is due 21 hours after it went out. That is not a read model getting it wrong. The command decided it, the event recorded it, and the reminder job reads it from a read model that does exactly what it should: examples/blog/wrong-event.ts ```ts export interface OpenLoan { readonly bookId: string readonly memberId: string readonly lentAt: DateTime.Utc readonly dueOn: DateTime.Utc } /** The loans still open, by loan id, with when each began and when it is due. */ export class OpenLoans extends Context.Service>>()( "library/OpenLoans" ) { static readonly layer = Layer.effect(OpenLoans, Ref.make>(new Map())) } const change = (update: (loans: Map) => void) => OpenLoans.use((open) => Ref.update(open, (loans) => { const next = new Map(loans) update(next) return next }) ) export const OpenLoansView = ViewModel.make({ name: "open-loans", aggregates: [Book], dispose: OpenLoans.use((open) => Ref.set(open, new Map())), handlers: { BookLent: ({ dueOn, loanId, memberId }, { aggregateId, timestamp }) => change((loans) => loans.set(loanId, { bookId: aggregateId, memberId, lentAt: timestamp, dueOn })), BookReturned: ({ loanId }) => change((loans) => loans.delete(loanId)), DueDateCorrected: ({ dueOn, loanId }) => change((loans) => { const loan = loans.get(loanId) if (loan !== undefined) loans.set(loanId, { ...loan, dueOn }) }) } }) /** The loans that are overdue at a given time, as the reminder job asks for them. */ export const overdueAt = (at: DateTime.Utc) => OpenLoans.use((open) => Ref.get(open).pipe( Effect.map((loans) => [...loans].filter(([, { dueOn }]) => DateTime.isGreaterThan(at, dueOn)).map(([loanId]) => loanId) ) ) ) ``` Alice borrows *Dune* during the bad week. The fix is deployed, and Bob borrows *Neuromancer* after it: examples/blog/wrong-event.ts ```ts const withTheBug = yield* AggregateService.make(BookWithTheBug) yield* withTheBug.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* withTheBug.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) ``` examples/blog/wrong-event.ts ```ts const books = yield* AggregateService.make(Book) yield* books.commands.add("book-2", { title: "Neuromancer", author: "William Gibson" }) const { position } = yield* books.commands.lend("book-2", { loanId: "loan-2", memberId: "bob" }) yield* viewModels.caughtUp(position) const inTwoDays = DateTime.add(yield* DateTime.now, { days: 2 }) const beforeCorrection = yield* overdueAt(inTwoDays) // ["loan-1"]: Alice's book is due 21 hours after she took it ``` Bob’s loan is right. Alice’s is still due the day after she took it, and two days later the reminder job tells her she is late. Fixing the code stopped new damage. It didn’t touch the event that says Alice’s book was due on Tuesday, and no replay will, because a replay reads that event as faithfully as the first time. ## Three things not to do **Don’t edit the event.** It is tempting: one `UPDATE` on the events table and the date is right. But the log is what everything else trusts to never change. Read models have already handled the old value and won’t see the new one, since their checkpoints are past it. An outbox may have published it to another service. A test or an audit that compares the log with what happened now finds the past rewritten, with no trace of who did it or why. **Don’t hide it in an upcaster.** An upcaster reads events of an old schema version as the current shape, as in [part 5](/blog/tests-schema-changes-and-production/). The week’s events have the same version as every other `BookLent`, so an upcaster can’t even tell them apart. And if you found a way, the fix would live in the read path forever, invisible in the log, changing what the aggregate decided after the fact. An upcaster is for a change of *shape*, true of every old event. This is a change of *value*, true of some. **Don’t patch the read models.** Correcting the due date in the reminder job’s table fixes the reminders until the next replay brings the 21 hours back. And the book itself still believes it: its own state, folded from the same event, has the wrong `dueOn`, so when Alice returns it on day ten, `returnBook` marks her late. ## Record the correction The log is a record of what happened, and something did happen: the library found a mistake and fixed it. That is a fact like any other, so it gets an event: examples/blog/wrong-event.ts ```ts export const BookEvent = Schema.TaggedUnion({ BookAdded: { title: Schema.NonEmptyString, author: Schema.NonEmptyString }, BookLent: { loanId: Schema.String, memberId: Schema.String, dueOn: Schema.DateTimeUtc }, BookReturned: { loanId: Schema.String, memberId: Schema.String, late: Schema.Boolean }, // The correction is a fact of its own: what the due date is now, and why it changed. DueDateCorrected: { loanId: Schema.String, dueOn: Schema.DateTimeUtc, reason: Schema.String } }) ``` The book handles it like any other event. Its projection replaces the due date, and the `correctDueDate` command in the aggregate above checks that the loan is the one the book is on, and records nothing if the date is already right, so the script that sends it can run twice. The script finds the loans to correct from the read model: every open loan due sooner than 21 days after it began came from the bug. Then it sends one command per loan, with the reason: examples/blog/wrong-event.ts ```ts // Every open loan that is due sooner than 21 days after it began came from the bug. const affected = [...(yield* OpenLoans.use(Ref.get))].filter(([, { dueOn, lentAt }]) => DateTime.isLessThan(dueOn, DateTime.add(lentAt, { days: 21 })) ) for (const [loanId, { bookId, lentAt }] of affected) { yield* books.commands.correctDueDate(bookId, { loanId, dueOn: DateTime.add(lentAt, { days: 21 }), reason: "The loan period was 21 hours instead of 21 days" }).pipe(Metadata.withMetadata({ ranBy: "scripts/fix-due-dates.ts" })) } yield* viewModels.caughtUp() const afterCorrection = yield* overdueAt(inTwoDays) // [] const history = yield* books.history("book-1", { payload: true }) // BookAdded, BookLent, DueDateCorrected: the mistake and the correction, whose // metadata says { ranBy: "scripts/fix-due-dates.ts" } ``` Everything that follows the log picks the correction up in order. The reminder job’s read model handles `DueDateCorrected` and moves the date. The book’s own state has the new date, so when Alice brings *Dune* back on day ten, the test shows `returnBook` deciding she is on time. And `history` tells the whole story: lent with the wrong date, then corrected, by which command, with which reason. Note A correction is a command like any other, so it gets everything commands get: validation, the aggregate’s rules and a version check against concurrent writers. `Metadata.withMetadata` adds what the command carries beside its payload, here the script that ran it, and every event it writes records it. Put the user who ran the script there too, and the log says who corrected it. ## What a table would have done With a `loans` table, the fix is an `UPDATE`: ```sql UPDATE loans SET due_on = lent_on + interval '21 days' WHERE lent_on BETWEEN '2026-11-02' AND '2026-11-09' AND returned = false; ``` The rows are right afterwards. Everything else is gone: that Alice’s row ever said Tuesday, that she got a reminder because of it, that anyone changed it. If she disputes a fine next month, the table says she was never late and has no idea why she was told so. Unless someone built an audit table beforehand, the database now holds a cleaner past than the one that happened. ## Rules of thumb * **A wrong fact is corrected by a new fact.** Name it after what happened, `DueDateCorrected` rather than `BookLentAgain`, and give it a reason. * **Make the correction a command on the aggregate.** It goes through the same rules and the same version check as everything else, and the aggregate’s state learns from it. * **Find what to correct with a read model, fix it with commands.** The query can be thrown away after the script runs. The corrections stay. * **Design for the corrections you will need.** A domain where people make mistakes, which is every domain, needs a way to say “that was wrong”. Events like `DueDateCorrected`, `PaymentReversed` or `AddressAmended` are part of the model, not an embarrassment. * **Some consequences are already out.** Alice got the reminder. A view model or a saga that handles `DueDateCorrected` can send her an apology, which a silent `UPDATE` never could. ## Next [Fix the code, not the data](/blog/fix-the-code-not-the-data/) covers the other half: bugs in read models, which a replay repairs. The [aggregates guide](/guides/aggregates/) covers commands, projections and history. # Three ways to read > A lent book read from its aggregate, from a read model as it is, and from a read model that waits for the command. Which one is current, when to use each, and how a browser picks one over HTTP. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![Three readers and one book: one reads the book itself, one studies the catalog board, and one waits with a numbered ticket beside an hourglass](/_astro/three-ways-to-read.VWpQQPa4_NOfP2.webp) Alice borrows *Dune* at the front desk, and the catalog on the screen still shows it on the shelf. She reloads, and it’s gone. Nothing is broken: the catalog is a read model, and read models run a moment behind the commands. “CQRS means eventual consistency” is repeated so often that it sounds like a rule. It isn’t. Delay comes from reading something that is updated *after* the write, and that is one way of reading among several. In effect-cqrs a read is one of three kinds, and each query picks the one it needs. Every snippet below runs in [`examples/blog/reading.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/reading.test.ts). ## 1. The aggregate: always current The first way doesn’t use a read model at all. It folds the book’s own events, as [part 2](/blog/your-first-aggregate/#reading) did: examples/blog/reading.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) const book = yield* books.state("book-1") // { status: "lent", …, loan: { memberId: "alice", … } }, every time ``` `state` reads the store, from the state the service remembers onwards, so it sees every event that was appended, by this process or another. It is the same state the next command on the book will be decided on. It can’t be behind, because there is nothing in between to fall behind. The cost is that it answers one question only: the state of *this* book, by its id. It can’t list the available books or find Alice’s loans without loading every book. For the page of one thing, though, such as a book, an order or an account, it is often all you need. That is also the page a user tends to land on right after acting on it. ## 2. A read model as it is: a moment behind The catalog of [part 3](/blog/read-models/) is a view model. It handles events in a fiber of its own, after the command has stored them: examples/blog/reading.ts ```ts const rightNow = (yield* Catalog.use(Ref.get)).get("book-1") // { available: false }, { available: true } or undefined: the catalog may not have // seen the loan, or even the book, yet ``` Right after the command, the view model may or may not have run. The gap is usually milliseconds. A slow handler, a restart or a handler that keeps failing makes it longer, and `viewModels.status` says how far each view model has got. For most reads that doesn’t matter. Nobody can tell whether the catalog they just opened is twenty milliseconds old, and a search, a dashboard or a monthly total is no different. This is the cheapest read there is: no fold, no wait, data already in the shape of the page. ## 3. A read model after the command: your own writes The trouble is only with the person who just acted. Alice brings *Dune* back, and the next screen she sees is the catalog. She knows what she did, and expects to see it. Every command returns the position of its last event in the store. `caughtUp` waits until every view model of the process has handled that position: examples/blog/reading.ts ```ts const { position } = yield* books.commands.returnBook("book-1", {}) yield* viewModels.caughtUp(position) const afterReturn = (yield* Catalog.use(Ref.get)).get("book-1") // { title: "Dune", author: "Frank Herbert", available: true }, every time ``` The catalog now includes the return, and every command stored before it. Commands that others ran after it may be there or not. This is “read your own writes”, not a lock on the library, and it only costs a wait for the one read that needs it. ## Over HTTP In an application, the command and the query are two requests, often from a browser. The position has to make the trip between them. Here is the library as an HTTP API, the book’s commands from [`CommandApi.group`](/guides/integration/#http) next to a catalog endpoint of our own: examples/blog/reading.ts ```ts export const BookState = Schema.Struct({ status: Schema.Literals(["unknown", "available", "lent"]), title: Schema.String, author: Schema.String, loan: Schema.NullOr(Schema.Struct({ loanId: Schema.String, memberId: Schema.String, dueOn: Schema.DateTimeUtc })) }) export const CatalogEntry = Schema.Struct({ bookId: Schema.String, title: Schema.String, author: Schema.String, available: Schema.Boolean }) export class LibraryApi extends HttpApi.make("library") // POST /book/:id/add, /lend and /returnBook, and GET /book/:id, which folds the events .add( CommandApi.group(Book, { state: BookState, errors: [BookAlreadyAdded, BookNotFound, BookAlreadyLent, BookNotLent] }) ) // GET /catalog, from the view model, after the position in Read-After if there is one .add( HttpApiGroup.make("catalog") .add(HttpApiEndpoint.get("list", "/catalog", { success: Schema.Array(CatalogEntry) })) .middleware(CommandApi.ReadAfter) ) {} ``` `CommandApi.ReadAfter` is middleware for the query endpoints. It needs the view models, and the handlers need the catalog: examples/blog/reading.ts ```ts const CatalogHandlers = HttpApiBuilder.group(LibraryApi, "catalog", (handlers) => Effect.gen(function*() { const catalog = yield* Catalog return handlers.handle("list", () => Ref.get(catalog).pipe(Effect.map((entries) => Array.from(entries, ([bookId, entry]) => ({ bookId, ...entry })) ))) })) export const Server = HttpApiBuilder.layer(LibraryApi).pipe( Layer.provide([CommandApi.layer(LibraryApi, "book", Effect.service(Books)), CatalogHandlers]), // The catalog's handlers need the middleware, and the middleware needs the view models. Layer.provide(CommandApi.layerReadAfter({ timeout: "2 seconds" })), Layer.provide(ReadSide) ) ``` Every command answers with its position, in the body and in an `Event-Position` header. A query that sends it back in a `Read-After` header waits until the view models have handled it. Here is a client, the way a browser would call it: examples/blog/reading.ts ```ts const post = (path: string, body: object) => fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }) await post("/book/book-1/add", { title: "Dune", author: "Frank Herbert" }) const lent = await post("/book/book-1/lend", { loanId: "loan-1", memberId: "alice" }) const position = lent.headers.get("event-position")! // The book's own page reads the aggregate: always current. const book = await (await fetch("/book/book-1")).json() // The catalog page sends the position back, and waits until the catalog has the loan. const catalog = await (await fetch("/catalog", { headers: { "read-after": position } })).json() // [{ bookId: "book-1", title: "Dune", author: "Frank Herbert", available: false }] ``` The client chooses, request by request. The book’s page reads the aggregate, and is always current. The catalog page after Alice’s loan sends the position, and includes the loan. Someone who is just browsing the catalog sends nothing, and gets an answer at once. A few details matter in production: * **A timeout.** A view model that is stalled on a failing handler would hold the request forever. After `timeout` the request is answered with `503` and `NotCaughtUp`, and the client can try again, or read without the header and accept an older answer. * **One process.** `ReadAfter` waits for the view models of the process that serves the request. With several replicas, send the read to one that runs the view models it needs. * **Browsers on another origin** only see `Event-Position` if the server lists it in `Access-Control-Expose-Headers`. ## Why not update the read model with the command? Some CQRS setups update their read tables in the same transaction as the write, so a read is never behind. effect-cqrs doesn’t, on purpose. * **Every command would wait for every read model.** A slow handler would slow down loans, and a failing one would refuse them. A broken catalog should show an old catalog, not stop the library. * **Read models are rebuilt from the log.** A replay, or a new view model that starts from the first event, runs the same handlers on the same events. A projection that also runs inside commands is a second path that has to give exactly the same result. * **They live elsewhere.** A read model in a search index or another database can’t join a transaction with the event store. When a screen must be exact, it reads the aggregate. When it must include the user’s own change, it waits for the position. Either way the cost falls on the one read that needs it, not on every command. ## Which one when | The question | Read | Up to date | | -------------------------------------- | ------------------------------------------------------------ | ---------------- | | What is the state of this book? | `state(id)`, `GET /book/:id` | Always | | The catalog, right after I lent a book | The catalog after `caughtUp(position)`, or with `Read-After` | Includes my loan | | Which books are on the shelf? | The catalog as it is | A moment behind | The read side is not one property of the system, fixed when it was designed. It is a choice each query makes, and the three choices sit side by side in one application. The [view models guide](/guides/view-models/#reading-your-own-writes) and the [HTTP guide](/guides/integration/#reading-your-own-writes) have the reference. # Two pods, two emails > The service is scaled to two replicas, and every member gets every email twice. Background work that reads the log has to run in one place, and take over somewhere else when that place goes away. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![Two clerks at identical desks each put the same letter into an envelope, while one of them reaches for the single brass key hanging on the wall between them](/_astro/two-pods-two-emails.99X9e91k_Z1PFJKU.webp) The library service runs fine on one machine. Then traffic grows, or someone wants zero downtime deploys, and the Kubernetes deployment goes from one replica to two. Commands are fine: both replicas append to the same store, and optimistic concurrency settles any race. The next morning a member writes in: *why did I get two emails for one loan?* Everything that reads the log in the background, view models, sagas and outboxes, now runs twice. This post shows what goes wrong and the fix. Every snippet runs in [`examples/blog/two-pods.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/two-pods.test.ts). ## Emails through an outbox The library emails a member when a book is lent to them. An email is something that must happen once, outside the system, so it goes through an [outbox](/blog/the-double-write/), which reads the log from its checkpoint and sends what it finds: examples/blog/two-pods.ts ```ts export interface Email { readonly to: string readonly subject: string } /** The mail service both replicas send through. It keeps what it sent. */ export class Mailer extends Context.Service readonly sent: Effect.Effect> }>()("library/Mailer") { static readonly layer = Layer.effect( Mailer, Effect.gen(function*() { const sent = yield* Ref.make>([]) return Mailer.of({ send: (email) => Ref.update(sent, (all) => [...all, email]), sent: Ref.get(sent) }) }) ) } // Members are emailed when a book is lent to them. export const Notifications = Outbox.make({ name: "notifications", aggregates: [Book], messages: { BookLent: ({ memberId }, { aggregateId }) => ({ type: "email", payload: { to: memberId, subject: `You borrowed ${aggregateId}` } }) }, publish: (messages) => Mailer.use((mailer) => Effect.forEach(messages, ({ payload }) => mailer.send(payload as Email), { discard: true })) }) ``` Each replica runs the outbox, against what they share: the event store, the checkpoints and the mail service. examples/blog/two-pods.ts ```ts // What each replica runs: the outbox, on the shared store, checkpoints and mailer. const replica = Layer.fresh(Outbox.layer([Notifications])) // What the replicas share: in production, the database and the mail service. export const Shared = Layer.mergeAll(EventStore.layerMemory, Checkpoints.layerMemory, Mailer.layer) ``` ## Two replicas examples/blog/two-pods.ts ```ts const first = yield* Scope.make() const second = yield* Scope.make() const replicaA = Context.get(yield* Layer.buildWithScope(replica, first), Outbox.Outboxes) const replicaB = Context.get(yield* Layer.buildWithScope(replica, second), Outbox.Outboxes) yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) const lent = yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) yield* replicaA.published(lent.position) yield* replicaB.published(lent.position) const afterOneLoan = yield* mailer.sent ``` With nothing else, `afterOneLoan` holds Alice’s email twice. Each replica read the `BookLent`, from the same checkpoint, and each sent it. Both did their job. Neither knew the other existed. It’s not only emails. Every view model in both replicas handles every event: * **Read models in a shared database** get every update twice. Setting a value twice is harmless, but adding to a count, appending to a list or inserting a row is not. * **Checkpoints race.** Both replicas save their position under the same name, and the slower one can move it back, so events are handled again after a restart. * **Sagas start twice.** The trigger is a view model. The workflow engine’s idempotency key catches the duplicate start, if it is durable and shared, but every trigger still runs on both. Read models kept in each replica’s memory are the exception: each replica *should* build its own. That’s why running everything everywhere is the default. ## One holder, one standby The fix is to decide, for each view model, which replica runs it. effect-cqrs calls this a *lease*: `ViewModel.layer` takes one per view model before running it, from the `Leases` service in context. The replica holding the lease runs the view model, and the others wait as standbys. In production that is `Leases.layerPostgres()`, which holds each lease as a PostgreSQL advisory lock, on the same database as the events: ```ts const Live = Layer.mergeAll(ViewModel.layer([Catalog]), Outbox.layer([Notifications])).pipe( Layer.provideMerge(Layer.mergeAll( PostgresEventStore.layer(), PostgresEventStore.layerCheckpoints(), Leases.layerPostgres() )) ) ``` The example runs both replicas in one test, so it uses leases in memory that work the same way: the first to ask holds the lease, and the next one in line gets it when the holder goes away. examples/blog/two-pods.ts ```ts /** * Leases in memory that two replicas in one process can share: the first to ask holds a * lease, and when its scope closes the next one in line gets it. `Leases.layerPostgres()` * does the same across processes, with PostgreSQL advisory locks. */ export const sharedLeases = Layer.sync(Leases.Leases, () => { const lines = new Map>>() return Leases.Leases.of({ acquire: (name) => Effect.gen(function*() { const held = yield* SubscriptionRef.make(false) const line = lines.get(name) ?? [] lines.set(name, [...line, held]) if (line.length === 0) yield* SubscriptionRef.set(held, true) yield* Effect.addFinalizer(() => Effect.gen(function*() { const rest = (lines.get(name) ?? []).filter((ref) => ref !== held) lines.set(name, rest) const next = rest[0] if (next !== undefined) yield* SubscriptionRef.set(next, true) }) ) return { held: SubscriptionRef.get(held), changes: SubscriptionRef.changes(held) } }) }) }) ``` With them, the same two replicas send Alice one email. Replica A holds the lease and runs the outbox. Replica B’s outbox waits, and its `status` says `holdsLease: false`. ## The holder goes away Replicas don’t live forever. A deploy replaces them, a node is drained, a process runs out of memory. When the holder stops, its lease is released, and the standby takes over from the checkpoint the holder saved: examples/blog/two-pods.ts ```ts // Replica A is stopped: a deploy, a crash, the node it ran on is drained. yield* Scope.close(first, Exit.void) yield* books.commands.add("book-2", { title: "Emma", author: "Jane Austen" }) const lentToBob = yield* books.commands.lend("book-2", { loanId: "loan-2", memberId: "bob" }) yield* holding(replicaB) yield* replicaB.published(lentToBob.position) const afterFailover = yield* mailer.sent ``` Bob gets his email from replica B. Nothing was sent twice and nothing was skipped, because the checkpoint is shared and only ever moved by the holder. With PostgreSQL, the lock lives on a database connection, so it goes when the process dies or loses its connection. Standbys try to take over every 5 seconds by default. Caution A holder notices it lost its connection at its next check, so for up to one interval the old holder and the new one can both handle the same events. A lease makes duplicates rare, not impossible. Consumers of an outbox still deduplicate on the message id, and handlers that write to a shared database still write idempotently. ## What else changes with replicas * **`caughtUp` waits only for local view models.** On a standby it doesn’t wait for the view models another replica runs, since nothing in this process moves them. Read your own writes where the view model runs, or from the shared read model directly. * **`replay` and `retryParked` run on the holder.** A standby refuses them. * **In an Effect cluster**, `Singleton` from `effect/cluster` does the same job with the cluster’s shard ownership, and needs no leases. ## Rules of thumb * **Commands scale out, background work runs once.** Any number of replicas can take commands. Each view model, saga trigger and outbox runs in one of them at a time. * **Turn leases on before the second replica.** The first duplicate email is the cheapest reminder you will get. * **Read models in memory run everywhere, shared ones run once.** Leases are for view models whose data, or effects, are shared. * **Stay idempotent anyway.** Failover has a window, and at-least-once delivery has always allowed a repeat. ## Next The [view models guide](/guides/view-models/#several-processes) covers leases and standbys in full, and [The double write](/blog/the-double-write/) covers outboxes. # What it looked like last Tuesday > A member was refused a book that is on the shelf today, and asks why. A table only knows today. The log can show the book as it was at that moment, and every event the request caused, across aggregates. *A post after the series [CQRS with effect-cqrs](/blog/), on its lending library. November 2026.* ![A librarian lays a transparent sheet over one card in a ribbon of loan cards on the desk, while a puzzled member waits beside her and a past day is circled on the wall calendar](/_astro/what-it-looked-like-last-tuesday.Dqtnk3Lz_2oKBCV.webp) Monday morning, Bob is at the desk. Last Tuesday he asked for *Dune* on the website, and the library refused. Today *Dune* is on the shelf. Was the website broken? Is he being treated differently? He would like to know, and so would the librarian. Support tickets look like this everywhere: why did the system do *that*, back *then*? This post answers Bob’s from the library’s log. Every snippet runs in [`examples/blog/last-tuesday.test.ts`](https://github.com/davidnussio/effect-cqrs/blob/main/examples/blog/last-tuesday.test.ts), which lets the days pass in between. ## Tuesday This is what happened, with the library and the borrowing saga of [the series](/blog/sagas/): examples/blog/last-tuesday.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) yield* members.commands.join("alice", { name: "Alice Rossi" }) yield* members.commands.join("bob", { name: "Bob Bianchi" }) yield* members.commands.requestLoan("alice", { loanId: "loan-1", bookId: "book-1" }) yield* settled("alice", "loan-1") yield* members.commands.requestLoan("bob", { loanId: "loan-2", bookId: "book-1" }) yield* settled("bob", "loan-2") ``` Alice asked first and got the book. Bob asked a moment later, and the saga cancelled his loan. On Friday Alice brought it back. ## What a table can say The `books` table says *Dune* is available, `lent_to` empty. That is true, and it is all the table knows. Was it available last Tuesday at 10:04? It doesn’t say. Why was Bob refused? A request log might have a line, if it is still kept, if it was logged at all, and if someone can find it among a million others. Most support tickets like Bob’s end with “we can’t tell, sorry”, or with a developer reading code and guessing. The log can say more, because it never threw anything away. examples/blog/last-tuesday.ts ```ts const today = yield* books.state("book-1") // { status: "available", loan: null, … }: on the shelf ``` ## Start from Bob Bob’s own history says what happened to his request, with the reason the saga recorded: examples/blog/last-tuesday.ts ```ts const bob = yield* members.history("bob", { payload: true }) const refusal = bob.find(({ type }) => type === "LoanCancelled")! // { type: "LoanCancelled", payload: { loanId: "loan-2", reason: "BookAlreadyLent" }, // timestamp: Tuesday, causationId: "member/bob@2", correlationId: "…" } ``` `LoanCancelled`, reason `BookAlreadyLent`, on Tuesday. Two more fields come with every event: * the **causation id** names what directly caused it: `member/bob@2`, the second event in Bob’s stream, his own `LoanRequested`. The saga that cancelled the loan was started by his request, and said so. * the **correlation id** is shared by everything that followed from the same original request, however many steps and aggregates away. ## The book, back then Was the book really lent? Ask the book, as it was at the moment of the refusal: examples/blog/last-tuesday.ts ```ts // The book as it was when Bob was refused. `finishTime` leaves out events from that // instant on, so take the next millisecond. const backThen = yield* books.state("book-1", { finishTime: DateTime.add(refusal.timestamp, { milliseconds: 1 }) }) // { status: "lent", loan: { loanId: "loan-1", memberId: "alice", dueOn: … }, … } ``` `state` with a `finishTime` folds the book’s events up to that instant, the same fold that gives today’s state, cut short. At the moment Bob was refused, the book was lent to Alice, with its due date. The refusal was right, and the librarian can show why, with the evidence. This isn’t a feature added for support. It falls out of keeping events: a state is a fold of events, and a fold can stop anywhere. Every aggregate can be seen as it was at any instant, for free, from the day the first event was written. ## One request, end to end The correlation id goes further. Gather every event that shares it, from every aggregate: examples/blog/last-tuesday.ts ```ts // Everything one request caused, across aggregates, by its correlation id. const causedBy = (correlationId: string | null) => store.read().pipe( Stream.filter((event) => event.correlationId === correlationId), Stream.map(({ aggregateId, aggregateName, type, version }) => `${aggregateName}/${aggregateId}@${version} ${type}` ), Stream.runCollect ) const bobsRequest = yield* causedBy(refusal.correlationId) // ["member/bob@2 LoanRequested", "member/bob@3 LoanCancelled"] const alicesRequest = yield* causedBy( (yield* members.history("alice")).find(({ type }) => type === "LoanRequested")!.correlationId ) // ["member/alice@2 LoanRequested", "book/book-1@2 BookLent", "member/alice@3 LoanConfirmed"] ``` Bob’s request is two events: asked, cancelled. The book has none, because it refused and a refusal writes nothing. Alice’s request, read the same way, is three events in two aggregates: she asked, the book was lent, her loan was confirmed. The whole saga, in order, from a single id. Note Reading the whole log to filter it is fine in a test or a support script. For a screen support uses every day, keep a read model indexed by correlation id, written by a view model like any other. ## What else it answers The same two tools, a state at an instant and events by correlation, answer questions that are hard or impossible with tables: * **Disputes.** “I returned it on time.” The `BookReturned` has a timestamp and the `late` decision, and the state just before it has the due date. * **Audits.** Who changed what, when, as a result of which request. With [metadata](/blog/the-wrong-event/), also by which user or script. * **Bug reports.** “It said available, then refused me.” The events of that request, and the state each aggregate was in, are the reproduction. * **Reports as of a date.** Fold the events before a date into a report, and it says what the numbers were then, not what they are now with today’s corrections mixed in. ## Rules of thumb * **Record the reason in the event.** `LoanCancelled { reason }` answers Bob in one line. A bare `LoanCancelled` sends someone to read code. * **Keep correlation and causation flowing.** effect-cqrs carries them through commands, view models and sagas. Bring the correlation id in from the edge, the HTTP request or the message, and the trail starts where the user did. * **Answer support from the log, not from logs.** Application logs are for operators, rotate away, and say what the code thought. The event log says what the system decided, and it is kept. ## Next [Sagas](/blog/sagas/) in the series builds the borrowing process this post investigates, and the [aggregates guide](/guides/aggregates/) covers state, history, correlation and causation. # Why CQRS and event sourcing > Part 1 of the series: what goes missing when you store only the current state, event sourcing by hand in forty lines, and what CQRS adds on top. *Part 1 of 5 of [CQRS with effect-cqrs](/blog/). November 2026.* ![A librarian stamps loan cards onto a long paper ribbon that runs across the library to display boards where readers look at what it records](/_astro/why-cqrs-and-event-sourcing.Du_QX-M9_2qCiU5.webp) Most applications keep the current state of things. A library keeps a table of books, and lending one updates a row: ```sql UPDATE books SET lent_to = 'bob' WHERE id = 'book-1'; ``` The row is now right, and everything it said before is gone. Who had the book before Bob? How many times has it been lent this year? Was it brought back late? Nobody asked those questions when the table was designed, so the answers were thrown away one `UPDATE` at a time. You can add an audit table, a `loans` history and a `returned_late` flag, but each one has to be planned before the data it needs comes in, and kept in step with the row by hand. This series is about the other way of doing it: keep what happened, and work out the current state from that. Over five posts we build a small lending library with [effect-cqrs](/), from the first event to a saga, tests and a database: 1. **Why CQRS and event sourcing**, this post: the idea, by hand, without a framework. 2. [Your first aggregate](/blog/your-first-aggregate/): events, commands and typed errors. 3. [Read models you can throw away](/blog/read-models/): view models, checkpoints and replays. 4. [Sagas](/blog/sagas/): processes that span several aggregates. 5. [Tests, schema changes and production](/blog/tests-schema-changes-and-production/): fixtures, upcasting, SQLite and PostgreSQL, and forgetting a person. ## Event sourcing in forty lines Instead of a row, a book is the list of things that happened to it. Each event is a fact in the past tense: examples/blog/by-hand.ts ```ts type BookEvent = | { readonly _tag: "BookAdded"; readonly title: string } | { readonly _tag: "BookLent"; readonly memberId: string } | { readonly _tag: "BookReturned" } ``` The current state is not stored anywhere. It is what you get when you apply the events one after another, starting from an empty book. That is a fold, `reduce` in JavaScript: examples/blog/by-hand.ts ```ts interface Book { readonly title: string readonly lentTo: string | null } const initial: Book = { title: "", lentTo: null } // Applies one event to the state: what happened, folded into what is true now. const evolve = (state: Book, event: BookEvent): Book => { switch (event._tag) { case "BookAdded": return { ...state, title: event.title } case "BookLent": return { ...state, lentTo: event.memberId } case "BookReturned": return { ...state, lentTo: null } } } export const currentState = (events: ReadonlyArray): Book => events.reduce(evolve, initial) ``` Changing a book means deciding on new events. A command looks at the state, checks what the rules allow, and either returns the events that happen next or refuses: examples/blog/by-hand.ts ```ts // Looks at the state and decides what happens next, or refuses. export const lend = (state: Book, memberId: string): ReadonlyArray => { if (state.lentTo !== null) throw new Error(`Already lent to ${state.lentTo}`) return [{ _tag: "BookLent", memberId }] } ``` Put together, the log only ever grows: examples/blog/by-hand.ts ```ts const log: Array = [{ _tag: "BookAdded", title: "Dune" }] log.push(...lend(currentState(log), "alice")) log.push({ _tag: "BookReturned" }) log.push(...lend(currentState(log), "bob")) currentState(log) // { title: "Dune", lentTo: "bob" } timesLent(log) // 2 ``` Two functions carry the whole model: `evolve` turns events into state, and `lend` turns state and a request into events. Neither of them touches a database, a clock or the network, so both are trivial to test. ## What the log gives you The questions from the start of this post are now easy, because the facts they need were never thrown away: examples/blog/by-hand.ts ```ts // A question nobody asked when the events were written, answered from the same log. export const timesLent = (events: ReadonlyArray): number => events.filter((event) => event._tag === "BookLent").length ``` `timesLent` was written after the events. It works on every book ever lent, including the ones lent before anyone thought of counting. That is the main argument for event sourcing: **the log answers questions you haven’t thought of yet.** Some more: * **History for free.** Every change has a time, an order and, as we’ll see, a cause. An audit trail is not a feature you add, it is the storage. * **Time travel.** The state of a book last March is the fold of the events up to last March. * **Debugging by replay.** A bug report comes with the exact sequence of events that led to it, and you can run them again in a test. * **Decisions you can explain.** A refused command can say which earlier event it was refused because of. Why not keep a `loans` table instead? With one, `timesLent` is a `SELECT count(*)`, and just as right. The difference is when the table was made and what it holds. It has the loans recorded since someone created it, in the columns they thought of then, and the next question, about late returns or renewals, needs another table that starts empty. It also says the same thing as `books.lent_to`, so the two are kept in step by hand, and when they disagree nothing tells you which one is right. The log is the one place a fact is written, and every other shape is worked out from it. That doesn’t take the table away: with CQRS, below, a `loans` table becomes a read model filled by replaying the log from the first event, so it holds the loans from before it existed, and you can drop it and build it again when it is wrong, as [Fix the code, not the data](/blog/fix-the-code-not-the-data/) shows. ## Where CQRS comes in The log is a great way to *write*, and a poor way to *read*. “Which books are available right now?” would mean folding every book in the library on every request. “Which books does Alice have?” would mean folding them all and filtering. CQRS, Command Query Responsibility Segregation, splits the two: * **The write side** takes commands. It loads one thing, an *aggregate* such as a book, folds its events, decides, and appends new events. It only cares about the rules, and it only ever needs one aggregate’s events at a time. * **The read side** follows the log and keeps *read models*, each shaped for the queries it serves: a catalog with an `available` flag, a map from member to the books they have, a count of loans per month. They are projections of the log, so you can throw one away and rebuild it, or add a new one that starts from the first event ever written. The two ideas are separate, and each works without the other. Plenty of applications do CQRS on ordinary tables: commands update rows, and queries read a view or a second table, often in the same database, with no queue in between. What CQRS asks for is that the code that changes things and the code that answers questions don’t share a model. effect-cqrs does both because they fit: once the log is the truth, every read model is a projection of it, which you can rebuild, change or add at any time without migrating data. This is the path a command takes in effect-cqrs, and the outline of the next three posts: ## How a command flows 1. ### Command A typed call, or JSON from a transport through `dispatch`. The payload is checked against its schema first. [Commands guide](/guides/commands/) 2. ### Aggregate The service rebuilds the aggregate's state from its events, and the handler decides what happened, or fails with a typed error. [Aggregates guide](/guides/aggregates/) 3. ### Event store The events are appended, but only if nobody wrote to that aggregate since its state was loaded. [Storage guide](/guides/storage/) 4. ### View models Each one reads the new events from its own checkpoint. `caughtUp` waits until your own write is there. [View models guide](/guides/view-models/) The price is that the read side is a moment behind the write side. After a command, a read model may not have seen its events yet. Part 3 shows how to wait for your own write when you need to, and [Three ways to read](/blog/three-ways-to-read/) when you don’t need a read model at all. ## What the forty lines leave out The version above works on an array in memory, for one user at a time. A real application needs more, and each of these is what the rest of the series is about: * **Storage and concurrency.** Two librarians lending the same copy at the same moment must not both succeed. The store has to append only if nobody wrote in between. * **Typed, validated input.** `lend` above trusts its arguments and refuses with a thrown `Error`. Refusals are part of the domain, and the caller should know which ones can come. * **Read models that keep up.** They must follow the log as it grows, pick up where they stopped after a restart, and never miss an event. * **Processes.** Borrowing a book involves the book *and* the member, who may only have three books at a time. One command changes one aggregate, so something has to coordinate the two. * **Change.** Events written today will be read in five years by code that looks different. The log can’t be rewritten, so old events have to be read as new ones. * **Forgetting.** The GDPR lets a person ask to be forgotten, in a log that is never edited. ## When not to use it Event sourcing is not free. You trade a table you can query with SQL for a log plus read models you write yourself, and most reads run a moment behind the writes. Events are a contract with your own future, so their shape needs more care than a column does. It pays off where the history *is* the domain, such as loans, accounts, orders, bookings or anything audited, and where the rules are rich enough that “what happened” matters more than “what it looks like now”. A settings page or a list of tags is happier as a table. Nothing stops you from mixing the two in one application. ## Next In [part 2](/blog/your-first-aggregate/) we write the book as an aggregate with effect-cqrs: the same `evolve` and `decide`, with schemas, typed errors and a store that handles concurrent writers. # Your first aggregate > Part 2 of the series: a book as an aggregate, with events as schemas, commands that decide, typed errors, and a store that handles concurrent writers. *Part 2 of 5 of [CQRS with effect-cqrs](/blog/). November 2026.* ![One book stands in a glowing frame on the librarian's desk; request cards pass through it on a belt, and one is refused with a red mark](/_astro/your-first-aggregate.Dpajqwaz_HgmFX.webp) In [part 1](/blog/why-cqrs-and-event-sourcing/) we wrote event sourcing by hand: a list of events, an `evolve` function that folds them into a state, and a `lend` function that decides on new events. This post turns that book into an *aggregate* with [effect-cqrs](/), and runs commands against a store. effect-cqrs is built on [Effect 4](https://effect.website), so commands are effects with typed errors, and storage, clocks and configuration are services you provide. It needs Node 24 or later: ```bash pnpm add effect-cqrs effect ``` All the code of this series lives in [`examples/blog`](https://github.com/davidnussio/effect-cqrs/tree/main/examples/blog), and its tests run every snippet you see here. ## Events are schemas The events of part 1 were a TypeScript union. Here they are a `Schema.TaggedUnion`, which gives the same type plus a way to check, encode and decode values: examples/blog/library.ts ```ts export const BookEvent = Schema.TaggedUnion({ BookAdded: { title: Schema.NonEmptyString, author: Schema.NonEmptyString }, BookLent: { loanId: Schema.String, memberId: Schema.String, dueOn: Schema.DateTimeUtc }, BookReturned: { loanId: Schema.String, memberId: Schema.String, late: Schema.Boolean } }) ``` The store keeps events as JSON, and the schema says how to get there and back. `dueOn` is a `DateTime` in your code and an ISO string in the database, and every event read back is checked against the schema. `BookEvent.cases.BookLent.make({ ... })` builds an event and fills in its `_tag`. The events now carry more than in part 1. A loan has an id, so that the same loan can be recognised when it comes again, and a due date. A return says whether it was late. Events are the only record of what happened, so it pays to put in them what you’ll want to know later. ## Refusals are errors with a type Part 1 refused with a thrown `Error`. Here each refusal is a class of its own: examples/blog/library.ts ```ts export class BookAlreadyAdded extends Schema.TaggedError()("BookAlreadyAdded", { bookId: Schema.String }) {} export class BookNotFound extends Schema.TaggedError()("BookNotFound", { bookId: Schema.String }) {} export class BookAlreadyLent extends Schema.TaggedError()("BookAlreadyLent", { bookId: Schema.String }) {} export class BookNotLent extends Schema.TaggedError()("BookNotLent", { bookId: Schema.String }) {} ``` A `Schema.TaggedError` is an error with a `_tag`, so the caller can tell them apart and handle each one. As we’ll see in a moment, they also show up in the type of every command that can fail with them. ## The aggregate An aggregate puts three things together: its events, a projection that folds them into its state, and its commands. Here is the book: examples/blog/library.ts ```ts /** How long a member may keep a book. */ export const loanPeriod = { days: 21 } export interface BookState { readonly status: "unknown" | "available" | "lent" readonly title: string readonly author: string readonly loan: { readonly loanId: string; readonly memberId: string; readonly dueOn: DateTime.Utc } | null } export const Book = Aggregate.make()({ name: "book", events: BookEvent, initial: { status: "unknown", title: "", author: "", loan: null }, projection: { BookAdded: (state, { author, title }) => ({ ...state, status: "available", title, author }), BookLent: (state, { dueOn, loanId, memberId }) => ({ ...state, status: "lent", loan: { loanId, memberId, dueOn } }), BookReturned: (state) => ({ ...state, status: "available", loan: null }) }, versions: bookVersions }) .command("add", { payload: Schema.Struct({ title: Schema.NonEmptyString, author: Schema.NonEmptyString }), handler: Effect.fnUntraced(function*(state, { author, title }, { aggregateId }) { if (state.status !== "unknown") return yield* new BookAlreadyAdded({ bookId: aggregateId }) return BookEvent.cases.BookAdded.make({ title, author }) }) }) .command("lend", { payload: Schema.Struct({ loanId: Schema.String, memberId: Schema.String }), handler: Effect.fnUntraced(function*(state, { loanId, memberId }, { aggregateId }) { if (state.status === "unknown") return yield* new BookNotFound({ bookId: aggregateId }) // Lending it again for the same loan records nothing, so a retry is harmless. if (state.loan?.loanId === loanId) return [] if (state.loan !== null) return yield* new BookAlreadyLent({ bookId: aggregateId }) const dueOn = DateTime.add(yield* DateTime.now, loanPeriod) return BookEvent.cases.BookLent.make({ loanId, memberId, dueOn }) }) }) .command("returnBook", { payload: Schema.Struct({}), handler: Effect.fnUntraced(function*(state, _, { aggregateId }) { if (state.loan === null) return yield* new BookNotLent({ bookId: aggregateId }) const { dueOn, loanId, memberId } = state.loan const late = DateTime.isGreaterThan(yield* DateTime.now, dueOn) return BookEvent.cases.BookReturned.make({ loanId, memberId, late }) }) }) ``` Read it against part 1: * **`projection` is `evolve`**, split into one function per event. Each gets the state and the event and returns the new state. If you add an event to `BookEvent` and forget its projection, the aggregate doesn’t compile. * **Each `.command` is a `decide`.** It has a payload schema, checked before the handler runs, and a handler that gets the current state, the payload and the aggregate’s id and version. It returns one event, an array of events, or `[]` to record nothing, or fails with one of the errors above. * **The state type is given explicitly**, as in `Aggregate.make()`, so that `status: "lent"` stays the literal `"lent"` rather than widening to `string`. * **Time comes from Effect’s clock.** `DateTime.now` reads the `Clock` service, so a test can stop or move time without mocking anything. We do that in part 5. * **`versions` comes in part 5**, when the book’s first event changes shape. Look at `lend` once more. Lending a book for a loan it is already lent for returns `[]` instead of failing. A retry of the same request is then harmless, which part 4’s saga will rely on. Handlers have a few rules, and they all follow from one idea: **a handler only decides.** * It may run more than once. A command that loses a race with another process reloads the state and decides again, so a handler must not send an email or call a payment provider. Reactions to events belong to read models and sagas, the subject of parts 3 and 4. * It must not change the state it receives. Recent states are kept in memory between commands, so a projection or handler that mutates one corrupts the next command. Always return new objects. ## Running commands `AggregateService.make` turns the aggregate into a service with one typed function per command. We give it a name with `Context.Service`, so that the rest of the application can ask for `Books`, and provide an event store kept in memory: examples/blog/library.ts ```ts export class Books extends Context.Service>()("library/Books") { static readonly layer = Layer.effect(Books, AggregateService.make(Book)) } export const BooksInMemory = Books.layer.pipe(Layer.provide(EventStore.layerMemory)) ``` Now we can lend a book: examples/blog/library.ts ```ts yield* books.commands.add("book-1", { title: "Dune", author: "Frank Herbert" }) const lent = yield* books.commands.lend("book-1", { loanId: "loan-1", memberId: "alice" }) // lent.events: [{ _tag: "BookLent", loanId: "loan-1", memberId: "alice", dueOn: … }] // lent.version: 2 ``` What happens inside `lend`: 1. The payload is checked against the command’s schema. 2. The service loads the book’s events from the store and folds them into its state. It keeps recent states in memory, so the next command on `book-1` only reads what is new. 3. The handler decides, and the new events are appended. The append says which version the decision was based on, here 1, and the store refuses it if someone else wrote to `book-1` in between. 4. The command returns the events, the book’s new version and the event’s position in the whole store, which part 3 uses. The result is a receipt: what was recorded, and where in the log. It is not the book as a screen would show it. That is a question, and questions go to `state` or to the read models of part 3. Keeping the two apart is Bertrand Meyer’s command-query separation, the idea CQRS takes its name from: asking a question should not change the answer. Commands on one book from one process take turns, so they never race. Writers in different processes race optimistically: the loser gets the store’s refusal, reloads, decides again, and gives up with `ConcurrencyConflict` after a few tries. ## Handling a refusal The second loan of the same copy is refused. The error is in the command’s type, so `Effect.catchTag` knows which tags exist and what each carries: examples/blog/library.ts ```ts const answer = yield* books.commands.lend("book-1", { loanId: "loan-2", memberId: "bob" }).pipe( Effect.as("Enjoy your book"), Effect.catchTag("BookAlreadyLent", ({ bookId }) => Effect.succeed(`${bookId} is out, try again later`)) ) ``` Hover over `books.commands.lend` in an editor and its error channel reads `BookNotFound | BookAlreadyLent | AggregateService.CommandFailure`: the handler’s own errors, plus what any command can fail with, such as `InvalidCommand` for a bad payload, `ConcurrencyConflict` for a race lost too often, and errors of the store. Leave one out of a `catchTag` and it stays in the type, where the caller can see it. ## JSON from the outside `books.commands` is for TypeScript callers. An HTTP or RPC endpoint receives JSON it knows nothing about, and hands it to `dispatch`, which checks the envelope and decodes the payload with the command’s schema before anything runs: examples/blog/library.ts ```ts // What an HTTP handler hands over: JSON it knows nothing about. yield* books.dispatch({ aggregateId: "book-2", type: "add", payload: { title: "Neuromancer", author: "William Gibson" } }) // An empty title never reaches the handler. const invalid = yield* Effect.flip(books.dispatch({ aggregateId: "book-3", type: "add", payload: { title: "" } })) // invalid._tag: "InvalidCommand" ``` An ISO date in the JSON becomes a `DateTime`, and a payload that doesn’t match fails with `InvalidCommand` listing what was wrong. The handler only ever sees valid input. ## Reading `state` folds an aggregate’s events into its state, and `history` lists them: examples/blog/library.ts ```ts const state = yield* books.state("book-1") // { status: "lent", title: "Dune", author: "Frank Herbert", loan: { loanId: "loan-1", … } } const history = yield* books.history("book-1") // [{ version: 1, type: "BookAdded", timestamp: … }, { version: 2, type: "BookLent", … }] ``` `state` also takes a `finishTime`, which gives the state as it was at that moment: the time travel of part 1. `history` with `{ payload: true }` includes each event. To run all of this, provide the layer and run the effect: ```ts Effect.runPromise(firstSteps.pipe(Effect.provide(BooksInMemory))) ``` ## What we have Compared with the forty lines of part 1, the book now has validated input, typed refusals, a store with optimistic concurrency, and a clock it can be tested with. It still answers only one kind of question: what is the state of *this* book? To find every available book, we would have to load them all. That is what read models are for, in [part 3](/blog/read-models/). The [commands guide](/guides/commands/) covers what this post skipped: command ids for retries over the network, `expectedVersion` for edits based on what a user saw, metadata, snapshots and metrics. # Limitations > What the port leaves out. effect-cqrs is a trial port, not a maintained library. These are the gaps that matter before using it for anything real. * **No broker.** New events wake view models and subscribers in every process, through PostgreSQL notifications or by polling, but there is no transport like the Moleculer broker, so `dispatch` is the entry point to wire into your own HTTP or RPC layer. * **SQLite, PostgreSQL and memory only.** There are no adapters for MongoDB or MySQL. * **Snapshots are opt-in.** Without them, recent state is only kept in memory, so after a restart each aggregate is replayed the first time it is used. A changed state needs a new snapshot `version` by hand. * **Event upcasts are pure.** An upcast can’t read a service or fail with its own error, and an event can only be read in a version its aggregate declares. * **No code generator.** There is no equivalent of `cqrs generate`. * **No inline projections.** A view model never runs in the transaction that appends the events, so a read model is always a moment behind its command. Read one aggregate with `state`, which is always current, or wait for a command’s position with `caughtUp` or `CommandApi.ReadAfter`. * **Slow handlers don’t slow writers.** There is no backpressure, so a view model with a slow handler falls behind and catches up when writes stop. * **One thread.** Concurrent writers share commits, but the work itself doesn’t spread over cores, and synchronous SQLite calls block the event loop while they run. # Modules > What each module of effect-cqrs exports. Everything is exported from `effect-cqrs` as a namespace, as in `import { Aggregate, ViewModel } from "effect-cqrs"`, and each module also has its own path, such as `effect-cqrs/ViewModel`. `PersonalDataNode` only has its own path, which keeps `node:crypto` out of code for browsers. ## Aggregate Defines aggregates. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Aggregate.ts) | Export | What it is | | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `make()({ name, events, initial, projection, versions })` | Starts an aggregate, to which `.command(name, { payload, handler })` adds commands | | `evolve(aggregate, state, event, meta)` | Applies one event with the aggregate’s projection | | `historical(aggregate, tag, version, value)`, `Historical` | An event as an older version of its schema stored it | | `schemaVersion(aggregate, tag)`, `Versions` | The version new events are written in, and the type of `versions` | | `EventMeta`, `CommandContext` | What projections and handlers receive besides the event or payload | | `State`, `Event`, `CommandName`, `Payload`, `CommandError`, `Context` | Types derived from an aggregate | ## AggregateService Runs an aggregate against the event store. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/AggregateService.ts) | Export | What it is | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `make(aggregate, { maxRetries, stateCacheSize, commandIdsPerStream, snapshots })` | The service: `commands`, `dispatch`, `state`, `history` | | `SnapshotOptions` | `every`, `schema` and `version` of the snapshots | | `CommandOptions` | `commandId` and `expectedVersion`, the last argument of a typed command | | `CommandResult`, `HistoryEntry` | What a command and `history` return | | `UnknownCommand`, `InvalidCommand`, `WrongExpectedVersion`, `EventDecodeError` | Its errors, besides the store’s | | `CommandFailure`, `ReadError` | What every command, and every read, can fail with | | `commandIdKey` | The metadata key that records a command’s [`commandId`](/guides/commands/#retries-and-command-ids) | | `makeEventDecoder(aggregate, keys)`, `makeEventEncoder(aggregate, keys)` | Convert between stored JSON and an aggregate’s events, decrypting and encrypting personal fields with `keys` | | `MetricNames` | The names of the [metrics](/guides/commands/#metrics) the service records | ## CommandGateway One entry point for the commands of several aggregates, see [several aggregates](/guides/commands/#several-aggregates). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/CommandGateway.ts) | Export | What it is | | ---------------------------------- | ----------------------------------------------------------------------------- | | `make(entries, { interceptors })` | The gateway over aggregates or their services: `send`, `dispatch`, `services` | | `Interceptor`, `CommandInfo` | What runs around every command, and what it learns about the command | | `Command` | The typed command `send` takes | | `UnroutableCommand` | `dispatch` can’t tell which aggregate a command is for | ## PersonalData Crypto-shredding of personal fields, see [personal data](/guides/personal-data/). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/PersonalData.ts) | Export | What it is | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `field(schema, { redacted, subject })` | Marks a field of an event as personal | | `forget(subject)` | Destroys the key of a subject and records it in the log | | `Keys`, `layerMemory`, `layerSql`, `make(backend)` | The keys of subjects, and where they are stored | | `MasterKey`, `layerMasterKey`, `layerMasterKeyConfig`, `layerMasterKeyEphemeral` | What wraps the keys of subjects | | `Backend`, `makeMemoryBackend`, `makeSqlBackend` | Key stores, to implement another | | `migrations({ table })` | The migrations of the key table, for `layerSql({ migrate: false })` | | `Cipher`, `webCrypto` | AES-256-GCM, on WebCrypto unless another cipher is provided | | `SubjectForgotten`, `PersonalDataError` | Its errors | | `streamName`, `subjectForgottenType` | Where forgetting is recorded | ## PersonalDataNode The [personal data](/guides/personal-data/#on-node) cipher on `node:crypto`, imported from `effect-cqrs/PersonalDataNode`. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/PersonalDataNode.ts) | Export | What it is | | ------------- | ----------------------------------------------------------------------- | | `layerCipher` | Encrypts personal fields synchronously, in the same format as WebCrypto | | `cipher` | The `Cipher` it provides | ## EventVersions Older shapes of an event. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/EventVersions.ts) | Export | What it is | | ------------------------- | ----------------------------------------------------------------------------- | | `from(schema)` | Starts a history at version 1, to which `.next(schema, upcast)` adds versions | | `decoders(tag, versions)` | One decoder per old version, all the way to the current one | | `current(versions)` | The version new events are written in | ## Causation Why events were recorded. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Causation.ts) | Export | What it is | | --------------------------------------- | ------------------------------------------------------------- | | `withCausation(causation)` | Runs an effect whose commands record these ids | | `causedBy(stored)`, `fromEvent(stored)` | Makes a stored event the cause of the commands an effect runs | | `CurrentCausation` | The reference that holds them, `undefined` outside any | ## Ids The ids the framework makes up, such as new correlations. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Ids.ts) | Export | What it is | | -------------------- | --------------------------------------------------------------- | | `IdGenerator` | The reference that makes them, `crypto.randomUUID()` by default | | `next` | A new id from the current generator | | `sequential(prefix)` | A generator of `prefix-1`, `prefix-2` and so on, for tests | ## Metadata What commands carry beside their payload, such as the software version. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Metadata.ts) | Export | What it is | | ------------------------ | --------------------------------------------------------------------------------------- | | `withMetadata(metadata)` | Runs an effect whose commands record this metadata on top of the current one | | `layer(metadata)` | Provides metadata to the services built on top, for values such as the software version | | `CurrentMetadata` | The reference that holds it, `{}` outside any | | `Metadata` | The type and schema of metadata: a JSON object | ## ViewModel Projects events into read models. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/ViewModel.ts) | Export | What it is | | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `make({ name, aggregates, handlers, dispose, onForget, onFailure, transaction })` | Defines a view model. `onForget` erases what it stored about a [forgotten subject](/guides/personal-data/#view-models) | | `layer([...])` | Runs view models in the background and provides `ViewModels` | | `ViewModels` | The service: `replay`, `caughtUp`, `status`, `parked`, `retryParked` | | `stall`, `park({ attempts })`, `FailurePolicy` | What a view model does about an event that keeps failing, see [parking events](/guides/view-models/#parking-events) | | `sqlTransaction`, `Transaction` | Runs each batch with its checkpoint in one transaction, see [checkpoints in a transaction](/guides/view-models/#checkpoints-in-a-transaction) | | `Status`, `RetryParkedOptions`, `RetryParkedResult` | What `status` and `retryParked` give and take | | `MetricNames` | The names of the metrics view models record | | `ViewModelError`, `UnknownViewModel` | A handler’s failure, and a replay of an unknown name | ## ParkedEvents Events view models gave up on, see [parking events](/guides/view-models/#parking-events). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/ParkedEvents.ts) | Export | What it is | | ----------------------------------------- | --------------------------------------------------------------- | | `ParkedEvents` | The service: `park`, `list`, `remove` | | `layerMemory`, `makeMemory` | Parked events in memory | | `layerSql({ table, migrate })`, `makeSql` | Parked events in a table, on SQLite or PostgreSQL | | `migrations({ table })` | The migrations of the table, for `layerSql({ migrate: false })` | | `ParkedEvent` | What it records, and its schema | ## Leases Which process runs each view model, see [several processes](/guides/view-models/#several-processes). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Leases.ts) | Export | What it is | | ----------------------------------------------------- | ---------------------------------------- | | `Leases` | The service: `acquire(name)` | | `layerMemory`, `memory` | Leases that are always held, the default | | `layerPostgres({ interval, prefix })`, `makePostgres` | Leases on PostgreSQL advisory locks | | `Lease` | A lease: `held` and `changes` | ## Saga Processes that span several aggregates, as durable workflows from `effect/workflow`, see [sagas](/guides/sagas/). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Saga.ts) | Export | What it is | | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | | `workflow(tag, { payload, idempotencyKey, success, error })` | Defines a saga’s workflow: `Workflow.make` with a `causation` field added to the payload | | `toLayer(workflow, execute)` | Registers the workflow’s body with the engine. Its commands record the event that started the saga as their cause | | `command(name, { execute, error })` | Runs a command as an activity, so a replay of the workflow returns the saved outcome instead of running it again | | `start(workflow, payload)` | Starts a run without waiting for it, and returns its execution id | | `make({ name, aggregates, handlers, onFailure })` | The trigger: a view model whose handlers start workflows with `start` | | `layer([...])` | Runs triggers in the background, apart from the view models, and provides `Sagas` | | `Sagas` | The service that manages the triggers, with what `ViewModel.ViewModels` has | | `SagaWorkflow` | The type of a workflow whose payload carries its causation | | `CausationSchema`, `CommandOutcome` | Schemas of that causation, and of what a saga keeps of a command it ran: aggregate, version and position | ## CommandApi Serves an aggregate over HTTP, see [integration](/guides/integration/#http). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/CommandApi.ts) | Export | What it is | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `group(aggregate, { name, prefix, state, errors })` | An `HttpApiGroup` with a `POST` endpoint per command, `history` and, with the `state` option, `state` | | `layer(api, name, service)` | Implements the group with an aggregate service, or an effect that makes one | | `InvalidCommandError`, `ConcurrencyConflictError`, `CommandRejected` | The errors it answers with `400`, `409` and `422` | | `commandResult(events)`, `historyEntry(events)` | Schemas of what a command and `history` answer | ## EventCatalog JSON Schema of events and commands, and a compatibility guard, see [integration](/guides/integration/#event-catalog). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/EventCatalog.ts) | Export | What it is | | ------------------------------------------------------- | --------------------------------------------------------------------- | | `events(aggregate)`, `commands(aggregate)` | JSON Schema of every event version and command payload | | `document(aggregates, { title, version, description })` | An AsyncAPI 3.0 document of them | | `captureSamples(aggregates, { perVersion })` | Stored payloads of each event type and version, without personal data | | `saveSamples(path, samples)`, `readSamples(path)` | Keep samples in a JSON file | | `checkCompatibility(aggregate, samples)` | Fails with `IncompatibleSamples` when a sample no longer decodes | | `Sample`, `IncompatibleSamples` | A sample, and the guard’s error | ## Outbox Publishes events to other systems, see [integration](/guides/integration/#outbox). [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Outbox.ts) | Export | What it is | | ------------------------------------------------------------------ | ------------------------------------------------------- | | `make({ name, aggregates, messages, publish, batchSize, linger })` | Defines an outbox | | `layer([...])` | Runs outboxes in the background and provides `Outboxes` | | `Outboxes` | The service: `published` and `status` | | `Message`, `Mapped`, `Mappings` | What is published, and what a mapping returns | | `OutboxError` | A failure to publish | ## EventStore The append-only log. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/EventStore.ts) | Export | What it is | | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | | `EventStore` | The service: `append`, `read`, `subscribe`, `head`, `changes` | | `layerMemory`, `makeMemory` | The store in memory | | `make(backend, options)`, `Backend`, `MakeOptions` | Builds a store on another database | | `groupCommit(appendGroup)` | Group commit for a backend: concurrent appends to different streams in one transaction | | `StoredEvent`, `EventFilter`, `AppendInput` | What it stores, selects and takes | | `eventId(event)` | An event’s id as a cause, like `account/alice@4` | | `toStored(input, event, index, position, timestamp)` | A new event’s stored form, for a backend | | `ConcurrencyConflict`, `EventStoreError` | Its errors | ## Checkpoints How far each view model has read. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Checkpoints.ts) | Export | What it is | | ------------- | ---------------------------- | | `Checkpoints` | The service: `get` and `set` | | `layerMemory` | Checkpoints in memory | ## SqliteEventStore and PostgresEventStore Durable stores through `effect/sql`. [SQLite](https://github.com/davidnussio/effect-cqrs/blob/main/src/SqliteEventStore.ts), [PostgreSQL](https://github.com/davidnussio/effect-cqrs/blob/main/src/PostgresEventStore.ts) | Export | What it is | | ---------------------------------- | ---------------------------------------------------------------------------------------- | | `layer(options)` | The event store, given a `SqlClient` | | `layerCheckpoints(options)` | Checkpoints in the same database | | `migrations(options)`, `Migration` | The schema as SQL, for your own migration tooling | | `Options` | `eventsTable`, `checkpointsTable`, `migrate`, `pollInterval`, and `listen` on PostgreSQL | ## Fixture Tests an aggregate. [Source](https://github.com/davidnussio/effect-cqrs/blob/main/src/Fixture.ts) | Export | What it is | | -------------------------- | -------------------------------------------------------------------------------- | | `make(aggregate)` | A fixture: `givenEvents`, `when`, `whenFails`, `inspectState`, `checkInvariants` | | `InvariantOptions`, `Step` | What `checkInvariants` takes, and one command it generates | | `Scenario`, `Outcome` | The chain, and what yielding it gives | | `ExpectationFailed` | The defect a failed expectation raises | # Coming from moleculer-cqrs > Where each part of moleculer-cqrs went. effect-cqrs keeps the ideas of [moleculer-cqrs](https://github.com/davidnussio/moleculer-cqrs) and changes how they are put together: services and actions become an Effect service, validation becomes `Schema`, and thrown errors become typed ones. | moleculer-cqrs | effect-cqrs | | ---------------------------------------- | ---------------------------------------------------------- | | `CQRSEventSourcing({ aggregate })` mixin | `AggregateService.make(aggregate)` | | `call todo.command` | `todos.commands.x(id, payload)` or `todos.dispatch(json)` | | `call todo.read-model` | `todos.state(id, { finishTime })` | | `call todo.history` | `todos.history(id, { payload, startTime, finishTime })` | | `call todo.replay` | `viewModels.replay({ viewModels, startTime, finishTime })` | | view model service with `events` | `ViewModel.make({ handlers, dispose })` | | `new CQRSFixture(aggregate)` | `Fixture.make(aggregate)` | | `.givenEvents([...])` | `.givenEvents([...])` | | `.when(createNews, payload)` | `.when("createNews", payload)` | | `.expectEvent(event)` | `.expectEvent(event)` or `.expectEvent("Tag")` | | `.inspectState(cb)` | `.inspectState(cb)` | | `.whenThrow(command).toThrow(...)` | `.whenFails(command, payload)`, which returns the error | | `fastest-validator` | `Schema` | | `throw` in a command | typed errors | | `resolve-storage-*` adapters | `EventStore.Backend` | ## The read side The largest change is how view models get events. moleculer-cqrs broadcasts them over the broker, so an event sent while a service is down is lost until someone replays. Here view models pull from the store from their own checkpoint, so they can’t miss one, and `replay` is for rebuilding a view model rather than for recovering events. ## What isn’t there There is no transport like the Moleculer broker: `dispatch` is the entry point to wire into your own HTTP or RPC layer. There is also no `cqrs generate`. # Performance > What a command costs on each store, and what the stress tests checked. These numbers come from one laptop, an Apple M4 Pro on Node 26, with PostgreSQL 18 in Docker on the same machine, measured on 10 November 2026. Treat them as orders of magnitude, not as a promise. | | SQLite file | PostgreSQL | | --------------------------------------------------------------- | ---------------- | --------------- | | Command on an aggregate, p50 | 0.10 ms | 0.73 ms | | Command on an aggregate with 100,000 events, p50 | 0.17 ms | about 1 ms | | First load of an aggregate with 100,000 events, after a restart | 368 ms | 312 ms | | Append, 1 event per transaction | 13,461 events/s | 1,650 events/s | | Append, 100 events per transaction | 252,768 events/s | 71,316 events/s | | Ingest a million events with a view model following¹ | 35 s | 118 s | | Replay a million events¹ | 4.6 s | 12.4 s | | Space for a million events¹ | 196 MB | 312 MB | ¹ From the stress tests of 31 October 2026, before group commit. * **A command costs the same whatever the store holds.** The service remembers recent state and reads only new events. * **Commits are what cost.** Batching events into fewer commands helps more than anything else. Concurrent writers help too, because appends that wait for each other share a commit, as the next table shows. * **The first use of a long aggregate after a restart replays it**, at about 4 µs per event on SQLite, unless [snapshots](/guides/commands/#snapshots) are on. ## Concurrent writers Commands on new streams, run by a number of workers at once: | Workers | SQLite file | PostgreSQL | | ------- | ----------------- | ------------------------- | | 1 | 9,319 commands/s | 1,307 commands/s | | 16 | 14,592 commands/s | 4,000 to 8,000 commands/s | | 256 | 15,895 commands/s | 16,958 commands/s | Before [group commit](/guides/storage/#postgresql), every worker count gave about the same: 8,400 to 8,900 commands/s on a SQLite file and 700 to 1,100 on PostgreSQL, where an append took five round trips and held the counter until it committed. Now an append is one statement, and the appends that queue behind it are written together in the next one. With 16 workers on PostgreSQL the result varies from run to run, depending on how many appends happen to queue. A database in memory has no commit to share, so its throughput stays where it was. ## Personal data [Personal fields](/guides/personal-data/) cost one AES-GCM operation each. The benchmark compares an account whose movement reference is personal with the same account without it, on WebCrypto, the default, and on `node:crypto`: | | SQLite file | PostgreSQL | | ------------------------------------------------------------------ | ---------------- | ---------------- | | Command on a new stream, without personal fields | 9,091 commands/s | 1,348 commands/s | | Command on a new stream, one personal field, WebCrypto | 6,764 commands/s | 1,230 commands/s | | Command on a new stream, one personal field, `node:crypto` | 7,681 commands/s | 1,168 commands/s | | First load of 1,000 events, without | 4.0 ms | 3.5 ms | | First load of 1,000 events, one personal field each, WebCrypto | 12.4 ms | 12.7 ms | | First load of 1,000 events, one personal field each, `node:crypto` | 9.8 ms | 10.0 ms | * **A new subject costs the most.** Each of these commands is the first for its subject, so it also creates, wraps and stores a key. Later commands reuse the key from memory. * **Reading costs about 8 µs per encrypted field on WebCrypto, and 6 µs on `node:crypto`.** WebCrypto is asynchronous, so the events of a page decrypt together while a stream is replayed, still folded in order. `node:crypto` is synchronous and needs no such detour. A command on a state already in memory decrypts nothing, and one it has just written isn’t decrypted again. * **Snapshots matter more.** With personal fields the first load of a long stream is two to three times slower, so [snapshots](/guides/commands/#snapshots) pay off sooner. ## Nothing lost Ten stress scenarios ran on both databases and broke no invariant. They included two runs of a million events, writer processes killed with `SIGKILL` while committing, three at a time on the same aggregates, view models restarted mid-batch, handlers failing on purpose, and 128 writers on one aggregate. Every acknowledged event was stored exactly once, positions and versions had no gaps, and no killed command left half its events behind. ## Run them yourself ```bash pnpm bench pnpm stress ``` [The full report](https://github.com/davidnussio/effect-cqrs/blob/main/bench/REPORT.md)Every measurement, what the tests found and fixed, and the limits that remain. # API reference ## Modules * [Aggregate](/reference/api/aggregate/) * [AggregateService](/reference/api/aggregateservice/) * [Causation](/reference/api/causation/) * [Checkpoints](/reference/api/checkpoints/) * [CommandApi](/reference/api/commandapi/) * [CommandGateway](/reference/api/commandgateway/) * [EventCatalog](/reference/api/eventcatalog/) * [EventStore](/reference/api/eventstore/) * [EventVersions](/reference/api/eventversions/) * [Fixture](/reference/api/fixture/) * [Ids](/reference/api/ids/) * [Leases](/reference/api/leases/) * [Metadata](/reference/api/metadata/) * [Outbox](/reference/api/outbox/) * [ParkedEvents](/reference/api/parkedevents/) * [PersonalData](/reference/api/personaldata/) * [PersonalDataNode](/reference/api/personaldatanode/) * [PostgresEventStore](/reference/api/postgreseventstore/) * [Saga](/reference/api/saga/) * [SqliteEventStore](/reference/api/sqliteeventstore/) * [ViewModel](/reference/api/viewmodel/) # Aggregate The write side of the domain: a schema of events, a projection that folds them into state, and commands that inspect that state and decide on new events. An aggregate is plain data. [AggregateService](/reference/api/aggregateservice/) gives it a store to run against, and `Fixture` runs it in tests. ## Classes ### Historical Defined in: [src/Aggregate.ts:176](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L176) An event as it was stored in the past, in an older version of its schema. Construct one directly to replay JSON taken from a database. #### Constructors ##### Constructor > **new Historical**(`options`): [`Historical`](/reference/api/aggregate/#historical) Defined in: [src/Aggregate.ts:180](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L180) ###### Parameters ###### options ###### payload `Json` ###### schemaVersion `number` ###### type `string` ###### Returns [`Historical`](/reference/api/aggregate/#historical) #### Properties ##### payload > `readonly` **payload**: `Json` Defined in: [src/Aggregate.ts:179](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L179) ##### schemaVersion > `readonly` **schemaVersion**: `number` Defined in: [src/Aggregate.ts:178](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L178) ##### type > `readonly` **type**: `string` Defined in: [src/Aggregate.ts:177](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L177) ## Interfaces ### Aggregate Defined in: [src/Aggregate.ts:86](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L86) #### Type Parameters ##### Name `Name` *extends* `string` ##### State `State` ##### Events `Events` *extends* [`EventSchema`](/reference/api/aggregate/#eventschema)<`any`> ##### Commands `Commands` *extends* `Record`<`string`, [`AnyCommand`](/reference/api/aggregate/#anycommand)> ##### V `V` *extends* [`Versions`](/reference/api/aggregate/#versions-1)<`Events`\[`"Type"`]> = { } #### Properties ##### commands > `readonly` **commands**: `Commands` Defined in: [src/Aggregate.ts:97](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L97) ##### events > `readonly` **events**: `Events` Defined in: [src/Aggregate.ts:94](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L94) ##### initial > `readonly` **initial**: `State` Defined in: [src/Aggregate.ts:95](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L95) ##### name > `readonly` **name**: `Name` Defined in: [src/Aggregate.ts:93](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L93) ##### projection > `readonly` **projection**: [`Projection`](/reference/api/aggregate/#projection-1)<`State`, `Events`\[`"Type"`]> Defined in: [src/Aggregate.ts:96](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L96) ##### versions > `readonly` **versions**: `V` Defined in: [src/Aggregate.ts:98](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L98) #### Methods ##### command() > **command**<`CommandName`, `Payload`, `E`, `R`>(`name`, `options`): [`Aggregate`](/reference/api/aggregate/#aggregate)<`Name`, `State`, `Events`, `Commands` & `Record`<`CommandName`, [`Command`](/reference/api/aggregate/#command-1)<`Payload`, `State`, `Events`\[`"Type"`], `E`, `R`>>, `V`> Defined in: [src/Aggregate.ts:100](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L100) Returns a new aggregate that also accepts this command. ###### Type Parameters ###### CommandName `CommandName` *extends* `string` ###### Payload `Payload` *extends* [`PayloadSchema`](/reference/api/aggregate/#payloadschema) ###### E `E` = `never` ###### R `R` = `never` ###### Parameters ###### name `CommandName` ###### options ###### handler [`CommandHandler`](/reference/api/aggregate/#commandhandler-1)<`State`, `Payload`\[`"Type"`], `Events`\[`"Type"`], `E`, `R`> ###### payload `Payload` ###### Returns [`Aggregate`](/reference/api/aggregate/#aggregate)<`Name`, `State`, `Events`, `Commands` & `Record`<`CommandName`, [`Command`](/reference/api/aggregate/#command-1)<`Payload`, `State`, `Events`\[`"Type"`], `E`, `R`>>, `V`> *** ### AnyCommand Defined in: [src/Aggregate.ts:73](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L73) #### Properties ##### handler > `readonly` **handler**: [`CommandHandler`](/reference/api/aggregate/#commandhandler-1)<`any`, `any`, `any`, `any`, `any`> Defined in: [src/Aggregate.ts:75](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L75) ##### payload > `readonly` **payload**: [`PayloadSchema`](/reference/api/aggregate/#payloadschema) Defined in: [src/Aggregate.ts:74](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L74) *** ### Command Defined in: [src/Aggregate.ts:68](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L68) #### Type Parameters ##### Payload `Payload` *extends* [`PayloadSchema`](/reference/api/aggregate/#payloadschema) ##### State `State` ##### Event `Event` ##### E `E` ##### R `R` #### Properties ##### handler > `readonly` **handler**: [`CommandHandler`](/reference/api/aggregate/#commandhandler-1)<`State`, `Payload`\[`"Type"`], `Event`, `E`, `R`> Defined in: [src/Aggregate.ts:70](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L70) ##### payload > `readonly` **payload**: `Payload` Defined in: [src/Aggregate.ts:69](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L69) *** ### CommandContext Defined in: [src/Aggregate.ts:43](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L43) #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/Aggregate.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L44) ##### version > `readonly` **version**: `number` Defined in: [src/Aggregate.ts:45](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L45) *** ### EventMeta Defined in: [src/Aggregate.ts:29](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L29) Where an event sits in its stream and in the store. #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/Aggregate.ts:31](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L31) ##### aggregateName > `readonly` **aggregateName**: `string` Defined in: [src/Aggregate.ts:30](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L30) ##### causationId > `readonly` **causationId**: `string` | `null` Defined in: [src/Aggregate.ts:38](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L38) What directly caused the command that recorded the event. ##### correlationId > `readonly` **correlationId**: `string` | `null` Defined in: [src/Aggregate.ts:36](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L36) Shared by everything that follows from one original request, see `Causation`. ##### metadata > `readonly` **metadata**: `JsonObject` Defined in: [src/Aggregate.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L40) What the command carried beside its payload, such as the software version, see `Metadata`. ##### position > `readonly` **position**: `number` Defined in: [src/Aggregate.ts:33](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L33) ##### timestamp > `readonly` **timestamp**: `Utc` Defined in: [src/Aggregate.ts:34](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L34) ##### version > `readonly` **version**: `number` Defined in: [src/Aggregate.ts:32](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L32) *** ### Tagged Defined in: [src/Aggregate.ts:13](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L13) #### Properties ##### \_tag > `readonly` **\_tag**: `string` Defined in: [src/Aggregate.ts:14](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L14) ## Type Aliases ### Any > **Any** = [`Aggregate`](/reference/api/aggregate/#aggregate)<`string`, `any`, [`EventSchema`](/reference/api/aggregate/#eventschema)<`any`>, `Record`<`string`, [`AnyCommand`](/reference/api/aggregate/#anycommand)>, `any`> Defined in: [src/Aggregate.ts:109](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L109) *** ### CommandError > **CommandError**<`A`, `K`> = `A`\[`"commands"`]\[`K`] *extends* [`Command`](/reference/api/aggregate/#command-1)<`any`, `any`, `any`, infer E, `any`> ? `E` : `never` Defined in: [src/Aggregate.ts:117](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L117) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### K `K` *extends* [`CommandName`](/reference/api/aggregate/#commandname-1)<`A`> = [`CommandName`](/reference/api/aggregate/#commandname-1)<`A`> *** ### CommandHandler > **CommandHandler**<`State`, `Payload`, `Event`, `E`, `R`> = (`state`, `payload`, `context`) => `Effect.Effect`<`Event` | `ReadonlyArray`<`Event`>, `E`, `R`> Defined in: [src/Aggregate.ts:62](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L62) Decides on the events a command produces. It may run several times when concurrent writers collide, so it should only decide and leave side effects to view models. Returning an empty array records nothing. #### Type Parameters ##### State `State` ##### Payload `Payload` ##### Event `Event` ##### E `E` ##### R `R` #### Parameters ##### state `State` ##### payload `Payload` ##### context [`CommandContext`](/reference/api/aggregate/#commandcontext) #### Returns `Effect.Effect`<`Event` | `ReadonlyArray`<`Event`>, `E`, `R`> *** ### CommandName > **CommandName**<`A`> = keyof `A`\[`"commands"`] & `string` Defined in: [src/Aggregate.ts:115](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L115) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Commands > **Commands**<`A`> = `A`\[`"commands"`] Defined in: [src/Aggregate.ts:114](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L114) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Context > **Context**<`A`> = `A`\[`"commands"`]\[[`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>] *extends* [`Command`](/reference/api/aggregate/#command-1)<`any`, `any`, `any`, `any`, infer R> ? `R` : `never` Defined in: [src/Aggregate.ts:119](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L119) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Event > **Event**<`A`> = `A`\[`"events"`]\[`"Type"`] Defined in: [src/Aggregate.ts:113](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L113) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### EventSchema > **EventSchema**<`Event`> = `Schema.Top` & `object` Defined in: [src/Aggregate.ts:17](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L17) #### Type Parameters ##### Event `Event` *extends* [`Tagged`](/reference/api/aggregate/#tagged) = [`Tagged`](/reference/api/aggregate/#tagged) #### Type Declaration ##### DecodingServices > `readonly` **DecodingServices**: `never` ##### EncodingServices > `readonly` **EncodingServices**: `never` ##### Type > `readonly` **Type**: `Event` *** ### Name > **Name**<`A`> = `A`\[`"name"`] Defined in: [src/Aggregate.ts:111](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L111) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Payload > **Payload**<`A`, `K`> = `A`\[`"commands"`]\[`K`]\[`"payload"`]\[`"Type"`] Defined in: [src/Aggregate.ts:116](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L116) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### K `K` *extends* [`CommandName`](/reference/api/aggregate/#commandname-1)<`A`> *** ### PayloadSchema > **PayloadSchema** = `Schema.Top` & `object` Defined in: [src/Aggregate.ts:23](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L23) #### Type Declaration ##### DecodingServices > `readonly` **DecodingServices**: `never` ##### EncodingServices > `readonly` **EncodingServices**: `never` *** ### Projection > **Projection**<`State`, `Event`> = `{ readonly [Tag in Event["_tag"]]: (state: State, event: Extract, meta: EventMeta) => State }` Defined in: [src/Aggregate.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L49) Exhaustive by event tag, so adding an event to the schema without folding it does not compile. #### Type Parameters ##### State `State` ##### Event `Event` *extends* [`Tagged`](/reference/api/aggregate/#tagged) *** ### State > **State**<`A`> = `A`\[`"initial"`] Defined in: [src/Aggregate.ts:112](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L112) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Versions > **Versions**<`Event`> = `{ readonly [Tag in Event["_tag"]]?: Any> }` Defined in: [src/Aggregate.ts:82](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L82) The older shapes of some events, keyed by tag. Each history has to end in the event’s current shape. #### Type Parameters ##### Event `Event` *extends* [`Tagged`](/reference/api/aggregate/#tagged) ## Functions ### evolve() > **evolve**<`A`>(`aggregate`, `state`, `event`, `meta`): [`State`](/reference/api/aggregate/#state-4)<`A`> Defined in: [src/Aggregate.ts:159](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L159) Folds one event into the state. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Parameters ##### aggregate `A` ##### state [`State`](/reference/api/aggregate/#state-4)<`A`> ##### event [`Event`](/reference/api/aggregate/#event-2)<`A`> ##### meta [`EventMeta`](/reference/api/aggregate/#eventmeta) #### Returns [`State`](/reference/api/aggregate/#state-4)<`A`> *** ### historical() > **historical**<`A`, `Tag`, `Version`>(`aggregate`, `tag`, `version`, `value`): [`Historical`](/reference/api/aggregate/#historical) Defined in: [src/Aggregate.ts:192](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L192) Encodes an event in an older version of its schema, as code from that time would have stored it. The value is checked against that version, so a test can’t seed a shape that never existed. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### Tag `Tag` *extends* `string` ##### Version `Version` *extends* `number` #### Parameters ##### aggregate `A` ##### tag `Tag` ##### version `Version` ##### value [`SchemaAt`](/reference/api/eventversions/#schemaat)<`NonNullable`<`A`\[`"versions"`]\[`Tag`]>, `Version`>\[`"Type"`] #### Returns [`Historical`](/reference/api/aggregate/#historical) *** ### make() > **make**<`State`>(): <`Name`, `Events`, `V`>(`options`) => [`Aggregate`](/reference/api/aggregate/#aggregate)<`Name`, `State`, `Events`, { }, `V`> Defined in: [src/Aggregate.ts:147](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L147) The state is given explicitly, `Aggregate.make()({ ... })`, so that the projection’s return values are checked against it rather than widened. #### Type Parameters ##### State `State` #### Returns <`Name`, `Events`, `V`>(`options`) => [`Aggregate`](/reference/api/aggregate/#aggregate)<`Name`, `State`, `Events`, { }, `V`> *** ### schemaVersion() > **schemaVersion**(`aggregate`, `tag`): `number` Defined in: [src/Aggregate.ts:169](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Aggregate.ts#L169) The schema version new events with this tag are written with. #### Parameters ##### aggregate [`Any`](/reference/api/aggregate/#any) ##### tag `string` #### Returns `number` # AggregateService Runs an aggregate against the [EventStore](/reference/api/eventstore/#eventstore): the `command`, `read-model` and `history` actions of moleculer-cqrs, as a typed service. ## Classes ### CommandIdReused Defined in: [src/AggregateService.ts:83](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L83) A `commandId` that already identifies another command on the aggregate. Running a command again returns the first result only for the same command: a client that reuses an id for another one gets this rather than a result it didn’t ask for. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new CommandIdReused**(…`args`): [`CommandIdReused`](/reference/api/aggregateservice/#commandidreused) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`CommandIdReused`](/reference/api/aggregateservice/#commandidreused) ###### Inherited from `Schema.TaggedError()("CommandIdReused", { aggregateName: Schema.String, aggregateId: Schema.String, commandId: Schema.String, /** The command that ran with the id first. */ previous: Schema.String, /** The command that came with it now. */ command: Schema.String }).constructor` #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:85](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L85) ###### Inherited from `Schema.TaggedError()("CommandIdReused", { aggregateName: Schema.String, aggregateId: Schema.String, commandId: Schema.String, /** The command that ran with the id first. */ previous: Schema.String, /** The command that came with it now. */ command: Schema.String }).aggregateId` ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:84](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L84) ###### Inherited from `Schema.TaggedError()("CommandIdReused", { aggregateName: Schema.String, aggregateId: Schema.String, commandId: Schema.String, /** The command that ran with the id first. */ previous: Schema.String, /** The command that came with it now. */ command: Schema.String }).aggregateName` ##### command > `readonly` **command**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:90](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L90) The command that came with it now. ###### Inherited from `Schema.TaggedError()("CommandIdReused", { aggregateName: Schema.String, aggregateId: Schema.String, commandId: Schema.String, /** The command that ran with the id first. */ previous: Schema.String, /** The command that came with it now. */ command: Schema.String }).command` ##### commandId > `readonly` **commandId**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:86](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L86) ###### Inherited from `Schema.TaggedError()("CommandIdReused", { aggregateName: Schema.String, aggregateId: Schema.String, commandId: Schema.String, /** The command that ran with the id first. */ previous: Schema.String, /** The command that came with it now. */ command: Schema.String }).commandId` ##### previous > `readonly` **previous**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:88](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L88) The command that ran with the id first. ###### Inherited from `Schema.TaggedError()("CommandIdReused", { aggregateName: Schema.String, aggregateId: Schema.String, commandId: Schema.String, /** The command that ran with the id first. */ previous: Schema.String, /** The command that came with it now. */ command: Schema.String }).previous` *** ### EventDecodeError Defined in: [src/AggregateService.ts:57](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L57) A stored event doesn’t match the schema of its version, or was written in a version the aggregate doesn’t declare. Old shapes are declared with `EventVersions`. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new EventDecodeError**(…`args`): [`EventDecodeError`](/reference/api/aggregateservice/#eventdecodeerror) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`EventDecodeError`](/reference/api/aggregateservice/#eventdecodeerror) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).constructor` #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:59](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L59) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).aggregateId` ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:58](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L58) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).aggregateName` ##### message > **message**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:63](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L63) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).message` ##### schemaVersion > `readonly` **schemaVersion**: `number` = `Schema.Int` Defined in: [src/AggregateService.ts:62](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L62) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).schemaVersion` ##### type > `readonly` **type**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:61](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L61) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).type` ##### version > `readonly` **version**: `number` = `Schema.Int` Defined in: [src/AggregateService.ts:60](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L60) ###### Inherited from `Schema.TaggedError()("EventDecodeError", { aggregateName: Schema.String, aggregateId: Schema.String, version: Schema.Int, type: Schema.String, schemaVersion: Schema.Int, message: Schema.String }).version` *** ### InvalidCommand Defined in: [src/AggregateService.ts:47](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L47) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new InvalidCommand**(…`args`): [`InvalidCommand`](/reference/api/aggregateservice/#invalidcommand) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`InvalidCommand`](/reference/api/aggregateservice/#invalidcommand) ###### Inherited from `Schema.TaggedError()("InvalidCommand", { aggregateName: Schema.String, type: Schema.String, message: Schema.String }).constructor` #### Properties ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:48](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L48) ###### Inherited from `Schema.TaggedError()("InvalidCommand", { aggregateName: Schema.String, type: Schema.String, message: Schema.String }).aggregateName` ##### message > **message**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:50](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L50) ###### Inherited from `Schema.TaggedError()("InvalidCommand", { aggregateName: Schema.String, type: Schema.String, message: Schema.String }).message` ##### type > `readonly` **type**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L49) ###### Inherited from `Schema.TaggedError()("InvalidCommand", { aggregateName: Schema.String, type: Schema.String, message: Schema.String }).type` *** ### UnknownCommand Defined in: [src/AggregateService.ts:42](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L42) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new UnknownCommand**(…`args`): [`UnknownCommand`](/reference/api/aggregateservice/#unknowncommand) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`UnknownCommand`](/reference/api/aggregateservice/#unknowncommand) ###### Inherited from `Schema.TaggedError()("UnknownCommand", { aggregateName: Schema.String, type: Schema.String }).constructor` #### Properties ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:43](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L43) ###### Inherited from `Schema.TaggedError()("UnknownCommand", { aggregateName: Schema.String, type: Schema.String }).aggregateName` ##### type > `readonly` **type**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L44) ###### Inherited from `Schema.TaggedError()("UnknownCommand", { aggregateName: Schema.String, type: Schema.String }).type` *** ### WrongExpectedVersion Defined in: [src/AggregateService.ts:71](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L71) The caller’s `expectedVersion` is not the aggregate’s version: someone else changed it since the caller read it. Unlike a `ConcurrencyConflict`, this is not retried, as deciding again would overwrite a change the caller hasn’t seen. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new WrongExpectedVersion**(…`args`): [`WrongExpectedVersion`](/reference/api/aggregateservice/#wrongexpectedversion) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`WrongExpectedVersion`](/reference/api/aggregateservice/#wrongexpectedversion) ###### Inherited from `Schema.TaggedError()("WrongExpectedVersion", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int, actualVersion: Schema.Int }).constructor` #### Properties ##### actualVersion > `readonly` **actualVersion**: `number` = `Schema.Int` Defined in: [src/AggregateService.ts:75](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L75) ###### Inherited from `Schema.TaggedError()("WrongExpectedVersion", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int, actualVersion: Schema.Int }).actualVersion` ##### aggregateId > `readonly` **aggregateId**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:73](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L73) ###### Inherited from `Schema.TaggedError()("WrongExpectedVersion", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int, actualVersion: Schema.Int }).aggregateId` ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/AggregateService.ts:72](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L72) ###### Inherited from `Schema.TaggedError()("WrongExpectedVersion", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int, actualVersion: Schema.Int }).aggregateName` ##### expectedVersion > `readonly` **expectedVersion**: `number` = `Schema.Int` Defined in: [src/AggregateService.ts:74](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L74) ###### Inherited from `Schema.TaggedError()("WrongExpectedVersion", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int, actualVersion: Schema.Int }).expectedVersion` ## Interfaces ### AggregateService Defined in: [src/AggregateService.ts:164](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L164) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Properties ##### aggregate > `readonly` **aggregate**: `A` Defined in: [src/AggregateService.ts:165](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L165) ##### commands > `readonly` **commands**: { readonly \[K in string]: (aggregateId: string, payload: Payload\, options?: CommandOptions) => Effect\>, CommandError\ | CommandFailure> } Defined in: [src/AggregateService.ts:167](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L167) One typed function per command. ##### dispatch > `readonly` **dispatch**: (`input`) => `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<[`Event`](/reference/api/aggregate/#event-2)<`A`>>, [`UnknownCommand`](/reference/api/aggregateservice/#unknowncommand) | [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, [`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>>> Defined in: [src/AggregateService.ts:179](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L179) Runs a `{ aggregateId, type, payload }` envelope received as JSON from outside the process. It can also carry `commandId` and `expectedVersion`, see `CommandOptions`, `correlationId`, `causationId` and `metadata`. ###### Parameters ###### input `unknown` ###### Returns `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<[`Event`](/reference/api/aggregate/#event-2)<`A`>>, [`UnknownCommand`](/reference/api/aggregateservice/#unknowncommand) | [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, [`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>>> ##### history > `readonly` **history**: (`aggregateId`, `options?`) => `Effect`\>\[], [`ReadError`](/reference/api/aggregateservice/#readerror)> Defined in: [src/AggregateService.ts:200](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L200) Lists the events of one aggregate, with their decoded payload when `payload` is set. Without it the events aren’t decoded. `afterVersion` and `limit` read a long stream in pages: pass the version of the last entry to get the next page. ###### Parameters ###### aggregateId `string` ###### options? [`HistoryOptions`](/reference/api/aggregateservice/#historyoptions) ###### Returns `Effect`\>\[], [`ReadError`](/reference/api/aggregateservice/#readerror)> ##### ~~readModel~~ > `readonly` **readModel**: (`aggregateId`, `options?`) => `Effect`<[`State`](/reference/api/aggregate/#state-4)<`A`>, [`ReadError`](/reference/api/aggregateservice/#readerror)> Defined in: [src/AggregateService.ts:194](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L194) Deprecated Use `state`. The name came from the `read-model` action of moleculer-cqrs, and reads as the read models that view models keep. ###### Parameters ###### aggregateId `string` ###### options? ###### finishTime? `Utc` ###### Returns `Effect`<[`State`](/reference/api/aggregate/#state-4)<`A`>, [`ReadError`](/reference/api/aggregateservice/#readerror)> ##### state > `readonly` **state**: (`aggregateId`, `options?`) => `Effect`<[`State`](/reference/api/aggregate/#state-4)<`A`>, [`ReadError`](/reference/api/aggregateservice/#readerror)> Defined in: [src/AggregateService.ts:186](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L186) Folds the aggregate’s events into its state, optionally as it was before `finishTime`. It is the state the next command is decided on, so it is never behind. ###### Parameters ###### aggregateId `string` ###### options? ###### finishTime? `Utc` ###### Returns `Effect`<[`State`](/reference/api/aggregate/#state-4)<`A`>, [`ReadError`](/reference/api/aggregateservice/#readerror)> *** ### CommandOptions Defined in: [src/AggregateService.ts:109](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L109) What a typed command takes besides the aggregate id and the payload. #### Properties ##### commandId? > `readonly` `optional` **commandId?**: `string` Defined in: [src/AggregateService.ts:115](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L115) Identifies the command, so that running it again on the same aggregate returns the first result instead of deciding again. The payload isn’t compared, so an id belongs to one request. See [commandIdKey](/reference/api/aggregateservice/#commandidkey). ##### expectedVersion? > `readonly` `optional` **expectedVersion?**: `number` Defined in: [src/AggregateService.ts:117](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L117) The version the caller based its command on. If the aggregate is at another, the command fails with `WrongExpectedVersion`. *** ### CommandResult Defined in: [src/AggregateService.ts:132](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L132) #### Type Parameters ##### Event `Event` #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/AggregateService.ts:134](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L134) ##### aggregateName > `readonly` **aggregateName**: `string` Defined in: [src/AggregateService.ts:133](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L133) ##### correlationId > `readonly` **correlationId**: `string` Defined in: [src/AggregateService.ts:141](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L141) Recorded on the events, see `Causation`. ##### events > `readonly` **events**: readonly `Event`\[] Defined in: [src/AggregateService.ts:139](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L139) ##### position > `readonly` **position**: `number` Defined in: [src/AggregateService.ts:138](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L138) The store position of the last event, to pass to `ViewModels.caughtUp`. ##### version > `readonly` **version**: `number` Defined in: [src/AggregateService.ts:136](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L136) The stream version after the command. *** ### HistoryEntry Defined in: [src/AggregateService.ts:144](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L144) #### Type Parameters ##### Event `Event` #### Properties ##### causationId > `readonly` **causationId**: `string` | `null` Defined in: [src/AggregateService.ts:149](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L149) ##### correlationId > `readonly` **correlationId**: `string` | `null` Defined in: [src/AggregateService.ts:148](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L148) ##### metadata > `readonly` **metadata**: `JsonObject` Defined in: [src/AggregateService.ts:150](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L150) ##### payload? > `readonly` `optional` **payload?**: `Event` Defined in: [src/AggregateService.ts:151](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L151) ##### timestamp > `readonly` **timestamp**: `Utc` Defined in: [src/AggregateService.ts:146](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L146) ##### type > `readonly` **type**: `string` Defined in: [src/AggregateService.ts:147](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L147) ##### version > `readonly` **version**: `number` Defined in: [src/AggregateService.ts:145](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L145) *** ### HistoryOptions Defined in: [src/AggregateService.ts:154](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L154) #### Properties ##### afterVersion? > `readonly` `optional` **afterVersion?**: `number` Defined in: [src/AggregateService.ts:159](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L159) Lists the events after this version. Defaults to `0`, the start of the stream. ##### finishTime? > `readonly` `optional` **finishTime?**: `Utc` Defined in: [src/AggregateService.ts:157](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L157) ##### limit? > `readonly` `optional` **limit?**: `number` Defined in: [src/AggregateService.ts:161](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L161) Lists at most this many events. ##### payload? > `readonly` `optional` **payload?**: `boolean` Defined in: [src/AggregateService.ts:155](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L155) ##### startTime? > `readonly` `optional` **startTime?**: `Utc` Defined in: [src/AggregateService.ts:156](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L156) *** ### Options Defined in: [src/AggregateService.ts:406](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L406) #### Properties ##### commandIdsPerStream? > `readonly` `optional` **commandIdsPerStream?**: `number` Defined in: [src/AggregateService.ts:415](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L415) How many of its latest command ids each aggregate remembers, so that a command run again with one of them returns its first result. Defaults to 100. ##### maxRetries? > `readonly` `optional` **maxRetries?**: `number` Defined in: [src/AggregateService.ts:408](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L408) How many times a command is re-decided after losing a race. Defaults to 5. ##### stateCacheSize? > `readonly` `optional` **stateCacheSize?**: `number` Defined in: [src/AggregateService.ts:410](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L410) How many aggregates keep their state in memory between commands. Defaults to 10,000. *** ### SnapshotOptions Defined in: [src/AggregateService.ts:393](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L393) Saves the state of each aggregate every so often, so a cold start replays only what came after. #### Type Parameters ##### State `State` #### Properties ##### every > `readonly` **every**: `number` Defined in: [src/AggregateService.ts:395](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L395) How many events to fold after a snapshot before saving the next one. ##### schema > `readonly` **schema**: `Codec`<`State`, `any`, `never`, `never`> Defined in: [src/AggregateService.ts:397](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L397) How the state is stored. It is encoded as JSON. ##### version? > `readonly` `optional` **version?**: `string` | `number` Defined in: [src/AggregateService.ts:403](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L403) Part of every key. Change it whenever the state or the projection changes, so that snapshots of the old state are ignored and rebuilt from the log. Defaults to `1`. ## Type Aliases ### CommandFailure > **CommandFailure** = [`ConcurrencyConflict`](/reference/api/eventstore/#concurrencyconflict) | [`InvalidCommand`](/reference/api/aggregateservice/#invalidcommand) | [`WrongExpectedVersion`](/reference/api/aggregateservice/#wrongexpectedversion) | [`ReadError`](/reference/api/aggregateservice/#readerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten) | [`CommandIdReused`](/reference/api/aggregateservice/#commandidreused) Defined in: [src/AggregateService.ts:100](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L100) How a command can fail, on top of the errors its own handler declares. `SubjectForgotten` means an event would record personal data of a forgotten subject. *** ### ReadError > **ReadError** = [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) | [`EventDecodeError`](/reference/api/aggregateservice/#eventdecodeerror) | [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) Defined in: [src/AggregateService.ts:94](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L94) How reading events can fail. `PersonalDataError` comes from aggregates with personal fields, see `PersonalData`. ## Variables ### commandIdKey > `const` **commandIdKey**: `"cqrs.commandId"` = `"cqrs.commandId"` Defined in: [src/AggregateService.ts:124](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L124) The metadata key that records the `commandId` on the events a command writes. It is reserved: metadata given under this key is dropped. *** ### commandTypeKey > `const` **commandTypeKey**: `"cqrs.command"` = `"cqrs.command"` Defined in: [src/AggregateService.ts:130](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L130) The metadata key that records, beside the [commandIdKey](/reference/api/aggregateservice/#commandidkey), which command wrote the events. It is reserved too. *** ### make > `const` **make**: {<`A`>(`aggregate`, `options`): `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `never`, [`EventStore`](/reference/api/eventstore/#eventstore) | `KeyValueStore` | `Scope` | [`Context`](/reference/api/aggregate/#context)<`A`>>; <`A`>(`aggregate`, `options?`): `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `never`, [`EventStore`](/reference/api/eventstore/#eventstore) | [`Context`](/reference/api/aggregate/#context)<`A`>>; } Defined in: [src/AggregateService.ts:1187](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L1187) Builds the service for one aggregate. With `snapshots` it saves states through a `KeyValueStore` from `effect/persistence`, which has layers for memory, files, SQL and the browser’s `localStorage`. Saves run in the background, and closing the scope waits for the ones still running. #### Call Signature > <`A`>(`aggregate`, `options`): `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `never`, [`EventStore`](/reference/api/eventstore/#eventstore) | `KeyValueStore` | `Scope` | [`Context`](/reference/api/aggregate/#context)<`A`>> ##### Type Parameters ###### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### Parameters ###### aggregate `A` ###### options [`Options`](/reference/api/aggregateservice/#options) & `object` ##### Returns `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `never`, [`EventStore`](/reference/api/eventstore/#eventstore) | `KeyValueStore` | `Scope` | [`Context`](/reference/api/aggregate/#context)<`A`>> #### Call Signature > <`A`>(`aggregate`, `options?`): `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `never`, [`EventStore`](/reference/api/eventstore/#eventstore) | [`Context`](/reference/api/aggregate/#context)<`A`>> ##### Type Parameters ###### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### Parameters ###### aggregate `A` ###### options? [`Options`](/reference/api/aggregateservice/#options) ##### Returns `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `never`, [`EventStore`](/reference/api/eventstore/#eventstore) | [`Context`](/reference/api/aggregate/#context)<`A`>> *** ### MetricNames > `const` **MetricNames**: `object` Defined in: [src/AggregateService.ts:460](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L460) The names of the metrics every aggregate service records through Effect’s `Metric` module. Each series carries an `aggregate` attribute with the aggregate’s name. Read them with `Metric.snapshot`, or send them on with an exporter such as `OtlpMetrics` from `effect/observability`. #### Type Declaration ##### commandDuration > `readonly` **commandDuration**: `"cqrs_command_duration"` = `"cqrs_command_duration"` Timer: how long a command took, including its wait for an earlier command on the same stream, by `command` and `outcome`. The outcome is `success`, the tag of the error, `defect` or `interrupted`. A payload that doesn’t decode, or a type the aggregate doesn’t know, fails before this point and isn’t recorded. ##### concurrencyConflicts > `readonly` **concurrencyConflicts**: `"cqrs_concurrency_conflicts_total"` = `"cqrs_concurrency_conflicts_total"` Counter: appends that lost a race with a writer in another process. Each is retried until `maxRetries` runs out. ##### eventsReplayed > `readonly` **eventsReplayed**: `"cqrs_events_replayed"` = `"cqrs_events_replayed"` Histogram: how many events a load read from the store on top of that state. ##### snapshotReadFailures > `readonly` **snapshotReadFailures**: `"cqrs_snapshot_read_failures_total"` = `"cqrs_snapshot_read_failures_total"` Counter: snapshots that couldn’t be read, so the stream was replayed instead. ##### snapshotSaves > `readonly` **snapshotSaves**: `"cqrs_snapshot_saves_total"` = `"cqrs_snapshot_saves_total"` Counter: snapshots saved, by `result`: `success` or `failure`. ##### stateCacheEvictions > `readonly` **stateCacheEvictions**: `"cqrs_state_cache_evictions_total"` = `"cqrs_state_cache_evictions_total"` Counter: states dropped from memory to make room for others, see `stateCacheSize`. ##### stateCacheSize > `readonly` **stateCacheSize**: `"cqrs_state_cache_size"` = `"cqrs_state_cache_size"` Gauge: how many states are remembered in memory. ##### stateLoads > `readonly` **stateLoads**: `"cqrs_state_loads_total"` = `"cqrs_state_loads_total"` Counter: where loading the current state found a state to start from, by `source`: `memory`, `snapshot`, or `replay` when it starts from the first event. ## Functions ### makeEventDecoder() > **makeEventDecoder**<`A`>(`aggregate`, `keys?`): (`stored`) => `Effect`<[`Event`](/reference/api/aggregate/#event-2)<`A`>, [`EventDecodeError`](/reference/api/aggregateservice/#eventdecodeerror) | [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/AggregateService.ts:328](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L328) Decodes a stored event with the schema of the aggregate it belongs to. An event stored in an older version is upcast to the current one on the way. An aggregate with personal fields needs `keys` to decrypt them, see `PersonalData`. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Parameters ##### aggregate `A` ##### keys? ###### cipher [`Cipher`](/reference/api/personaldata/#cipher) What encrypts and decrypts with the keys. ###### decryptionKey (`subject`, `keyId`) => `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> The key to decrypt with, `None` when the subject was forgotten. A subject without any key fails: losing the key store must not pass for forgetting. ###### encryptionKey (`subject`) => `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> The key to encrypt with, created when the subject has none. Fails for a forgotten subject. ###### forget (`subject`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Destroys the key of a subject. Prefer [forget](/reference/api/personaldata/#forget-1), which also records it in the log. ###### isForgotten (`subject`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ###### rewrap (`options?`) => `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> See [rewrap](/reference/api/personaldata/#rewrap). #### Returns (`stored`) => `Effect`<[`Event`](/reference/api/aggregate/#event-2)<`A`>, [`EventDecodeError`](/reference/api/aggregateservice/#eventdecodeerror) | [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> *** ### makeEventEncoder() > **makeEventEncoder**<`A`>(`aggregate`, `keys?`): (`event`, `aggregateId?`) => `Effect`<[`NewEvent`](/reference/api/eventstore/#newevent), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> Defined in: [src/AggregateService.ts:383](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/AggregateService.ts#L383) Encodes events into the JSON form the store keeps, in the current version of their schema. An aggregate with personal fields needs `keys` to encrypt them, and the id of the stream the event goes to, the default subject. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Parameters ##### aggregate `A` ##### keys? ###### cipher [`Cipher`](/reference/api/personaldata/#cipher) What encrypts and decrypts with the keys. ###### decryptionKey (`subject`, `keyId`) => `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> The key to decrypt with, `None` when the subject was forgotten. A subject without any key fails: losing the key store must not pass for forgetting. ###### encryptionKey (`subject`) => `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> The key to encrypt with, created when the subject has none. Fails for a forgotten subject. ###### forget (`subject`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Destroys the key of a subject. Prefer [forget](/reference/api/personaldata/#forget-1), which also records it in the log. ###### isForgotten (`subject`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ###### rewrap (`options?`) => `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> See [rewrap](/reference/api/personaldata/#rewrap). #### Returns (`event`, `aggregateId?`) => `Effect`<[`NewEvent`](/reference/api/eventstore/#newevent), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> # Causation Why an event was recorded. Every event carries two ids: * `correlationId` is shared by everything that follows from one original request, across aggregates and view models. * `causationId` names what directly caused the command that recorded it: the request, or the event a view model or saga was reacting to. They travel in the fiber’s context rather than in payloads. A command run outside any causation starts a new correlation with a fresh id. Inside a view model handler the event being handled is the cause, so a chain of reactions can be drawn as a tree without passing ids around by hand. ## Interfaces ### Causation Defined in: [src/Causation.ts:19](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L19) #### Properties ##### causationId > `readonly` **causationId**: `string` Defined in: [src/Causation.ts:21](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L21) ##### correlationId > `readonly` **correlationId**: `string` Defined in: [src/Causation.ts:20](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L20) ## Variables ### CurrentCausation > `const` **CurrentCausation**: `Reference`<[`Causation`](/reference/api/causation/#causation) | `undefined`> Defined in: [src/Causation.ts:25](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L25) The causation of commands run in this fiber, `undefined` outside any. *** ### withCausation > `const` **withCausation**: {(`causation`): <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`>; <`A`, `E`, `R`>(`effect`, `causation`): `Effect`<`A`, `E`, `R`>; } Defined in: [src/Causation.ts:30](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L30) Runs an effect with this causation, for instance the id of an incoming HTTP request. #### Call Signature > (`causation`): <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`> ##### Parameters ###### causation [`Causation`](/reference/api/causation/#causation) ##### Returns <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`> #### Call Signature > <`A`, `E`, `R`>(`effect`, `causation`): `Effect`<`A`, `E`, `R`> ##### Type Parameters ###### A `A` ###### E `E` ###### R `R` ##### Parameters ###### effect `Effect`<`A`, `E`, `R`> ###### causation [`Causation`](/reference/api/causation/#causation) ##### Returns `Effect`<`A`, `E`, `R`> ## Functions ### causedBy() > **causedBy**(`event`): <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`> Defined in: [src/Causation.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L49) Runs an effect in reaction to a stored event, so commands it runs record that event as their cause. View models do this for their handlers already. #### Parameters ##### event ###### aggregateId `string` = `Schema.String` ###### aggregateName `string` = `Schema.String` ###### causationId `string` | `null` = `...` What directly caused the command that recorded the event: a request, a command or another event. ###### correlationId `string` | `null` = `...` Shared by everything that follows from one original request. ###### metadata {\[`key`: `string`]: `Json`; } = `Schema.JsonObject` What the command carried beside its payload, see `Metadata`. Events from before metadata existed have `{}`. ###### payload `Json` = `Schema.Json` ###### position `number` = `Schema.Int` ###### schemaVersion `number` = `Schema.Int` The version of the event’s schema the payload was written with. Events from before versions existed are `1`. ###### timestamp `Utc` = `Schema.DateTimeUtcFromMillis` ###### type `string` = `Schema.String` ###### version `number` = `Schema.Int` #### Returns <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`> *** ### fromEvent() > **fromEvent**(`event`): [`Causation`](/reference/api/causation/#causation) Defined in: [src/Causation.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Causation.ts#L40) The causation of commands run in reaction to a stored event. #### Parameters ##### event ###### aggregateId `string` = `Schema.String` ###### aggregateName `string` = `Schema.String` ###### causationId `string` | `null` = `...` What directly caused the command that recorded the event: a request, a command or another event. ###### correlationId `string` | `null` = `...` Shared by everything that follows from one original request. ###### metadata {\[`key`: `string`]: `Json`; } = `Schema.JsonObject` What the command carried beside its payload, see `Metadata`. Events from before metadata existed have `{}`. ###### payload `Json` = `Schema.Json` ###### position `number` = `Schema.Int` ###### schemaVersion `number` = `Schema.Int` The version of the event’s schema the payload was written with. Events from before versions existed are `1`. ###### timestamp `Utc` = `Schema.DateTimeUtcFromMillis` ###### type `string` = `Schema.String` ###### version `number` = `Schema.Int` #### Returns [`Causation`](/reference/api/causation/#causation) # Checkpoints Remembers how far each view model has read. A checkpoint has to live as long as the view model it tracks: pair an in-memory view model with [layerMemory](/reference/api/checkpoints/#layermemory) so it rebuilds from the event store on restart, and a durable one with a durable checkpoint store. ## Classes ### Checkpoints Defined in: [src/Checkpoints.ts:13](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Checkpoints.ts#L13) #### Extends * `Shape`<`"effect-cqrs/Checkpoints"`, { `advance?`: (`viewModel`, `from`, `to`) => `Effect`<`boolean`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `get`(`viewModel`): `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `set`(`viewModel`, `position`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, `this`> #### Constructors ##### Constructor > **new Checkpoints**(`_`): [`Checkpoints`](/reference/api/checkpoints/#checkpoints) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`Checkpoints`](/reference/api/checkpoints/#checkpoints) ###### Inherited from Context.Service\ set(viewModel: string, position: number): Effect.Effect\ /\*\* \* Moves a checkpoint to \`to\` only if it is still at \`from\`, and returns whether it \* moved. View models use it to notice that another process moved their checkpoint, \* which can happen for a moment while a lease changes hands: in a \`transaction\` \* the batch then rolls back instead of being committed twice. Stores without it \* fall back to \`set\`, which can’t notice. \*/ readonly advance?: | ((viewModel: string, from: number, to: number) => Effect.Effect\) | undefined }>()(“effect-cqrs/Checkpoints”).constructor ## Variables ### layerMemory > `const` **layerMemory**: `Layer.Layer`<[`Checkpoints`](/reference/api/checkpoints/#checkpoints)> Defined in: [src/Checkpoints.ts:29](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Checkpoints.ts#L29) # CommandApi Serves an aggregate over HTTP with `effect/http-api`: one `POST` endpoint per command, and `GET` endpoints for `history` and, given a schema of the state, `state`. ```ts const TodoApi = CommandApi.group(Todo, { state: TodoStateSchema, errors: [TodoAlreadyCreated, TodoNotFound, TodoAlreadyDeleted] }) class Api extends HttpApi.make("api").add(TodoApi) {} const TodoHandlers = CommandApi.layer(Api, "todo", AggregateService.make(Todo)) ``` | Endpoint | Does | | ------------------------------------ | ----------------------------------------------------------------------------------------- | | `POST /todo/:aggregateId/createTodo` | Runs `createTodo` with the body as payload | | `GET /todo/:aggregateId` | `state`, with an optional `finishTime` | | `GET /todo/:aggregateId/history` | `history`, with optional `payload`, `startTime`, `finishTime`, `afterVersion` and `limit` | The group is an ordinary `HttpApiGroup`, so it takes middleware, a different prefix and OpenAPI annotations like any other, and `HttpApiClient` calls it. A command’s response carries the store position of its last event, in the body and in an `Event-Position` header. [ReadAfter](/reference/api/commandapi/#readafter) is middleware for your own query endpoints: a request with a `Read-After` header waits until the view models have handled that position, so a client reads what it just wrote. ## Classes ### CommandRejected Defined in: [src/CommandApi.ts:82](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L82) A command handler failed with an error that `errors` doesn’t declare. Its tag and message are kept, and the rest of it is left out of the response. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new CommandRejected**(…`args`): [`CommandRejected`](/reference/api/commandapi/#commandrejected) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`CommandRejected`](/reference/api/commandapi/#commandrejected) ###### Inherited from `Schema.TaggedError()("CommandRejected", { aggregateName: Schema.String, command: Schema.String, /** The tag of the handler's error. */ error: Schema.String, message: Schema.String }, { httpApiStatus: 422 }).constructor` #### Properties ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/CommandApi.ts:83](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L83) ###### Inherited from `Schema.TaggedError()("CommandRejected", { aggregateName: Schema.String, command: Schema.String, /** The tag of the handler's error. */ error: Schema.String, message: Schema.String }, { httpApiStatus: 422 }).aggregateName` ##### command > `readonly` **command**: `string` = `Schema.String` Defined in: [src/CommandApi.ts:84](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L84) ###### Inherited from `Schema.TaggedError()("CommandRejected", { aggregateName: Schema.String, command: Schema.String, /** The tag of the handler's error. */ error: Schema.String, message: Schema.String }, { httpApiStatus: 422 }).command` ##### error > `readonly` **error**: `string` = `Schema.String` Defined in: [src/CommandApi.ts:86](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L86) The tag of the handler’s error. ###### Inherited from `Schema.TaggedError()("CommandRejected", { aggregateName: Schema.String, command: Schema.String, /** The tag of the handler's error. */ error: Schema.String, message: Schema.String }, { httpApiStatus: 422 }).error` ##### message > **message**: `string` = `Schema.String` Defined in: [src/CommandApi.ts:87](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L87) ###### Inherited from `Schema.TaggedError()("CommandRejected", { aggregateName: Schema.String, command: Schema.String, /** The tag of the handler's error. */ error: Schema.String, message: Schema.String }, { httpApiStatus: 422 }).message` *** ### NotCaughtUp Defined in: [src/CommandApi.ts:375](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L375) The view models didn’t reach the position a `Read-After` header asked for in time, because they are behind or stalled on a failing handler. Answered with `503`: the client can try again, or read without the header and accept an older answer. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new NotCaughtUp**(…`args`): [`NotCaughtUp`](/reference/api/commandapi/#notcaughtup) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`NotCaughtUp`](/reference/api/commandapi/#notcaughtup) ###### Inherited from `Schema.TaggedError()("NotCaughtUp", { /** The position the request asked for. */ position: Schema.Int, message: Schema.String }, { httpApiStatus: 503 }).constructor` #### Properties ##### message > **message**: `string` = `Schema.String` Defined in: [src/CommandApi.ts:378](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L378) ###### Inherited from `Schema.TaggedError()("NotCaughtUp", { /** The position the request asked for. */ position: Schema.Int, message: Schema.String }, { httpApiStatus: 503 }).message` ##### position > `readonly` **position**: `number` = `Schema.Int` Defined in: [src/CommandApi.ts:377](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L377) The position the request asked for. ###### Inherited from `Schema.TaggedError()("NotCaughtUp", { /** The position the request asked for. */ position: Schema.Int, message: Schema.String }, { httpApiStatus: 503 }).position` *** ### ReadAfter Defined in: [src/CommandApi.ts:398](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L398) Middleware for query endpoints that read view models. A request with a `Read-After: ` header waits until every view model of this process has handled that position, the one a command answered with, before the endpoint runs. A request without the header runs at once. A header that isn’t a position is answered with `400`. ```ts const Catalog = HttpApiGroup.make("catalog") .add(HttpApiEndpoint.get("available", "/books/available", { success: Books })) .middleware(CommandApi.ReadAfter) const ReadAfterLive = CommandApi.layerReadAfter({ timeout: "2 seconds" }) ``` Implement it with [layerReadAfter](/reference/api/commandapi/#layerreadafter). #### Extends * `Shape`<`"effect-cqrs/CommandApi/ReadAfter"`, `HttpApiMiddleware`<`never`, readonly \[*typeof* [`NotCaughtUp`](/reference/api/commandapi/#notcaughtup), *typeof* `BadRequest`], `never`>, `this`> & `object` #### Constructors ##### Constructor > **new ReadAfter**(`_`): [`ReadAfter`](/reference/api/commandapi/#readafter) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/http-api/HttpApiMiddleware.d.ts:265 ###### Parameters ###### \_ `never` ###### Returns [`ReadAfter`](/reference/api/commandapi/#readafter) ###### Inherited from `HttpApiMiddleware.Service()("effect-cqrs/CommandApi/ReadAfter", { error: [NotCaughtUp, HttpApiError.BadRequest] }).constructor` ## Interfaces ### LayerOptions Defined in: [src/CommandApi.ts:276](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L276) #### Properties ##### historyLimit? > `readonly` `optional` **historyLimit?**: `number` Defined in: [src/CommandApi.ts:282](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L282) The most events one `history` response lists, so that a long stream can’t make one response as large as the stream. A client reads further with `afterVersion`. Defaults to 1,000. *** ### Options Defined in: [src/CommandApi.ts:176](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L176) #### Type Parameters ##### Name `Name` *extends* `string` ##### State `State` *extends* `Schema.Top` ##### Errors `Errors` *extends* `ReadonlyArray`<`Schema.Top`> #### Properties ##### errors? > `readonly` `optional` **errors?**: `Errors` Defined in: [src/CommandApi.ts:192](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L192) Errors of the command handlers to answer with as they are. Others become [CommandRejected](/reference/api/commandapi/#commandrejected). An error without an `httpApiStatus` annotation is answered with `422`. ##### name? > `readonly` `optional` **name?**: `Name` Defined in: [src/CommandApi.ts:182](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L182) The identifier of the group, the aggregate’s name by default. ##### prefix? > `readonly` `optional` **prefix?**: `` `/${string}` `` Defined in: [src/CommandApi.ts:184](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L184) Where the endpoints start, `/` by default. ##### state? > `readonly` `optional` **state?**: `State` Defined in: [src/CommandApi.ts:186](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L186) The schema of the state. Without it there is no `state` endpoint. *** ### ReadAfterOptions Defined in: [src/CommandApi.ts:402](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L402) #### Properties ##### timeout? > `readonly` `optional` **timeout?**: `Input` Defined in: [src/CommandApi.ts:404](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L404) How long a request waits before it is answered with [NotCaughtUp](/reference/api/commandapi/#notcaughtup). Defaults to 5 seconds. ## Type Aliases ### Endpoints > **Endpoints**<`A`, `State`, `Errors`> = `{ readonly [K in CommandName]: CommandEndpoint }`\[[`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>] | `HistoryEndpoint`<`A`> | \[`State`] *extends* \[`never`] ? `never` : `StateEndpoint`<`State`> Defined in: [src/CommandApi.ts:171](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L171) The endpoints of [group](/reference/api/commandapi/#group). #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### State `State` *extends* `Schema.Top` ##### Errors `Errors` *extends* `Schema.Top` ## Variables ### ConcurrencyConflictError > `const` **ConcurrencyConflictError**: `decodeTo`<`declareConstructor`<[`ConcurrencyConflict`](/reference/api/eventstore/#concurrencyconflict), { `aggregateId`: `string`; `aggregateName`: `string`; `expectedVersion`: `number`; }, readonly \[`TaggedStruct`<`"ConcurrencyConflict"`, { `aggregateId`: `String`; `aggregateName`: `String`; `expectedVersion`: `Int`; }>], { `aggregateId`: `string`; `aggregateName`: `string`; `expectedVersion`: `number`; }>, `TaggedStruct`<`"ConcurrencyConflict"`, { `aggregateId`: `String`; `aggregateName`: `String`; `expectedVersion`: `Int`; }>, `never`, `never`> Defined in: [src/CommandApi.ts:73](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L73) The stream moved on while the command was decided, and retries ran out. Answered with `409`. *** ### InvalidCommandError > `const` **InvalidCommandError**: `decodeTo`<`declareConstructor`<[`InvalidCommand`](/reference/api/aggregateservice/#invalidcommand), { `aggregateName`: `string`; `message`: `string`; `type`: `string`; }, readonly \[`TaggedStruct`<`"InvalidCommand"`, { `aggregateName`: `String`; `message`: `String`; `type`: `String`; }>], { `aggregateName`: `string`; `message`: `string`; `type`: `string`; }>, `TaggedStruct`<`"InvalidCommand"`, { `aggregateName`: `String`; `message`: `String`; `type`: `String`; }>, `never`, `never`> Defined in: [src/CommandApi.ts:70](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L70) A command whose payload doesn’t match its schema, or a body that isn’t JSON. The same error as `AggregateService.InvalidCommand`, answered with `400`. *** ### WrongExpectedVersionError > `const` **WrongExpectedVersionError**: `decodeTo`<`declareConstructor`<[`WrongExpectedVersion`](/reference/api/aggregateservice/#wrongexpectedversion), { `actualVersion`: `number`; `aggregateId`: `string`; `aggregateName`: `string`; `expectedVersion`: `number`; }, readonly \[`TaggedStruct`<`"WrongExpectedVersion"`, { `actualVersion`: `Int`; `aggregateId`: `String`; `aggregateName`: `String`; `expectedVersion`: `Int`; }>], { `actualVersion`: `number`; `aggregateId`: `string`; `aggregateName`: `string`; `expectedVersion`: `number`; }>, `TaggedStruct`<`"WrongExpectedVersion"`, { `actualVersion`: `Int`; `aggregateId`: `String`; `aggregateName`: `String`; `expectedVersion`: `Int`; }>, `never`, `never`> Defined in: [src/CommandApi.ts:76](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L76) The stream is no longer at the version `If-Match` named. Answered with `409`. ## Functions ### commandResult() > **commandResult**<`Events`>(`events`): `Struct`<{ `aggregateId`: `String`; `aggregateName`: `String`; `correlationId`: `String`; `events`: `$Array`<`Events`>; `position`: `Int`; `version`: `Int`; }> Defined in: [src/CommandApi.ts:91](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L91) The body of a successful command, `CommandResult` with the events in their JSON form. #### Type Parameters ##### Events `Events` *extends* [`EventSchema`](/reference/api/aggregate/#eventschema)<`any`> #### Parameters ##### events `Events` #### Returns `Struct`<{ `aggregateId`: `String`; `aggregateName`: `String`; `correlationId`: `String`; `events`: `$Array`<`Events`>; `position`: `Int`; `version`: `Int`; }> *** ### group() > **group**<`A`, `Name`, `State`, `Errors`>(`aggregate`, `options?`): `HttpApiGroup`<`Name`, [`Endpoints`](/reference/api/commandapi/#endpoints)<`A`, `State`, `Errors`\[`number`]>> Defined in: [src/CommandApi.ts:205](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L205) Builds the endpoints of an aggregate. Add the group to an `HttpApi` and implement it with [layer](/reference/api/commandapi/#layer). #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### Name `Name` *extends* `string` = `A`\[`"name"`] ##### State `State` *extends* `Top` = `never` ##### Errors `Errors` *extends* readonly `Top`\[] = \[] #### Parameters ##### aggregate `A` ##### options? [`Options`](/reference/api/commandapi/#options)<`Name`, `State`, `Errors`> #### Returns `HttpApiGroup`<`Name`, [`Endpoints`](/reference/api/commandapi/#endpoints)<`A`, `State`, `Errors`\[`number`]>> *** ### historyEntry() > **historyEntry**<`Events`>(`events`): `Struct`<{ `causationId`: `NullOr`<`String`>; `correlationId`: `NullOr`<`String`>; `metadata`: `Codec`<`JsonObject`, `JsonObject`, `never`, `never`>; `payload`: `optionalKey`<`Events`>; `timestamp`: `DateTimeUtc`; `type`: `String`; `version`: `Int`; }> Defined in: [src/CommandApi.ts:102](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L102) One entry of `history`. `payload` is there when the request asks for it. #### Type Parameters ##### Events `Events` *extends* [`EventSchema`](/reference/api/aggregate/#eventschema)<`any`> #### Parameters ##### events `Events` #### Returns `Struct`<{ `causationId`: `NullOr`<`String`>; `correlationId`: `NullOr`<`String`>; `metadata`: `Codec`<`JsonObject`, `JsonObject`, `never`, `never`>; `payload`: `optionalKey`<`Events`>; `timestamp`: `DateTimeUtc`; `type`: `String`; `version`: `Int`; }> *** ### layer() > **layer**<`ApiId`, `Groups`, `Name`, `A`, `E`, `R`>(`api`, `name`, `service`, `options?`): `Layer`<`Service`<`ApiId`, `Name`>, `E`, `Exclude`<`R`, `Scope`>> Defined in: [src/CommandApi.ts:292](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L292) Implements a group built by [group](/reference/api/commandapi/#group) with an aggregate service, or an effect that builds one when the layer starts. Unexpected failures, such as a store that can’t be reached, are defects and answered with `500`. #### Type Parameters ##### ApiId `ApiId` *extends* `string` ##### Groups `Groups` *extends* `Constraint` ##### Name `Name` *extends* `string` ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### E `E` = `never` ##### R `R` = `never` #### Parameters ##### api `HttpApi`<`ApiId`, `Groups`> ##### name `Name` ##### service [`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`> | `Effect`<[`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`A`>, `E`, `R`> ##### options? [`LayerOptions`](/reference/api/commandapi/#layeroptions) #### Returns `Layer`<`Service`<`ApiId`, `Name`>, `E`, `Exclude`<`R`, `Scope`>> *** ### layerReadAfter() > **layerReadAfter**(`options?`): `Layer`<[`ReadAfter`](/reference/api/commandapi/#readafter), `never`, [`ViewModels`](/reference/api/viewmodel/#viewmodels)> Defined in: [src/CommandApi.ts:415](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandApi.ts#L415) Implements [ReadAfter](/reference/api/commandapi/#readafter) with the `ViewModels` of this process. View models that another process holds are not waited for, as with `ViewModels.caughtUp`. #### Parameters ##### options? [`ReadAfterOptions`](/reference/api/commandapi/#readafteroptions) #### Returns `Layer`<[`ReadAfter`](/reference/api/commandapi/#readafter), `never`, [`ViewModels`](/reference/api/viewmodel/#viewmodels)> # CommandGateway One entry point for the commands of several aggregates, like Axon’s `CommandGateway`: the caller hands over a command and the gateway finds the aggregate that takes it. ```ts const gateway = yield* CommandGateway.make([Todo, Account], { interceptors: [audit] }) // Typed: the payload and the errors follow from the aggregate and the type. yield* gateway.send({ aggregateName: "todo", aggregateId: "a", type: "createTodo", payload: { title: "Buy Milk" } }) // Untyped JSON from a transport, routed by `aggregateName`, or by `type` alone // when a single aggregate has a command of that name. yield* gateway.dispatch({ aggregateId: "a", type: "completeTodo", payload: {} }) ``` The gateway composes aggregate services. Give it an aggregate and it builds the service with the default options; give it a service built with `AggregateService.make` to choose them, or to share the service, and its state cache, with code that uses it directly. Interceptors run around every command the gateway routes, for what Axon’s dispatch interceptors do: authorize, log, add metadata with `Metadata.withMetadata`. They run in the caller’s fiber, so what they need, such as the current user, comes from the caller’s context rather than the gateway’s. ## Classes ### UnroutableCommand Defined in: [src/CommandGateway.ts:41](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L41) The gateway can’t tell which aggregate a command is for: the envelope names an aggregate the gateway doesn’t have, or it names none and the type is a command of no aggregate, or of more than one. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new UnroutableCommand**(…`args`): [`UnroutableCommand`](/reference/api/commandgateway/#unroutablecommand) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`UnroutableCommand`](/reference/api/commandgateway/#unroutablecommand) ###### Inherited from `Schema.TaggedError()("UnroutableCommand", { type: Schema.String, /** The aggregate the envelope named, if it named one. */ aggregateName: Schema.optional(Schema.String), /** The aggregates of the gateway that have a command of this type. */ candidates: Schema.Array(Schema.String), message: Schema.String }).constructor` #### Properties ##### aggregateName? > `readonly` `optional` **aggregateName?**: `string` Defined in: [src/CommandGateway.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L44) The aggregate the envelope named, if it named one. ###### Inherited from `Schema.TaggedError()("UnroutableCommand", { type: Schema.String, /** The aggregate the envelope named, if it named one. */ aggregateName: Schema.optional(Schema.String), /** The aggregates of the gateway that have a command of this type. */ candidates: Schema.Array(Schema.String), message: Schema.String }).aggregateName` ##### candidates > `readonly` **candidates**: readonly `string`\[] Defined in: [src/CommandGateway.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L46) The aggregates of the gateway that have a command of this type. ###### Inherited from `Schema.TaggedError()("UnroutableCommand", { type: Schema.String, /** The aggregate the envelope named, if it named one. */ aggregateName: Schema.optional(Schema.String), /** The aggregates of the gateway that have a command of this type. */ candidates: Schema.Array(Schema.String), message: Schema.String }).candidates` ##### message > **message**: `string` = `Schema.String` Defined in: [src/CommandGateway.ts:47](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L47) ###### Inherited from `Schema.TaggedError()("UnroutableCommand", { type: Schema.String, /** The aggregate the envelope named, if it named one. */ aggregateName: Schema.optional(Schema.String), /** The aggregates of the gateway that have a command of this type. */ candidates: Schema.Array(Schema.String), message: Schema.String }).message` ##### type > `readonly` **type**: `string` = `Schema.String` Defined in: [src/CommandGateway.ts:42](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L42) ###### Inherited from `Schema.TaggedError()("UnroutableCommand", { type: Schema.String, /** The aggregate the envelope named, if it named one. */ aggregateName: Schema.optional(Schema.String), /** The aggregates of the gateway that have a command of this type. */ candidates: Schema.Array(Schema.String), message: Schema.String }).type` ## Interfaces ### CommandGateway Defined in: [src/CommandGateway.ts:119](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L119) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### E `E` = `never` ##### R `R` = `never` #### Properties ##### dispatch > `readonly` **dispatch**: (`input`) => `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<[`EventOf`](/reference/api/commandgateway/#eventof)<`A`>>, [`UnknownCommand`](/reference/api/aggregateservice/#unknowncommand) | [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`UnroutableCommand`](/reference/api/commandgateway/#unroutablecommand) | `E` | [`HandlerError`](/reference/api/commandgateway/#handlererror)<`A`, `string`>, `R`> Defined in: [src/CommandGateway.ts:139](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L139) Runs a `{ aggregateName, aggregateId, type, payload }` envelope received as JSON from outside the process, with the other fields `AggregateService.dispatch` takes. `aggregateName` can be left out when a single aggregate of the gateway has a command of that type. ###### Parameters ###### input `unknown` ###### Returns `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<[`EventOf`](/reference/api/commandgateway/#eventof)<`A`>>, [`UnknownCommand`](/reference/api/aggregateservice/#unknowncommand) | [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`UnroutableCommand`](/reference/api/commandgateway/#unroutablecommand) | `E` | [`HandlerError`](/reference/api/commandgateway/#handlererror)<`A`, `string`>, `R`> ##### send > `readonly` **send**: <`C`>(`command`) => `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<[`Event`](/reference/api/aggregate/#event-2)<`Extract`<`A`, { `name`: `C`\[`"aggregateName"`]; }>>>, [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | `E` | [`HandlerError`](/reference/api/commandgateway/#handlererror)<`Extract`<`A`, { `name`: `C`\[`"aggregateName"`]; }>, `C`\[`"type"`]>, `R`> Defined in: [src/CommandGateway.ts:126](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L126) Runs a typed command on the aggregate it names. ###### Type Parameters ###### C `C` *extends* `object` & [`CommandOptions`](/reference/api/aggregateservice/#commandoptions) ###### Parameters ###### command `C` ###### Returns `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<[`Event`](/reference/api/aggregate/#event-2)<`Extract`<`A`, { `name`: `C`\[`"aggregateName"`]; }>>>, [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | `E` | [`HandlerError`](/reference/api/commandgateway/#handlererror)<`Extract`<`A`, { `name`: `C`\[`"aggregateName"`]; }>, `C`\[`"type"`]>, `R`> ##### services > `readonly` **services**: `{ readonly [X in Any as Name]: AggregateService }` Defined in: [src/CommandGateway.ts:124](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L124) The service of each aggregate, by name, for reading with `state` and `history`. Commands run through a service directly skip the interceptors. *** ### CommandInfo Defined in: [src/CommandGateway.ts:51](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L51) What an interceptor learns about the command it runs around. #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/CommandGateway.ts:53](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L53) ##### aggregateName > `readonly` **aggregateName**: `string` Defined in: [src/CommandGateway.ts:52](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L52) ##### type > `readonly` **type**: `string` Defined in: [src/CommandGateway.ts:54](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L54) *** ### Interceptor() Defined in: [src/CommandGateway.ts:71](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L71) Runs around a command. It may run effects before and after `next`, fail instead of running it, or run it with more context, and adds its own errors `E` and requirements `R` to every command of the gateway. ```ts const audit: CommandGateway.Interceptor = (command, next) => Effect.gen(function*() { const user = yield* CurrentUser if (!user.may(command.type)) return yield* new Unauthorized({ type: command.type }) return yield* Metadata.withMetadata(next, { userId: user.id }) }) ``` #### Type Parameters ##### E `E` = `never` ##### R `R` = `never` > **Interceptor**<`A`, `E1`, `R1`>(`command`, `next`): `Effect`<`A`, `E` | `E1`, `R` | `R1`> Defined in: [src/CommandGateway.ts:72](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L72) Runs around a command. It may run effects before and after `next`, fail instead of running it, or run it with more context, and adds its own errors `E` and requirements `R` to every command of the gateway. ```ts const audit: CommandGateway.Interceptor = (command, next) => Effect.gen(function*() { const user = yield* CurrentUser if (!user.may(command.type)) return yield* new Unauthorized({ type: command.type }) return yield* Metadata.withMetadata(next, { userId: user.id }) }) ``` #### Type Parameters ##### A `A` ##### E1 `E1` ##### R1 `R1` #### Parameters ##### command [`CommandInfo`](/reference/api/commandgateway/#commandinfo) ##### next `Effect`<`A`, `E1`, `R1`> #### Returns `Effect`<`A`, `E` | `E1`, `R` | `R1`> *** ### Options Defined in: [src/CommandGateway.ts:152](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L152) #### Type Parameters ##### Interceptors `Interceptors` *extends* `ReadonlyArray`<[`Interceptor`](/reference/api/commandgateway/#interceptor)<`any`, `any`>> #### Properties ##### interceptors? > `readonly` `optional` **interceptors?**: `Interceptors` Defined in: [src/CommandGateway.ts:154](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L154) Run around every command, the first outermost. ## Type Aliases ### AggregateOf > **AggregateOf**<`X`> = `X` *extends* `object` ? `A` : `X` *extends* [`Any`](/reference/api/aggregate/#any) ? `X` : `never` Defined in: [src/CommandGateway.ts:79](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L79) The aggregate of an entry. #### Type Parameters ##### X `X` *** ### Command > **Command**<`A`> = `A` *extends* [`Any`](/reference/api/aggregate/#any) ? `{ readonly [K in CommandName]: { aggregateId: string; aggregateName: Name; payload: Payload; type: K } & CommandOptions }`\[[`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>] : `never` Defined in: [src/CommandGateway.ts:96](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L96) A typed command for one of the aggregates `A`, what [CommandGateway.send](/reference/api/commandgateway/#commandgateway) takes. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Entry > **Entry** = [`Any`](/reference/api/aggregate/#any) | [`AggregateService`](/reference/api/aggregateservice/#aggregateservice)<`any`> Defined in: [src/CommandGateway.ts:76](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L76) What a gateway routes to: an aggregate, or the service of one. *** ### EventOf > **EventOf**<`A`> = `A` *extends* [`Any`](/reference/api/aggregate/#any) ? [`Event`](/reference/api/aggregate/#event-2)<`A`> : `never` Defined in: [src/CommandGateway.ts:117](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L117) The events of any aggregate of `A`. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### HandlerError > **HandlerError**<`A`, `Type`> = `A` *extends* [`Any`](/reference/api/aggregate/#any) ? [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `Type` & [`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>> : `never` Defined in: [src/CommandGateway.ts:112](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L112) The errors the handler of a command declares. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### Type `Type` *** ### InterceptorContext > **InterceptorContext**<`I`> = `I` *extends* [`Interceptor`](/reference/api/commandgateway/#interceptor)<`any`, infer R> ? `R` : `never` Defined in: [src/CommandGateway.ts:93](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L93) What an interceptor requires from the caller, read from its type like [InterceptorError](/reference/api/commandgateway/#interceptorerror). #### Type Parameters ##### I `I` *** ### InterceptorError > **InterceptorError**<`I`> = `I` *extends* [`Interceptor`](/reference/api/commandgateway/#interceptor)\ ? `E` : `never` Defined in: [src/CommandGateway.ts:90](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L90) The errors an interceptor adds. They are read from its type, so declare an interceptor as an `Interceptor` rather than inline in the options. #### Type Parameters ##### I `I` *** ### Named > **Named**<`A`, `Name`> = `Extract`<`A`, { `name`: `Name`; }> Defined in: [src/CommandGateway.ts:109](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L109) The aggregate of `A` with this name. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### Name `Name` *** ### Requirements > **Requirements**<`X`> = `X` *extends* [`Any`](/reference/api/aggregate/#any) ? [`EventStore`](/reference/api/eventstore/#eventstore) | [`Context`](/reference/api/aggregate/#context)<`X`> : `never` Defined in: [src/CommandGateway.ts:84](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L84) What building the services of the entries requires: entries that are services are already built. #### Type Parameters ##### X `X` ## Functions ### make() > **make**<`Entries`, `Interceptors`>(`entries`, `options?`): `Effect`<[`CommandGateway`](/reference/api/commandgateway/#commandgateway)<[`AggregateOf`](/reference/api/commandgateway/#aggregateof)<`Entries`\[`number`]>, [`InterceptorError`](/reference/api/commandgateway/#interceptorerror)<`Interceptors`\[`number`]>, [`InterceptorContext`](/reference/api/commandgateway/#interceptorcontext)<`Interceptors`\[`number`]>>, `never`, [`Requirements`](/reference/api/commandgateway/#requirements)<`Entries`\[`number`]>> Defined in: [src/CommandGateway.ts:263](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/CommandGateway.ts#L263) Builds a gateway over aggregates, or over services of them. Two entries for the same aggregate are a defect. #### Type Parameters ##### Entries `Entries` *extends* readonly [`Entry`](/reference/api/commandgateway/#entry)\[] ##### Interceptors `Interceptors` *extends* readonly [`Interceptor`](/reference/api/commandgateway/#interceptor)<`any`, `any`>\[] = \[] #### Parameters ##### entries `Entries` ##### options? [`Options`](/reference/api/commandgateway/#options)<`Interceptors`> #### Returns `Effect`<[`CommandGateway`](/reference/api/commandgateway/#commandgateway)<[`AggregateOf`](/reference/api/commandgateway/#aggregateof)<`Entries`\[`number`]>, [`InterceptorError`](/reference/api/commandgateway/#interceptorerror)<`Interceptors`\[`number`]>, [`InterceptorContext`](/reference/api/commandgateway/#interceptorcontext)<`Interceptors`\[`number`]>>, `never`, [`Requirements`](/reference/api/commandgateway/#requirements)<`Entries`\[`number`]>> # EventCatalog Describes what an aggregate writes and accepts, for consumers outside the process, and guards stored events against schema changes that would break them. * [events](/reference/api/eventcatalog/#events) and [commands](/reference/api/eventcatalog/#commands) give the JSON Schema of every event, in every version declared with `EventVersions`, and of every command payload. * [document](/reference/api/eventcatalog/#document) lists them for several aggregates in an AsyncAPI 3 document. * [captureSamples](/reference/api/eventcatalog/#capturesamples) takes a few stored payloads of each event and version, [saveSamples](/reference/api/eventcatalog/#savesamples) keeps them in a JSON file, and [checkCompatibility](/reference/api/eventcatalog/#checkcompatibility) fails when one of them no longer decodes with the current aggregate. JSON Schema describes the JSON form of a schema, as `Schema.toCodecJson` encodes it. Personal fields, see `PersonalData`, are described in plaintext: in the store they are encrypted. ## Classes ### IncompatibleSamples Defined in: [src/EventCatalog.ts:230](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L230) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new IncompatibleSamples**(…`args`): [`IncompatibleSamples`](/reference/api/eventcatalog/#incompatiblesamples) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`IncompatibleSamples`](/reference/api/eventcatalog/#incompatiblesamples) ###### Inherited from `Schema.TaggedError()("IncompatibleSamples", { aggregateName: Schema.String, failures: Schema.Array(Schema.Struct({ type: Schema.String, schemaVersion: Schema.Int, message: Schema.String })) }).constructor` #### Properties ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/EventCatalog.ts:231](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L231) ###### Inherited from `Schema.TaggedError()("IncompatibleSamples", { aggregateName: Schema.String, failures: Schema.Array(Schema.Struct({ type: Schema.String, schemaVersion: Schema.Int, message: Schema.String })) }).aggregateName` ##### failures > `readonly` **failures**: readonly `object`\[] Defined in: [src/EventCatalog.ts:232](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L232) ###### Inherited from `Schema.TaggedError()("IncompatibleSamples", { aggregateName: Schema.String, failures: Schema.Array(Schema.Struct({ type: Schema.String, schemaVersion: Schema.Int, message: Schema.String })) }).failures` #### Accessors ##### message ###### Get Signature > **get** **message**(): `string` Defined in: [src/EventCatalog.ts:238](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L238) ###### Returns `string` ###### Overrides `Schema.TaggedError()("IncompatibleSamples", { aggregateName: Schema.String, failures: Schema.Array(Schema.Struct({ type: Schema.String, schemaVersion: Schema.Int, message: Schema.String })) }).message` ## Interfaces ### AsyncApiDocument Defined in: [src/EventCatalog.ts:106](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L106) An AsyncAPI 3.0 document, with JSON Schema draft-07 payloads as AsyncAPI expects. #### Properties ##### asyncapi > `readonly` **asyncapi**: `"3.0.0"` Defined in: [src/EventCatalog.ts:107](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L107) ##### channels > `readonly` **channels**: `Record`<`string`, `unknown`> Defined in: [src/EventCatalog.ts:109](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L109) ##### components > `readonly` **components**: `object` Defined in: [src/EventCatalog.ts:111](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L111) ###### messages > `readonly` **messages**: `Record`<`string`, `unknown`> ###### schemas > `readonly` **schemas**: `Record`<`string`, `JsonSchema.JsonSchema`> ##### info > `readonly` **info**: `object` Defined in: [src/EventCatalog.ts:108](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L108) ###### description? > `readonly` `optional` **description?**: `string` ###### title > `readonly` **title**: `string` ###### version > `readonly` **version**: `string` ##### operations > `readonly` **operations**: `Record`<`string`, `unknown`> Defined in: [src/EventCatalog.ts:110](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L110) *** ### CommandEntry Defined in: [src/EventCatalog.ts:47](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L47) #### Properties ##### aggregate > `readonly` **aggregate**: `string` Defined in: [src/EventCatalog.ts:48](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L48) ##### name > `readonly` **name**: `string` Defined in: [src/EventCatalog.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L49) ##### payload > `readonly` **payload**: `JsonSchema` Defined in: [src/EventCatalog.ts:51](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L51) A JSON Schema draft 2020-12 of the payload. *** ### DocumentOptions Defined in: [src/EventCatalog.ts:99](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L99) #### Properties ##### description? > `readonly` `optional` **description?**: `string` Defined in: [src/EventCatalog.ts:102](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L102) ##### title > `readonly` **title**: `string` Defined in: [src/EventCatalog.ts:100](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L100) ##### version > `readonly` **version**: `string` Defined in: [src/EventCatalog.ts:101](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L101) *** ### EventEntry Defined in: [src/EventCatalog.ts:36](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L36) #### Properties ##### aggregate > `readonly` **aggregate**: `string` Defined in: [src/EventCatalog.ts:37](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L37) ##### personalFields > `readonly` **personalFields**: readonly `string`\[] Defined in: [src/EventCatalog.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L44) Paths of the personal fields in the current version, encrypted in the store. ##### schemaVersion > `readonly` **schemaVersion**: `number` Defined in: [src/EventCatalog.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L40) The version new events are written in, the last of `versions`. ##### type > `readonly` **type**: `string` Defined in: [src/EventCatalog.ts:38](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L38) ##### versions > `readonly` **versions**: readonly [`EventVersion`](/reference/api/eventcatalog/#eventversion)\[] Defined in: [src/EventCatalog.ts:42](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L42) Oldest first. Events written before versions were declared are version 1. *** ### EventVersion Defined in: [src/EventCatalog.ts:30](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L30) #### Properties ##### schema > `readonly` **schema**: `JsonSchema` Defined in: [src/EventCatalog.ts:33](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L33) A JSON Schema draft 2020-12 of the payload in this version. ##### schemaVersion > `readonly` **schemaVersion**: `number` Defined in: [src/EventCatalog.ts:31](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L31) ## Type Aliases ### Sample > **Sample** = *typeof* `Sample.Type` Defined in: [src/EventCatalog.ts:220](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L220) A payload as the store keeps it, to check that the current aggregate still reads it. ## Variables ### Sample > `const` **Sample**: `Struct`<{ `aggregateName`: `String`; `payload`: `Codec`<`Json`, `Json`, `never`, `never`>; `schemaVersion`: `Int`; `type`: `String`; }> Defined in: [src/EventCatalog.ts:220](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L220) A payload as the store keeps it, to check that the current aggregate still reads it. ## Functions ### captureSamples() > **captureSamples**(`aggregates`, `options?`): `Effect`\ Defined in: [src/EventCatalog.ts:281](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L281) Reads the store and keeps the last `perVersion` payloads, one by default, of every event type and schema version of the given aggregates. Personal fields are replaced by the value they read as once their subject is forgotten, so samples hold no personal data and can be committed, and checking them needs no keys. The rest of the payload is kept as it was stored. #### Parameters ##### aggregates readonly [`Any`](/reference/api/aggregate/#any)\[] ##### options? ###### perVersion? `number` #### Returns `Effect`\ *** ### checkCompatibility() > **checkCompatibility**(`aggregate`, `samples`): `Effect`<`number`, [`IncompatibleSamples`](/reference/api/eventcatalog/#incompatiblesamples)> Defined in: [src/EventCatalog.ts:340](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L340) Decodes every sample of the aggregate with its current schemas and upcasts, as a store read would, and fails with [IncompatibleSamples](/reference/api/eventcatalog/#incompatiblesamples) listing the ones that no longer decode. Returns how many samples were checked. Personal fields in samples are expected in plaintext, as [captureSamples](/reference/api/eventcatalog/#capturesamples) leaves them, so no keys are needed. #### Parameters ##### aggregate [`Any`](/reference/api/aggregate/#any) ##### samples readonly `object`\[] #### Returns `Effect`<`number`, [`IncompatibleSamples`](/reference/api/eventcatalog/#incompatiblesamples)> *** ### commands() > **commands**(`aggregate`): readonly [`CommandEntry`](/reference/api/eventcatalog/#commandentry)\[] Defined in: [src/EventCatalog.ts:88](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L88) The JSON Schema of the payload of every command of an aggregate. #### Parameters ##### aggregate [`Any`](/reference/api/aggregate/#any) #### Returns readonly [`CommandEntry`](/reference/api/eventcatalog/#commandentry)\[] *** ### document() > **document**(`aggregates`, `options`): [`AsyncApiDocument`](/reference/api/eventcatalog/#asyncapidocument) Defined in: [src/EventCatalog.ts:138](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L138) Lists the events and commands of some aggregates in an AsyncAPI 3.0 document. Each aggregate has a channel of events it sends and a channel of commands it receives. Older versions of an event are under the message’s `x-versions`, and its personal fields under `x-personal-fields`. #### Parameters ##### aggregates readonly [`Any`](/reference/api/aggregate/#any)\[] ##### options [`DocumentOptions`](/reference/api/eventcatalog/#documentoptions) #### Returns [`AsyncApiDocument`](/reference/api/eventcatalog/#asyncapidocument) *** ### events() > **events**(`aggregate`): readonly [`EventEntry`](/reference/api/eventcatalog/#evententry)\[] Defined in: [src/EventCatalog.ts:75](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L75) The JSON Schema of every event of an aggregate, in every version it declares. #### Parameters ##### aggregate [`Any`](/reference/api/aggregate/#any) #### Returns readonly [`EventEntry`](/reference/api/eventcatalog/#evententry)\[] *** ### readSamples() > **readSamples**(`path`): `Effect`\ Defined in: [src/EventCatalog.ts:311](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L311) Reads samples written by [saveSamples](/reference/api/eventcatalog/#savesamples). #### Parameters ##### path `string` #### Returns `Effect`\ *** ### saveSamples() > **saveSamples**(`path`, `samples`): `Effect`<`object`\[], `PlatformError` | `SchemaError`, `FileSystem`> Defined in: [src/EventCatalog.ts:322](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventCatalog.ts#L322) Adds samples to a JSON file, creating it if needed. Samples of an event type and version already in the file are left as they are, so the file keeps the shapes the store held over time, also after those events are gone from it. #### Parameters ##### path `string` ##### samples readonly `object`\[] #### Returns `Effect`<`object`\[], `PlatformError` | `SchemaError`, `FileSystem`> # EventStore The append-only log every aggregate and view model is built on. Each event belongs to a stream, identified by aggregate name and id, and has two coordinates: a `version` that orders it inside its stream and a `position` that orders it across the whole store. Writers use the version for optimistic concurrency, readers use the position as a durable cursor. ## Classes ### ConcurrencyConflict Defined in: [src/EventStore.ts:43](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L43) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new ConcurrencyConflict**(…`args`): [`ConcurrencyConflict`](/reference/api/eventstore/#concurrencyconflict) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`ConcurrencyConflict`](/reference/api/eventstore/#concurrencyconflict) ###### Inherited from `Schema.TaggedError()("ConcurrencyConflict", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int }).constructor` #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` = `Schema.String` Defined in: [src/EventStore.ts:45](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L45) ###### Inherited from `Schema.TaggedError()("ConcurrencyConflict", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int }).aggregateId` ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` Defined in: [src/EventStore.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L44) ###### Inherited from `Schema.TaggedError()("ConcurrencyConflict", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int }).aggregateName` ##### expectedVersion > `readonly` **expectedVersion**: `number` = `Schema.Int` Defined in: [src/EventStore.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L46) ###### Inherited from `Schema.TaggedError()("ConcurrencyConflict", { aggregateName: Schema.String, aggregateId: Schema.String, expectedVersion: Schema.Int }).expectedVersion` *** ### EventStore Defined in: [src/EventStore.ts:139](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L139) #### Extends * `Shape`<`"effect-cqrs/EventStore"`, { `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, `this`> #### Constructors ##### Constructor > **new EventStore**(`_`): [`EventStore`](/reference/api/eventstore/#eventstore) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`EventStore`](/reference/api/eventstore/#eventstore) ###### Inherited from Context.Service\, ConcurrencyConflict | EventStoreError> /\*\* Reads the events stored so far, ordered by position, then completes. \*/ read(filter?: EventFilter): Stream.Stream\ /\*\* Reads the events stored so far, then keeps following new ones. Never completes. \*/ subscribe(filter?: EventFilter): Stream.Stream\ /\*\* The position of the latest event, \`0\` when the store is empty. \*/ readonly head: Effect.Effect\ /\*\* \* Emits the current latest position, then each newer one. Consumers can treat an \* element as “there may be something new”. Appends from this process are always \* observed, appends from other processes when the backend announces them or the \* store polls for them: see \`MakeOptions\`. \*/ readonly changes: Stream.Stream\ /\*\* \* Runs an effect once what was just appended can be read by everyone: at once, \* or when the caller’s transaction commits, and never if it rolls back. Services \* that keep what they read or wrote in memory go through it, so they don’t keep \* what a rollback undid. Missing on stores without transactions. \*/ readonly afterCommit?: (\(effect: Effect.Effect\) => Effect.Effect\) | undefined }>()(“effect-cqrs/EventStore”).constructor *** ### EventStoreError Defined in: [src/EventStore.ts:38](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L38) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new EventStoreError**(…`args`): [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) ###### Inherited from `Schema.TaggedError()("EventStoreError", { operation: Schema.String, cause: Schema.Defect() }).constructor` #### Properties ##### cause > **cause**: `unknown` Defined in: [src/EventStore.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L40) ###### Inherited from `Schema.TaggedError()("EventStoreError", { operation: Schema.String, cause: Schema.Defect() }).cause` ##### operation > `readonly` **operation**: `string` = `Schema.String` Defined in: [src/EventStore.ts:39](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L39) ###### Inherited from `Schema.TaggedError()("EventStoreError", { operation: Schema.String, cause: Schema.Defect() }).operation` ## Interfaces ### AppendInput Defined in: [src/EventStore.ts:75](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L75) #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/EventStore.ts:77](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L77) ##### aggregateName > `readonly` **aggregateName**: `string` Defined in: [src/EventStore.ts:76](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L76) ##### causationId? > `readonly` `optional` **causationId?**: `string` Defined in: [src/EventStore.ts:82](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L82) ##### correlationId? > `readonly` `optional` **correlationId?**: `string` Defined in: [src/EventStore.ts:81](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L81) ##### events > `readonly` **events**: readonly [`NewEvent`](/reference/api/eventstore/#newevent)\[] Defined in: [src/EventStore.ts:80](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L80) ##### expectedVersion > `readonly` **expectedVersion**: `number` Defined in: [src/EventStore.ts:79](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L79) The stream version the writer based its decision on, `0` for a new stream. ##### metadata? > `readonly` `optional` **metadata?**: `JsonObject` Defined in: [src/EventStore.ts:84](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L84) Recorded on every event of the append. Defaults to `{}`. *** ### Backend Defined in: [src/EventStore.ts:165](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L165) What a storage adapter implements. [make](/reference/api/eventstore/#make) builds the rest on top. #### Properties ##### afterCommit? > `readonly` `optional` **afterCommit?**: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`> Defined in: [src/EventStore.ts:185](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L185) Runs an effect once an append made now can be read by everyone: at once, or when the caller’s transaction commits, and never if it rolls back. `SqlClient.afterCommit` is one. Without it appends are announced as soon as they return, which makes subscribers skip those of a transaction that is still open. ###### Type Parameters ###### R `R` ###### Parameters ###### effect `Effect`<`void`, `never`, `R`> ###### Returns `Effect`<`void`, `never`, `R`> ##### append > `readonly` **append**: (`input`) => `Effect`\ Defined in: [src/EventStore.ts:166](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L166) Appends atomically, failing if the stream moved past `expectedVersion`. ###### Parameters ###### input [`AppendInput`](/reference/api/eventstore/#appendinput) ###### Returns `Effect`\ ##### changes? > `readonly` `optional` **changes?**: `Stream`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `never`> Defined in: [src/EventStore.ts:178](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L178) Head positions written by other processes, as the database announces them. It should start with the head once it listens, so nothing between two runs is missed. When it fails or ends, the store logs it and runs it again a second later. ##### head > `readonly` **head**: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)> Defined in: [src/EventStore.ts:172](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L172) ##### readBatch > `readonly` **readBatch**: (`filter`, `afterPosition`, `limit`) => `Effect`\ Defined in: [src/EventStore.ts:167](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L167) ###### Parameters ###### filter [`EventFilter`](/reference/api/eventstore/#eventfilter) ###### afterPosition `number` ###### limit `number` ###### Returns `Effect`\ *** ### EventFilter Defined in: [src/EventStore.ts:121](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L121) Selects events by stream, type and time. Omitted fields match everything. `startTime` is inclusive and `finishTime` is exclusive, so adding one millisecond to a history timestamp selects up to and including that event. #### Properties ##### afterPosition? > `readonly` `optional` **afterPosition?**: `number` Defined in: [src/EventStore.ts:127](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L127) ##### afterVersion? > `readonly` `optional` **afterVersion?**: `number` Defined in: [src/EventStore.ts:132](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L132) For a filter on one stream: where to continue in it. Versions follow the stream’s own index, so resuming there costs the same however far the log has moved on. ##### aggregateIds? > `readonly` `optional` **aggregateIds?**: readonly `string`\[] Defined in: [src/EventStore.ts:123](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L123) ##### aggregateNames? > `readonly` `optional` **aggregateNames?**: readonly `string`\[] Defined in: [src/EventStore.ts:122](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L122) ##### eventTypes? > `readonly` `optional` **eventTypes?**: readonly `string`\[] Defined in: [src/EventStore.ts:124](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L124) ##### finishTime? > `readonly` `optional` **finishTime?**: `Utc` Defined in: [src/EventStore.ts:126](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L126) ##### startTime? > `readonly` `optional` **startTime?**: `Utc` Defined in: [src/EventStore.ts:125](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L125) *** ### MakeOptions Defined in: [src/EventStore.ts:188](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L188) #### Properties ##### batchSize? > `readonly` `optional` **batchSize?**: `number` Defined in: [src/EventStore.ts:190](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L190) Events per read. Defaults to 500. ##### pollInterval? > `readonly` `optional` **pollInterval?**: `Input` Defined in: [src/EventStore.ts:195](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L195) Reads the head this often, to notice appends from other processes when the backend doesn’t announce them, or as a safety net when it does. Off by default. *** ### Migration Defined in: [src/EventStore.ts:32](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L32) A schema change for a SQL table, with the table names filled in. Each module that keeps a table exports its migrations in the order to apply them, for teams that run them with their own tooling and turn off `migrate`. #### Properties ##### id > `readonly` **id**: `number` Defined in: [src/EventStore.ts:33](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L33) ##### name > `readonly` **name**: `string` Defined in: [src/EventStore.ts:34](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L34) ##### statements > `readonly` **statements**: readonly `string`\[] Defined in: [src/EventStore.ts:35](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L35) *** ### NewEvent Defined in: [src/EventStore.ts:68](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L68) #### Properties ##### payload > `readonly` **payload**: `Json` Defined in: [src/EventStore.ts:70](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L70) ##### schemaVersion? > `readonly` `optional` **schemaVersion?**: `number` Defined in: [src/EventStore.ts:72](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L72) Defaults to `1`. ##### type > `readonly` **type**: `string` Defined in: [src/EventStore.ts:69](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L69) ## Type Aliases ### StoredEvent > **StoredEvent** = *typeof* `StoredEvent.Type` Defined in: [src/EventStore.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L49) ## Variables ### groupCommit > `const` **groupCommit**: (…`args`) => `Effect`<(`input`) => `Effect`\, `never`, `Scope`> Defined in: [src/EventStore.ts:331](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L331) Group commit for a backend. An append that finds nothing being written goes straight through `appendGroup`, in its own fiber. Appends that arrive meanwhile wait, then go together into the next call, which writes them in one transaction and returns an exit for each, in order: however many wait, they pay for one commit. A group holds at most one append per stream, so they can’t conflict with each other, and at most `maxEvents` events unless a single append has more. An append left out waits for the next group, where it comes first, so appends to a stream are written in the order they came. Groups of waiting appends are written by a fiber of the `Scope`. An append interrupted while it waits is not written, while one interrupted once its group is being written may be: a caller that times out should retry with a `commandId`, which finds the first result if there is one. The others in its group are written and answered as if nothing happened. #### Parameters ##### args …\[`appendGroup`: (`inputs`) => `Effect`\, `options?`: `object`] #### Returns `Effect`<(`input`) => `Effect`\, `never`, `Scope`> *** ### layerMemory > `const` **layerMemory**: `Layer.Layer`<[`EventStore`](/reference/api/eventstore/#eventstore), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)> Defined in: [src/EventStore.ts:540](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L540) Keeps events in process memory. They do not survive a restart. *** ### make > `const` **make**: (…`args`) => `Effect`<{ `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `Scope`> Defined in: [src/EventStore.ts:208](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L208) Builds a store on a backend. Watching for appends from other processes takes fibers that live as long as the `Scope`, which `Layer.effect` provides. #### Parameters ##### args …\[`backend`: [`Backend`](/reference/api/eventstore/#backend), `options?`: [`MakeOptions`](/reference/api/eventstore/#makeoptions)] #### Returns `Effect`<{ `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `Scope`> *** ### makeMemory > `const` **makeMemory**: `Effect.Effect`<[`EventStore`](/reference/api/eventstore/#eventstore)\[`"Service"`], [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)> Defined in: [src/EventStore.ts:487](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L487) *** ### StoredEvent > `const` **StoredEvent**: `Struct`<{ `aggregateId`: `String`; `aggregateName`: `String`; `causationId`: `NullOr`<`String`>; `correlationId`: `NullOr`<`String`>; `metadata`: `$Record`<`String`, `Codec`<`Json`, `Json`, `never`, `never`>>; `payload`: `Codec`<`Json`, `Json`, `never`, `never`>; `position`: `Int`; `schemaVersion`: `Int`; `timestamp`: `DateTimeUtcFromMillis`; `type`: `String`; `version`: `Int`; }> Defined in: [src/EventStore.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L49) ## Functions ### eventId() > **eventId**(`event`): `string` Defined in: [src/EventStore.ts:92](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L92) Identifies an event wherever it is stored, as the `causationId` of what it causes: `account/alice@4`. Name and id are URI-encoded, so a `/` or `@` inside one can’t make two events look alike. #### Parameters ##### event `Pick`<[`StoredEvent`](/reference/api/eventstore/#storedevent), `"aggregateName"` | `"aggregateId"` | `"version"`> #### Returns `string` *** ### isOneStream() > **isOneStream**(`__namedParameters`): `boolean` Defined in: [src/EventStore.ts:136](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L136) A filter on exactly one stream, which a store can read through that stream’s index. #### Parameters ##### \_\_namedParameters [`EventFilter`](/reference/api/eventstore/#eventfilter) #### Returns `boolean` *** ### toStored() > **toStored**(`input`, `event`, `index`, `position`, `timestamp`): `object` Defined in: [src/EventStore.ts:96](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventStore.ts#L96) The columns every adapter stores for a new event, with the defaults filled in. #### Parameters ##### input [`AppendInput`](/reference/api/eventstore/#appendinput) ##### event [`NewEvent`](/reference/api/eventstore/#newevent) ##### index `number` ##### position `number` ##### timestamp `Utc` #### Returns ##### aggregateId > `readonly` **aggregateId**: `string` = `Schema.String` ##### aggregateName > `readonly` **aggregateName**: `string` = `Schema.String` ##### causationId > `readonly` **causationId**: `string` | `null` What directly caused the command that recorded the event: a request, a command or another event. ##### correlationId > `readonly` **correlationId**: `string` | `null` Shared by everything that follows from one original request. ##### metadata > `readonly` **metadata**: `object` = `Schema.JsonObject` What the command carried beside its payload, see `Metadata`. Events from before metadata existed have `{}`. ###### Index Signature \[`key`: `string`]: `Json` ##### payload > `readonly` **payload**: `Json` = `Schema.Json` ##### position > `readonly` **position**: `number` = `Schema.Int` ##### schemaVersion > `readonly` **schemaVersion**: `number` = `Schema.Int` The version of the event’s schema the payload was written with. Events from before versions existed are `1`. ##### timestamp > `readonly` **timestamp**: `Utc` = `Schema.DateTimeUtcFromMillis` ##### type > `readonly` **type**: `string` = `Schema.String` ##### version > `readonly` **version**: `number` = `Schema.Int` # EventVersions The older shapes of one event, so that events stored long ago still reach projections and view models in today’s shape. The log is never rewritten: old payloads are upcast as they are read. ```ts const AccountOpenedV1 = Schema.Struct({ owner: Schema.String }) const Account = Aggregate.make()({ name: "account", events: AccountEvent, initial, projection, versions: { AccountOpened: EventVersions.from(AccountOpenedV1).next( AccountEvent.cases.AccountOpened, ({ owner }) => ({ owner, currency: "CHF" }) ) } }) ``` The first schema is version 1, the shape events had before the history was declared, and every `next` adds a version. The last one must be the event’s current schema, which is what new events are written with. ## Interfaces ### EventVersions Defined in: [src/EventVersions.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L44) #### Type Parameters ##### Schemas `Schemas` *extends* `ReadonlyArray`<[`VersionSchema`](/reference/api/eventversions/#versionschema)> ##### Latest `Latest` #### Properties ##### \~latest? > `readonly` `optional` **\~latest?**: `Latest` Defined in: [src/EventVersions.ts:50](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L50) The type of the last version, which an aggregate checks against its event. ##### schemas > `readonly` **schemas**: `Schemas` Defined in: [src/EventVersions.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L46) Oldest first: `schemas[0]` is version 1. ##### upcasts > `readonly` **upcasts**: readonly ((`previous`) => `unknown`)\[] Defined in: [src/EventVersions.ts:48](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L48) `upcasts[i]` turns version `i + 1` into version `i + 2`. #### Methods ##### next() > **next**<`Next`>(`schema`, `upcast`): [`EventVersions`](/reference/api/eventversions/#eventversions)\ Defined in: [src/EventVersions.ts:52](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L52) Adds a version, with the function that turns the previous one into it. ###### Type Parameters ###### Next `Next` *extends* [`VersionSchema`](/reference/api/eventversions/#versionschema) ###### Parameters ###### schema `Next` ###### upcast (`previous`) => [`Untagged`](/reference/api/eventversions/#untagged)<`Next`\[`"Type"`]> ###### Returns [`EventVersions`](/reference/api/eventversions/#eventversions)\ ## Type Aliases ### Any > **Any**<`Latest`> = [`EventVersions`](/reference/api/eventversions/#eventversions)<`ReadonlyArray`<[`VersionSchema`](/reference/api/eventversions/#versionschema)>, `Latest`> Defined in: [src/EventVersions.ts:59](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L59) Any history whose last version is `Latest`. #### Type Parameters ##### Latest `Latest` = `any` *** ### SchemaAt > **SchemaAt**<`H`, `V`> = `H` *extends* [`EventVersions`](/reference/api/eventversions/#eventversions)\ ? \[`never`, `...Schemas`]\[`V`] *extends* infer S ? `S` : `never` : `never` Defined in: [src/EventVersions.ts:62](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L62) The schema of one version, or `never` when the history has no such version. #### Type Parameters ##### H `H` *extends* [`Any`](/reference/api/eventversions/#any) ##### V `V` *extends* `number` *** ### Untagged > **Untagged**<`T`> = `T` *extends* `object` ? `Omit`<`T`, `"_tag"`> : `T` Defined in: [src/EventVersions.ts:38](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L38) What an upcast returns: the next version, without the `_tag` the event’s name already gives. #### Type Parameters ##### T `T` *** ### VersionSchema > **VersionSchema** = `Schema.Top` & `object` Defined in: [src/EventVersions.ts:32](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L32) A schema that decodes and encodes without services, as stored payloads need. #### Type Declaration ##### DecodingServices > `readonly` **DecodingServices**: `never` ##### EncodingServices > `readonly` **EncodingServices**: `never` ## Functions ### current() > **current**(`versions`): `number` Defined in: [src/EventVersions.ts:80](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L80) The version new events of this kind are written with. #### Parameters ##### versions [`Any`](/reference/api/eventversions/#any)<`any`> | `undefined` #### Returns `number` *** ### decoders() > **decoders**(`tag`, `versions`): readonly `Decoder`<`unknown`, `never`>\[] Defined in: [src/EventVersions.ts:90](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L90) Builds one decoder per old version, each going all the way to the current one. The chains are composed once with `Schema.decodeTo`, so reading an old event costs one decode however many versions it is behind. `decoders[v - 1]` decodes a JSON payload stored at version `v`. The current version has none, as the aggregate’s own schema reads it. #### Parameters ##### tag `string` ##### versions [`Any`](/reference/api/eventversions/#any) #### Returns readonly `Decoder`<`unknown`, `never`>\[] *** ### from() > **from**<`V1`>(`schema`): [`EventVersions`](/reference/api/eventversions/#eventversions)\ Defined in: [src/EventVersions.ts:76](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/EventVersions.ts#L76) Starts a history at version 1. #### Type Parameters ##### V1 `V1` *extends* [`VersionSchema`](/reference/api/eventversions/#versionschema) #### Parameters ##### schema `V1` #### Returns [`EventVersions`](/reference/api/eventversions/#eventversions)\ # Fixture Tests an aggregate in isolation: given these events, when this command runs, what happens? The moleculer-cqrs `CQRSFixture`, as a chain that runs when it is yielded: ```ts yield* Fixture.make(Todo) .givenEvents([created]) .when("deleteTodo", { message: "Done" }) .expectEvent("TodoDeleted") .inspectState((state) => assert.strictEqual(state.status, "deleted")) ``` Commands run through the real [AggregateService](/reference/api/aggregateservice/) on an in-memory store, so payload validation and event encoding are exercised too. The history can include events stored in older versions of their schema, made with `Aggregate.historical`, which then go through the same upcasting as events read from a real store. ## Classes ### ExpectationFailed Defined in: [src/Fixture.ts:31](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L31) Raised as a defect, so a command’s error channel only holds what the command can do. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new ExpectationFailed**(…`args`): [`ExpectationFailed`](/reference/api/fixture/#expectationfailed) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`ExpectationFailed`](/reference/api/fixture/#expectationfailed) ###### Inherited from `Schema.TaggedError()("ExpectationFailed", { message: Schema.String, expected: Schema.Unknown, actual: Schema.Unknown }).constructor` #### Properties ##### actual > `readonly` **actual**: `unknown` = `Schema.Unknown` Defined in: [src/Fixture.ts:34](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L34) ###### Inherited from `Schema.TaggedError()("ExpectationFailed", { message: Schema.String, expected: Schema.Unknown, actual: Schema.Unknown }).actual` ##### expected > `readonly` **expected**: `unknown` = `Schema.Unknown` Defined in: [src/Fixture.ts:33](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L33) ###### Inherited from `Schema.TaggedError()("ExpectationFailed", { message: Schema.String, expected: Schema.Unknown, actual: Schema.Unknown }).expected` ##### message > **message**: `string` = `Schema.String` Defined in: [src/Fixture.ts:32](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L32) ###### Inherited from `Schema.TaggedError()("ExpectationFailed", { message: Schema.String, expected: Schema.Unknown, actual: Schema.Unknown }).message` ## Interfaces ### Fixture Defined in: [src/Fixture.ts:97](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L97) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Properties ##### state > `readonly` **state**: `Effect`<[`State`](/reference/api/aggregate/#state-4)<`A`>, [`ReadError`](/reference/api/aggregateservice/#readerror), [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:103](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L103) The state after the given events, without running a command. #### Methods ##### checkInvariants() > **checkInvariants**(`options`): `Effect`<`void`, `never`, [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:123](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L123) Runs random sequences of commands from the given history and checks that the invariant holds after each one that succeeds. Commands that fail are part of the game and are skipped. A violation dies with [ExpectationFailed](/reference/api/fixture/#expectationfailed), holding the shortest sequence found that breaks it. ###### Parameters ###### options [`InvariantOptions`](/reference/api/fixture/#invariantoptions)<`A`> ###### Returns `Effect`<`void`, `never`, [`Context`](/reference/api/aggregate/#context)<`A`>> ##### given() > **given**(`events`): [`Fixture`](/reference/api/fixture/#fixture)<`A`> Defined in: [src/Fixture.ts:99](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L99) Replaces the history the next command runs against. ###### Parameters ###### events readonly [`GivenEvent`](/reference/api/fixture/#givenevent)<`A`>\[] ###### Returns [`Fixture`](/reference/api/fixture/#fixture)<`A`> ##### givenEvents() > **givenEvents**(`events?`, `aggregateId?`): [`Fixture`](/reference/api/fixture/#fixture)<`A`> Defined in: [src/Fixture.ts:101](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L101) Like `given`, starting from an empty history by default and optionally switching aggregate id. ###### Parameters ###### events? readonly [`GivenEvent`](/reference/api/fixture/#givenevent)<`A`>\[] ###### aggregateId? `string` ###### Returns [`Fixture`](/reference/api/fixture/#fixture)<`A`> ##### inspectState() > **inspectState**(`check`): [`Scenario`](/reference/api/fixture/#scenario)<`A`, [`ReadError`](/reference/api/aggregateservice/#readerror), [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:105](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L105) Passes the state after the given events to a callback, which throws to fail the test. ###### Parameters ###### check (`state`) => `void` ###### Returns [`Scenario`](/reference/api/fixture/#scenario)<`A`, [`ReadError`](/reference/api/aggregateservice/#readerror), [`Context`](/reference/api/aggregate/#context)<`A`>> ##### when() > **when**<`K`>(`command`, `payload`): [`Scenario`](/reference/api/fixture/#scenario)<`A`, [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:108](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L108) ###### Type Parameters ###### K `K` *extends* `string` ###### Parameters ###### command `K` ###### payload [`Payload`](/reference/api/aggregate/#payload-6)<`A`, `K`> ###### Returns [`Scenario`](/reference/api/fixture/#scenario)<`A`, [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, [`Context`](/reference/api/aggregate/#context)<`A`>> ##### whenFails() > **whenFails**<`K`>(`command`, `payload`): `Effect`<[`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, `never`, [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:113](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L113) Runs a command that is expected to fail and returns the error. Dies if it succeeds. ###### Type Parameters ###### K `K` *extends* `string` ###### Parameters ###### command `K` ###### payload [`Payload`](/reference/api/aggregate/#payload-6)<`A`, `K`> ###### Returns `Effect`<[`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, `never`, [`Context`](/reference/api/aggregate/#context)<`A`>> *** ### InvariantOptions Defined in: [src/Fixture.ts:56](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L56) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Properties ##### check? > `readonly` `optional` **check?**: `CheckOptions` Defined in: [src/Fixture.ts:71](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L71) Passed on to `Arbitrary.checkEffect`, for the number of runs or a seed. ##### commands? > `readonly` `optional` **commands?**: { readonly \[K in string]?: false | Arbitrary\> } Defined in: [src/Fixture.ts:63](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L63) How to generate each command’s payload. By default it comes from the payload schema. `false` leaves a command out. ##### invariant > `readonly` **invariant**: (`state`) => `boolean` Defined in: [src/Fixture.ts:58](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L58) Must hold for the given state and after every command that succeeds. ###### Parameters ###### state [`State`](/reference/api/aggregate/#state-4)<`A`> ###### Returns `boolean` ##### maxCommands? > `readonly` `optional` **maxCommands?**: `number` Defined in: [src/Fixture.ts:69](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L69) The longest sequence of commands to try. Defaults to 20. *** ### Outcome Defined in: [src/Fixture.ts:37](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L37) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Properties ##### events > `readonly` **events**: readonly [`Event`](/reference/api/aggregate/#event-2)<`A`>\[] Defined in: [src/Fixture.ts:38](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L38) ##### fixture > `readonly` **fixture**: [`Fixture`](/reference/api/fixture/#fixture)<`A`> Defined in: [src/Fixture.ts:42](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L42) Continues from here, with this outcome’s events as the new history. ##### state > `readonly` **state**: [`State`](/reference/api/aggregate/#state-4)<`A`> Defined in: [src/Fixture.ts:39](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L39) ##### version > `readonly` **version**: `number` Defined in: [src/Fixture.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L40) *** ### Scenario Defined in: [src/Fixture.ts:78](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L78) The steps so far. Nothing runs until it is yielded or run as an Effect, which produces the [Outcome](/reference/api/fixture/#outcome) of the last command. #### Extends * `Effect`<[`Outcome`](/reference/api/fixture/#outcome)<`A`>, `E`, `R`> #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ##### E `E` ##### R `R` #### Methods ##### expectEvent() > **expectEvent**(`event`): [`Scenario`](/reference/api/fixture/#scenario)<`A`, `E`, `R`> Defined in: [src/Fixture.ts:90](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L90) Expects the last command to have emitted exactly this event. ###### Parameters ###### event [`ExpectedEvent`](/reference/api/fixture/#expectedevent)<`A`> ###### Returns [`Scenario`](/reference/api/fixture/#scenario)<`A`, `E`, `R`> ##### expectEvents() > **expectEvents**(`events`): [`Scenario`](/reference/api/fixture/#scenario)<`A`, `E`, `R`> Defined in: [src/Fixture.ts:92](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L92) Expects the last command to have emitted exactly these events, in order. ###### Parameters ###### events readonly [`ExpectedEvent`](/reference/api/fixture/#expectedevent)<`A`>\[] ###### Returns [`Scenario`](/reference/api/fixture/#scenario)<`A`, `E`, `R`> ##### inspectState() > **inspectState**(`check`): [`Scenario`](/reference/api/fixture/#scenario)<`A`, `E`, `R`> Defined in: [src/Fixture.ts:94](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L94) Passes the current state to a callback, which throws to fail the test. ###### Parameters ###### check (`state`) => `void` ###### Returns [`Scenario`](/reference/api/fixture/#scenario)<`A`, `E`, `R`> ##### when() > **when**<`K`>(`command`, `payload`): [`Scenario`](/reference/api/fixture/#scenario)<`A`, [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | `E` | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, `R` | [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:80](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L80) Runs another command on top of the previous one. ###### Type Parameters ###### K `K` *extends* `string` ###### Parameters ###### command `K` ###### payload [`Payload`](/reference/api/aggregate/#payload-6)<`A`, `K`> ###### Returns [`Scenario`](/reference/api/fixture/#scenario)<`A`, [`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | `E` | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, `R` | [`Context`](/reference/api/aggregate/#context)<`A`>> ##### whenFails() > **whenFails**<`K`>(`command`, `payload`): `Effect`<[`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, `E`, `R` | [`Context`](/reference/api/aggregate/#context)<`A`>> Defined in: [src/Fixture.ts:85](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L85) Runs a command that is expected to fail and returns the error. Dies if it succeeds. ###### Type Parameters ###### K `K` *extends* `string` ###### Parameters ###### command `K` ###### payload [`Payload`](/reference/api/aggregate/#payload-6)<`A`, `K`> ###### Returns `Effect`<[`CommandFailure`](/reference/api/aggregateservice/#commandfailure) | [`CommandError`](/reference/api/aggregate/#commanderror)<`A`, `K`>, `E`, `R` | [`Context`](/reference/api/aggregate/#context)<`A`>> ## Type Aliases ### ExpectedEvent > **ExpectedEvent**<`A`> = [`Event`](/reference/api/aggregate/#event-2)<`A`> | [`Event`](/reference/api/aggregate/#event-2)<`A`>\[`"_tag"`] Defined in: [src/Fixture.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L46) An event to compare with, or only its tag when the rest doesn’t matter. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### GivenEvent > **GivenEvent**<`A`> = [`Event`](/reference/api/aggregate/#event-2)<`A`> | [`Historical`](/reference/api/aggregate/#historical) Defined in: [src/Fixture.ts:49](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L49) An event in today’s shape, or one stored in an older version of its schema. #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) *** ### Step > **Step**<`A`> = `{ readonly [K in CommandName]: { command: K; payload: Payload } }`\[[`CommandName`](/reference/api/aggregate/#commandname-1)<`A`>] Defined in: [src/Fixture.ts:52](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L52) One generated step of [Fixture.checkInvariants](/reference/api/fixture/#fixture). #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) ## Functions ### make() > **make**<`A`>(`aggregate`, `options?`): [`Fixture`](/reference/api/fixture/#fixture)<`A`> Defined in: [src/Fixture.ts:188](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Fixture.ts#L188) #### Type Parameters ##### A `A` *extends* [`Any`](/reference/api/aggregate/#any) #### Parameters ##### aggregate `A` ##### options? ###### aggregateId? `string` ###### events? readonly [`GivenEvent`](/reference/api/fixture/#givenevent)<`A`>\[] #### Returns [`Fixture`](/reference/api/fixture/#fixture)<`A`> # Ids The ids the framework makes up: the correlation of a command run outside any causation, and of a saga started outside one. They are random UUIDs, and a test that compares them provides a generator of its own: ```ts program.pipe(Effect.provideService(Ids.IdGenerator, Ids.sequential("id"))) ``` ## Variables ### IdGenerator > `const` **IdGenerator**: `Reference`<() => `string`> Defined in: [src/Ids.ts:15](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Ids.ts#L15) Makes a new id each time it is called. Defaults to `crypto.randomUUID()`. *** ### next > `const` **next**: `Effect.Effect`<`string`> Defined in: [src/Ids.ts:20](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Ids.ts#L20) A new id from the current generator. ## Functions ### sequential() > **sequential**(`prefix?`): () => `string` Defined in: [src/Ids.ts:23](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Ids.ts#L23) A generator of `id-1`, `id-2` and so on, for tests. Each call starts its own count. #### Parameters ##### prefix? `string` = `"id"` #### Returns () => `string` # Leases Decides which process runs each view model. Replicas that share an event store would otherwise all run the same view models and race on their checkpoints. `ViewModel.layer` takes a lease on each view model before running it: the process that holds the lease runs it, and the others wait as standbys until it is given up. ## Classes ### Leases Defined in: [src/Leases.ts:21](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L21) #### Extends * `Shape`<`"effect-cqrs/Leases"`, { `acquire`(`name`): `Effect`<[`Lease`](/reference/api/leases/#lease), `never`, `Scope`>; }, `this`> #### Constructors ##### Constructor > **new Leases**(`_`): [`Leases`](/reference/api/leases/#leases) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`Leases`](/reference/api/leases/#leases) ###### Inherited from ``Context.Service }>()("effect-cqrs/Leases").constructor`` ## Interfaces ### Lease Defined in: [src/Leases.ts:14](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L14) #### Properties ##### changes > `readonly` **changes**: `Stream`<`boolean`> Defined in: [src/Leases.ts:18](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L18) Emits whether the lease is held, then every change. ##### held > `readonly` **held**: `Effect`<`boolean`> Defined in: [src/Leases.ts:16](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L16) Whether this process holds the lease now. *** ### PostgresOptions Defined in: [src/Leases.ts:39](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L39) #### Properties ##### interval? > `readonly` `optional` **interval?**: `Input` Defined in: [src/Leases.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L44) How often a standby tries to take a lease over, and the holder checks that its connection is alive. Defaults to 5 seconds. ##### prefix? > `readonly` `optional` **prefix?**: `string` Defined in: [src/Leases.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L46) Prefixes the names, so that several applications can share a database. Defaults to `effect-cqrs/`. ## Variables ### layerMemory > `const` **layerMemory**: `Layer.Layer`<[`Leases`](/reference/api/leases/#leases)> Defined in: [src/Leases.ts:37](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L37) *** ### makePostgres > `const` **makePostgres**: (…`args`) => `Effect`<{ `acquire`(`name`): `Effect`<[`Lease`](/reference/api/leases/#lease), `never`, `Scope`>; }, `never`, `Scope` | `SqlClient`> Defined in: [src/Leases.ts:70](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L70) Leases on PostgreSQL advisory locks, `pg_try_advisory_lock` on a hash of the name. The locks belong to one connection that the service reserves from the client’s pool for as long as anything competes, so a process holds them until it gives them up or its connection closes: when it dies, the database releases them and a standby takes over at its next attempt. A holder whose connection fails stops at its next check, up to `interval` later, while a standby may already have started: handlers can run in two processes for that long. #### Parameters ##### args …\[`options?`: [`PostgresOptions`](/reference/api/leases/#postgresoptions)] #### Returns `Effect`<{ `acquire`(`name`): `Effect`<[`Lease`](/reference/api/leases/#lease), `never`, `Scope`>; }, `never`, `Scope` | `SqlClient`> *** ### memory > `const` **memory**: [`Leases`](/reference/api/leases/#leases)\[`"Service"`] Defined in: [src/Leases.ts:35](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L35) Leases that are always held. It is the default of `ViewModel.layer`, for a single process or for view models that keep their data in that process. ## Functions ### layerPostgres() > **layerPostgres**(`options?`): `Layer`<[`Leases`](/reference/api/leases/#leases), `never`, `SqlClient`> Defined in: [src/Leases.ts:156](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Leases.ts#L156) Leases on PostgreSQL advisory locks, see [makePostgres](/reference/api/leases/#makepostgres). #### Parameters ##### options? [`PostgresOptions`](/reference/api/leases/#postgresoptions) #### Returns `Layer`<[`Leases`](/reference/api/leases/#leases), `never`, `SqlClient`> # Metadata Metadata: what a command carries beside its payload that the domain doesn’t decide on, such as the version of the software that ran it, the user who sent it or the address it came from. It is a JSON object recorded on every event the command writes, and projections, view models and `history` can read it. Like `Causation`, it travels in the fiber’s context rather than in payloads. A command records, key by key, from the lowest precedence: 1. the metadata where its aggregate service was built, such as the software version provided once for the whole application with [layer](/reference/api/metadata/#layer); 2. the metadata where it runs, added with [withMetadata](/reference/api/metadata/#withmetadata); 3. the `metadata` of a `dispatch` envelope. A view model or saga reacting to an event doesn’t inherit that event’s metadata: its commands record their own. The `causationId` leads back to the event, and from there to its metadata. ## Type Aliases ### Metadata > **Metadata** = `Schema.JsonObject` Defined in: [src/Metadata.ts:24](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Metadata.ts#L24) A JSON object. ## Variables ### CurrentMetadata > `const` **CurrentMetadata**: `Reference`<`JsonObject`> Defined in: [src/Metadata.ts:29](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Metadata.ts#L29) The metadata commands run in this fiber record, `{}` outside any. *** ### Metadata > **Metadata**: `Codec`<`JsonObject`, `JsonObject`, `never`, `never`> Defined in: [src/Metadata.ts:24](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Metadata.ts#L24) *** ### withMetadata > `const` **withMetadata**: {(`metadata`): <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`>; <`A`, `E`, `R`>(`effect`, `metadata`): `Effect`<`A`, `E`, `R`>; } Defined in: [src/Metadata.ts:34](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Metadata.ts#L34) Runs an effect whose commands record this metadata on top of the current one. #### Call Signature > (`metadata`): <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`> ##### Parameters ###### metadata `JsonObject` ##### Returns <`A`, `E`, `R`>(`effect`) => `Effect`<`A`, `E`, `R`> #### Call Signature > <`A`, `E`, `R`>(`effect`, `metadata`): `Effect`<`A`, `E`, `R`> ##### Type Parameters ###### A `A` ###### E `E` ###### R `R` ##### Parameters ###### effect `Effect`<`A`, `E`, `R`> ###### metadata `JsonObject` ##### Returns `Effect`<`A`, `E`, `R`> ## Functions ### layer() > **layer**(`metadata`): `Layer`<`never`> Defined in: [src/Metadata.ts:54](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Metadata.ts#L54) Provides metadata to everything built on top of this layer, for values that hold for the whole process such as the software version: ```ts AppLayer.pipe(Layer.provide(Metadata.layer({ appVersion: "1.4.0" }))) ``` Aggregate services remember it, so it is recorded even by commands run where the layer isn’t visible. #### Parameters ##### metadata `JsonObject` #### Returns `Layer`<`never`> # Outbox Publishes events to other systems, such as Kafka, NATS or SQS, as integration events. ```ts const TodoOutbox = Outbox.make({ name: "todo-outbox", aggregates: [Todo], messages: { TodoCreated: ({ title }) => ({ type: "todo.created.v1", payload: { title } }) }, publish: (messages) => producer.send(messages) }) const Publishing = Outbox.layer([TodoOutbox]) ``` An outbox is a view model: it reads the store from its checkpoint, so it publishes what was stored while the process was down, and nothing is lost between storing an event and publishing it. Delivery is at least once. After a crash, events published since the last checkpoint are published again, so consumers deduplicate on [Message.id](/reference/api/outbox/#message), which stays the same. Messages are published in batches of up to `batchSize`. A batch goes out when it is full or `linger` after its first message, and the checkpoint never moves past a message that wasn’t published. A failed batch is retried, in order, until it succeeds. ## Classes ### OutboxError Defined in: [src/Outbox.ts:83](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L83) Publishing failed, which the outbox retries. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new OutboxError**(…`args`): [`OutboxError`](/reference/api/outbox/#outboxerror) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`OutboxError`](/reference/api/outbox/#outboxerror) ###### Inherited from `Schema.TaggedError()("OutboxError", { outbox: Schema.String, cause: Schema.Defect() }).constructor` #### Properties ##### cause > **cause**: `unknown` Defined in: [src/Outbox.ts:85](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L85) ###### Inherited from `Schema.TaggedError()("OutboxError", { outbox: Schema.String, cause: Schema.Defect() }).cause` ##### outbox > `readonly` **outbox**: `string` = `Schema.String` Defined in: [src/Outbox.ts:84](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L84) ###### Inherited from `Schema.TaggedError()("OutboxError", { outbox: Schema.String, cause: Schema.Defect() }).outbox` *** ### Outboxes Defined in: [src/Outbox.ts:142](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L142) #### Extends * `Shape`<`"effect-cqrs/Outboxes"`, { `status`: `Effect`\; `published`(`position?`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) | [`OutboxError`](/reference/api/outbox/#outboxerror)>; }, `this`> #### Constructors ##### Constructor > **new Outboxes**(`_`): [`Outboxes`](/reference/api/outbox/#outboxes) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`Outboxes`](/reference/api/outbox/#outboxes) ###### Inherited from Context.Service\ /\*\* Where each outbox is, and the failure it is retrying, see \`ViewModel.Status\`. \*/ readonly status: Effect.Effect\> }>()(“effect-cqrs/Outboxes”).constructor ## Interfaces ### Mapped Defined in: [src/Outbox.ts:57](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L57) What a mapping makes of an event. `undefined` publishes nothing for it. #### Properties ##### payload > `readonly` **payload**: `unknown` Defined in: [src/Outbox.ts:59](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L59) ##### type > `readonly` **type**: `string` Defined in: [src/Outbox.ts:58](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L58) *** ### Message Defined in: [src/Outbox.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L40) An integration event, what a publisher sends. #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/Outbox.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L46) ##### aggregateName > `readonly` **aggregateName**: `string` Defined in: [src/Outbox.ts:45](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L45) ##### causationId > `readonly` **causationId**: `string` | `null` Defined in: [src/Outbox.ts:53](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L53) ##### correlationId > `readonly` **correlationId**: `string` | `null` Defined in: [src/Outbox.ts:52](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L52) ##### id > `readonly` **id**: `string` Defined in: [src/Outbox.ts:42](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L42) The id of the event it comes from, the same however often it is published: `todo/a@1`. ##### payload > `readonly` **payload**: `unknown` Defined in: [src/Outbox.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L44) ##### position > `readonly` **position**: `number` Defined in: [src/Outbox.ts:50](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L50) The position of the event in the store. ##### timestamp > `readonly` **timestamp**: `Utc` Defined in: [src/Outbox.ts:51](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L51) ##### type > `readonly` **type**: `string` Defined in: [src/Outbox.ts:43](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L43) ##### version > `readonly` **version**: `number` Defined in: [src/Outbox.ts:48](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L48) The version of the event in its stream. *** ### Outbox Defined in: [src/Outbox.ts:69](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L69) #### Type Parameters ##### Name `Name` *extends* `string` ##### E `E` ##### R `R` #### Properties ##### aggregates > `readonly` **aggregates**: readonly [`Any`](/reference/api/aggregate/#any)\[] Defined in: [src/Outbox.ts:71](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L71) ##### batchSize > `readonly` **batchSize**: `number` Defined in: [src/Outbox.ts:74](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L74) ##### linger > `readonly` **linger**: `Duration` Defined in: [src/Outbox.ts:75](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L75) ##### mappings > `readonly` **mappings**: `Record`<`string`, (`event`, `meta`) => [`Mapped`](/reference/api/outbox/#mapped) | `undefined`> Defined in: [src/Outbox.ts:72](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L72) ##### name > `readonly` **name**: `Name` Defined in: [src/Outbox.ts:70](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L70) ##### publish > `readonly` **publish**: (`messages`) => `Effect`<`void`, `E`, `R`> Defined in: [src/Outbox.ts:73](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L73) ###### Parameters ###### messages readonly [`Message`](/reference/api/outbox/#message)\[] ###### Returns `Effect`<`void`, `E`, `R`> ## Type Aliases ### Any > **Any** = [`Outbox`](/reference/api/outbox/#outbox-1)<`string`, `any`, `any`> Defined in: [src/Outbox.ts:78](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L78) *** ### Context > **Context**<`O`> = `O` *extends* [`Outbox`](/reference/api/outbox/#outbox-1)<`any`, `any`, infer R> ? `R` : `never` Defined in: [src/Outbox.ts:80](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L80) #### Type Parameters ##### O `O` *extends* [`Any`](/reference/api/outbox/#any) *** ### Mappings > **Mappings**<`Event`> = { readonly \[Tag in Event\[“\_tag”]]?: (event: Extract\, meta: EventMeta) => Mapped | undefined } Defined in: [src/Outbox.ts:62](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L62) #### Type Parameters ##### Event `Event` *extends* [`Tagged`](/reference/api/aggregate/#tagged) ## Functions ### layer() > **layer**<`Os`>(`outboxes`): `Layer`<[`Outboxes`](/reference/api/outbox/#outboxes), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), [`EventStore`](/reference/api/eventstore/#eventstore) | [`Checkpoints`](/reference/api/checkpoints/#checkpoints) | [`Context`](/reference/api/outbox/#context)<`Os`\[`number`]>> Defined in: [src/Outbox.ts:233](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L233) Runs the outboxes in the background for as long as the layer lives. Each one keeps a checkpoint under its name in `Checkpoints`, which should be durable: with an in-memory checkpoint an outbox publishes everything again on restart. #### Type Parameters ##### Os `Os` *extends* readonly [`Any`](/reference/api/outbox/#any)\[] #### Parameters ##### outboxes `Os` #### Returns `Layer`<[`Outboxes`](/reference/api/outbox/#outboxes), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), [`EventStore`](/reference/api/eventstore/#eventstore) | [`Checkpoints`](/reference/api/checkpoints/#checkpoints) | [`Context`](/reference/api/outbox/#context)<`Os`\[`number`]>> *** ### make() > **make**<`Name`, `Aggregates`, `E`, `R`>(`options`): [`Outbox`](/reference/api/outbox/#outbox-1)<`Name`, `E`, `R`> Defined in: [src/Outbox.ts:99](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Outbox.ts#L99) Defines an outbox. `messages` maps events, by tag, to integration events, and only those are published. Without it every event of the aggregates is published, typed `.`, with its JSON form as payload. Mapping is where domain events become a contract with other teams, and where personal data is left out: events reach it decrypted, see `PersonalData`. #### Type Parameters ##### Name `Name` *extends* `string` ##### Aggregates `Aggregates` *extends* readonly [`Any`](/reference/api/aggregate/#any)\[] ##### E `E` = `never` ##### R `R` = `never` #### Parameters ##### options ###### aggregates `Aggregates` ###### batchSize? `number` The most messages in one call to `publish`. Defaults to 100. ###### linger? `Input` How long a batch waits for more messages before it is published. Defaults to 10 milliseconds. ###### messages? [`Mappings`](/reference/api/outbox/#mappings-1)<[`Event`](/reference/api/aggregate/#event-2)<`Aggregates`\[`number`]>> ###### name `Name` ###### publish (`messages`) => `Effect`<`void`, `E`, `R`> #### Returns [`Outbox`](/reference/api/outbox/#outbox-1)<`Name`, `E`, `R`> # ParkedEvents Events a view model gave up on, so the rest of the log could move past them. A view model with the `ViewModel.park` failure policy records an event here once it has failed often enough, skips it from then on, and handles it again when `ViewModels.retryParked` asks it to. ## Classes ### ParkedEvents Defined in: [src/ParkedEvents.ts:30](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L30) #### Extends * `Shape`<`"effect-cqrs/ParkedEvents"`, { `list`(`viewModel?`): `Effect`\; `park`(`event`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `remove`(`viewModel`, `position`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, `this`> #### Constructors ##### Constructor > **new ParkedEvents**(`_`): [`ParkedEvents`](/reference/api/parkedevents/#parkedevents) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`ParkedEvents`](/reference/api/parkedevents/#parkedevents) ###### Inherited from `Context.Service /** The parked events of one view model, or of all of them, by view model and position. */ list(viewModel?: string): Effect.Effect, EventStoreError> /** Forgets an event, once it has been handled. */ remove(viewModel: string, position: number): Effect.Effect }>()("effect-cqrs/ParkedEvents").constructor` ## Interfaces ### SqlOptions Defined in: [src/ParkedEvents.ts:61](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L61) #### Properties ##### migrate? > `readonly` `optional` **migrate?**: `boolean` Defined in: [src/ParkedEvents.ts:68](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L68) Creates the table on startup when it is missing. Defaults to `true`. Turn it off where the schema is managed elsewhere, from [migrations](/reference/api/parkedevents/#migrations). ##### table? > `readonly` `optional` **table?**: `string` Defined in: [src/ParkedEvents.ts:63](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L63) Defaults to `cqrs_parked_events`. ## Type Aliases ### ParkedEvent > **ParkedEvent** = *typeof* `ParkedEvent.Type` Defined in: [src/ParkedEvents.ts:15](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L15) ## Variables ### layerMemory > `const` **layerMemory**: `Layer.Layer`<[`ParkedEvents`](/reference/api/parkedevents/#parkedevents)> Defined in: [src/ParkedEvents.ts:59](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L59) *** ### makeMemory > `const` **makeMemory**: `Effect.Effect`<[`ParkedEvents`](/reference/api/parkedevents/#parkedevents)\[`"Service"`]> Defined in: [src/ParkedEvents.ts:45](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L45) Parked events in process memory. They do not survive a restart. *** ### makeSql > `const` **makeSql**: (…`args`) => `Effect`<{ `list`(`viewModel?`): `Effect`\; `park`(`event`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `remove`(`viewModel`, `position`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> Defined in: [src/ParkedEvents.ts:97](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L97) Parked events in a SQL table, on SQLite or PostgreSQL. #### Parameters ##### args …\[`options?`: [`SqlOptions`](/reference/api/parkedevents/#sqloptions)] #### Returns `Effect`<{ `list`(`viewModel?`): `Effect`\; `park`(`event`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `remove`(`viewModel`, `position`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> *** ### ParkedEvent > `const` **ParkedEvent**: `Struct`<{ `attempts`: `Int`; `error`: `String`; `eventId`: `String`; `parkedAt`: `DateTimeUtcFromMillis`; `position`: `Int`; `viewModel`: `String`; }> Defined in: [src/ParkedEvents.ts:15](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L15) ## Functions ### layerSql() > **layerSql**(`options?`): `Layer`<[`ParkedEvents`](/reference/api/parkedevents/#parkedevents), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> Defined in: [src/ParkedEvents.ts:152](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L152) Parked events in a SQL table, see [makeSql](/reference/api/parkedevents/#makesql). Give it the client of the read models, and a retried event leaves the table in the same transaction as its handler’s writes when the view model has a `transaction`. #### Parameters ##### options? [`SqlOptions`](/reference/api/parkedevents/#sqloptions) #### Returns `Layer`<[`ParkedEvents`](/reference/api/parkedevents/#parkedevents), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> *** ### migrations() > **migrations**(`options?`): readonly [`Migration`](/reference/api/eventstore/#migration)\[] Defined in: [src/ParkedEvents.ts:77](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ParkedEvents.ts#L77) The migrations of the table, the same on SQLite and PostgreSQL. They are what [layerSql](/reference/api/parkedevents/#layersql) runs unless `migrate` is `false`, and are safe to run again. #### Parameters ##### options? `Pick`<[`SqlOptions`](/reference/api/parkedevents/#sqloptions), `"table"`> #### Returns readonly [`Migration`](/reference/api/eventstore/#migration)\[] # PersonalData Crypto-shredding for personal data. The log is append-only, so the personal fields of an event are encrypted with a key that belongs to the person they describe, the subject. Forgetting a subject destroys that key: the events stay where they are and keep their amounts, dates and ids, while their personal fields read as the replacement each field declares. ```ts const AccountEvent = Schema.TaggedUnion({ AccountOpened: { owner: PersonalData.field(Schema.String, { redacted: "[forgotten]" }), currency: Schema.Literals(["CHF", "EUR"]) }, TransferSent: { to: Schema.String, // Encrypted with the key of the receiver rather than the account holder. beneficiary: PersonalData.field(Schema.String, { redacted: "[forgotten]", subject: "to" }), amount: Schema.Number } }) ``` A field belongs to the aggregate id of its stream unless it names a sibling property that holds its subject. Schemas stay plain: encryption happens where `AggregateService` writes and reads the store, which then needs [Keys](/reference/api/personaldata/#keys). Each subject has its own data key. The key store keeps it only wrapped by a [MasterKey](/reference/api/personaldata/#masterkey), which can live in a KMS, so the master key can change without touching events. [forget](/reference/api/personaldata/#forget-1) destroys the data key, refuses keys for that subject from then on and records the request in the log. Encryption is AES-256-GCM through a [Cipher](/reference/api/personaldata/#cipher-1): WebCrypto by default, which also runs in browsers, or `node:crypto` from `PersonalDataNode`. ## Classes ### Keys Defined in: [src/PersonalData.ts:455](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L455) The data keys of subjects, created on first use and cached for a while. #### Extends * `Shape`<`"effect-cqrs/PersonalData/Keys"`, { `cipher`: [`Cipher`](/reference/api/personaldata/#cipher); `decryptionKey`(`subject`, `keyId`): `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `encryptionKey`(`subject`): `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)>; `forget`(`subject`): `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `isForgotten`(`subject`): `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `rewrap`(`options?`): `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; }, `this`> #### Constructors ##### Constructor > **new Keys**(`_`): [`Keys`](/reference/api/personaldata/#keys) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`Keys`](/reference/api/personaldata/#keys) ###### Inherited from Context.Service\ /\*\* \* The key to decrypt with, \`None\` when the subject was forgotten. A subject \* without any key fails: losing the key store must not pass for forgetting. \*/ decryptionKey(subject: string, keyId: string): Effect.Effect\, PersonalDataError> isForgotten(subject: string): Effect.Effect\ /\*\* Destroys the key of a subject. Prefer {@link forget}, which also records it in the log. \*/ forget(subject: string): Effect.Effect\ /\*\* See {@link rewrap}. \*/ rewrap(options?: RewrapOptions): Effect.Effect\ }>()(“effect-cqrs/PersonalData/Keys”).constructor *** ### MasterKey Defined in: [src/PersonalData.ts:376](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L376) Wraps the data keys of subjects, so the key store only ever holds them encrypted. Implement it on top of a KMS to keep the master key out of the process, or use [layerMasterKey](/reference/api/personaldata/#layermasterkey) with a key from configuration. #### Extends * `Shape`<`"effect-cqrs/PersonalData/MasterKey"`, { `keyId`: `string`; `unwrap`(`wrapped`, `context`): `Effect`<`Uint8Array`<`ArrayBuffer`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `wrap`(`dataKey`, `context`): `Effect`<`Uint8Array`<`ArrayBuffer`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; }, `this`> #### Constructors ##### Constructor > **new MasterKey**(`_`): [`MasterKey`](/reference/api/personaldata/#masterkey) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`MasterKey`](/reference/api/personaldata/#masterkey) ###### Inherited from ``Context.Service, context: { readonly subject: string; readonly keyId: string } ): Effect.Effect, PersonalDataError> /** Unwraps a key wrapped by `masterKeyId`, which may be an earlier master key. */ unwrap( wrapped: Uint8Array, context: { readonly subject: string; readonly keyId: string; readonly masterKeyId: string } ): Effect.Effect, PersonalDataError> }>()("effect-cqrs/PersonalData/MasterKey").constructor`` *** ### PersonalDataError Defined in: [src/PersonalData.ts:68](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L68) A key that couldn’t be read, created or used, or a field that didn’t decrypt. #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new PersonalDataError**(…`args`): [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) ###### Inherited from `Schema.TaggedError()("PersonalDataError", { operation: Schema.String, subject: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Defect()) }).constructor` #### Properties ##### cause? > `optional` **cause?**: `unknown` Defined in: [src/PersonalData.ts:72](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L72) ###### Inherited from `Schema.TaggedError()("PersonalDataError", { operation: Schema.String, subject: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Defect()) }).cause` ##### message > **message**: `string` = `Schema.String` Defined in: [src/PersonalData.ts:71](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L71) ###### Inherited from `Schema.TaggedError()("PersonalDataError", { operation: Schema.String, subject: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Defect()) }).message` ##### operation > `readonly` **operation**: `string` = `Schema.String` Defined in: [src/PersonalData.ts:69](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L69) ###### Inherited from `Schema.TaggedError()("PersonalDataError", { operation: Schema.String, subject: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Defect()) }).operation` ##### subject > `readonly` **subject**: `string` = `Schema.String` Defined in: [src/PersonalData.ts:70](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L70) ###### Inherited from `Schema.TaggedError()("PersonalDataError", { operation: Schema.String, subject: Schema.String, message: Schema.String, cause: Schema.optional(Schema.Defect()) }).subject` *** ### SubjectForgotten Defined in: [src/PersonalData.ts:63](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L63) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new SubjectForgotten**(…`args`): [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten) ###### Inherited from `Schema.TaggedError()("SubjectForgotten", { subject: Schema.String }).constructor` #### Properties ##### subject > `readonly` **subject**: `string` = `Schema.String` Defined in: [src/PersonalData.ts:64](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L64) ###### Inherited from `Schema.TaggedError()("SubjectForgotten", { subject: Schema.String }).subject` ## Interfaces ### Backend Defined in: [src/PersonalData.ts:402](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L402) Where keys are kept. Keep it apart from the events, with backups that expire. #### Properties ##### afterCommit? > `readonly` `optional` **afterCommit?**: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`> Defined in: [src/PersonalData.ts:420](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L420) Runs an effect when the caller’s transaction commits, or at once outside one. ###### Type Parameters ###### R `R` ###### Parameters ###### effect `Effect`<`void`, `never`, `R`> ###### Returns `Effect`<`void`, `never`, `R`> ##### create > `readonly` **create**: (`subject`, `key`) => `Effect`<[`KeyRecord`](/reference/api/personaldata/#keyrecord), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/PersonalData.ts:408](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L408) Stores a key unless the subject already has a record, and returns the record the subject has afterwards: a racing writer’s key, or `Forgotten`. ###### Parameters ###### subject `string` ###### key ###### \_tag `"Active"` ###### keyId `string` ###### masterKeyId `string` ###### wrapped `Uint8Array`<`ArrayBuffer`> The data key, wrapped by the master key. ###### Returns `Effect`<[`KeyRecord`](/reference/api/personaldata/#keyrecord), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ##### forget > `readonly` **forget**: (`subject`, `forgottenAt`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/PersonalData.ts:413](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L413) Deletes the key and records that the subject is forgotten, in one step. ###### Parameters ###### subject `string` ###### forgottenAt `number` ###### Returns `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ##### get > `readonly` **get**: (`subject`) => `Effect`<`Option`<[`KeyRecord`](/reference/api/personaldata/#keyrecord)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/PersonalData.ts:403](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L403) ###### Parameters ###### subject `string` ###### Returns `Effect`<`Option`<[`KeyRecord`](/reference/api/personaldata/#keyrecord)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ##### inTransaction? > `readonly` `optional` **inTransaction?**: `Effect`<`boolean`, `never`, `never`> Defined in: [src/PersonalData.ts:418](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L418) Whether a call now joins a transaction of the caller, whose writes a rollback can still undo. What is read or created there is cached only once it commits. ##### replaceWrapped? > `readonly` `optional` **replaceWrapped?**: (`subject`, `from`, `to`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/PersonalData.ts:440](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L440) Stores a key wrapped anew, if the subject still has the key `from`: not forgotten, nor wrapped again meanwhile. Returns whether it did. ###### Parameters ###### subject `string` ###### from ###### keyId `string` ###### masterKeyId `string` ###### to ###### masterKeyId `string` ###### wrapped `Uint8Array`<`ArrayBuffer`> ###### Returns `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ##### wrappedByOthers? > `readonly` `optional` **wrappedByOthers?**: (`masterKeyId`, `afterSubject`, `limit`) => `Effect`\ Defined in: [src/PersonalData.ts:426](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L426) Up to `limit` subjects after `afterSubject`, in order, whose key is wrapped by another master key than `masterKeyId`. With [Backend.replaceWrapped](/reference/api/personaldata/#backend), what [rewrap](/reference/api/personaldata/#rewrap) needs. ###### Parameters ###### masterKeyId `string` ###### afterSubject `string` ###### limit `number` ###### Returns `Effect`\ *** ### Cipher Defined in: [src/PersonalData.ts:296](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L296) AES-256-GCM, which encrypts personal fields and wraps data keys. What it produces is a random IV of [ivLength](/reference/api/personaldata/#ivlength) bytes, the ciphertext and a tag of [tagLength](/reference/api/personaldata/#taglength) bytes, so data written by one implementation reads with any other. The default, [webCrypto](/reference/api/personaldata/#webcrypto), works in Node and in browsers but is asynchronous. `PersonalDataNode.layerCipher` runs on `node:crypto` synchronously, which costs a fraction per field. #### Properties ##### asynchronous > `readonly` **asynchronous**: `boolean` Defined in: [src/PersonalData.ts:301](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L301) Whether an operation waits on something outside the fiber, as WebCrypto does. Events of a stream then decrypt several at a time. #### Methods ##### decrypt() > **decrypt**(`key`, `sealed`, `additionalData`): `Effect`<`Uint8Array`<`ArrayBuffer`>, `UnknownError`> Defined in: [src/PersonalData.ts:310](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L310) Fails when the data was altered, or belongs to another key or other additional data. ###### Parameters ###### key [`CipherKey`](/reference/api/personaldata/#cipherkey) ###### sealed `Uint8Array`<`ArrayBuffer`> ###### additionalData `Uint8Array`<`ArrayBuffer`> ###### Returns `Effect`<`Uint8Array`<`ArrayBuffer`>, `UnknownError`> ##### encrypt() > **encrypt**(`key`, `plaintext`, `additionalData`): `Effect`<`Uint8Array`<`ArrayBuffer`>, `UnknownError`> Defined in: [src/PersonalData.ts:304](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L304) ###### Parameters ###### key [`CipherKey`](/reference/api/personaldata/#cipherkey) ###### plaintext `Uint8Array`<`ArrayBuffer`> ###### additionalData `Uint8Array`<`ArrayBuffer`> ###### Returns `Effect`<`Uint8Array`<`ArrayBuffer`>, `UnknownError`> ##### importKey() > **importKey**(`bytes`): `Effect`<[`CipherKey`](/reference/api/personaldata/#cipherkey), `UnknownError`> Defined in: [src/PersonalData.ts:303](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L303) Imports a key of 32 bytes. The caller zeroes the bytes afterwards. ###### Parameters ###### bytes `Uint8Array`<`ArrayBuffer`> ###### Returns `Effect`<[`CipherKey`](/reference/api/personaldata/#cipherkey), `UnknownError`> *** ### CipherKey Defined in: [src/PersonalData.ts:282](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L282) A key imported by a [Cipher](/reference/api/personaldata/#cipher-1), which only that cipher can use. #### Properties ##### \[CipherKeyTypeId] > `readonly` **\[CipherKeyTypeId]**: *typeof* `CipherKeyTypeId` Defined in: [src/PersonalData.ts:283](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L283) *** ### DataKey Defined in: [src/PersonalData.ts:449](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L449) #### Properties ##### key > `readonly` **key**: [`CipherKey`](/reference/api/personaldata/#cipherkey) Defined in: [src/PersonalData.ts:451](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L451) ##### keyId > `readonly` **keyId**: `string` Defined in: [src/PersonalData.ts:450](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L450) *** ### Field Defined in: [src/PersonalData.ts:113](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L113) Where a personal field sits in an encoded payload, and how to find its subject. #### Properties ##### path > `readonly` **path**: readonly `string`\[] Defined in: [src/PersonalData.ts:114](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L114) ##### redacted > `readonly` **redacted**: `Json` Defined in: [src/PersonalData.ts:117](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L117) ##### subject > `readonly` **subject**: `string` | `undefined` Defined in: [src/PersonalData.ts:116](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L116) The sibling property holding the subject, or `undefined` for the aggregate id. *** ### KeysOptions Defined in: [src/PersonalData.ts:482](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L482) #### Properties ##### cacheSize? > `readonly` `optional` **cacheSize?**: `number` Defined in: [src/PersonalData.ts:484](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L484) How many subjects to keep in memory, with their unwrapped keys. Defaults to 10,000. ##### cacheTtl? > `readonly` `optional` **cacheTtl?**: `Input` Defined in: [src/PersonalData.ts:490](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L490) How long what the store said about a subject is used before it is asked again. It bounds how long another process keeps using the key of a subject forgotten here. Defaults to one minute. *** ### Location Defined in: [src/PersonalData.ts:1087](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1087) Where a payload is stored, which the ciphertext of its fields is bound to. #### Properties ##### aggregateId > `readonly` **aggregateId**: `string` Defined in: [src/PersonalData.ts:1089](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1089) ##### aggregateName > `readonly` **aggregateName**: `string` Defined in: [src/PersonalData.ts:1088](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1088) ##### type > `readonly` **type**: `string` Defined in: [src/PersonalData.ts:1090](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1090) *** ### RewrapOptions Defined in: [src/PersonalData.ts:472](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L472) #### Properties ##### batchSize? > `readonly` `optional` **batchSize?**: `number` Defined in: [src/PersonalData.ts:474](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L474) How many keys are read from the store at a time. Defaults to 100. *** ### RewrapResult Defined in: [src/PersonalData.ts:477](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L477) #### Properties ##### rewrapped > `readonly` **rewrapped**: `number` Defined in: [src/PersonalData.ts:479](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L479) The keys now wrapped by the current master key. *** ### SqlOptions Defined in: [src/PersonalData.ts:864](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L864) #### Properties ##### migrate? > `readonly` `optional` **migrate?**: `boolean` Defined in: [src/PersonalData.ts:871](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L871) Creates the table on startup when it doesn’t exist. Defaults to `true`. Turn it off to run [migrations](/reference/api/personaldata/#migrations) with your own tooling instead. ##### table? > `readonly` `optional` **table?**: `string` Defined in: [src/PersonalData.ts:866](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L866) Defaults to `cqrs_personal_data_keys`. *** ### WithSubjects Defined in: [src/PersonalData.ts:1174](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1174) A payload, with the subjects whose personal data it holds. #### Properties ##### payload > `readonly` **payload**: `Json` Defined in: [src/PersonalData.ts:1175](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1175) ##### subjects > `readonly` **subjects**: readonly `string`\[] Defined in: [src/PersonalData.ts:1176](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1176) ## Type Aliases ### KeyRecord > **KeyRecord** = { `_tag`: `"Active"`; `keyId`: `string`; `masterKeyId`: `string`; `wrapped`: `Uint8Array`<`ArrayBuffer`>; } | { `_tag`: `"Forgotten"`; `forgottenAt`: `number`; } Defined in: [src/PersonalData.ts:391](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L391) What the key store holds for a subject. #### Union Members ##### Type Literal { `_tag`: `"Active"`; `keyId`: `string`; `masterKeyId`: `string`; `wrapped`: `Uint8Array`<`ArrayBuffer`>; } ###### \_tag > `readonly` **\_tag**: `"Active"` ###### keyId > `readonly` **keyId**: `string` ###### masterKeyId > `readonly` **masterKeyId**: `string` ###### wrapped > `readonly` **wrapped**: `Uint8Array`<`ArrayBuffer`> The data key, wrapped by the master key. *** ##### Type Literal { `_tag`: `"Forgotten"`; `forgottenAt`: `number`; } *** ### Sealed > **Sealed** = *typeof* `Sealed.Type` Defined in: [src/PersonalData.ts:1247](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1247) Text encrypted once with the key of each subject, so destroying any one of the keys makes it unreadable. ## Variables ### Cipher > **Cipher**: `Reference`<[`Cipher`](/reference/api/personaldata/#cipher)> Defined in: [src/PersonalData.ts:296](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L296) The cipher that keys, master keys and fields use, [webCrypto](/reference/api/personaldata/#webcrypto) unless another is provided. *** ### forget > `const` **forget**: (…`args`) => `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) | [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), [`Keys`](/reference/api/personaldata/#keys) | [`EventStore`](/reference/api/eventstore/#eventstore)> Defined in: [src/PersonalData.ts:1045](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1045) Forgets a subject: destroys its key, so its personal fields read as their replacements everywhere, and records a `SubjectForgotten` event in the stream of the subject, with the current causation and metadata as the audit trail. Running it again records nothing new. The key goes first, so that no event says a subject is forgotten while its key still exists. A store that can’t be reached is tried a few more times; when it still fails, the subject is forgotten but the event is missing, and `onForget` handlers haven’t run. Call `forget` again until it succeeds. Other processes stop using the key once their cache expires, see [KeysOptions.cacheTtl](/reference/api/personaldata/#keysoptions). #### Parameters ##### args …\[`subject`: `string`] #### Returns `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) | [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), [`Keys`](/reference/api/personaldata/#keys) | [`EventStore`](/reference/api/eventstore/#eventstore)> *** ### ivLength > `const` **ivLength**: `12` = `12` Defined in: [src/PersonalData.ts:317](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L317) *** ### keyLength > `const` **keyLength**: `32` = `32` Defined in: [src/PersonalData.ts:319](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L319) *** ### layerMasterKeyEphemeral > `const` **layerMasterKeyEphemeral**: `Layer.Layer`<[`MasterKey`](/reference/api/personaldata/#masterkey)> Defined in: [src/PersonalData.ts:642](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L642) A random master key that lasts as long as the layer. Keys it wrapped can’t be read by another process or after a restart, so it suits tests and demos. *** ### make > `const` **make**: (…`args`) => `Effect`<{ `cipher`: [`Cipher`](/reference/api/personaldata/#cipher); `decryptionKey`(`subject`, `keyId`): `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `encryptionKey`(`subject`): `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)>; `forget`(`subject`): `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `isForgotten`(`subject`): `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `rewrap`(`options?`): `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; }, `never`, [`MasterKey`](/reference/api/personaldata/#masterkey)> Defined in: [src/PersonalData.ts:664](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L664) Builds [Keys](/reference/api/personaldata/#keys) on a key store. Reads of the store are cached per subject, and concurrent misses for one subject share a single read, unwrap and, for a new subject, a single key creation. #### Parameters ##### args …\[`backend`: [`Backend`](/reference/api/personaldata/#backend), `options?`: [`KeysOptions`](/reference/api/personaldata/#keysoptions)] #### Returns `Effect`<{ `cipher`: [`Cipher`](/reference/api/personaldata/#cipher); `decryptionKey`(`subject`, `keyId`): `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `encryptionKey`(`subject`): `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)>; `forget`(`subject`): `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `isForgotten`(`subject`): `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `rewrap`(`options?`): `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; }, `never`, [`MasterKey`](/reference/api/personaldata/#masterkey)> *** ### makeMasterKey > `const` **makeMasterKey**: (…`args`) => `Effect`<{ `keyId`: `string`; `unwrap`(`wrapped`, `context`): `Effect`<`Uint8Array`<`ArrayBuffer`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `wrap`(`dataKey`, `context`): `Effect`<`Uint8Array`<`ArrayBuffer`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; }, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`> Defined in: [src/PersonalData.ts:543](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L543) A master key held by the process, given as 32 bytes. Keys wrapped by earlier master keys stay readable while those are listed in `previous`. The bytes are copied, so the caller can zero its own. #### Parameters ##### args …\[`options`: `object`] #### Returns `Effect`<{ `keyId`: `string`; `unwrap`(`wrapped`, `context`): `Effect`<`Uint8Array`<`ArrayBuffer`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; `wrap`(`dataKey`, `context`): `Effect`<`Uint8Array`<`ArrayBuffer`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)>; }, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`> *** ### makeSqlBackend > `const` **makeSqlBackend**: (…`args`) => `Effect`<{ `afterCommit`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `create`: (`subject`, `key`) => `Effect`<[`KeyRecord`](/reference/api/personaldata/#keyrecord), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `forget`: (`subject`, `forgottenAt`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `get`: (`subject`) => `Effect`<`Option`<[`KeyRecord`](/reference/api/personaldata/#keyrecord)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `inTransaction`: `Effect`<`boolean`, `never`, `never`>; `replaceWrapped`: (`subject`, `from`, `to`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `wrappedByOthers`: (`masterKeyId`, `afterSubject`, `limit`) => `Effect`<`object`\[], [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; }, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `SqlClient`> Defined in: [src/PersonalData.ts:918](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L918) A key store in a SQL table, on SQLite or PostgreSQL. The table is created on startup unless `migrate` is `false`. Provide it a client of its own to keep keys in another database than the events, with other permissions and backups. Forgetting overwrites the row. The database may keep the old bytes for a while, in free pages, dead rows or its write-ahead log, and backups keep them as long as they are kept: let backups of this table expire. #### Parameters ##### args …\[`options?`: [`SqlOptions`](/reference/api/personaldata/#sqloptions)] #### Returns `Effect`<{ `afterCommit`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `create`: (`subject`, `key`) => `Effect`<[`KeyRecord`](/reference/api/personaldata/#keyrecord), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `forget`: (`subject`, `forgottenAt`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `get`: (`subject`) => `Effect`<`Option`<[`KeyRecord`](/reference/api/personaldata/#keyrecord)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `inTransaction`: `Effect`<`boolean`, `never`, `never`>; `replaceWrapped`: (`subject`, `from`, `to`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; `wrappedByOthers`: (`masterKeyId`, `afterSubject`, `limit`) => `Effect`<`object`\[], [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`>; }, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `SqlClient`> *** ### openText > `const` **openText**: (…`args`) => `Effect`<`Option`<`string`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`> Defined in: [src/PersonalData.ts:1271](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1271) Opens text from [sealText](/reference/api/personaldata/#sealtext), `None` when one of its subjects was forgotten. #### Parameters ##### args …\[`keys`: `object`, `sealed`: `object`, `purpose`: `string`] #### Returns `Effect`<`Option`<`string`>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `never`> *** ### Sealed > `const` **Sealed**: `Struct`<{ `data`: `String`; `subjects`: `$Array`<`Struct`<{ `keyId`: `String`; `subject`: `String`; }>>; }> Defined in: [src/PersonalData.ts:1247](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1247) Text encrypted once with the key of each subject, so destroying any one of the keys makes it unreadable. *** ### sealText > `const` **sealText**: (…`args`) => `Effect`<{ `data`: `string`; `subjects`: `object`\[]; }, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten), `never`> Defined in: [src/PersonalData.ts:1254](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1254) Seals text such as a snapshot of a state that holds the personal data of `subjects`. #### Parameters ##### args …\[`keys`: `object`, `subjects`: readonly `string`\[], `text`: `string`, `purpose`: `string`] #### Returns `Effect`<{ `data`: `string`; `subjects`: `object`\[]; }, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten), `never`> *** ### streamName > `const` **streamName**: `"effect-cqrs/personal-data"` = `"effect-cqrs/personal-data"` Defined in: [src/PersonalData.ts:1026](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1026) The stream that records forgotten subjects, one stream per subject. *** ### subjectForgottenType > `const` **subjectForgottenType**: `"SubjectForgotten"` = `"SubjectForgotten"` Defined in: [src/PersonalData.ts:1029](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1029) The type of the event [forget](/reference/api/personaldata/#forget-1) records. Its payload is empty. *** ### tagLength > `const` **tagLength**: `16` = `16` Defined in: [src/PersonalData.ts:318](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L318) *** ### webCrypto > `const` **webCrypto**: [`Cipher`](/reference/api/personaldata/#cipher) Defined in: [src/PersonalData.ts:329](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L329) AES-256-GCM on WebCrypto. ## Functions ### field() > **field**<`S`>(`schema`, `options`): `S` Defined in: [src/PersonalData.ts:95](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L95) Marks a field as personal data. `redacted` is what the field reads as once its subject is forgotten, and has to be a valid value of the field. `subject` names a property next to the field that holds the id of the person it describes, by default the aggregate id of the stream. #### Type Parameters ##### S `S` *extends* `PlainSchema` #### Parameters ##### schema `S` ##### options ###### redacted `S`\[`"Type"`] ###### subject? `string` #### Returns `S` *** ### fieldsByTag() > **fieldsByTag**(`schema`, `where`): `ReadonlyMap`<`string`, readonly [`Field`](/reference/api/personaldata/#field)\[]> Defined in: [src/PersonalData.ts:259](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L259) The personal fields of each event in a union of tagged events. Events without any are left out. #### Parameters ##### schema `Top` ##### where `string` #### Returns `ReadonlyMap`<`string`, readonly [`Field`](/reference/api/personaldata/#field)\[]> *** ### fieldsOf() > **fieldsOf**(`schema`, `where`): readonly [`Field`](/reference/api/personaldata/#field)\[] Defined in: [src/PersonalData.ts:252](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L252) The personal fields of one schema, such as an old version of an event. #### Parameters ##### schema `Top` ##### where `string` #### Returns readonly [`Field`](/reference/api/personaldata/#field)\[] *** ### layerMasterKey() > **layerMasterKey**(`options`): `Layer`<[`MasterKey`](/reference/api/personaldata/#masterkey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/PersonalData.ts:587](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L587) #### Parameters ##### options ###### key `Redacted`<`Uint8Array`<`ArrayBufferLike`>> ###### keyId `string` ###### previous? readonly `object`\[] #### Returns `Layer`<[`MasterKey`](/reference/api/personaldata/#masterkey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> *** ### layerMasterKeyConfig() > **layerMasterKeyConfig**(`options?`): `Layer`<[`MasterKey`](/reference/api/personaldata/#masterkey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | `ConfigError`> Defined in: [src/PersonalData.ts:600](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L600) A master key from configuration: an id, and 32 bytes in base64. By default `PERSONAL_DATA_MASTER_KEY_ID` and `PERSONAL_DATA_MASTER_KEY`. Earlier master keys, which keys created before a rotation are still wrapped by, come from `PERSONAL_DATA_MASTER_KEY_PREVIOUS` by default, as `id:base64` pairs separated by commas. Leave it unset when there are none, and empty it once [rewrap](/reference/api/personaldata/#rewrap) has moved every key to the current master key. #### Parameters ##### options? ###### key? `Config`<`Redacted`<`string`>> ###### keyId? `Config`<`string`> ###### previous? `Config`<`Redacted`<`string`>> #### Returns `Layer`<[`MasterKey`](/reference/api/personaldata/#masterkey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | `ConfigError`> *** ### layerMemory() > **layerMemory**(`options?`): `Layer`<[`Keys`](/reference/api/personaldata/#keys), `never`, [`MasterKey`](/reference/api/personaldata/#masterkey)> Defined in: [src/PersonalData.ts:861](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L861) Keys kept in process memory, for tests and demos. #### Parameters ##### options? [`KeysOptions`](/reference/api/personaldata/#keysoptions) #### Returns `Layer`<[`Keys`](/reference/api/personaldata/#keys), `never`, [`MasterKey`](/reference/api/personaldata/#masterkey)> *** ### layerSql() > **layerSql**(`options?`): `Layer`<[`Keys`](/reference/api/personaldata/#keys), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `SqlClient` | [`MasterKey`](/reference/api/personaldata/#masterkey)> Defined in: [src/PersonalData.ts:1007](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1007) Keys in a SQL table, see [makeSqlBackend](/reference/api/personaldata/#makesqlbackend). #### Parameters ##### options? [`SqlOptions`](/reference/api/personaldata/#sqloptions) & [`KeysOptions`](/reference/api/personaldata/#keysoptions) #### Returns `Layer`<[`Keys`](/reference/api/personaldata/#keys), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), `SqlClient` | [`MasterKey`](/reference/api/personaldata/#masterkey)> *** ### makeMemoryBackend() > **makeMemoryBackend**(): [`Backend`](/reference/api/personaldata/#backend) Defined in: [src/PersonalData.ts:823](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L823) A key store in process memory, gone after a restart along with access to everything it encrypted. #### Returns [`Backend`](/reference/api/personaldata/#backend) *** ### migrations() > **migrations**(`options?`): readonly [`Migration`](/reference/api/eventstore/#migration)\[] Defined in: [src/PersonalData.ts:883](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L883) The migrations of the key table of [makeSqlBackend](/reference/api/personaldata/#makesqlbackend), the same on SQLite and PostgreSQL, for teams that run migrations themselves. They are safe to run again. #### Parameters ##### options? ###### table? `string` #### Returns readonly [`Migration`](/reference/api/eventstore/#migration)\[] *** ### open() > **open**(`keys`, `fields`, `location`, `payload`): `Effect`<[`WithSubjects`](/reference/api/personaldata/#withsubjects), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Defined in: [src/PersonalData.ts:1186](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1186) Decrypts the personal fields of a stored payload, putting the replacement in place of the fields of forgotten subjects. A field stored in plaintext, from before it was marked personal, is left as it is. A field has to be encrypted for the subject it would be sealed for now: the aggregate id, or the sibling it names. #### Parameters ##### keys ###### cipher [`Cipher`](/reference/api/personaldata/#cipher) What encrypts and decrypts with the keys. ###### decryptionKey (`subject`, `keyId`) => `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> The key to decrypt with, `None` when the subject was forgotten. A subject without any key fails: losing the key store must not pass for forgetting. ###### encryptionKey (`subject`) => `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> The key to encrypt with, created when the subject has none. Fails for a forgotten subject. ###### forget (`subject`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Destroys the key of a subject. Prefer [forget](/reference/api/personaldata/#forget-1), which also records it in the log. ###### isForgotten (`subject`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ###### rewrap (`options?`) => `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> See [rewrap](/reference/api/personaldata/#rewrap). ##### fields readonly [`Field`](/reference/api/personaldata/#field)\[] ##### location [`Location`](/reference/api/personaldata/#location) ##### payload `Json` #### Returns `Effect`<[`WithSubjects`](/reference/api/personaldata/#withsubjects), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> *** ### rewrap() > **rewrap**(`options?`): `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), [`Keys`](/reference/api/personaldata/#keys)> Defined in: [src/PersonalData.ts:1022](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1022) Wraps every subject key that an earlier master key wrapped with the current one, so the earlier one can be dropped from `previous`. Keys stay the same, so nothing encrypted with them changes, and running it again only rewraps what is left. Keys created meanwhile are wrapped by the current master key already. #### Parameters ##### options? [`RewrapOptions`](/reference/api/personaldata/#rewrapoptions) #### Returns `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror), [`Keys`](/reference/api/personaldata/#keys)> *** ### seal() > **seal**(`keys`, `fields`, `location`, `payload`): `Effect`<[`WithSubjects`](/reference/api/personaldata/#withsubjects), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> Defined in: [src/PersonalData.ts:1127](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalData.ts#L1127) Encrypts the personal fields of an encoded payload. Fields without a value are left alone. `aggregateId` is the subject of fields that don’t name one. Returns the subjects whose keys were used. #### Parameters ##### keys ###### cipher [`Cipher`](/reference/api/personaldata/#cipher) What encrypts and decrypts with the keys. ###### decryptionKey (`subject`, `keyId`) => `Effect`<`Option`<[`CipherKey`](/reference/api/personaldata/#cipherkey)>, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> The key to decrypt with, `None` when the subject was forgotten. A subject without any key fails: losing the key store must not pass for forgetting. ###### encryptionKey (`subject`) => `Effect`<[`DataKey`](/reference/api/personaldata/#datakey), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> The key to encrypt with, created when the subject has none. Fails for a forgotten subject. ###### forget (`subject`) => `Effect`<`void`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> Destroys the key of a subject. Prefer [forget](/reference/api/personaldata/#forget-1), which also records it in the log. ###### isForgotten (`subject`) => `Effect`<`boolean`, [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> ###### rewrap (`options?`) => `Effect`<[`RewrapResult`](/reference/api/personaldata/#rewrapresult), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror)> See [rewrap](/reference/api/personaldata/#rewrap). ##### fields readonly [`Field`](/reference/api/personaldata/#field)\[] ##### location `Omit`<[`Location`](/reference/api/personaldata/#location), `"aggregateId"`> & `object` ##### payload `Json` #### Returns `Effect`<[`WithSubjects`](/reference/api/personaldata/#withsubjects), [`PersonalDataError`](/reference/api/personaldata/#personaldataerror) | [`SubjectForgotten`](/reference/api/personaldata/#subjectforgotten)> # PersonalDataNode The cipher of `PersonalData` on `node:crypto`. WebCrypto, the default, is asynchronous, which makes each personal field cost a trip through the event loop. This one encrypts and decrypts synchronously, in the same format, so data written with either reads with the other. ```ts const Keys = PersonalData.layerSql().pipe( Layer.provide(PersonalData.layerMasterKeyConfig()), Layer.provide(PersonalDataNode.layerCipher) ) ``` It lives in a module of its own so that code for browsers never imports `node:crypto`. ## Variables ### cipher > `const` **cipher**: [`Cipher`](/reference/api/personaldata/#cipher) Defined in: [src/PersonalDataNode.ts:27](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalDataNode.ts#L27) AES-256-GCM on `node:crypto`. *** ### layerCipher > `const` **layerCipher**: `Layer.Layer`<`never`> Defined in: [src/PersonalDataNode.ts:68](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PersonalDataNode.ts#L68) Provides [cipher](/reference/api/personaldatanode/#cipher) to the keys and master keys built with it. # PostgresEventStore Durable storage on PostgreSQL, through any `effect/sql` PostgreSQL driver. A sequence can’t number events here. Transactions commit in a different order from the one they drew their numbers in, so a view model that has read up to 6 would never come back for a 5 that commits later. Positions come from a counter row instead, which an append holds until it commits: writers wait for each other at that one step, and positions follow commit order with no gaps. Because of that row, appends from one process are written in groups: those that arrive while a group is being written go together into the next one, a single statement that checks each stream’s version, takes positions for the appends that pass and inserts their events. An append that fails its check fails alone. An append inside a transaction of the caller’s is written there and then, and holds the counter row until that transaction ends: every other writer of the store waits for it meanwhile, so such transactions should be short. The statement also sends a notification with the new head on a channel named after the events table. A store on a `PgClient` listens to it, so view models in every process wake up on every append. ## Interfaces ### Options Defined in: [src/PostgresEventStore.ts:42](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L42) #### Properties ##### checkpointsTable? > `readonly` `optional` **checkpointsTable?**: `string` Defined in: [src/PostgresEventStore.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L44) ##### eventsTable? > `readonly` `optional` **eventsTable?**: `string` Defined in: [src/PostgresEventStore.ts:43](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L43) ##### listen? > `readonly` `optional` **listen?**: `boolean` Defined in: [src/PostgresEventStore.ts:57](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L57) Listens for appends from other processes when the `SqlClient` is a `PgClient` from `@effect/sql-pg`. Listening holds a connection. Defaults to `true`. Turn it off behind a pooler that doesn’t keep sessions, such as PgBouncer in transaction mode, and set `pollInterval` instead. ##### migrate? > `readonly` `optional` **migrate?**: `boolean` Defined in: [src/PostgresEventStore.ts:50](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L50) Creates the tables, or brings tables of an earlier version up to date, on startup. Defaults to `true`. Set it to `false` where the application may not change the schema, and apply [migrations](/reference/api/postgreseventstore/#migrations) with your own tooling. ##### pollInterval? > `readonly` `optional` **pollInterval?**: `Input` Defined in: [src/PostgresEventStore.ts:59](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L59) Reads the head this often, see `EventStore.MakeOptions`. Off by default. ## Type Aliases ### Migration > **Migration** = [`Migration`](/reference/api/eventstore/#migration) Defined in: [src/PostgresEventStore.ts:63](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L63) A schema change, see `EventStore.Migration`. Migrations are listed in the order to apply them. ## Variables ### make > `const` **make**: (…`args`) => `Effect`<{ `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `Scope` | `SqlClient`> Defined in: [src/PostgresEventStore.ts:222](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L222) #### Parameters ##### args …\[`options?`: [`Options`](/reference/api/postgreseventstore/#options)] #### Returns `Effect`<{ `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `Scope` | `SqlClient`> ## Functions ### layer() > **layer**(`options?`): `Layer`<[`EventStore`](/reference/api/eventstore/#eventstore), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> Defined in: [src/PostgresEventStore.ts:408](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L408) #### Parameters ##### options? [`Options`](/reference/api/postgreseventstore/#options) #### Returns `Layer`<[`EventStore`](/reference/api/eventstore/#eventstore), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> *** ### layerCheckpoints() > **layerCheckpoints**(`options?`): `Layer`<[`Checkpoints`](/reference/api/checkpoints/#checkpoints), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> Defined in: [src/PostgresEventStore.ts:414](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L414) Keeps checkpoints in the same database, for view models that are durable too. #### Parameters ##### options? [`Options`](/reference/api/postgreseventstore/#options) #### Returns `Layer`<[`Checkpoints`](/reference/api/checkpoints/#checkpoints), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> *** ### migrations() > **migrations**(`options?`): readonly [`Migration`](/reference/api/eventstore/#migration)\[] Defined in: [src/PostgresEventStore.ts:142](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/PostgresEventStore.ts#L142) The schema of the events and checkpoints tables, for tools that apply migrations themselves. `layer` and `layerCheckpoints` run the same statements on startup unless `migrate` is `false`. #### Parameters ##### options? [`Options`](/reference/api/postgreseventstore/#options) #### Returns readonly [`Migration`](/reference/api/eventstore/#migration)\[] # Saga Sagas: reactions that span several aggregates and outlive one command, run as durable workflows from `effect/workflow`. A saga has two halves. Its trigger is a view model that follows the log from a checkpoint and starts a workflow for each event it cares about. The workflow is the process itself: each command it runs is an `Activity`, so when the engine replays the workflow after a restart it skips the commands that already finished, and `withCompensation` undoes earlier steps when a later one fails. Triggers see an event at least once, and the workflow’s idempotency key turns a second start into a join of the first execution. That only holds while the engine remembers the execution, so pair `WorkflowEngine.layerMemory` with in-memory checkpoints, as for view models, and a durable engine such as `ClusterWorkflowEngine` from `effect/cluster` with durable ones. An activity that is cut off before its result is saved runs again, so the commands a saga runs should be idempotent: give each a `commandId` made from the execution id and the step, so that running it again returns the first result, or record the id of the transfer it belongs to. ## Classes ### Sagas Defined in: [src/Saga.ts:144](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L144) The triggers that [layer](/reference/api/saga/#layer) runs, managed like view models: their status, the events they parked and retrying those. It is `ViewModel.ViewModels` under another name, so it doesn’t stand in for the view models’ own. #### Extends * `Shape`<`"effect-cqrs/Sagas"`, { `status`: `Effect`\; `caughtUp`(`position?`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `parked`(`viewModel?`): `Effect`\; `replay`(`options`): `Effect`<[`ReplayResult`](/reference/api/viewmodel/#replayresult), [`UnknownViewModel`](/reference/api/viewmodel/#unknownviewmodel) | [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror)>; `retryParked`(`options`): `Effect`<[`RetryParkedResult`](/reference/api/viewmodel/#retryparkedresult), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) | [`UnknownViewModel`](/reference/api/viewmodel/#unknownviewmodel) | [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror)>; }, `this`> #### Constructors ##### Constructor > **new Sagas**(`_`): [`Sagas`](/reference/api/saga/#sagas) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`Sagas`](/reference/api/saga/#sagas) ###### Inherited from `Context.Service()("effect-cqrs/Sagas").constructor` ## Type Aliases ### SagaWorkflow > **SagaWorkflow**<`Tag`, `Fields`, `Success`, `Error`> = `Workflow.Workflow`<`Tag`, `Schema.Struct`<`Fields` & `object`>, `Success`, `Error`> Defined in: [src/Saga.ts:46](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L46) A workflow whose payload carries the causation of the event that started it. #### Type Parameters ##### Tag `Tag` *extends* `string` = `string` ##### Fields `Fields` *extends* `Schema.Struct.Fields` = `Schema.Struct.Fields` ##### Success `Success` *extends* `Schema.Top` = `Schema.Top` ##### Error `Error` *extends* `Schema.Top` = `Schema.Top` ## Variables ### CausationSchema > `const` **CausationSchema**: `Struct`<{ `causationId`: `String`; `correlationId`: `String`; }> Defined in: [src/Saga.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L40) The causation a workflow carries in its payload. A workflow body runs in the engine’s fibers, not in the trigger’s, so this is how commands it runs learn which event caused them. *** ### CommandOutcome > `const` **CommandOutcome**: `Struct`<{ `aggregateId`: `String`; `position`: `Int`; `version`: `Int`; }> Defined in: [src/Saga.ts:160](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L160) What a saga keeps of a command it ran: enough to wait for view models to see it. ## Functions ### command() > **command**<`Err`, `E`, `R`>(`name`, `options`): `Activity`<`Struct`<{ `aggregateId`: `String`; `position`: `Int`; `version`: `Int`; }>, `Err`, `R`> Defined in: [src/Saga.ts:180](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L180) Runs a command as an activity of a saga’s workflow, so a replay of the workflow returns the saved outcome instead of running it again. Failures matching `error`, such as a domain error the saga reacts to, are saved like a success. A lost race or a store error is retried a few times. Anything else ends the workflow as a defect, which runs its compensations. #### Type Parameters ##### Err `Err` *extends* `Top` = `Never` ##### E `E` = `never` ##### R `R` = `never` #### Parameters ##### name `string` ##### options ###### error? `Err` ###### execute `Effect`<[`CommandResult`](/reference/api/aggregateservice/#commandresult)<`unknown`>, `E`, `R`> #### Returns `Activity`<`Struct`<{ `aggregateId`: `String`; `position`: `Int`; `version`: `Int`; }>, `Err`, `R`> *** ### layer() > **layer**<`Triggers`>(`sagas`): `Layer`<[`Sagas`](/reference/api/saga/#sagas), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), [`EventStore`](/reference/api/eventstore/#eventstore) | [`Checkpoints`](/reference/api/checkpoints/#checkpoints) | [`Context`](/reference/api/viewmodel/#context)<`Triggers`\[`number`]>> Defined in: [src/Saga.ts:152](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L152) Runs triggers in the background for as long as the layer lives, apart from the view models, and provides [Sagas](/reference/api/saga/#sagas) to manage them. Workflows usually run commands, so they sit on top of the command side, which view models are often provided to. #### Type Parameters ##### Triggers `Triggers` *extends* readonly [`Any`](/reference/api/viewmodel/#any)\[] #### Parameters ##### sagas `Triggers` #### Returns `Layer`<[`Sagas`](/reference/api/saga/#sagas), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), [`EventStore`](/reference/api/eventstore/#eventstore) | [`Checkpoints`](/reference/api/checkpoints/#checkpoints) | [`Context`](/reference/api/viewmodel/#context)<`Triggers`\[`number`]>> *** ### make() > **make**<`Name`, `Aggregates`, `H`>(`options`): [`ViewModel`](/reference/api/viewmodel/#viewmodel-2)<`Name`, `HandlerError`<`H`>, `HandlerContext`<`H`>> Defined in: [src/Saga.ts:127](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L127) The trigger of a saga: a view model whose handlers start workflows with [start](/reference/api/saga/#start), so it keeps a checkpoint and retries like view models do. Run it with [layer](/reference/api/saga/#layer), or with `ViewModel.layer` next to the other view models, where replaying it starts every saga again and joins the executions the engine still remembers. #### Type Parameters ##### Name `Name` *extends* `string` ##### Aggregates `Aggregates` *extends* readonly [`Any`](/reference/api/aggregate/#any)\[] ##### H `H` *extends* [`Handlers`](/reference/api/viewmodel/#handlers-1)<[`Event`](/reference/api/aggregate/#event-2)<`Aggregates`\[`number`]>> #### Parameters ##### options ###### aggregates `Aggregates` ###### handlers `H` ###### name `Name` ###### onFailure? [`FailurePolicy`](/reference/api/viewmodel/#failurepolicy) What to do with an event that keeps failing, see `ViewModel.park`. #### Returns [`ViewModel`](/reference/api/viewmodel/#viewmodel-2)<`Name`, `HandlerError`<`H`>, `HandlerContext`<`H`>> *** ### start() > **start**<`Tag`, `Fields`, `Success`, `Error`>(`self`, `payload`): `Effect`<`string`, `never`, `WorkflowEngine`> Defined in: [src/Saga.ts:104](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L104) Starts a saga’s workflow without waiting for it, and returns its execution id. Inside a trigger the event being handled becomes the saga’s cause; elsewhere the current causation does, or a new correlation when there is none. #### Type Parameters ##### Tag `Tag` *extends* `string` ##### Fields `Fields` *extends* `Fields` ##### Success `Success` *extends* `Top` ##### Error `Error` *extends* `Top` #### Parameters ##### self [`SagaWorkflow`](/reference/api/saga/#sagaworkflow)<`Tag`, `Fields`, `Success`, `Error`> ##### payload `MakeInView`<`Fields`> #### Returns `Effect`<`string`, `never`, `WorkflowEngine`> *** ### toLayer() > **toLayer**<`Tag`, `Fields`, `Success`, `Error`, `R`>(`self`, `execute`): `Layer`<`never`, `never`, `WorkflowEngine` | `DecodingServices`<`Fields` & `object`> | `EncodingServices`<`Fields` & `object`> | `Success`\[`"DecodingServices"`] | `Success`\[`"EncodingServices"`] | `Error`\[`"DecodingServices"`] | `Error`\[`"EncodingServices"`] | `Exclude`<`R`, `Scope` | `WorkflowEngine` | `WorkflowInstance` | `Execution`<`Tag`>>> Defined in: [src/Saga.ts:79](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L79) Registers the body of a saga’s workflow with the engine. Commands the body runs record the event that started the saga as their cause. #### Type Parameters ##### Tag `Tag` *extends* `string` ##### Fields `Fields` *extends* `Fields` ##### Success `Success` *extends* `Top` ##### Error `Error` *extends* `Top` ##### R `R` #### Parameters ##### self [`SagaWorkflow`](/reference/api/saga/#sagaworkflow)<`Tag`, `Fields`, `Success`, `Error`> ##### execute (`payload`, `executionId`) => `Effect`<`Success`\[`"Type"`], `Error`\[`"Type"`], `R`> #### Returns `Layer`<`never`, `never`, `WorkflowEngine` | `DecodingServices`<`Fields` & `object`> | `EncodingServices`<`Fields` & `object`> | `Success`\[`"DecodingServices"`] | `Success`\[`"EncodingServices"`] | `Error`\[`"DecodingServices"`] | `Error`\[`"EncodingServices"`] | `Exclude`<`R`, `Scope` | `WorkflowEngine` | `WorkflowInstance` | `Execution`<`Tag`>>> *** ### workflow() > **workflow**<`Tag`, `Fields`, `Success`, `Error`>(`tag`, `options`): [`SagaWorkflow`](/reference/api/saga/#sagaworkflow)<`Tag`, `Fields`, `Success`, `Error`> Defined in: [src/Saga.ts:57](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/Saga.ts#L57) Defines the workflow of a saga. It is `Workflow.make` with a `causation` field added to the payload, which [start](/reference/api/saga/#start) fills in and [toLayer](/reference/api/saga/#tolayer) applies. #### Type Parameters ##### Tag `Tag` *extends* `string` ##### Fields `Fields` *extends* `Fields` ##### Success `Success` *extends* `Top` = `Void` ##### Error `Error` *extends* `Top` = `Never` #### Parameters ##### tag `Tag` ##### options ###### error? `Error` ###### idempotencyKey (`payload`) => `string` Identifies one run of the saga, such as a transfer id. Starting it again joins that run. ###### payload `Fields` ###### success? `Success` ###### suspendedRetrySchedule? `Schedule`<`any`, `unknown`, `never`, `never`> #### Returns [`SagaWorkflow`](/reference/api/saga/#sagaworkflow)<`Tag`, `Fields`, `Success`, `Error`> # SqliteEventStore Durable storage on SQLite, through any `effect/sql` SQLite driver. The schema relies on SQLite’s single writer for positions to follow commit order, so it is not portable to other databases. An append is one statement, which checks the stream’s version and inserts the events only if it still matches. Appends that arrive while others are being written go together into one transaction, so they share the cost of a commit. ## Interfaces ### Options Defined in: [src/SqliteEventStore.ts:18](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L18) #### Properties ##### checkpointsTable? > `readonly` `optional` **checkpointsTable?**: `string` Defined in: [src/SqliteEventStore.ts:20](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L20) ##### eventsTable? > `readonly` `optional` **eventsTable?**: `string` Defined in: [src/SqliteEventStore.ts:19](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L19) ##### migrate? > `readonly` `optional` **migrate?**: `boolean` Defined in: [src/SqliteEventStore.ts:26](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L26) Creates the tables, or brings tables of an earlier version up to date, on startup. Defaults to `true`. Set it to `false` where the application may not change the schema, and apply [migrations](/reference/api/sqliteeventstore/#migrations) with your own tooling. ##### pollInterval? > `readonly` `optional` **pollInterval?**: `Input` Defined in: [src/SqliteEventStore.ts:31](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L31) Reads the head this often, so subscribers notice appends that other processes make to the same file. Off by default. See `EventStore.MakeOptions`. ## Type Aliases ### Migration > **Migration** = [`Migration`](/reference/api/eventstore/#migration) Defined in: [src/SqliteEventStore.ts:35](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L35) A schema change, see `EventStore.Migration`. Migrations are listed in the order to apply them. ## Variables ### make > `const` **make**: (…`args`) => `Effect`<{ `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `Scope` | `SqlClient`> Defined in: [src/SqliteEventStore.ts:173](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L173) #### Parameters ##### args …\[`options?`: [`Options`](/reference/api/sqliteeventstore/#options)] #### Returns `Effect`<{ `afterCommit?`: <`R`>(`effect`) => `Effect`<`void`, `never`, `R`>; `changes`: `Stream`<`number`>; `head`: `Effect`<`number`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `append`(`input`): `Effect`\; `read`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `subscribe`(`filter?`): `Stream`<{ `aggregateId`: `string`; `aggregateName`: `string`; `causationId`: `string` | `null`; `correlationId`: `string` | `null`; `metadata`: {\[`key`: `string`]: `Json`; }; `payload`: `Json`; `position`: `number`; `schemaVersion`: `number`; `timestamp`: `Utc`; `type`: `string`; `version`: `number`; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; }, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `Scope` | `SqlClient`> ## Functions ### layer() > **layer**(`options?`): `Layer`<[`EventStore`](/reference/api/eventstore/#eventstore), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> Defined in: [src/SqliteEventStore.ts:310](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L310) #### Parameters ##### options? [`Options`](/reference/api/sqliteeventstore/#options) #### Returns `Layer`<[`EventStore`](/reference/api/eventstore/#eventstore), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> *** ### layerCheckpoints() > **layerCheckpoints**(`options?`): `Layer`<[`Checkpoints`](/reference/api/checkpoints/#checkpoints), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> Defined in: [src/SqliteEventStore.ts:316](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L316) Keeps checkpoints in the same database, for view models that are durable too. #### Parameters ##### options? [`Options`](/reference/api/sqliteeventstore/#options) #### Returns `Layer`<[`Checkpoints`](/reference/api/checkpoints/#checkpoints), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), `SqlClient`> *** ### migrations() > **migrations**(`options?`): readonly [`Migration`](/reference/api/eventstore/#migration)\[] Defined in: [src/SqliteEventStore.ts:123](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/SqliteEventStore.ts#L123) The schema of the events and checkpoints tables, for tools that apply migrations themselves. `layer` and `layerCheckpoints` run the same statements on startup unless `migrate` is `false`. SQLite can’t add a column only if it is missing, so the `ALTER TABLE` statements must run exactly once. #### Parameters ##### options? [`Options`](/reference/api/sqliteeventstore/#options) #### Returns readonly [`Migration`](/reference/api/eventstore/#migration)\[] # ViewModel The read side: projections of events into whatever storage a query needs. Moleculer-cqrs pushes events to view models over the broker, so an event sent while a service is down is lost until someone replays. Here each view model pulls from the store from its own checkpoint, so it can’t miss anything, and delivery is at least once: a handler can see an event again after a crash and should be idempotent, unless the view model has a `transaction`. ## Classes ### UnknownViewModel Defined in: [src/ViewModel.ts:40](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L40) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new UnknownViewModel**(…`args`): [`UnknownViewModel`](/reference/api/viewmodel/#unknownviewmodel) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`UnknownViewModel`](/reference/api/viewmodel/#unknownviewmodel) ###### Inherited from `Schema.TaggedError()("UnknownViewModel", { name: Schema.String }).constructor` #### Properties ##### name > **name**: `string` = `Schema.String` Defined in: [src/ViewModel.ts:41](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L41) ###### Inherited from `Schema.TaggedError()("UnknownViewModel", { name: Schema.String }).name` *** ### ViewModelError Defined in: [src/ViewModel.ts:44](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L44) #### Extends * `object` & `YieldableError`<`this`> #### Constructors ##### Constructor > **new ViewModelError**(…`args`): [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Schema.d.ts:10979 ###### Parameters ###### args …\[`props`: `object`, `options?`: `MakeOptions`] ###### Returns [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror) ###### Inherited from ``Schema.TaggedError()("ViewModelError", { viewModel: Schema.String, /** The event being processed, `0` when the failure happened outside any event. */ position: Schema.Int, cause: Schema.Defect() }).constructor`` #### Properties ##### cause > **cause**: `unknown` Defined in: [src/ViewModel.ts:48](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L48) ###### Inherited from ``Schema.TaggedError()("ViewModelError", { viewModel: Schema.String, /** The event being processed, `0` when the failure happened outside any event. */ position: Schema.Int, cause: Schema.Defect() }).cause`` ##### position > `readonly` **position**: `number` = `Schema.Int` Defined in: [src/ViewModel.ts:47](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L47) The event being processed, `0` when the failure happened outside any event. ###### Inherited from ``Schema.TaggedError()("ViewModelError", { viewModel: Schema.String, /** The event being processed, `0` when the failure happened outside any event. */ position: Schema.Int, cause: Schema.Defect() }).position`` ##### viewModel > `readonly` **viewModel**: `string` = `Schema.String` Defined in: [src/ViewModel.ts:45](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L45) ###### Inherited from ``Schema.TaggedError()("ViewModelError", { viewModel: Schema.String, /** The event being processed, `0` when the failure happened outside any event. */ position: Schema.Int, cause: Schema.Defect() }).viewModel`` *** ### ViewModels Defined in: [src/ViewModel.ts:206](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L206) #### Extends * `Shape`<`"effect-cqrs/ViewModels"`, { `status`: `Effect`\; `caughtUp`(`position?`): `Effect`<`void`, [`EventStoreError`](/reference/api/eventstore/#eventstoreerror)>; `parked`(`viewModel?`): `Effect`\; `replay`(`options`): `Effect`<[`ReplayResult`](/reference/api/viewmodel/#replayresult), [`UnknownViewModel`](/reference/api/viewmodel/#unknownviewmodel) | [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror)>; `retryParked`(`options`): `Effect`<[`RetryParkedResult`](/reference/api/viewmodel/#retryparkedresult), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror) | [`UnknownViewModel`](/reference/api/viewmodel/#unknownviewmodel) | [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror)>; }, `this`> #### Constructors ##### Constructor > **new ViewModels**(`_`): [`ViewModels`](/reference/api/viewmodel/#viewmodels) Defined in: node_modules/.pnpm/effect\@4.0.6/node_modules/effect/dist/Context.d.ts:121 ###### Parameters ###### \_ `never` ###### Returns [`ViewModels`](/reference/api/viewmodel/#viewmodels) ###### Inherited from Context.Service\ /\*\* \* Waits until every view model has processed \`position\`, defaulting to the \* latest event. Pair it with \`CommandResult.position\` to read your own writes. \* A view model stalled on a failure doesn’t catch up, so consider a timeout. \* View models that another process holds are not waited for. \*/ caughtUp(position?: number): Effect.Effect\ readonly status: Effect.Effect\> /\*\* The events the view models of this layer parked, or those of one of them. \*/ parked(viewModel?: string): Effect.Effect\, UnknownViewModel | EventStoreError> /\*\* \* Handles parked events again, in order of position. Those that succeed are no \* longer parked, and the others stay parked with the new failure. \*/ retryParked( options: RetryParkedOptions ): Effect.Effect\ }>()(“effect-cqrs/ViewModels”).constructor ## Interfaces ### ReplayOptions Defined in: [src/ViewModel.ts:171](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L171) #### Properties ##### finishTime? > `readonly` `optional` **finishTime?**: `Utc` Defined in: [src/ViewModel.ts:174](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L174) ##### startTime? > `readonly` `optional` **startTime?**: `Utc` Defined in: [src/ViewModel.ts:173](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L173) ##### viewModels > `readonly` **viewModels**: readonly `string`\[] Defined in: [src/ViewModel.ts:172](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L172) *** ### ReplayResult Defined in: [src/ViewModel.ts:177](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L177) #### Properties ##### eventCount > `readonly` **eventCount**: `number` Defined in: [src/ViewModel.ts:179](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L179) ##### viewModels > `readonly` **viewModels**: readonly `string`\[] Defined in: [src/ViewModel.ts:178](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L178) *** ### RetryParkedOptions Defined in: [src/ViewModel.ts:193](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L193) #### Properties ##### positions? > `readonly` `optional` **positions?**: readonly `number`\[] Defined in: [src/ViewModel.ts:196](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L196) The positions of the events to retry. Defaults to every event the view model parked. ##### viewModel > `readonly` **viewModel**: `string` Defined in: [src/ViewModel.ts:194](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L194) *** ### RetryParkedResult Defined in: [src/ViewModel.ts:199](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L199) #### Properties ##### failed > `readonly` **failed**: readonly `object`\[] Defined in: [src/ViewModel.ts:203](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L203) The events that failed again, as they are parked now. ##### succeeded > `readonly` **succeeded**: readonly `number`\[] Defined in: [src/ViewModel.ts:201](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L201) The positions of the events that were handled, and are no longer parked. *** ### Status Defined in: [src/ViewModel.ts:182](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L182) #### Properties ##### holdsLease > `readonly` **holdsLease**: `boolean` Defined in: [src/ViewModel.ts:190](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L190) Whether this process holds the view model’s lease, and so runs it. See `Leases`. ##### lastError > `readonly` **lastError**: `Option`<[`ViewModelError`](/reference/api/viewmodel/#viewmodelerror)> Defined in: [src/ViewModel.ts:186](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L186) The failure the view model is retrying, while it is stalled on one. ##### name > `readonly` **name**: `string` Defined in: [src/ViewModel.ts:183](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L183) ##### parked > `readonly` **parked**: `number` Defined in: [src/ViewModel.ts:188](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L188) How many events the view model parked and skips, see [park](/reference/api/viewmodel/#park). ##### position > `readonly` **position**: `number` Defined in: [src/ViewModel.ts:184](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L184) *** ### ViewModel Defined in: [src/ViewModel.ts:66](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L66) #### Type Parameters ##### Name `Name` *extends* `string` ##### E `E` ##### R `R` #### Properties ##### aggregates > `readonly` **aggregates**: readonly [`Any`](/reference/api/aggregate/#any)\[] Defined in: [src/ViewModel.ts:68](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L68) ##### dispose > `readonly` **dispose**: `Effect`<`void`, `E`, `R`> Defined in: [src/ViewModel.ts:71](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L71) Clears everything the view model stored, before a replay rebuilds it. ##### handlers > `readonly` **handlers**: `Record`<`string`, (`event`, `meta`) => `Effect.Effect`<`unknown`, `E`, `R`>> Defined in: [src/ViewModel.ts:69](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L69) ##### name > `readonly` **name**: `Name` Defined in: [src/ViewModel.ts:67](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L67) ##### onFailure? > `readonly` `optional` **onFailure?**: [`FailurePolicy`](/reference/api/viewmodel/#failurepolicy) Defined in: [src/ViewModel.ts:75](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L75) What happens to an event its handler keeps failing on. Defaults to [stall](/reference/api/viewmodel/#stall). ##### onForget? > `readonly` `optional` **onForget?**: (`subject`, `meta`) => `Effect`<`unknown`, `E`, `R`> Defined in: [src/ViewModel.ts:73](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L73) Erases what the view model stored about a forgotten subject, see `PersonalData.forget`. ###### Parameters ###### subject `string` ###### meta [`EventMeta`](/reference/api/aggregate/#eventmeta) ###### Returns `Effect`<`unknown`, `E`, `R`> ##### transaction? > `readonly` `optional` **transaction?**: [`Transaction`](/reference/api/viewmodel/#transaction-1)<`R`> Defined in: [src/ViewModel.ts:77](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L77) Runs each batch of handlers together with the checkpoint they reach, see [sqlTransaction](/reference/api/viewmodel/#sqltransaction). ## Type Aliases ### Any > **Any** = [`ViewModel`](/reference/api/viewmodel/#viewmodel-2)<`string`, `any`, `any`> Defined in: [src/ViewModel.ts:114](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L114) *** ### Context > **Context**<`V`> = `V` *extends* [`ViewModel`](/reference/api/viewmodel/#viewmodel-2)<`any`, `any`, infer R> ? `R` : `never` Defined in: [src/ViewModel.ts:116](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L116) #### Type Parameters ##### V `V` *extends* [`Any`](/reference/api/viewmodel/#any) *** ### FailurePolicy > **FailurePolicy** = { `_tag`: `"Stall"`; } | { `_tag`: `"Park"`; `attempts`: `number`; } Defined in: [src/ViewModel.ts:101](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L101) What a view model does about an event whose handler keeps failing. * `Stall` retries it forever, with a growing delay, and nothing after it is processed in the meantime. Nothing is ever skipped. * `Park` retries it until it has failed `attempts` times, then records it in `ParkedEvents` and moves on. `ViewModels.retryParked` handles it again later. *** ### Handlers > **Handlers**<`Event`> = `{ readonly [Tag in Event["_tag"]]?: (event: Extract, meta: EventMeta) => Effect.Effect }` Defined in: [src/ViewModel.ts:51](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L51) #### Type Parameters ##### Event `Event` *extends* [`Tagged`](/reference/api/aggregate/#tagged) *** ### Transaction > **Transaction**<`R`> = (`effect`) => `Effect.Effect`<`void`, `unknown`, `R`> Defined in: [src/ViewModel.ts:84](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L84) Wraps a batch of handlers and the checkpoint after them, so that they commit or roll back together. #### Type Parameters ##### R `R` #### Parameters ##### effect `Effect.Effect`<`void`, [`ViewModelError`](/reference/api/viewmodel/#viewmodelerror)> #### Returns `Effect.Effect`<`void`, `unknown`, `R`> ## Variables ### MetricNames > `const` **MetricNames**: `object` Defined in: [src/ViewModel.ts:236](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L236) The names of the metrics view models record through Effect’s `Metric` module. Each series carries a `view_model` attribute with the view model’s name. #### Type Declaration ##### parkedEvents > `readonly` **parkedEvents**: `"cqrs_view_model_parked_events_total"` = `"cqrs_view_model_parked_events_total"` Counter: events a view model parked after its handler kept failing on them. *** ### sqlTransaction > `const` **sqlTransaction**: [`Transaction`](/reference/api/viewmodel/#transaction-1)<`SqlClient.SqlClient`> Defined in: [src/ViewModel.ts:90](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L90) A transaction of the `SqlClient` in context. The read model and the checkpoints have to use that same client for their writes to join it. *** ### stall > `const` **stall**: [`FailurePolicy`](/reference/api/viewmodel/#failurepolicy) Defined in: [src/ViewModel.ts:106](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L106) Retries a failing event until it succeeds, holding everything after it back. The default. ## Functions ### layer() > **layer**<`VMs`>(`viewModels`): `Layer`<[`ViewModels`](/reference/api/viewmodel/#viewmodels), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), [`EventStore`](/reference/api/eventstore/#eventstore) | [`Checkpoints`](/reference/api/checkpoints/#checkpoints) | [`Context`](/reference/api/viewmodel/#context)<`VMs`\[`number`]>> Defined in: [src/ViewModel.ts:723](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L723) Starts the view models in the background for as long as the layer lives. Each view model runs where its lease is held, see `Leases`: by default in every process, which suits a single one. Events that view models park go to the `ParkedEvents` in context, or to memory without one. #### Type Parameters ##### VMs `VMs` *extends* readonly [`Any`](/reference/api/viewmodel/#any)\[] #### Parameters ##### viewModels `VMs` #### Returns `Layer`<[`ViewModels`](/reference/api/viewmodel/#viewmodels), [`EventStoreError`](/reference/api/eventstore/#eventstoreerror), [`EventStore`](/reference/api/eventstore/#eventstore) | [`Checkpoints`](/reference/api/checkpoints/#checkpoints) | [`Context`](/reference/api/viewmodel/#context)<`VMs`\[`number`]>> *** ### make() > **make**<`Name`, `Aggregates`, `H`, `E`, `R`, `FE`, `FR`, `TR`>(`options`): [`ViewModel`](/reference/api/viewmodel/#viewmodel-2)<`Name`, `E` | `FE` | `HandlerError`<`H`>, `R` | `FR` | `TR` | `HandlerContext`<`H`>> Defined in: [src/ViewModel.ts:130](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L130) Handlers are keyed by event tag, and events without one are skipped. A view model that keeps personal data handles `onForget`, which runs when `PersonalData.forget` records that a subject was forgotten, in order with the events around it. Events the view model reads afterwards already carry the replacements, so it only has to erase what it stored before. `onFailure` decides what happens to an event that keeps failing, see [FailurePolicy](/reference/api/viewmodel/#failurepolicy). With `transaction`, each batch of handlers runs in one transaction with the checkpoint it reaches, so a crash never repeats an event. #### Type Parameters ##### Name `Name` *extends* `string` ##### Aggregates `Aggregates` *extends* readonly [`Any`](/reference/api/aggregate/#any)\[] ##### H `H` *extends* [`Handlers`](/reference/api/viewmodel/#handlers-1)<[`Event`](/reference/api/aggregate/#event-2)<`Aggregates`\[`number`]>> ##### E `E` = `never` ##### R `R` = `never` ##### FE `FE` = `never` ##### FR `FR` = `never` ##### TR `TR` = `never` #### Parameters ##### options ###### aggregates `Aggregates` ###### dispose `Effect`<`void`, `E`, `R`> ###### handlers `H` ###### name `Name` ###### onFailure? [`FailurePolicy`](/reference/api/viewmodel/#failurepolicy) ###### onForget? (`subject`, `meta`) => `Effect`<`unknown`, `FE`, `FR`> ###### transaction? [`Transaction`](/reference/api/viewmodel/#transaction-1)<`TR`> #### Returns [`ViewModel`](/reference/api/viewmodel/#viewmodel-2)<`Name`, `E` | `FE` | `HandlerError`<`H`>, `R` | `FR` | `TR` | `HandlerContext`<`H`>> *** ### park() > **park**(`options`): [`FailurePolicy`](/reference/api/viewmodel/#failurepolicy) Defined in: [src/ViewModel.ts:109](https://github.com/davidnussio/effect-cqrs/blob/9d66af7141782f3f89655e6868abe3f141c97632/src/ViewModel.ts#L109) Gives up on an event after it failed `attempts` times, and goes on with the next ones. #### Parameters ##### options ###### attempts `number` #### Returns [`FailurePolicy`](/reference/api/viewmodel/#failurepolicy)