effect-cqrs 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +326 -2
- package/dist/Aggregate.d.ts +127 -0
- package/dist/Aggregate.d.ts.map +1 -0
- package/dist/Aggregate.js +59 -0
- package/dist/Aggregate.js.map +1 -0
- package/dist/AggregateService.d.ts +154 -0
- package/dist/AggregateService.d.ts.map +1 -0
- package/dist/AggregateService.js +407 -0
- package/dist/AggregateService.js.map +1 -0
- package/dist/Causation.d.ts +34 -0
- package/dist/Causation.d.ts.map +1 -0
- package/dist/Causation.js +32 -0
- package/dist/Causation.js.map +1 -0
- package/dist/Checkpoints.d.ts +19 -0
- package/dist/Checkpoints.d.ts.map +1 -0
- package/dist/Checkpoints.js +18 -0
- package/dist/Checkpoints.js.map +1 -0
- package/dist/EventStore.d.ts +132 -0
- package/dist/EventStore.d.ts.map +1 -0
- package/dist/EventStore.js +156 -0
- package/dist/EventStore.js.map +1 -0
- package/dist/EventVersions.d.ts +64 -0
- package/dist/EventVersions.d.ts.map +1 -0
- package/dist/EventVersions.js +55 -0
- package/dist/EventVersions.js.map +1 -0
- package/dist/Fixture.d.ts +106 -0
- package/dist/Fixture.d.ts.map +1 -0
- package/dist/Fixture.js +149 -0
- package/dist/Fixture.js.map +1 -0
- package/dist/Metadata.d.ts +42 -0
- package/dist/Metadata.d.ts.map +1 -0
- package/dist/Metadata.js +39 -0
- package/dist/Metadata.js.map +1 -0
- package/dist/PostgresEventStore.d.ts +30 -0
- package/dist/PostgresEventStore.d.ts.map +1 -0
- package/dist/PostgresEventStore.js +153 -0
- package/dist/PostgresEventStore.js.map +1 -0
- package/dist/Saga.d.ts +105 -0
- package/dist/Saga.d.ts.map +1 -0
- package/dist/Saga.js +97 -0
- package/dist/Saga.js.map +1 -0
- package/dist/SqliteEventStore.d.ts +25 -0
- package/dist/SqliteEventStore.d.ts.map +1 -0
- package/dist/SqliteEventStore.js +142 -0
- package/dist/SqliteEventStore.js.map +1 -0
- package/dist/ViewModel.d.ts +90 -0
- package/dist/ViewModel.d.ts.map +1 -0
- package/dist/ViewModel.js +172 -0
- package/dist/ViewModel.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/package.json +89 -3
- package/src/Aggregate.ts +205 -0
- package/src/AggregateService.ts +705 -0
- package/src/Causation.ts +48 -0
- package/src/Checkpoints.ts +23 -0
- package/src/EventStore.ts +270 -0
- package/src/EventVersions.ts +104 -0
- package/src/Fixture.ts +298 -0
- package/src/Metadata.ts +52 -0
- package/src/PostgresEventStore.ts +199 -0
- package/src/Saga.ts +176 -0
- package/src/SqliteEventStore.ts +196 -0
- package/src/ViewModel.ts +350 -0
- package/src/index.ts +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Causation.d.ts","sourceRoot":"","sources":["../src/Causation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAY,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,0CAE3B,CAAA;AAEF,2FAA2F;AAC3F,eAAO,MAAM,aAAa,EAAE;IAC1B,CAAC,SAAS,EAAE,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IAC3F,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;CAKxF,CAAA;AAED,mEAAmE;AACnE,eAAO,MAAM,SAAS,UAAW,WAAW,KAAG,SAG9C,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,UAAW,WAAW,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAC7C,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why an event was recorded. Every event carries two ids:
|
|
3
|
+
*
|
|
4
|
+
* - `correlationId` is shared by everything that follows from one original
|
|
5
|
+
* request, across aggregates and view models.
|
|
6
|
+
* - `causationId` names what directly caused the command that recorded it: the
|
|
7
|
+
* request, or the event a view model or saga was reacting to.
|
|
8
|
+
*
|
|
9
|
+
* They travel in the fiber's context rather than in payloads. A command run
|
|
10
|
+
* outside any causation starts a new correlation with a fresh id. Inside a view
|
|
11
|
+
* model handler the event being handled is the cause, so a chain of reactions
|
|
12
|
+
* can be drawn as a tree without passing ids around by hand.
|
|
13
|
+
*/
|
|
14
|
+
import { Context, Effect, Function } from "effect";
|
|
15
|
+
import { eventId } from "./EventStore.js";
|
|
16
|
+
/** The causation of commands run in this fiber, `undefined` outside any. */
|
|
17
|
+
export const CurrentCausation = Context.Reference("effect-cqrs/Causation", {
|
|
18
|
+
defaultValue: () => undefined
|
|
19
|
+
});
|
|
20
|
+
/** Runs an effect with this causation, for instance the id of an incoming HTTP request. */
|
|
21
|
+
export const withCausation = Function.dual(2, (effect, causation) => Effect.provideService(effect, CurrentCausation, causation));
|
|
22
|
+
/** The causation of commands run in reaction to a stored event. */
|
|
23
|
+
export const fromEvent = (event) => {
|
|
24
|
+
const id = eventId(event);
|
|
25
|
+
return { correlationId: event.correlationId ?? id, causationId: id };
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Runs an effect in reaction to a stored event, so commands it runs record that
|
|
29
|
+
* event as their cause. View models do this for their handlers already.
|
|
30
|
+
*/
|
|
31
|
+
export const causedBy = (event) => (effect) => Effect.provideService(effect, CurrentCausation, fromEvent(event));
|
|
32
|
+
//# sourceMappingURL=Causation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Causation.js","sourceRoot":"","sources":["../src/Causation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAClD,OAAO,EAAE,OAAO,EAAoB,MAAM,iBAAiB,CAAA;AAO3D,4EAA4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAwB,uBAAuB,EAAE;IAChG,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS;CAC9B,CAAC,CAAA;AAEF,2FAA2F;AAC3F,MAAM,CAAC,MAAM,aAAa,GAGtB,QAAQ,CAAC,IAAI,CACf,CAAC,EACD,CAAU,MAA8B,EAAE,SAAoB,EAAE,EAAE,CAChE,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,CAAC,CAC7D,CAAA;AAED,mEAAmE;AACnE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAkB,EAAa,EAAE;IACzD,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IACzB,OAAO,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAA;AACtE,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAkB,EAAE,EAAE,CAAC,CAAU,MAA8B,EAA0B,EAAE,CAClH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA"}
|
|
@@ -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,132 @@
|
|
|
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
|
+
/** The version of the event's schema the payload was written with. Events from before versions existed are `1`. */
|
|
30
|
+
readonly schemaVersion: Schema.Int;
|
|
31
|
+
readonly payload: Schema.Codec<Schema.Json, Schema.Json, never, never>;
|
|
32
|
+
readonly timestamp: Schema.DateTimeUtcFromMillis;
|
|
33
|
+
/** Shared by everything that follows from one original request. */
|
|
34
|
+
readonly correlationId: Schema.NullOr<Schema.String>;
|
|
35
|
+
/** What directly caused the command that recorded the event: a request, a command or another event. */
|
|
36
|
+
readonly causationId: Schema.NullOr<Schema.String>;
|
|
37
|
+
/** What the command carried beside its payload, see `Metadata`. Events from before metadata existed have `{}`. */
|
|
38
|
+
readonly metadata: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
39
|
+
}>;
|
|
40
|
+
export type StoredEvent = typeof StoredEvent.Type;
|
|
41
|
+
export interface NewEvent {
|
|
42
|
+
readonly type: string;
|
|
43
|
+
readonly payload: Schema.Json;
|
|
44
|
+
/** Defaults to `1`. */
|
|
45
|
+
readonly schemaVersion?: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
export interface AppendInput {
|
|
48
|
+
readonly aggregateName: string;
|
|
49
|
+
readonly aggregateId: string;
|
|
50
|
+
/** The stream version the writer based its decision on, `0` for a new stream. */
|
|
51
|
+
readonly expectedVersion: number;
|
|
52
|
+
readonly events: ReadonlyArray<NewEvent>;
|
|
53
|
+
readonly correlationId?: string | undefined;
|
|
54
|
+
readonly causationId?: string | undefined;
|
|
55
|
+
/** Recorded on every event of the append. Defaults to `{}`. */
|
|
56
|
+
readonly metadata?: Schema.JsonObject | undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Identifies an event wherever it is stored, as the `causationId` of what it causes:
|
|
60
|
+
* `account/alice@4`. Name and id are URI-encoded, so a `/` or `@` inside one can't
|
|
61
|
+
* make two events look alike.
|
|
62
|
+
*/
|
|
63
|
+
export declare const eventId: (event: Pick<StoredEvent, "aggregateName" | "aggregateId" | "version">) => string;
|
|
64
|
+
/** The columns every adapter stores for a new event, with the defaults filled in. */
|
|
65
|
+
export declare const toStored: (input: AppendInput, event: NewEvent, index: number, position: number, timestamp: DateTime.Utc) => StoredEvent;
|
|
66
|
+
/**
|
|
67
|
+
* Selects events by stream, type and time. Omitted fields match everything.
|
|
68
|
+
* `startTime` is inclusive and `finishTime` is exclusive, so adding one
|
|
69
|
+
* millisecond to a history timestamp selects up to and including that event.
|
|
70
|
+
*/
|
|
71
|
+
export interface EventFilter {
|
|
72
|
+
readonly aggregateNames?: ReadonlyArray<string> | undefined;
|
|
73
|
+
readonly aggregateIds?: ReadonlyArray<string> | undefined;
|
|
74
|
+
readonly eventTypes?: ReadonlyArray<string> | undefined;
|
|
75
|
+
readonly startTime?: DateTime.Utc | undefined;
|
|
76
|
+
readonly finishTime?: DateTime.Utc | undefined;
|
|
77
|
+
readonly afterPosition?: number | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* For a filter on one stream: where to continue in it. Versions follow the stream's
|
|
80
|
+
* own index, so resuming there costs the same however far the log has moved on.
|
|
81
|
+
*/
|
|
82
|
+
readonly afterVersion?: number | undefined;
|
|
83
|
+
}
|
|
84
|
+
/** A filter on exactly one stream, which a store can read through that stream's index. */
|
|
85
|
+
export declare const isOneStream: ({ aggregateIds, aggregateNames }: EventFilter) => boolean;
|
|
86
|
+
declare const EventStore_base: Context.ServiceClass<EventStore, "effect-cqrs/EventStore", {
|
|
87
|
+
/** Appends atomically, failing if the stream moved past `expectedVersion`. */
|
|
88
|
+
append(input: AppendInput): Effect.Effect<ReadonlyArray<StoredEvent>, ConcurrencyConflict | EventStoreError>;
|
|
89
|
+
/** Reads the events stored so far, ordered by position, then completes. */
|
|
90
|
+
read(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
|
|
91
|
+
/** Reads the events stored so far, then keeps following new ones. Never completes. */
|
|
92
|
+
subscribe(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
|
|
93
|
+
/** The position of the latest event, `0` when the store is empty. */
|
|
94
|
+
readonly head: Effect.Effect<number, EventStoreError>;
|
|
95
|
+
/**
|
|
96
|
+
* Emits the current latest position, then each newly appended one. Consumers
|
|
97
|
+
* can treat an element as "there may be something new": appends from other
|
|
98
|
+
* processes are not observed.
|
|
99
|
+
*/
|
|
100
|
+
readonly changes: Stream.Stream<number>;
|
|
101
|
+
}>;
|
|
102
|
+
export declare class EventStore extends EventStore_base {
|
|
103
|
+
}
|
|
104
|
+
/** What a storage adapter implements. {@link make} builds the rest on top. */
|
|
105
|
+
export interface Backend {
|
|
106
|
+
readonly append: EventStore["Service"]["append"];
|
|
107
|
+
readonly readBatch: (filter: EventFilter, afterPosition: number, limit: number) => Effect.Effect<ReadonlyArray<StoredEvent>, EventStoreError>;
|
|
108
|
+
readonly head: Effect.Effect<number, EventStoreError>;
|
|
109
|
+
}
|
|
110
|
+
export declare const make: (backend: Backend, options?: {
|
|
111
|
+
readonly batchSize?: number;
|
|
112
|
+
} | undefined) => Effect.Effect<{
|
|
113
|
+
/** Appends atomically, failing if the stream moved past `expectedVersion`. */
|
|
114
|
+
append(input: AppendInput): Effect.Effect<ReadonlyArray<StoredEvent>, ConcurrencyConflict | EventStoreError>;
|
|
115
|
+
/** Reads the events stored so far, ordered by position, then completes. */
|
|
116
|
+
read(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
|
|
117
|
+
/** Reads the events stored so far, then keeps following new ones. Never completes. */
|
|
118
|
+
subscribe(filter?: EventFilter): Stream.Stream<StoredEvent, EventStoreError>;
|
|
119
|
+
/** The position of the latest event, `0` when the store is empty. */
|
|
120
|
+
readonly head: Effect.Effect<number, EventStoreError>;
|
|
121
|
+
/**
|
|
122
|
+
* Emits the current latest position, then each newly appended one. Consumers
|
|
123
|
+
* can treat an element as "there may be something new": appends from other
|
|
124
|
+
* processes are not observed.
|
|
125
|
+
*/
|
|
126
|
+
readonly changes: Stream.Stream<number>;
|
|
127
|
+
}, EventStoreError, never>;
|
|
128
|
+
export declare const makeMemory: Effect.Effect<EventStore["Service"], EventStoreError>;
|
|
129
|
+
/** Keeps events in process memory. They do not survive a restart. */
|
|
130
|
+
export declare const layerMemory: Layer.Layer<EventStore, EventStoreError>;
|
|
131
|
+
export {};
|
|
132
|
+
//# 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,156 @@
|
|
|
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
|
+
/** The version of the event's schema the payload was written with. Events from before versions existed are `1`. */
|
|
28
|
+
schemaVersion: Schema.Int,
|
|
29
|
+
payload: Schema.Json,
|
|
30
|
+
timestamp: Schema.DateTimeUtcFromMillis,
|
|
31
|
+
/** Shared by everything that follows from one original request. */
|
|
32
|
+
correlationId: Schema.NullOr(Schema.String),
|
|
33
|
+
/** What directly caused the command that recorded the event: a request, a command or another event. */
|
|
34
|
+
causationId: Schema.NullOr(Schema.String),
|
|
35
|
+
/** What the command carried beside its payload, see `Metadata`. Events from before metadata existed have `{}`. */
|
|
36
|
+
metadata: Schema.JsonObject
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Identifies an event wherever it is stored, as the `causationId` of what it causes:
|
|
40
|
+
* `account/alice@4`. Name and id are URI-encoded, so a `/` or `@` inside one can't
|
|
41
|
+
* make two events look alike.
|
|
42
|
+
*/
|
|
43
|
+
export const eventId = (event) => `${encodeURIComponent(event.aggregateName)}/${encodeURIComponent(event.aggregateId)}@${event.version}`;
|
|
44
|
+
/** The columns every adapter stores for a new event, with the defaults filled in. */
|
|
45
|
+
export const toStored = (input, event, index, position, timestamp) => ({
|
|
46
|
+
position,
|
|
47
|
+
aggregateName: input.aggregateName,
|
|
48
|
+
aggregateId: input.aggregateId,
|
|
49
|
+
version: input.expectedVersion + index + 1,
|
|
50
|
+
type: event.type,
|
|
51
|
+
schemaVersion: event.schemaVersion ?? 1,
|
|
52
|
+
payload: event.payload,
|
|
53
|
+
timestamp,
|
|
54
|
+
correlationId: input.correlationId ?? null,
|
|
55
|
+
causationId: input.causationId ?? null,
|
|
56
|
+
metadata: input.metadata ?? {}
|
|
57
|
+
});
|
|
58
|
+
/** A filter on exactly one stream, which a store can read through that stream's index. */
|
|
59
|
+
export const isOneStream = ({ aggregateIds, aggregateNames }) => aggregateNames?.length === 1 && aggregateIds?.length === 1;
|
|
60
|
+
export class EventStore extends Context.Service()("effect-cqrs/EventStore") {
|
|
61
|
+
}
|
|
62
|
+
export const make = Effect.fnUntraced(function* (backend, options) {
|
|
63
|
+
const batchSize = options?.batchSize ?? 500;
|
|
64
|
+
const latest = yield* SubscriptionRef.make(yield* backend.head);
|
|
65
|
+
const append = Effect.fn("EventStore.append")((input) => backend.append(input).pipe(Effect.tap((stored) => {
|
|
66
|
+
const last = stored.at(-1);
|
|
67
|
+
return last === undefined
|
|
68
|
+
? Effect.void
|
|
69
|
+
: SubscriptionRef.update(latest, (position) => Math.max(position, last.position));
|
|
70
|
+
})));
|
|
71
|
+
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) => {
|
|
72
|
+
const last = events.at(-1);
|
|
73
|
+
return [
|
|
74
|
+
events,
|
|
75
|
+
last === undefined || events.length < batchSize
|
|
76
|
+
? Option.none()
|
|
77
|
+
: Option.some({ afterPosition: last.position, afterVersion: last.version })
|
|
78
|
+
];
|
|
79
|
+
})));
|
|
80
|
+
const subscribe = (filter = {}) => Stream.unwrap(Effect.gen(function* () {
|
|
81
|
+
const cursor = yield* Ref.make({ position: filter.afterPosition ?? 0, version: filter.afterVersion ?? 0 });
|
|
82
|
+
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 })),
|
|
83
|
+
// Events the filter skipped must not trigger another read.
|
|
84
|
+
Stream.ensuring(Ref.update(cursor, (seen) => seen.position >= position ? seen : { ...seen, position })))));
|
|
85
|
+
}));
|
|
86
|
+
return EventStore.of({
|
|
87
|
+
append,
|
|
88
|
+
read,
|
|
89
|
+
subscribe,
|
|
90
|
+
head: backend.head,
|
|
91
|
+
changes: SubscriptionRef.changes(latest)
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
const matches = (filter, event) => (filter.aggregateNames?.includes(event.aggregateName) ?? true) &&
|
|
95
|
+
(filter.aggregateIds?.includes(event.aggregateId) ?? true) &&
|
|
96
|
+
(filter.eventTypes?.includes(event.type) ?? true) &&
|
|
97
|
+
(filter.startTime === undefined || !DateTime.isLessThan(event.timestamp, filter.startTime)) &&
|
|
98
|
+
(filter.finishTime === undefined || DateTime.isLessThan(event.timestamp, filter.finishTime));
|
|
99
|
+
/** The index of the first event past `position`, in events sorted by position. */
|
|
100
|
+
const firstAfter = (events, position) => {
|
|
101
|
+
let low = 0;
|
|
102
|
+
let high = events.length;
|
|
103
|
+
while (low < high) {
|
|
104
|
+
const middle = (low + high) >>> 1;
|
|
105
|
+
if (events[middle].position <= position)
|
|
106
|
+
low = middle + 1;
|
|
107
|
+
else
|
|
108
|
+
high = middle;
|
|
109
|
+
}
|
|
110
|
+
return low;
|
|
111
|
+
};
|
|
112
|
+
const streamKey = (aggregateName, aggregateId) => JSON.stringify([aggregateName, aggregateId]);
|
|
113
|
+
export const makeMemory = Effect.gen(function* () {
|
|
114
|
+
const events = [];
|
|
115
|
+
const streams = new Map();
|
|
116
|
+
// Every command reads one stream, which shouldn't cost a walk through the whole log.
|
|
117
|
+
const candidates = (filter) => isOneStream(filter)
|
|
118
|
+
? streams.get(streamKey(filter.aggregateNames[0], filter.aggregateIds[0])) ?? []
|
|
119
|
+
: events;
|
|
120
|
+
return yield* make({
|
|
121
|
+
append: Effect.fnUntraced(function* (input) {
|
|
122
|
+
const timestamp = yield* DateTime.now;
|
|
123
|
+
const key = streamKey(input.aggregateName, input.aggregateId);
|
|
124
|
+
const stream = streams.get(key) ?? [];
|
|
125
|
+
const version = stream.length;
|
|
126
|
+
if (version !== input.expectedVersion) {
|
|
127
|
+
return yield* new ConcurrencyConflict({
|
|
128
|
+
aggregateName: input.aggregateName,
|
|
129
|
+
aggregateId: input.aggregateId,
|
|
130
|
+
expectedVersion: input.expectedVersion
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const stored = input.events.map((event, index) => toStored(input, event, index, events.length + index + 1, timestamp));
|
|
134
|
+
for (const event of stored) {
|
|
135
|
+
events.push(event);
|
|
136
|
+
stream.push(event);
|
|
137
|
+
}
|
|
138
|
+
streams.set(key, stream);
|
|
139
|
+
return stored;
|
|
140
|
+
}),
|
|
141
|
+
readBatch: (filter, afterPosition, limit) => Effect.sync(() => {
|
|
142
|
+
const source = candidates(filter);
|
|
143
|
+
const batch = [];
|
|
144
|
+
for (let index = firstAfter(source, afterPosition); index < source.length && batch.length < limit; index++) {
|
|
145
|
+
const event = source[index];
|
|
146
|
+
if (matches(filter, event))
|
|
147
|
+
batch.push(event);
|
|
148
|
+
}
|
|
149
|
+
return batch;
|
|
150
|
+
}),
|
|
151
|
+
head: Effect.sync(() => events.length)
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
/** Keeps events in process memory. They do not survive a restart. */
|
|
155
|
+
export const layerMemory = Layer.effect(EventStore, makeMemory);
|
|
156
|
+
//# 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,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The older shapes of one event, so that events stored long ago still reach
|
|
3
|
+
* projections and view models in today's shape. The log is never rewritten:
|
|
4
|
+
* old payloads are upcast as they are read.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const AccountOpenedV1 = Schema.Struct({ owner: Schema.String })
|
|
8
|
+
*
|
|
9
|
+
* const Account = Aggregate.make<State>()({
|
|
10
|
+
* name: "account",
|
|
11
|
+
* events: AccountEvent,
|
|
12
|
+
* initial,
|
|
13
|
+
* projection,
|
|
14
|
+
* versions: {
|
|
15
|
+
* AccountOpened: EventVersions.from(AccountOpenedV1).next(
|
|
16
|
+
* AccountEvent.cases.AccountOpened,
|
|
17
|
+
* ({ owner }) => ({ owner, currency: "CHF" })
|
|
18
|
+
* )
|
|
19
|
+
* }
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The first schema is version 1, the shape events had before the history was
|
|
24
|
+
* declared, and every `next` adds a version. The last one must be the event's
|
|
25
|
+
* current schema, which is what new events are written with.
|
|
26
|
+
*/
|
|
27
|
+
import { Schema } from "effect";
|
|
28
|
+
/** A schema that decodes and encodes without services, as stored payloads need. */
|
|
29
|
+
export type VersionSchema = Schema.Top & {
|
|
30
|
+
readonly DecodingServices: never;
|
|
31
|
+
readonly EncodingServices: never;
|
|
32
|
+
};
|
|
33
|
+
/** What an upcast returns: the next version, without the `_tag` the event's name already gives. */
|
|
34
|
+
export type Untagged<T> = T extends {
|
|
35
|
+
readonly _tag: string;
|
|
36
|
+
} ? Omit<T, "_tag"> : T;
|
|
37
|
+
export interface EventVersions<Schemas extends ReadonlyArray<VersionSchema>, Latest> {
|
|
38
|
+
/** Oldest first: `schemas[0]` is version 1. */
|
|
39
|
+
readonly schemas: Schemas;
|
|
40
|
+
/** `upcasts[i]` turns version `i + 1` into version `i + 2`. */
|
|
41
|
+
readonly upcasts: ReadonlyArray<(previous: any) => unknown>;
|
|
42
|
+
/** The type of the last version, which an aggregate checks against its event. */
|
|
43
|
+
readonly "~latest"?: Latest;
|
|
44
|
+
/** Adds a version, with the function that turns the previous one into it. */
|
|
45
|
+
next<Next extends VersionSchema>(schema: Next, upcast: (previous: Latest) => Untagged<Next["Type"]>): EventVersions<readonly [...Schemas, Next], Next["Type"]>;
|
|
46
|
+
}
|
|
47
|
+
/** Any history whose last version is `Latest`. */
|
|
48
|
+
export type Any<Latest = any> = EventVersions<ReadonlyArray<VersionSchema>, Latest>;
|
|
49
|
+
/** The schema of one version, or `never` when the history has no such version. */
|
|
50
|
+
export type SchemaAt<H extends Any, V extends number> = H extends EventVersions<infer Schemas, any> ? [never, ...Schemas][V] extends infer S extends VersionSchema ? S : never : never;
|
|
51
|
+
/** Starts a history at version 1. */
|
|
52
|
+
export declare const from: <V1 extends VersionSchema>(schema: V1) => EventVersions<readonly [V1], V1["Type"]>;
|
|
53
|
+
/** The version new events of this kind are written with. */
|
|
54
|
+
export declare const current: (versions: Any | undefined) => number;
|
|
55
|
+
/**
|
|
56
|
+
* Builds one decoder per old version, each going all the way to the current one.
|
|
57
|
+
* The chains are composed once with `Schema.decodeTo`, so reading an old event
|
|
58
|
+
* costs one decode however many versions it is behind.
|
|
59
|
+
*
|
|
60
|
+
* `decoders[v - 1]` decodes a JSON payload stored at version `v`. The current
|
|
61
|
+
* version has none, as the aggregate's own schema reads it.
|
|
62
|
+
*/
|
|
63
|
+
export declare const decoders: (tag: string, versions: Any) => ReadonlyArray<Schema.Decoder<unknown>>;
|
|
64
|
+
//# sourceMappingURL=EventVersions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventVersions.d.ts","sourceRoot":"","sources":["../src/EventVersions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAA;AAE7C,mFAAmF;AACnF,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,GAAG;IACvC,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAA;IAChC,QAAQ,CAAC,gBAAgB,EAAE,KAAK,CAAA;CACjC,CAAA;AAED,mGAAmG;AACnG,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;AAMnF,MAAM,WAAW,aAAa,CAAC,OAAO,SAAS,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM;IACjF,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC,CAAA;IAC3D,iFAAiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,6EAA6E;IAC7E,IAAI,CAAC,IAAI,SAAS,aAAa,EAC7B,MAAM,EAAE,IAAI,EACZ,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GACnD,aAAa,CAAC,SAAS,CAAC,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAA;CAC5D;AAED,kDAAkD;AAClD,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,GAAG,IAAI,aAAa,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,CAAA;AAEnF,kFAAkF;AAClF,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,aAAa,CAAC,MAAM,OAAO,EAAE,GAAG,CAAC,GAC/F,CAAC,KAAK,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,SAAS,aAAa,GAAG,CAAC,GAAG,KAAK,GACxE,KAAK,CAAA;AAWT,qCAAqC;AACrC,eAAO,MAAM,IAAI,GAAI,EAAE,SAAS,aAAa,UAAU,EAAE,KAAG,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CACrE,CAAA;AAE9B,4DAA4D;AAC5D,eAAO,MAAM,OAAO,aAAc,GAAG,GAAG,SAAS,KAAG,MAAuC,CAAA;AAE3F;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,QAAS,MAAM,YAAY,GAAG,KAAG,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAgBxF,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The older shapes of one event, so that events stored long ago still reach
|
|
3
|
+
* projections and view models in today's shape. The log is never rewritten:
|
|
4
|
+
* old payloads are upcast as they are read.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const AccountOpenedV1 = Schema.Struct({ owner: Schema.String })
|
|
8
|
+
*
|
|
9
|
+
* const Account = Aggregate.make<State>()({
|
|
10
|
+
* name: "account",
|
|
11
|
+
* events: AccountEvent,
|
|
12
|
+
* initial,
|
|
13
|
+
* projection,
|
|
14
|
+
* versions: {
|
|
15
|
+
* AccountOpened: EventVersions.from(AccountOpenedV1).next(
|
|
16
|
+
* AccountEvent.cases.AccountOpened,
|
|
17
|
+
* ({ owner }) => ({ owner, currency: "CHF" })
|
|
18
|
+
* )
|
|
19
|
+
* }
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The first schema is version 1, the shape events had before the history was
|
|
24
|
+
* declared, and every `next` adds a version. The last one must be the event's
|
|
25
|
+
* current schema, which is what new events are written with.
|
|
26
|
+
*/
|
|
27
|
+
import { Schema, SchemaGetter } from "effect";
|
|
28
|
+
const build = (schemas, upcasts) => ({
|
|
29
|
+
schemas,
|
|
30
|
+
upcasts,
|
|
31
|
+
next: (schema, upcast) => build([...schemas, schema], [...upcasts, upcast])
|
|
32
|
+
});
|
|
33
|
+
/** Starts a history at version 1. */
|
|
34
|
+
export const from = (schema) => build([schema], []);
|
|
35
|
+
/** The version new events of this kind are written with. */
|
|
36
|
+
export const current = (versions) => versions?.schemas.length ?? 1;
|
|
37
|
+
/**
|
|
38
|
+
* Builds one decoder per old version, each going all the way to the current one.
|
|
39
|
+
* The chains are composed once with `Schema.decodeTo`, so reading an old event
|
|
40
|
+
* costs one decode however many versions it is behind.
|
|
41
|
+
*
|
|
42
|
+
* `decoders[v - 1]` decodes a JSON payload stored at version `v`. The current
|
|
43
|
+
* version has none, as the aggregate's own schema reads it.
|
|
44
|
+
*/
|
|
45
|
+
export const decoders = (tag, versions) => versions.schemas.slice(0, -1).map((schema, index) => versions.schemas.slice(index + 1).reduce((chain, next, step) => chain.pipe(Schema.decodeTo(Schema.toType(next), {
|
|
46
|
+
// Adding the tag lets the last step decode into the tagged event, and a
|
|
47
|
+
// schema in between without one drops it again.
|
|
48
|
+
decode: SchemaGetter.transform((previous) => ({
|
|
49
|
+
...versions.upcasts[index + step](previous),
|
|
50
|
+
_tag: tag
|
|
51
|
+
})),
|
|
52
|
+
// A payload is only ever written in its current version.
|
|
53
|
+
encode: SchemaGetter.forbidden(() => `${tag} can only be written in its current version`)
|
|
54
|
+
})), Schema.toCodecJson(schema)));
|
|
55
|
+
//# sourceMappingURL=EventVersions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventVersions.js","sourceRoot":"","sources":["../src/EventVersions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAqC7C,MAAM,KAAK,GAAG,CACZ,OAAgB,EAChB,OAAkD,EACH,EAAE,CAAC,CAAC;IACnD,OAAO;IACP,OAAO;IACP,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,CAAU,EAAE,CAAC,GAAG,OAAO,EAAE,MAAM,CAAC,CAAQ;CAC5F,CAAC,CAAA;AAEF,qCAAqC;AACrC,MAAM,CAAC,MAAM,IAAI,GAAG,CAA2B,MAAU,EAA4C,EAAE,CACrG,KAAK,CAAC,CAAC,MAAM,CAAU,EAAE,EAAE,CAAC,CAAA;AAE9B,4DAA4D;AAC5D,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,QAAyB,EAAU,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,IAAI,CAAC,CAAA;AAE3F;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,GAAW,EAAE,QAAa,EAA0C,EAAE,CAC7F,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAClD,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CACtC,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CACpB,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;IAC9C,wEAAwE;IACxE,gDAAgD;IAChD,MAAM,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC5C,GAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAE,CAAC,QAAQ,CAAY;QACxD,IAAI,EAAE,GAAG;KACV,CAAC,CAAC;IACH,yDAAyD;IACzD,MAAM,EAAE,YAAY,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,6CAA6C,CAAC;CAC1F,CAAC,CAAC,EACL,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CACA,CAC7B,CAAA"}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
* The history can include events stored in older versions of their schema, made
|
|
18
|
+
* with `Aggregate.historical`, which then go through the same upcasting as events
|
|
19
|
+
* read from a real store.
|
|
20
|
+
*/
|
|
21
|
+
import { Arbitrary, Effect, Schema } from "effect";
|
|
22
|
+
import * as Aggregate from "./Aggregate.ts";
|
|
23
|
+
import * as AggregateService from "./AggregateService.ts";
|
|
24
|
+
declare const ExpectationFailed_base: Schema.Class<ExpectationFailed, Schema.TaggedStruct<"ExpectationFailed", {
|
|
25
|
+
readonly message: Schema.String;
|
|
26
|
+
readonly expected: Schema.Unknown;
|
|
27
|
+
readonly actual: Schema.Unknown;
|
|
28
|
+
}>, import("effect/Cause").YieldableError>;
|
|
29
|
+
/** Raised as a defect, so a command's error channel only holds what the command can do. */
|
|
30
|
+
export declare class ExpectationFailed extends ExpectationFailed_base {
|
|
31
|
+
}
|
|
32
|
+
export interface Outcome<A extends Aggregate.Any> {
|
|
33
|
+
readonly events: ReadonlyArray<Aggregate.Event<A>>;
|
|
34
|
+
readonly state: Aggregate.State<A>;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
/** Continues from here, with this outcome's events as the new history. */
|
|
37
|
+
readonly fixture: Fixture<A>;
|
|
38
|
+
}
|
|
39
|
+
/** An event to compare with, or only its tag when the rest doesn't matter. */
|
|
40
|
+
export type ExpectedEvent<A extends Aggregate.Any> = Aggregate.Event<A> | Aggregate.Event<A>["_tag"];
|
|
41
|
+
/** An event in today's shape, or one stored in an older version of its schema. */
|
|
42
|
+
export type GivenEvent<A extends Aggregate.Any> = Aggregate.Event<A> | Aggregate.Historical;
|
|
43
|
+
/** One generated step of {@link Fixture.checkInvariants}. */
|
|
44
|
+
export type Step<A extends Aggregate.Any> = {
|
|
45
|
+
readonly [K in Aggregate.CommandName<A>]: {
|
|
46
|
+
readonly command: K;
|
|
47
|
+
readonly payload: Aggregate.Payload<A, K>;
|
|
48
|
+
};
|
|
49
|
+
}[Aggregate.CommandName<A>];
|
|
50
|
+
export interface InvariantOptions<A extends Aggregate.Any> {
|
|
51
|
+
/** Must hold for the given state and after every command that succeeds. */
|
|
52
|
+
readonly invariant: (state: Aggregate.State<A>) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* How to generate each command's payload. By default it comes from the payload
|
|
55
|
+
* schema. `false` leaves a command out.
|
|
56
|
+
*/
|
|
57
|
+
readonly commands?: {
|
|
58
|
+
readonly [K in Aggregate.CommandName<A>]?: Arbitrary.Arbitrary<Aggregate.Payload<A, K>> | false;
|
|
59
|
+
} | undefined;
|
|
60
|
+
/** The longest sequence of commands to try. Defaults to 20. */
|
|
61
|
+
readonly maxCommands?: number | undefined;
|
|
62
|
+
/** Passed on to `Arbitrary.checkEffect`, for the number of runs or a seed. */
|
|
63
|
+
readonly check?: Arbitrary.CheckOptions | undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The steps so far. Nothing runs until it is yielded or run as an Effect, which
|
|
67
|
+
* produces the {@link Outcome} of the last command.
|
|
68
|
+
*/
|
|
69
|
+
export interface Scenario<A extends Aggregate.Any, E, R> extends Effect.Effect<Outcome<A>, E, R> {
|
|
70
|
+
/** Runs another command on top of the previous one. */
|
|
71
|
+
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>>;
|
|
72
|
+
/** Runs a command that is expected to fail and returns the error. Dies if it succeeds. */
|
|
73
|
+
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>>;
|
|
74
|
+
/** Expects the last command to have emitted exactly this event. */
|
|
75
|
+
expectEvent(event: ExpectedEvent<A>): Scenario<A, E, R>;
|
|
76
|
+
/** Expects the last command to have emitted exactly these events, in order. */
|
|
77
|
+
expectEvents(events: ReadonlyArray<ExpectedEvent<A>>): Scenario<A, E, R>;
|
|
78
|
+
/** Passes the current state to a callback, which throws to fail the test. */
|
|
79
|
+
inspectState(check: (state: Aggregate.State<A>) => void): Scenario<A, E, R>;
|
|
80
|
+
}
|
|
81
|
+
export interface Fixture<A extends Aggregate.Any> {
|
|
82
|
+
/** Replaces the history the next command runs against. */
|
|
83
|
+
given(events: ReadonlyArray<GivenEvent<A>>): Fixture<A>;
|
|
84
|
+
/** Like `given`, starting from an empty history by default and optionally switching aggregate id. */
|
|
85
|
+
givenEvents(events?: ReadonlyArray<GivenEvent<A>>, aggregateId?: string): Fixture<A>;
|
|
86
|
+
/** The state after the given events, without running a command. */
|
|
87
|
+
readonly state: Effect.Effect<Aggregate.State<A>, AggregateService.ReadError, Aggregate.Context<A>>;
|
|
88
|
+
/** Passes the state after the given events to a callback, which throws to fail the test. */
|
|
89
|
+
inspectState(check: (state: Aggregate.State<A>) => void): Scenario<A, AggregateService.ReadError, Aggregate.Context<A>>;
|
|
90
|
+
when<K extends Aggregate.CommandName<A>>(command: K, payload: Aggregate.Payload<A, K>): Scenario<A, Aggregate.CommandError<A, K> | AggregateService.CommandFailure, Aggregate.Context<A>>;
|
|
91
|
+
/** Runs a command that is expected to fail and returns the error. Dies if it succeeds. */
|
|
92
|
+
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>>;
|
|
93
|
+
/**
|
|
94
|
+
* Runs random sequences of commands from the given history and checks that the
|
|
95
|
+
* invariant holds after each one that succeeds. Commands that fail are part of
|
|
96
|
+
* the game and are skipped. A violation dies with {@link ExpectationFailed},
|
|
97
|
+
* holding the shortest sequence found that breaks it.
|
|
98
|
+
*/
|
|
99
|
+
checkInvariants(options: InvariantOptions<A>): Effect.Effect<void, never, Aggregate.Context<A>>;
|
|
100
|
+
}
|
|
101
|
+
export declare const make: <A extends Aggregate.Any>(aggregate: A, options?: {
|
|
102
|
+
readonly aggregateId?: string;
|
|
103
|
+
readonly events?: ReadonlyArray<GivenEvent<A>>;
|
|
104
|
+
}) => Fixture<A>;
|
|
105
|
+
export {};
|
|
106
|
+
//# sourceMappingURL=Fixture.d.ts.map
|