effect-cqrs 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +298 -2
  3. package/dist/Aggregate.d.ts +86 -0
  4. package/dist/Aggregate.d.ts.map +1 -0
  5. package/dist/Aggregate.js +15 -0
  6. package/dist/Aggregate.js.map +1 -0
  7. package/dist/AggregateService.d.ts +82 -0
  8. package/dist/AggregateService.d.ts.map +1 -0
  9. package/dist/AggregateService.js +181 -0
  10. package/dist/AggregateService.js.map +1 -0
  11. package/dist/Checkpoints.d.ts +19 -0
  12. package/dist/Checkpoints.d.ts.map +1 -0
  13. package/dist/Checkpoints.js +18 -0
  14. package/dist/Checkpoints.js.map +1 -0
  15. package/dist/EventStore.d.ts +110 -0
  16. package/dist/EventStore.d.ts.map +1 -0
  17. package/dist/EventStore.js +136 -0
  18. package/dist/EventStore.js.map +1 -0
  19. package/dist/Fixture.d.ts +71 -0
  20. package/dist/Fixture.d.ts.map +1 -0
  21. package/dist/Fixture.js +108 -0
  22. package/dist/Fixture.js.map +1 -0
  23. package/dist/PostgresEventStore.d.ts +30 -0
  24. package/dist/PostgresEventStore.d.ts.map +1 -0
  25. package/dist/PostgresEventStore.js +144 -0
  26. package/dist/PostgresEventStore.js.map +1 -0
  27. package/dist/SqliteEventStore.d.ts +25 -0
  28. package/dist/SqliteEventStore.d.ts.map +1 -0
  29. package/dist/SqliteEventStore.js +123 -0
  30. package/dist/SqliteEventStore.js.map +1 -0
  31. package/dist/ViewModel.d.ts +90 -0
  32. package/dist/ViewModel.d.ts.map +1 -0
  33. package/dist/ViewModel.js +170 -0
  34. package/dist/ViewModel.js.map +1 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +9 -0
  38. package/dist/index.js.map +1 -0
  39. package/package.json +87 -3
  40. package/src/Aggregate.ts +133 -0
  41. package/src/AggregateService.ts +342 -0
  42. package/src/Checkpoints.ts +23 -0
  43. package/src/EventStore.ts +233 -0
  44. package/src/Fixture.ts +213 -0
  45. package/src/PostgresEventStore.ts +188 -0
  46. package/src/SqliteEventStore.ts +166 -0
  47. package/src/ViewModel.ts +348 -0
  48. package/src/index.ts +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-2026 David Nussio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,299 @@
