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
@@ -0,0 +1,181 @@
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 { Array as Arr, Effect, Schedule, Schema, Semaphore, Stream } from "effect";
6
+ import * as Aggregate from "./Aggregate.js";
7
+ import { ConcurrencyConflict, EventStore } from "./EventStore.js";
8
+ export class UnknownCommand extends Schema.TaggedError()("UnknownCommand", {
9
+ aggregateName: Schema.String,
10
+ type: Schema.String
11
+ }) {
12
+ }
13
+ export class InvalidCommand extends Schema.TaggedError()("InvalidCommand", {
14
+ aggregateName: Schema.String,
15
+ type: Schema.String,
16
+ message: Schema.String
17
+ }) {
18
+ }
19
+ /** A stored event no longer matches the schema, which means the data needs a migration. */
20
+ export class EventDecodeError extends Schema.TaggedError()("EventDecodeError", {
21
+ aggregateName: Schema.String,
22
+ aggregateId: Schema.String,
23
+ version: Schema.Int,
24
+ type: Schema.String,
25
+ message: Schema.String
26
+ }) {
27
+ }
28
+ const CommandEnvelope = Schema.Struct({
29
+ aggregateId: Schema.NonEmptyString,
30
+ type: Schema.String,
31
+ payload: Schema.Unknown
32
+ });
33
+ /** Decodes a stored event with the schema of the aggregate it belongs to. */
34
+ export const makeEventDecoder = (aggregate) => {
35
+ const decode = Schema.decodeUnknownEffect(Schema.toCodecJson(aggregate.events));
36
+ return (stored) => decode(stored.payload).pipe(Effect.mapError((error) => new EventDecodeError({
37
+ aggregateName: stored.aggregateName,
38
+ aggregateId: stored.aggregateId,
39
+ version: stored.version,
40
+ type: stored.type,
41
+ message: error.message
42
+ })));
43
+ };
44
+ /** Encodes events into the JSON form the store keeps. */
45
+ export const makeEventEncoder = (aggregate) => {
46
+ const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(aggregate.events));
47
+ return (event) => encode(event).pipe(
48
+ // Events come from the aggregate's own handlers, so this is a programming error.
49
+ Effect.orDie, Effect.map((payload) => ({ type: event._tag, payload: payload })));
50
+ };
51
+ /** Remembers recent states, dropping the least recently used. A `Map` iterates in insertion order. */
52
+ const makeStateCache = (capacity) => {
53
+ const entries = new Map();
54
+ return {
55
+ get: (aggregateId) => {
56
+ const entry = entries.get(aggregateId);
57
+ if (entry !== undefined) {
58
+ entries.delete(aggregateId);
59
+ entries.set(aggregateId, entry);
60
+ }
61
+ return entry;
62
+ },
63
+ set: (aggregateId, loaded) => {
64
+ if (capacity === 0 || (entries.get(aggregateId)?.position ?? 0) > loaded.position)
65
+ return;
66
+ entries.delete(aggregateId);
67
+ entries.set(aggregateId, loaded);
68
+ if (entries.size > capacity)
69
+ entries.delete(entries.keys().next().value);
70
+ }
71
+ };
72
+ };
73
+ /** Runs effects with the same key one at a time. A key only exists while it is in use. */
74
+ const makeKeyedLock = () => {
75
+ const locks = new Map();
76
+ return (key) => (effect) => Effect.acquireUseRelease(Effect.sync(() => {
77
+ const lock = locks.get(key) ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 };
78
+ lock.users++;
79
+ locks.set(key, lock);
80
+ return lock;
81
+ }), (lock) => lock.semaphore.withPermit(effect), (lock) => Effect.sync(() => {
82
+ if (--lock.users === 0)
83
+ locks.delete(key);
84
+ }));
85
+ };
86
+ /**
87
+ * Gives other fibers a turn once in a while. `Effect.forEach` with `concurrency`
88
+ * carries on as long as items don't suspend, and a store on `node:sqlite` never does,
89
+ * so a bulk run would finish before any view model ran. Yielding per command is
90
+ * expensive, so it only happens when a millisecond has passed, using real time
91
+ * because this is about the CPU. The clock restarts after the yield rather than
92
+ * before it, so commands started meanwhile suspend too and the loop lets go.
93
+ */
94
+ const makeTimeSlice = () => {
95
+ let started = performance.now();
96
+ return Effect.suspend(() => performance.now() - started < 1
97
+ ? Effect.void
98
+ : Effect.andThen(Effect.yieldNow, Effect.sync(() => void (started = performance.now()))));
99
+ };
100
+ // Only a rival outside this service can cause a conflict, so give it room to finish.
101
+ const conflictBackoff = Schedule.exponential("10 millis").pipe(Schedule.jittered);
102
+ export const make = Effect.fnUntraced(function* (aggregate, options) {
103
+ const store = yield* EventStore;
104
+ const context = yield* Effect.context();
105
+ const maxRetries = options?.maxRetries ?? 5;
106
+ const aggregateName = aggregate.name;
107
+ const decodeEvent = makeEventDecoder(aggregate);
108
+ const encodeEvent = makeEventEncoder(aggregate);
109
+ const states = makeStateCache(options?.stateCacheSize ?? 10_000);
110
+ const lockStream = makeKeyedLock();
111
+ const timeSlice = makeTimeSlice();
112
+ const replay = (stream, initial, step) => stream.pipe(Stream.runFoldEffect(() => initial, (acc, stored) => Effect.map(decodeEvent(stored), (event) => step(acc, event, stored))));
113
+ const streamOf = (aggregateId, range) => store.read({ aggregateNames: [aggregateName], aggregateIds: [aggregateId], ...range });
114
+ const replayFrom = (start, aggregateId, finishTime) => replay(streamOf(aggregateId, { finishTime, afterPosition: start.position, afterVersion: start.version }), start, (acc, event, stored) => ({
115
+ state: Aggregate.evolve(aggregate, acc.state, event, stored),
116
+ version: stored.version,
117
+ position: stored.position
118
+ }));
119
+ const initial = { state: aggregate.initial, version: 0, position: 0 };
120
+ // Streams only grow, so a remembered state plus what came after it is the current
121
+ // state, even if another process wrote those events.
122
+ const load = (aggregateId, finishTime) => finishTime === undefined
123
+ ? replayFrom(states.get(aggregateId) ?? initial, aggregateId).pipe(Effect.tap((loaded) => Effect.sync(() => states.set(aggregateId, loaded))))
124
+ : replayFrom(initial, aggregateId, finishTime);
125
+ const decide = Effect.fnUntraced(function* (type, aggregateId, payload) {
126
+ const { position, state, version } = yield* load(aggregateId);
127
+ const { handler } = aggregate.commands[type];
128
+ const events = Arr.ensure(yield* handler(state, payload, { aggregateId, version }));
129
+ if (events.length === 0) {
130
+ return { aggregateName, aggregateId, version, position, events };
131
+ }
132
+ const stored = yield* store.append({
133
+ aggregateName,
134
+ aggregateId,
135
+ expectedVersion: version,
136
+ events: yield* Effect.forEach(events, encodeEvent)
137
+ });
138
+ const last = stored[stored.length - 1];
139
+ return { aggregateName, aggregateId, version: last.version, position: last.position, events };
140
+ });
141
+ // Commands on one stream take turns, so they can't race each other and only a
142
+ // rival in another process causes a conflict. A handler must not run a command on
143
+ // its own stream, which would wait for itself.
144
+ const execute = (type, aggregateId, payload) => decide(type, aggregateId, payload).pipe(Effect.retry({
145
+ times: maxRetries,
146
+ while: (error) => error instanceof ConcurrencyConflict,
147
+ schedule: conflictBackoff
148
+ }), Effect.provideContext(context), lockStream(aggregateId), (decided) => Effect.andThen(timeSlice, decided), Effect.withSpan(`${aggregateName}.${type}`, { attributes: { "aggregate.id": aggregateId } }));
149
+ const decodePayload = (type, schema, input) => Schema.decodeUnknownEffect(schema)(input).pipe(Effect.mapError((error) => new InvalidCommand({ aggregateName, type, message: error.message })));
150
+ const commands = Object.fromEntries(Object.entries(aggregate.commands).map(([type, command]) => [
151
+ type,
152
+ (aggregateId, payload) => decodePayload(type, Schema.toType(command.payload), payload).pipe(Effect.flatMap((decoded) => execute(type, aggregateId, decoded)))
153
+ ]));
154
+ const decodeEnvelope = Schema.decodeUnknownEffect(CommandEnvelope);
155
+ const dispatch = (input) => Effect.gen(function* () {
156
+ const envelope = yield* decodeEnvelope(input).pipe(Effect.mapError((error) => new InvalidCommand({ aggregateName, type: "unknown", message: error.message })));
157
+ if (!Object.hasOwn(aggregate.commands, envelope.type)) {
158
+ return yield* new UnknownCommand({ aggregateName, type: envelope.type });
159
+ }
160
+ const payload = yield* decodePayload(envelope.type, Schema.toCodecJson(aggregate.commands[envelope.type].payload), envelope.payload);
161
+ return yield* execute(envelope.type, envelope.aggregateId, payload);
162
+ });
163
+ const readModel = (aggregateId, options) => load(aggregateId, options?.finishTime).pipe(Effect.map(({ state }) => state), Effect.withSpan(`${aggregateName}.readModel`, { attributes: { "aggregate.id": aggregateId } }));
164
+ const history = (aggregateId, options) => replay(streamOf(aggregateId, { startTime: options?.startTime, finishTime: options?.finishTime }), [], (entries, event, stored) => {
165
+ entries.push({
166
+ version: stored.version,
167
+ timestamp: stored.timestamp,
168
+ type: stored.type,
169
+ ...(options?.payload === true ? { payload: event } : {})
170
+ });
171
+ return entries;
172
+ }).pipe(Effect.withSpan(`${aggregateName}.history`, { attributes: { "aggregate.id": aggregateId } }));
173
+ return {
174
+ aggregate,
175
+ commands,
176
+ dispatch,
177
+ readModel,
178
+ history
179
+ };
180
+ });
181
+ //# sourceMappingURL=AggregateService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AggregateService.js","sourceRoot":"","sources":["../src/AggregateService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Remembers how far each view model has read.
3
+ *
4
+ * A checkpoint has to live as long as the view model it tracks: pair an
5
+ * in-memory view model with {@link layerMemory} so it rebuilds from the event
6
+ * store on restart, and a durable one with a durable checkpoint store.
7
+ */
8
+ import { Context, Effect, Layer } from "effect";
9
+ import type { EventStoreError } from "./EventStore.ts";
10
+ declare const Checkpoints_base: Context.ServiceClass<Checkpoints, "effect-cqrs/Checkpoints", {
11
+ /** The last processed position, `0` when the view model never ran. */
12
+ get(viewModel: string): Effect.Effect<number, EventStoreError>;
13
+ set(viewModel: string, position: number): Effect.Effect<void, EventStoreError>;
14
+ }>;
15
+ export declare class Checkpoints extends Checkpoints_base {
16
+ }
17
+ export declare const layerMemory: Layer.Layer<Checkpoints>;
18
+ export {};
19
+ //# sourceMappingURL=Checkpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Checkpoints.d.ts","sourceRoot":"","sources":["../src/Checkpoints.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;;IAGpD,sEAAsE;mBACvD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC;mBAC/C,MAAM,YAAY,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,CAAC;;AAHhF,qBAAa,WAAY,SAAQ,gBAIF;CAAG;AAElC,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,CAM/C,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Remembers how far each view model has read.
3
+ *
4
+ * A checkpoint has to live as long as the view model it tracks: pair an
5
+ * in-memory view model with {@link layerMemory} so it rebuilds from the event
6
+ * store on restart, and a durable one with a durable checkpoint store.
7
+ */
8
+ import { Context, Effect, Layer } from "effect";
9
+ export class Checkpoints extends Context.Service()("effect-cqrs/Checkpoints") {
10
+ }
11
+ export const layerMemory = Layer.sync(Checkpoints, () => {
12
+ const positions = new Map();
13
+ return Checkpoints.of({
14
+ get: (viewModel) => Effect.sync(() => positions.get(viewModel) ?? 0),
15
+ set: (viewModel, position) => Effect.sync(() => void positions.set(viewModel, position))
16
+ });
17
+ });
18
+ //# sourceMappingURL=Checkpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Checkpoints.js","sourceRoot":"","sources":["../src/Checkpoints.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAG/C,MAAM,OAAO,WAAY,SAAQ,OAAO,CAAC,OAAO,EAI5C,CAAC,yBAAyB,CAAC;CAAG;AAElC,MAAM,CAAC,MAAM,WAAW,GAA6B,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE;IAChF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC3C,OAAO,WAAW,CAAC,EAAE,CAAC;QACpB,GAAG,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACpE,GAAG,EAAE,CAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;KACzF,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The append-only log every aggregate and view model is built on.
3
+ *
4
+ * Each event belongs to a stream, identified by aggregate name and id, and has
5
+ * two coordinates: a `version` that orders it inside its stream and a `position`
6
+ * that orders it across the whole store. Writers use the version for optimistic
7
+ * concurrency, readers use the position as a durable cursor.
8
+ */
9
+ import { Context, DateTime, Effect, Layer, Schema, Stream } from "effect";
10
+ declare const EventStoreError_base: Schema.Class<EventStoreError, Schema.TaggedStruct<"EventStoreError", {
11
+ readonly operation: Schema.String;
12
+ readonly cause: Schema.Defect;
13
+ }>, import("effect/Cause").YieldableError>;
14
+ export declare class EventStoreError extends EventStoreError_base {
15
+ }
16
+ declare const ConcurrencyConflict_base: Schema.Class<ConcurrencyConflict, Schema.TaggedStruct<"ConcurrencyConflict", {
17
+ readonly aggregateName: Schema.String;
18
+ readonly aggregateId: Schema.String;
19
+ readonly expectedVersion: Schema.Int;
20
+ }>, import("effect/Cause").YieldableError>;
21
+ export declare class ConcurrencyConflict extends ConcurrencyConflict_base {
22
+ }
23
+ export declare const StoredEvent: Schema.Struct<{
24
+ readonly position: Schema.Int;
25
+ readonly aggregateName: Schema.String;
26
+ readonly aggregateId: Schema.String;
27
+ readonly version: Schema.Int;
28
+ readonly type: Schema.String;
29
+ readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
30
+ readonly timestamp: Schema.DateTimeUtcFromMillis;
31
+ }>;
32
+ export type StoredEvent = typeof StoredEvent.Type;
33
+ export interface NewEvent {
34
+ readonly type: string;
35
+ readonly payload: Schema.Json;
36
+ }
37
+ export interface AppendInput {
38
+ readonly aggregateName: string;
39
+ readonly aggregateId: string;
40
+ /** The stream version the writer based its decision on, `0` for a new stream. */
41
+ readonly expectedVersion: number;
42
+ readonly events: ReadonlyArray<NewEvent>;
43
+ }
44
+ /**
45
+ * Selects events by stream, type and time. Omitted fields match everything.
46
+ * `startTime` is inclusive and `finishTime` is exclusive, so adding one
47
+ * millisecond to a history timestamp selects up to and including that event.
48
+ */
49
+ export interface EventFilter {
50
+ readonly aggregateNames?: ReadonlyArray<string> | undefined;
51
+ readonly aggregateIds?: ReadonlyArray<string> | undefined;
52
+ readonly eventTypes?: ReadonlyArray<string> | undefined;
53
+ readonly startTime?: DateTime.Utc | undefined;
54
+ readonly finishTime?: DateTime.Utc | undefined;
55
+ readonly afterPosition?: number | undefined;
56
+ /**
57
+ * For a filter on one stream: where to continue in it. Versions follow the stream's
58
+ * own index, so resuming there costs the same however far the log has moved on.
59
+ */
60
+ readonly afterVersion?: number | undefined;
61
+ }
62
+ /** A filter on exactly one stream, which a store can read through that stream's index. */
63
+ export declare const isOneStream: ({ aggregateIds, aggregateNames }: EventFilter) => boolean;
64
+ declare const EventStore_base: Context.ServiceClass<EventStore, "effect-cqrs/EventStore", {
65
+ /** Appends atomically, failing if the stream moved past `expectedVersion`. */
66
+ append(input: AppendInput): Effect.Effect<ReadonlyArray<StoredEvent>, ConcurrencyConflict | EventStoreError>;
67
+ /** Reads the events stored so far, ordered by position, then completes. */
68
+ read(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
69
+ /** Reads the events stored so far, then keeps following new ones. Never completes. */
70
+ subscribe(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
71
+ /** The position of the latest event, `0` when the store is empty. */
72
+ readonly head: Effect.Effect<number, EventStoreError>;
73
+ /**
74
+ * Emits the current latest position, then each newly appended one. Consumers
75
+ * can treat an element as "there may be something new": appends from other
76
+ * processes are not observed.
77
+ */
78
+ readonly changes: Stream.Stream<number>;
79
+ }>;
80
+ export declare class EventStore extends EventStore_base {
81
+ }
82
+ /** What a storage adapter implements. {@link make} builds the rest on top. */
83
+ export interface Backend {
84
+ readonly append: EventStore["Service"]["append"];
85
+ readonly readBatch: (filter: EventFilter, afterPosition: number, limit: number) => Effect.Effect<ReadonlyArray<StoredEvent>, EventStoreError>;
86
+ readonly head: Effect.Effect<number, EventStoreError>;
87
+ }
88
+ export declare const make: (backend: Backend, options?: {
89
+ readonly batchSize?: number;
90
+ } | undefined) => Effect.Effect<{
91
+ /** Appends atomically, failing if the stream moved past `expectedVersion`. */
92
+ append(input: AppendInput): Effect.Effect<ReadonlyArray<StoredEvent>, ConcurrencyConflict | EventStoreError>;
93
+ /** Reads the events stored so far, ordered by position, then completes. */
94
+ read(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
95
+ /** Reads the events stored so far, then keeps following new ones. Never completes. */
96
+ subscribe(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
97
+ /** The position of the latest event, `0` when the store is empty. */
98
+ readonly head: Effect.Effect<number, EventStoreError>;
99
+ /**
100
+ * Emits the current latest position, then each newly appended one. Consumers
101
+ * can treat an element as "there may be something new": appends from other
102
+ * processes are not observed.
103
+ */
104
+ readonly changes: Stream.Stream<number>;
105
+ }, EventStoreError, never>;
106
+ export declare const makeMemory: Effect.Effect<EventStore["Service"], EventStoreError>;
107
+ /** Keeps events in process memory. They do not survive a restart. */
108
+ export declare const layerMemory: Layer.Layer<EventStore, EventStoreError>;
109
+ export {};
110
+ //# sourceMappingURL=EventStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventStore.d.ts","sourceRoot":"","sources":["../src/EventStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The append-only log every aggregate and view model is built on.
3
+ *
4
+ * Each event belongs to a stream, identified by aggregate name and id, and has
5
+ * two coordinates: a `version` that orders it inside its stream and a `position`
6
+ * that orders it across the whole store. Writers use the version for optimistic
7
+ * concurrency, readers use the position as a durable cursor.
8
+ */
9
+ import { Context, DateTime, Effect, Layer, Option, Ref, Schema, Stream, SubscriptionRef } from "effect";
10
+ export class EventStoreError extends Schema.TaggedError()("EventStoreError", {
11
+ operation: Schema.String,
12
+ cause: Schema.Defect()
13
+ }) {
14
+ }
15
+ export class ConcurrencyConflict extends Schema.TaggedError()("ConcurrencyConflict", {
16
+ aggregateName: Schema.String,
17
+ aggregateId: Schema.String,
18
+ expectedVersion: Schema.Int
19
+ }) {
20
+ }
21
+ export const StoredEvent = Schema.Struct({
22
+ position: Schema.Int,
23
+ aggregateName: Schema.String,
24
+ aggregateId: Schema.String,
25
+ version: Schema.Int,
26
+ type: Schema.String,
27
+ payload: Schema.Json,
28
+ timestamp: Schema.DateTimeUtcFromMillis
29
+ });
30
+ /** A filter on exactly one stream, which a store can read through that stream's index. */
31
+ export const isOneStream = ({ aggregateIds, aggregateNames }) => aggregateNames?.length === 1 && aggregateIds?.length === 1;
32
+ export class EventStore extends Context.Service()("effect-cqrs/EventStore") {
33
+ }
34
+ export const make = Effect.fnUntraced(function* (backend, options) {
35
+ const batchSize = options?.batchSize ?? 500;
36
+ const latest = yield* SubscriptionRef.make(yield* backend.head);
37
+ const append = Effect.fn("EventStore.append")((input) => backend.append(input).pipe(Effect.tap((stored) => {
38
+ const last = stored.at(-1);
39
+ return last === undefined
40
+ ? Effect.void
41
+ : SubscriptionRef.update(latest, (position) => Math.max(position, last.position));
42
+ })));
43
+ const read = (filter = {}) => Stream.paginate({ afterPosition: filter.afterPosition ?? 0, afterVersion: filter.afterVersion ?? 0 }, ({ afterPosition, afterVersion }) => backend.readBatch({ ...filter, afterVersion }, afterPosition, batchSize).pipe(Effect.map((events) => {
44
+ const last = events.at(-1);
45
+ return [
46
+ events,
47
+ last === undefined || events.length < batchSize
48
+ ? Option.none()
49
+ : Option.some({ afterPosition: last.position, afterVersion: last.version })
50
+ ];
51
+ })));
52
+ const subscribe = (filter = {}) => Stream.unwrap(Effect.gen(function* () {
53
+ const cursor = yield* Ref.make({ position: filter.afterPosition ?? 0, version: filter.afterVersion ?? 0 });
54
+ return SubscriptionRef.changes(latest).pipe(Stream.filterEffect((position) => Effect.map(Ref.get(cursor), (seen) => position > seen.position)), Stream.flatMap((position) => Stream.unwrap(Effect.map(Ref.get(cursor), (seen) => read({ ...filter, afterPosition: seen.position, afterVersion: seen.version }))).pipe(Stream.tap((event) => Ref.set(cursor, { position: event.position, version: event.version })),
55
+ // Events the filter skipped must not trigger another read.
56
+ Stream.ensuring(Ref.update(cursor, (seen) => seen.position >= position ? seen : { ...seen, position })))));
57
+ }));
58
+ return EventStore.of({
59
+ append,
60
+ read,
61
+ subscribe,
62
+ head: backend.head,
63
+ changes: SubscriptionRef.changes(latest)
64
+ });
65
+ });
66
+ const matches = (filter, event) => (filter.aggregateNames?.includes(event.aggregateName) ?? true) &&
67
+ (filter.aggregateIds?.includes(event.aggregateId) ?? true) &&
68
+ (filter.eventTypes?.includes(event.type) ?? true) &&
69
+ (filter.startTime === undefined || !DateTime.isLessThan(event.timestamp, filter.startTime)) &&
70
+ (filter.finishTime === undefined || DateTime.isLessThan(event.timestamp, filter.finishTime));
71
+ /** The index of the first event past `position`, in events sorted by position. */
72
+ const firstAfter = (events, position) => {
73
+ let low = 0;
74
+ let high = events.length;
75
+ while (low < high) {
76
+ const middle = (low + high) >>> 1;
77
+ if (events[middle].position <= position)
78
+ low = middle + 1;
79
+ else
80
+ high = middle;
81
+ }
82
+ return low;
83
+ };
84
+ const streamKey = (aggregateName, aggregateId) => JSON.stringify([aggregateName, aggregateId]);
85
+ export const makeMemory = Effect.gen(function* () {
86
+ const events = [];
87
+ const streams = new Map();
88
+ // Every command reads one stream, which shouldn't cost a walk through the whole log.
89
+ const candidates = (filter) => isOneStream(filter)
90
+ ? streams.get(streamKey(filter.aggregateNames[0], filter.aggregateIds[0])) ?? []
91
+ : events;
92
+ return yield* make({
93
+ append: Effect.fnUntraced(function* (input) {
94
+ const timestamp = yield* DateTime.now;
95
+ const key = streamKey(input.aggregateName, input.aggregateId);
96
+ const stream = streams.get(key) ?? [];
97
+ const version = stream.length;
98
+ if (version !== input.expectedVersion) {
99
+ return yield* new ConcurrencyConflict({
100
+ aggregateName: input.aggregateName,
101
+ aggregateId: input.aggregateId,
102
+ expectedVersion: input.expectedVersion
103
+ });
104
+ }
105
+ const stored = input.events.map((event, index) => ({
106
+ position: events.length + index + 1,
107
+ aggregateName: input.aggregateName,
108
+ aggregateId: input.aggregateId,
109
+ version: version + index + 1,
110
+ type: event.type,
111
+ payload: event.payload,
112
+ timestamp
113
+ }));
114
+ for (const event of stored) {
115
+ events.push(event);
116
+ stream.push(event);
117
+ }
118
+ streams.set(key, stream);
119
+ return stored;
120
+ }),
121
+ readBatch: (filter, afterPosition, limit) => Effect.sync(() => {
122
+ const source = candidates(filter);
123
+ const batch = [];
124
+ for (let index = firstAfter(source, afterPosition); index < source.length && batch.length < limit; index++) {
125
+ const event = source[index];
126
+ if (matches(filter, event))
127
+ batch.push(event);
128
+ }
129
+ return batch;
130
+ }),
131
+ head: Effect.sync(() => events.length)
132
+ });
133
+ });
134
+ /** Keeps events in process memory. They do not survive a restart. */
135
+ export const layerMemory = Layer.effect(EventStore, makeMemory);
136
+ //# sourceMappingURL=EventStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventStore.js","sourceRoot":"","sources":["../src/EventStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tests an aggregate in isolation: given these events, when this command runs,
3
+ * what happens? The moleculer-cqrs `CQRSFixture`, as a chain that runs when it
4
+ * is yielded:
5
+ *
6
+ * ```ts
7
+ * yield* Fixture.make(Todo)
8
+ * .givenEvents([created])
9
+ * .when("deleteTodo", { message: "Done" })
10
+ * .expectEvent("TodoDeleted")
11
+ * .inspectState((state) => assert.strictEqual(state.status, "deleted"))
12
+ * ```
13
+ *
14
+ * Commands run through the real {@link AggregateService} on an in-memory store,
15
+ * so payload validation and event encoding are exercised too.
16
+ */
17
+ import { Effect, Schema } from "effect";
18
+ import type * as Aggregate from "./Aggregate.ts";
19
+ import * as AggregateService from "./AggregateService.ts";
20
+ declare const ExpectationFailed_base: Schema.Class<ExpectationFailed, Schema.TaggedStruct<"ExpectationFailed", {
21
+ readonly message: Schema.String;
22
+ readonly expected: Schema.Unknown;
23
+ readonly actual: Schema.Unknown;
24
+ }>, import("effect/Cause").YieldableError>;
25
+ /** Raised as a defect, so a command's error channel only holds what the command can do. */
26
+ export declare class ExpectationFailed extends ExpectationFailed_base {
27
+ }
28
+ export interface Outcome<A extends Aggregate.Any> {
29
+ readonly events: ReadonlyArray<Aggregate.Event<A>>;
30
+ readonly state: Aggregate.State<A>;
31
+ readonly version: number;
32
+ /** Continues from here, with this outcome's events as the new history. */
33
+ readonly fixture: Fixture<A>;
34
+ }
35
+ /** An event to compare with, or only its tag when the rest doesn't matter. */
36
+ export type ExpectedEvent<A extends Aggregate.Any> = Aggregate.Event<A> | Aggregate.Event<A>["_tag"];
37
+ /**
38
+ * The steps so far. Nothing runs until it is yielded or run as an Effect, which
39
+ * produces the {@link Outcome} of the last command.
40
+ */
41
+ export interface Scenario<A extends Aggregate.Any, E, R> extends Effect.Effect<Outcome<A>, E, R> {
42
+ /** Runs another command on top of the previous one. */
43
+ when<K extends Aggregate.CommandName<A>>(command: K, payload: Aggregate.Payload<A, K>): Scenario<A, E | Aggregate.CommandError<A, K> | AggregateService.CommandFailure, R | Aggregate.Context<A>>;
44
+ /** Runs a command that is expected to fail and returns the error. Dies if it succeeds. */
45
+ whenFails<K extends Aggregate.CommandName<A>>(command: K, payload: Aggregate.Payload<A, K>): Effect.Effect<Aggregate.CommandError<A, K> | AggregateService.CommandFailure, E, R | Aggregate.Context<A>>;
46
+ /** Expects the last command to have emitted exactly this event. */
47
+ expectEvent(event: ExpectedEvent<A>): Scenario<A, E, R>;
48
+ /** Expects the last command to have emitted exactly these events, in order. */
49
+ expectEvents(events: ReadonlyArray<ExpectedEvent<A>>): Scenario<A, E, R>;
50
+ /** Passes the current state to a callback, which throws to fail the test. */
51
+ inspectState(check: (state: Aggregate.State<A>) => void): Scenario<A, E, R>;
52
+ }
53
+ export interface Fixture<A extends Aggregate.Any> {
54
+ /** Replaces the history the next command runs against. */
55
+ given(events: ReadonlyArray<Aggregate.Event<A>>): Fixture<A>;
56
+ /** Like `given`, starting from an empty history by default and optionally switching aggregate id. */
57
+ givenEvents(events?: ReadonlyArray<Aggregate.Event<A>>, aggregateId?: string): Fixture<A>;
58
+ /** The state after the given events, without running a command. */
59
+ readonly state: Effect.Effect<Aggregate.State<A>, AggregateService.ReadError, Aggregate.Context<A>>;
60
+ /** Passes the state after the given events to a callback, which throws to fail the test. */
61
+ inspectState(check: (state: Aggregate.State<A>) => void): Scenario<A, AggregateService.ReadError, Aggregate.Context<A>>;
62
+ when<K extends Aggregate.CommandName<A>>(command: K, payload: Aggregate.Payload<A, K>): Scenario<A, Aggregate.CommandError<A, K> | AggregateService.CommandFailure, Aggregate.Context<A>>;
63
+ /** Runs a command that is expected to fail and returns the error. Dies if it succeeds. */
64
+ whenFails<K extends Aggregate.CommandName<A>>(command: K, payload: Aggregate.Payload<A, K>): Effect.Effect<Aggregate.CommandError<A, K> | AggregateService.CommandFailure, never, Aggregate.Context<A>>;
65
+ }
66
+ export declare const make: <A extends Aggregate.Any>(aggregate: A, options?: {
67
+ readonly aggregateId?: string;
68
+ readonly events?: ReadonlyArray<Aggregate.Event<A>>;
69
+ }) => Fixture<A>;
70
+ export {};
71
+ //# sourceMappingURL=Fixture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Fixture.d.ts","sourceRoot":"","sources":["../src/Fixture.ts"],"names":[],"mappings":"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"}