1
- # Temporary Holding Version
1
+ # effect-cqrs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This is a trial port of an old project,
4
+ [moleculer-cqrs](https://github.com/davidnussio/moleculer-cqrs), to
5
+ [Effect](https://effect.website) v4. The original is a CQRS and event sourcing module
6
+ for [Moleculer](https://moleculer.services), written in 2019. This repository
7
+ rebuilds the same ideas on Effect to find out how well they carry over.
8
+
9
+ It's an experiment, not a maintained library: the API may change, and
10
+ [Limitations](#limitations) lists what the port leaves out.
11
+
12
+ The documentation, with a walkthrough of the bank demo, is at
13
+ https://davidnussio.github.io/effect-cqrs/.
14
+
15
+ Commands decide on events, events are the only source of truth, and read models
16
+ are projections you can throw away and rebuild.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add effect-cqrs effect
22
+ ```
23
+
24
+ It needs Node 24 or later and Effect 4. To store events in SQLite or PostgreSQL, add
25
+ that database's `effect/sql` driver too, `@effect/sql-sqlite-node` or `@effect/sql-pg`.
26
+
27
+ ## Develop
28
+
29
+ ```bash
30
+ pnpm install
31
+ pnpm test
32
+ pnpm example
33
+ ```
34
+
35
+ The example needs `node:sqlite` and runs TypeScript directly. `pnpm build` compiles
36
+ `src` into `dist`, which is what npm gets.
37
+
38
+ `pnpm ui` serves an interactive demo at http://localhost:5173, also published at
39
+ https://davidnussio.github.io/effect-cqrs/demo/. It runs the framework
40
+ in the browser on the in-memory store: a bank whose accounts are aggregates, three
41
+ view models, and a saga for transfers. Its devtools slow commands and projections
42
+ down, pause and step the command bus, crash a projection, scrub through the log and
43
+ replay view models up to a chosen event.
44
+
45
+ ## Define an aggregate
46
+
47
+ An aggregate is a schema of events, a projection folding them into state, and
48
+ commands deciding on new events.
49
+
50
+ ```ts
51
+ import { DateTime, Effect, Schema } from "effect"
52
+ import { Aggregate } from "effect-cqrs"
53
+
54
+ const TodoEvent = Schema.TaggedUnion({
55
+ TodoCreated: { title: Schema.NonEmptyString, createdAt: Schema.DateTimeUtc },
56
+ TodoDeleted: { deletedAt: Schema.DateTimeUtc }
57
+ })
58
+
59
+ class TodoAlreadyDeleted extends Schema.TaggedError<TodoAlreadyDeleted>()("TodoAlreadyDeleted", {}) {}
60
+
61
+ type TodoState = { readonly title: string; readonly deleted: boolean }
62
+
63
+ const Todo = Aggregate.make<TodoState>()({
64
+ name: "todo",
65
+ events: TodoEvent,
66
+ initial: { title: "", deleted: false },
67
+ projection: {
68
+ TodoCreated: (state, { title }) => ({ ...state, title }),
69
+ TodoDeleted: (state) => ({ ...state, deleted: true })
70
+ }
71
+ })
72
+ .command("createTodo", {
73
+ payload: Schema.Struct({ title: Schema.NonEmptyString }),
74
+ handler: Effect.fnUntraced(function*(_, { title }) {
75
+ return TodoEvent.cases.TodoCreated.make({ title, createdAt: yield* DateTime.now })
76
+ })
77
+ })
78
+ .command("deleteTodo", {
79
+ payload: Schema.Struct({}),
80
+ handler: Effect.fnUntraced(function*(state) {
81
+ if (state.deleted) return yield* new TodoAlreadyDeleted()
82
+ return TodoEvent.cases.TodoDeleted.make({ deletedAt: yield* DateTime.now })
83
+ })
84
+ })
85
+ ```
86
+
87
+ The projection must cover every event or it doesn't compile. A handler returns one
88
+ event, several, or `[]` to record nothing. It can use services, fail with typed
89
+ errors, and may run more than once, so it should only decide. Projections and
90
+ handlers must not mutate the state, which is kept between commands.
91
+
92
+ ## Run commands
93
+
94
+ ```ts
95
+ import { AggregateService, EventStore } from "effect-cqrs"
96
+
97
+ const program = Effect.gen(function*() {
98
+ const todos = yield* AggregateService.make(Todo)
99
+
100
+ // Typed call
101
+ yield* todos.commands.createTodo("todo-1", { title: "Buy Milk" })
102
+
103
+ // Untyped JSON from a transport, validated before running
104
+ yield* todos.dispatch({ aggregateId: "todo-2", type: "createTodo", payload: { title: "Buy Eggs" } })
105
+
106
+ yield* todos.readModel("todo-1")
107
+ yield* todos.readModel("todo-1", { finishTime }) // state as it was before finishTime
108
+ yield* todos.history("todo-1", { payload: true })
109
+ }).pipe(Effect.provide(EventStore.layerMemory))
110
+ ```
111
+
112
+ Commands on one stream take turns inside a service, so they can't race. A handler must
113
+ not run a command on its own stream, which would wait for itself. Across processes
114
+ writers use optimistic concurrency: a command that loses a race backs off, reloads the
115
+ state and decides again, up to `maxRetries` times (5 by default), then fails with
116
+ `ConcurrencyConflict`.
117
+
118
+ The service remembers the state of recent aggregates (`stateCacheSize`, 10,000 by
119
+ default), so a command only reads the events it hasn't seen.
120
+
121
+ ## Project into view models
122
+
123
+ A view model reads the event store from its own checkpoint, so it catches up after
124
+ downtime and can't miss an event. Delivery is at least once, so handlers should be
125
+ idempotent.
126
+
127
+ ```ts
128
+ import { Checkpoints, ViewModel } from "effect-cqrs"
129
+
130
+ const TodoList = ViewModel.make({
131
+ name: "todo-list",
132
+ aggregates: [Todo],
133
+ dispose: clearList, // runs before a replay
134
+ handlers: {
135
+ TodoCreated: ({ title }, { aggregateId }) => addToList(aggregateId, title),
136
+ TodoDeleted: (_, { aggregateId }) => removeFromList(aggregateId)
137
+ }
138
+ })
139
+
140
+ const program = Effect.gen(function*() {
141
+ const todos = yield* AggregateService.make(Todo)
142
+ const viewModels = yield* ViewModel.ViewModels
143
+
144
+ const { position } = yield* todos.commands.createTodo("todo-1", { title: "Buy Milk" })
145
+ yield* viewModels.caughtUp(position) // read your own write
146
+
147
+ yield* viewModels.replay({ viewModels: ["todo-list"] })
148
+ yield* viewModels.status
149
+ }).pipe(
150
+ Effect.provide(ViewModel.layer([TodoList]).pipe(
151
+ Layer.provideMerge(Layer.merge(EventStore.layerMemory, Checkpoints.layerMemory))
152
+ ))
153
+ )
154
+ ```
155
+
156
+ A failing handler stalls its view model on that event and retries with backoff, so
157
+ events are never skipped or reordered. `viewModels.status` exposes the failure.
158
+
159
+ Anything else that reacts to events can use `store.subscribe(filter)`, a stream of
160
+ past events followed by live ones.
161
+
162
+ ## Test an aggregate
163
+
164
+ `Fixture` runs commands through the real service on an in-memory store, so payload
165
+ validation and event encoding are tested too. It reads like `CQRSFixture`, and the
166
+ chain runs when you yield it or hand it to `it.effect`:
167
+
168
+ ```ts
169
+ import { assert, it } from "@effect/vitest"
170
+ import { Fixture } from "effect-cqrs"
171
+
172
+ it.effect("creates a todo", () =>
173
+ Fixture.make(Todo)
174
+ .givenEvents()
175
+ .when("createTodo", { title: "Buy Milk" })
176
+ .expectEvent(TodoEvent.cases.TodoCreated.make({ title: "Buy Milk", createdAt: DateTime.makeUnsafe(0) }))
177
+ .inspectState((state) => assert.strictEqual(state.title, "Buy Milk")))
178
+
179
+ it.effect("runs commands one after another", () =>
180
+ Fixture.make(Todo)
181
+ .givenEvents([created])
182
+ .when("deleteTodo", {})
183
+ .expectEvent("TodoDeleted")
184
+ .when("createTodo", { title: "Buy Eggs" })
185
+ .expectEvents(["TodoCreated"]))
186
+
187
+ it.effect("rejects deleting twice", () =>
188
+ Effect.gen(function*() {
189
+ const error = yield* Fixture.make(Todo)
190
+ .givenEvents([created, deleted])
191
+ .whenFails("deleteTodo", {})
192
+
193
+ assert.strictEqual(error._tag, "TodoAlreadyDeleted")
194
+ }))
195
+ ```
196
+
197
+ - `when` runs a command by name, typed against the aggregate. Commands that fail
198
+ stop the chain with their typed error.
199
+ - `expectEvent` expects exactly one event and `expectEvents` an exact, ordered list,
200
+ so `expectEvents([])` asserts that nothing was recorded. Each entry is a whole
201
+ event, compared with the aggregate's schema, or only its tag.
202
+ - `inspectState` takes a callback that throws to fail the test, so any assertion
203
+ library works.
204
+ - Yielding the chain gives `{ events, state, version, fixture }` for the last
205
+ command, if you prefer plain assertions.
206
+
207
+ `it.effect` runs on a test clock stopped at zero, so timestamps need no mocking: an
208
+ event created by `DateTime.now` has `DateTime.makeUnsafe(0)` until
209
+ `TestClock.adjust` moves time.
210
+
211
+ ## Storage
212
+
213
+ `EventStore.layerMemory` keeps events in memory. For SQLite, provide any
214
+ `effect/sql` SQLite client:
215
+
216
+ ```ts
217
+ import { SqliteClient } from "@effect/sql-sqlite-node"
218
+ import { SqliteEventStore } from "effect-cqrs"
219
+
220
+ const Storage = Layer.merge(SqliteEventStore.layer(), SqliteEventStore.layerCheckpoints()).pipe(
221
+ Layer.provide(SqliteClient.layer({ filename: "./data/events.sqlite" }))
222
+ )
223
+ ```
224
+
225
+ Tables are created on startup. Use durable checkpoints only with a durable view
226
+ model: an in-memory view model paired with `Checkpoints.layerMemory` rebuilds itself
227
+ on every start, which is usually what you want.
228
+
229
+ For PostgreSQL, provide an `effect/sql` PostgreSQL client:
230
+
231
+ ```ts
232
+ import { PgClient } from "@effect/sql-pg"
233
+ import { PostgresEventStore } from "effect-cqrs"
234
+
235
+ const Storage = Layer.merge(PostgresEventStore.layer(), PostgresEventStore.layerCheckpoints()).pipe(
236
+ Layer.provide(PgClient.layer({ url: Redacted.make("postgres://localhost:5432/cqrs") }))
237
+ )
238
+ ```
239
+
240
+ Writers wait for each other at one step of each commit, which keeps positions in commit
241
+ order. Several processes can write to one database, but a view model only wakes on
242
+ appends from its own process. After a bulk load, run `ANALYZE`, or reads for view models
243
+ are slow until autovacuum gets to the table.
244
+
245
+ To run the tests on Postgres, `docker compose up -d --wait` starts one, and setting
246
+ `POSTGRES_URL=postgres://postgres:postgres@localhost:5432/cqrs` includes it in
247
+ `pnpm test`.
248
+
249
+ Another database needs an `EventStore.Backend`: `append`, `readBatch` and `head`.
250
+ Positions must follow commit order, so a backend on a database with concurrent
251
+ writers has to guarantee that itself.
252
+
253
+ ## Performance
254
+
255
+ On a SQLite file a command takes about 0.1 ms, however long its stream is and however
256
+ many events the store holds, and a million events replay in under 5 seconds. On Postgres
257
+ a command takes about 1 ms. The first use of a stream after a restart replays it, at
258
+ about 3 µs per event. See the [performance and stress report](bench/REPORT.md) for the
259
+ numbers and the remaining limits.
260
+
261
+ `pnpm bench` measures each operation and `pnpm stress` loads the framework while
262
+ checking that nothing is lost.
263
+
264
+ ## Coming from moleculer-cqrs
265
+
266
+ | moleculer-cqrs | effect-cqrs |
267
+ | --- | --- |
268
+ | `CQRSEventSourcing({ aggregate })` mixin | `AggregateService.make(aggregate)` |
269
+ | `call todo.command` | `todos.commands.x(id, payload)` or `todos.dispatch(json)` |
270
+ | `call todo.read-model` | `todos.readModel(id, { finishTime })` |
271
+ | `call todo.history` | `todos.history(id, { payload, startTime, finishTime })` |
272
+ | `call todo.replay` | `viewModels.replay({ viewModels, startTime, finishTime })` |
273
+ | view model service with `events` | `ViewModel.make({ handlers, dispose })` |
274
+ | `new CQRSFixture(aggregate)` | `Fixture.make(aggregate)` |
275
+ | `.givenEvents([...])` | `.givenEvents([...])` |
276
+ | `.when(createNews, payload)` | `.when("createNews", payload)` |
277
+ | `.expectEvent(event)` | `.expectEvent(event)` or `.expectEvent("Tag")` |
278
+ | `.inspectState(cb)` | `.inspectState(cb)` |
279
+ | `.whenThrow(command).toThrow(...)` | `.whenFails(command, payload)`, which returns the error |
280
+ | `fastest-validator` | `Schema` |
281
+ | `throw` in a command | typed errors |
282
+ | `resolve-storage-*` adapters | `EventStore.Backend` |
283
+
284
+ The largest change is on the read side. moleculer-cqrs broadcasts events over the
285
+ broker and only replay recovers one that was lost; here view models pull from the
286
+ store, so `replay` is for rebuilding, not for recovery.
287
+
288
+ ## Limitations
289
+
290
+ - **New events stay in their process.** They wake subscribers in the same process only.
291
+ There is no transport equivalent to the Moleculer broker, so `dispatch` is the entry
292
+ point to wire into your own HTTP or RPC layer.
293
+ - **SQLite, PostgreSQL and memory only.** There are no adapters for MongoDB or MySQL.
294
+ - **No code generator.** There is no equivalent of `cqrs generate`.
295
+ - **No persistent snapshots.** Recent state is only kept in memory, so after a restart
296
+ each stream is replayed the first time it is used, at a cost that grows with its
297
+ length.
298
+ - **No schema migration.** A stored event that no longer decodes fails with
299
+ `EventDecodeError`, so evolve event schemas compatibly.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The write side of the domain: a schema of events, a projection that folds them
3
+ * into state, and commands that inspect that state and decide on new events.
4
+ *
5
+ * An aggregate is plain data. {@link AggregateService} gives it a store to run
6
+ * against, and `Fixture` runs it in tests.
7
+ */
8
+ import type { DateTime, Effect, Schema } from "effect";
9
+ export interface Tagged {
10
+ readonly _tag: string;
11
+ }
12
+ export type EventSchema<Event extends Tagged = Tagged> = Schema.Top & {
13
+ readonly Type: Event;
14
+ readonly DecodingServices: never;
15
+ readonly EncodingServices: never;
16
+ };
17
+ export type PayloadSchema = Schema.Top & {
18
+ readonly DecodingServices: never;
19
+ readonly EncodingServices: never;
20
+ };
21
+ /** Where an event sits in its stream and in the store. */
22
+ export interface EventMeta {
23
+ readonly aggregateName: string;
24
+ readonly aggregateId: string;
25
+ readonly version: number;
26
+ readonly position: number;
27
+ readonly timestamp: DateTime.Utc;
28
+ }
29
+ export interface CommandContext {
30
+ readonly aggregateId: string;
31
+ readonly version: number;
32
+ }
33
+ /** Exhaustive by event tag, so adding an event to the schema without folding it does not compile. */
34
+ export type Projection<State, Event extends Tagged> = {
35
+ readonly [Tag in Event["_tag"]]: (state: State, event: Extract<Event, {
36
+ readonly _tag: Tag;
37
+ }>, meta: EventMeta) => State;
38
+ };
39
+ /**
40
+ * Decides on the events a command produces. It may run several times when
41
+ * concurrent writers collide, so it should only decide and leave side effects to
42
+ * view models. Returning an empty array records nothing.
43
+ */
44
+ export type CommandHandler<State, Payload, Event, E, R> = (state: State, payload: Payload, context: CommandContext) => Effect.Effect<Event | ReadonlyArray<Event>, E, R>;
45
+ export interface Command<Payload extends PayloadSchema, State, Event, E, R> {
46
+ readonly payload: Payload;
47
+ readonly handler: CommandHandler<State, Payload["Type"], Event, E, R>;
48
+ }
49
+ export interface AnyCommand {
50
+ readonly payload: PayloadSchema;
51
+ readonly handler: CommandHandler<any, any, any, any, any>;
52
+ }
53
+ export interface Aggregate<Name extends string, State, Events extends EventSchema<any>, Commands extends Record<string, AnyCommand>> {
54
+ readonly name: Name;
55
+ readonly events: Events;
56
+ readonly initial: State;
57
+ readonly projection: Projection<State, Events["Type"]>;
58
+ readonly commands: Commands;
59
+ /** Returns a new aggregate that also accepts this command. */
60
+ command<const CommandName extends string, Payload extends PayloadSchema, E = never, R = never>(name: CommandName, options: {
61
+ readonly payload: Payload;
62
+ readonly handler: CommandHandler<State, Payload["Type"], Events["Type"], E, R>;
63
+ }): Aggregate<Name, State, Events, Commands & Record<CommandName, Command<Payload, State, Events["Type"], E, R>>>;
64
+ }
65
+ export type Any = Aggregate<string, any, EventSchema<any>, Record<string, AnyCommand>>;
66
+ export type Name<A extends Any> = A["name"];
67
+ export type State<A extends Any> = A["initial"];
68
+ export type Event<A extends Any> = A["events"]["Type"];
69
+ export type Commands<A extends Any> = A["commands"];
70
+ export type CommandName<A extends Any> = keyof A["commands"] & string;
71
+ export type Payload<A extends Any, K extends CommandName<A>> = A["commands"][K]["payload"]["Type"];
72
+ export type CommandError<A extends Any, K extends CommandName<A> = CommandName<A>> = A["commands"][K] extends Command<any, any, any, infer E, any> ? E : never;
73
+ export type Context<A extends Any> = A["commands"][CommandName<A>] extends Command<any, any, any, any, infer R> ? R : never;
74
+ /**
75
+ * The state is given explicitly, `Aggregate.make<State>()({ ... })`, so that the
76
+ * projection's return values are checked against it rather than widened.
77
+ */
78
+ export declare const make: <State>() => <const Name extends string, Events extends EventSchema<any>>(options: {
79
+ readonly name: Name;
80
+ readonly events: Events;
81
+ readonly initial: State;
82
+ readonly projection: Projection<State, Events["Type"]>;
83
+ }) => Aggregate<Name, State, Events, {}>;
84
+ /** Folds one event into the state. */
85
+ export declare const evolve: <A extends Any>(aggregate: A, state: State<A>, event: Event<A>, meta: EventMeta) => State<A>;
86
+ //# sourceMappingURL=Aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Aggregate.d.ts","sourceRoot":"","sources":["../src/Aggregate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ const build = (definition) => ({
2
+ ...definition,
3
+ command: (name, options) => build({
4
+ ...definition,
5
+ commands: { ...definition.commands, [name]: options }
6
+ })
7
+ });
8
+ /**
9
+ * The state is given explicitly, `Aggregate.make<State>()({ ... })`, so that the
10
+ * projection's return values are checked against it rather than widened.
11
+ */
12
+ export const make = () => (options) => build({ ...options, commands: {} });
13
+ /** Folds one event into the state. */
14
+ export const evolve = (aggregate, state, event, meta) => aggregate.projection[event._tag](state, event, meta);
15
+ //# sourceMappingURL=Aggregate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Aggregate.js","sourceRoot":"","sources":["../src/Aggregate.ts"],"names":[],"mappings":"AAkGA,MAAM,KAAK,GAAG,CAMZ,UAAqH,EAC3E,EAAE,CAAC,CAAC;IAC9C,GAAG,UAAU;IACb,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CACzB,KAAK,CAAC;QACJ,GAAG,UAAU;QACb,QAAQ,EAAE,EAAE,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAS;KAC7D,CAAC;CACL,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAAU,EAAE,CAChC,CAA6D,OAK5D,EAAsC,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAA;AAE7E,sCAAsC;AACtC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAgB,SAAY,EAAE,KAAe,EAAE,KAAe,EAAE,IAAe,EAAY,EAAE,CAChH,SAAS,CAAC,UAA8F,CAAC,KAAK,CAAC,IAAI,CAAE,CACpH,KAAK,EACL,KAAK,EACL,IAAI,CACL,CAAA"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Runs an aggregate against the {@link EventStore}: the `command`, `read-model`
3
+ * and `history` actions of moleculer-cqrs, as a typed service.
4
+ */
5
+ import { type DateTime, Effect, Schema } from "effect";
6
+ import * as Aggregate from "./Aggregate.ts";
7
+ import { ConcurrencyConflict, EventStore, type EventStoreError, type StoredEvent } from "./EventStore.ts";
8
+ declare const UnknownCommand_base: Schema.Class<UnknownCommand, Schema.TaggedStruct<"UnknownCommand", {
9
+ readonly aggregateName: Schema.String;
10
+ readonly type: Schema.String;
11
+ }>, import("effect/Cause").YieldableError>;
12
+ export declare class UnknownCommand extends UnknownCommand_base {
13
+ }
14
+ declare const InvalidCommand_base: Schema.Class<InvalidCommand, Schema.TaggedStruct<"InvalidCommand", {
15
+ readonly aggregateName: Schema.String;
16
+ readonly type: Schema.String;
17
+ readonly message: Schema.String;
18
+ }>, import("effect/Cause").YieldableError>;
19
+ export declare class InvalidCommand extends InvalidCommand_base {
20
+ }
21
+ declare const EventDecodeError_base: Schema.Class<EventDecodeError, Schema.TaggedStruct<"EventDecodeError", {
22
+ readonly aggregateName: Schema.String;
23
+ readonly aggregateId: Schema.String;
24
+ readonly version: Schema.Int;
25
+ readonly type: Schema.String;
26
+ readonly message: Schema.String;
27
+ }>, import("effect/Cause").YieldableError>;
28
+ /** A stored event no longer matches the schema, which means the data needs a migration. */
29
+ export declare class EventDecodeError extends EventDecodeError_base {
30
+ }
31
+ export type ReadError = EventStoreError | EventDecodeError;
32
+ /** How a command can fail, on top of the errors its own handler declares. */
33
+ export type CommandFailure = ConcurrencyConflict | InvalidCommand | ReadError;
34
+ export interface CommandResult<Event> {
35
+ readonly aggregateName: string;
36
+ readonly aggregateId: string;
37
+ /** The stream version after the command. */
38
+ readonly version: number;
39
+ /** The store position of the last event, to pass to `ViewModels.caughtUp`. */
40
+ readonly position: number;
41
+ readonly events: ReadonlyArray<Event>;
42
+ }
43
+ export interface HistoryEntry<Event> {
44
+ readonly version: number;
45
+ readonly timestamp: DateTime.Utc;
46
+ readonly type: string;
47
+ readonly payload?: Event;
48
+ }
49
+ export interface AggregateService<A extends Aggregate.Any> {
50
+ readonly aggregate: A;
51
+ /** One typed function per command. */
52
+ readonly commands: {
53
+ readonly [K in Aggregate.CommandName<A>]: (aggregateId: string, payload: Aggregate.Payload<A, K>) => Effect.Effect<CommandResult<Aggregate.Event<A>>, Aggregate.CommandError<A, K> | CommandFailure>;
54
+ };
55
+ /** Runs a `{ aggregateId, type, payload }` envelope received as JSON from outside the process. */
56
+ readonly dispatch: (input: unknown) => Effect.Effect<CommandResult<Aggregate.Event<A>>, Aggregate.CommandError<A> | CommandFailure | UnknownCommand>;
57
+ /** Materializes the state on the fly, optionally as it was before `finishTime`. */
58
+ readonly readModel: (aggregateId: string, options?: {
59
+ readonly finishTime?: DateTime.Utc | undefined;
60
+ }) => Effect.Effect<Aggregate.State<A>, ReadError>;
61
+ /** Lists the events of one aggregate, with their decoded payload when `payload` is set. */
62
+ readonly history: (aggregateId: string, options?: {
63
+ readonly payload?: boolean | undefined;
64
+ readonly startTime?: DateTime.Utc | undefined;
65
+ readonly finishTime?: DateTime.Utc | undefined;
66
+ }) => Effect.Effect<ReadonlyArray<HistoryEntry<Aggregate.Event<A>>>, ReadError>;
67
+ }
68
+ /** Decodes a stored event with the schema of the aggregate it belongs to. */
69
+ export declare const makeEventDecoder: <A extends Aggregate.Any>(aggregate: A) => (stored: StoredEvent) => Effect.Effect<Aggregate.Event<A>, EventDecodeError>;
70
+ /** Encodes events into the JSON form the store keeps. */
71
+ export declare const makeEventEncoder: <A extends Aggregate.Any>(aggregate: A) => (event: Aggregate.Event<A>) => Effect.Effect<{
72
+ type: any;
73
+ payload: Schema.Json;
74
+ }, never, never>;
75
+ export declare const make: <A extends Aggregate.Any>(aggregate: A, options?: {
76
+ /** How many times a command is re-decided after losing a race. Defaults to 5. */
77
+ readonly maxRetries?: number;
78
+ /** How many aggregates keep their state in memory between commands. Defaults to 10,000. */
79
+ readonly stateCacheSize?: number;
80
+ } | undefined) => Effect.Effect<AggregateService<A>, never, EventStore | Aggregate.Context<A>>;
81
+ export {};
82
+ //# sourceMappingURL=AggregateService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AggregateService.d.ts","sourceRoot":"","sources":["../src/AggregateService.ts"],"names":[],"mappings":"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"}