@arki/event-sourcing 0.1.1 → 0.1.3
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/dist/dot.d.ts +1 -1
- package/dist/dot.js +1 -1
- package/package.json +10 -4
- package/src/builders/command-handler.ts +77 -0
- package/src/builders/command.ts +72 -0
- package/src/builders/decider.ts +288 -0
- package/src/builders/event.ts +68 -0
- package/src/builders/index.ts +40 -0
- package/src/builders/process-manager.ts +153 -0
- package/src/builders/projection.ts +151 -0
- package/src/builders/stateful-process-manager.ts +338 -0
- package/src/bus.ts +3 -0
- package/src/command-flow.ts +491 -0
- package/src/command.ts +52 -0
- package/src/debug.ts +36 -0
- package/src/decide.ts +10 -0
- package/src/dot.ts +161 -0
- package/src/error.ts +1 -0
- package/src/event-sourcing-features.ts +215 -0
- package/src/event.ts +46 -0
- package/src/index.ts +10 -0
- package/src/process-manager.ts +335 -0
- package/src/simple-process-manager.ts +70 -0
- package/src/store.ts +367 -0
- package/src/types.ts +9 -0
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Command,
|
|
3
|
+
CommandHandlerResult,
|
|
4
|
+
CommandHandlerRetryOptions,
|
|
5
|
+
Decider,
|
|
6
|
+
DefaultCommandMetadata,
|
|
7
|
+
DefaultRecord,
|
|
8
|
+
Event,
|
|
9
|
+
EventStore,
|
|
10
|
+
HandleOptions,
|
|
11
|
+
} from '@event-driven-io/emmett';
|
|
12
|
+
import { DeciderCommandHandler } from '@event-driven-io/emmett';
|
|
13
|
+
|
|
14
|
+
import type { z, ZodType, ZodTypeAny } from '@arki/contracts';
|
|
15
|
+
|
|
16
|
+
import { debugCommand } from './debug.js';
|
|
17
|
+
|
|
18
|
+
type BuildCommand<
|
|
19
|
+
CommandType extends string,
|
|
20
|
+
Data extends DefaultRecord,
|
|
21
|
+
Metadata extends DefaultCommandMetadata | undefined,
|
|
22
|
+
> = Command<CommandType, Data, Metadata extends undefined ? undefined : Metadata>;
|
|
23
|
+
|
|
24
|
+
type HandlerMetadata<Metadata> = Metadata extends undefined ? undefined : Metadata;
|
|
25
|
+
|
|
26
|
+
type MetadataInput<Metadata> = Metadata extends undefined ? { metadata?: undefined } : { metadata: Metadata };
|
|
27
|
+
|
|
28
|
+
type StreamResolver<Context, CommandType> = (params: { command: CommandType; context: Context }) => string;
|
|
29
|
+
|
|
30
|
+
export type CommandFlowExecuteOptions<
|
|
31
|
+
Context,
|
|
32
|
+
Store extends EventStore,
|
|
33
|
+
Input extends DefaultRecord,
|
|
34
|
+
Metadata extends DefaultCommandMetadata | undefined,
|
|
35
|
+
> = {
|
|
36
|
+
context: Context;
|
|
37
|
+
eventStore: Store;
|
|
38
|
+
input: Input;
|
|
39
|
+
streamId?: string;
|
|
40
|
+
handleOptions?: HandleOptions<Store>;
|
|
41
|
+
} & MetadataInput<Metadata>;
|
|
42
|
+
|
|
43
|
+
export type CommandFlowHandlerArgs<
|
|
44
|
+
Context,
|
|
45
|
+
Store extends EventStore,
|
|
46
|
+
CommandType,
|
|
47
|
+
Input extends DefaultRecord,
|
|
48
|
+
Metadata extends DefaultCommandMetadata | undefined,
|
|
49
|
+
State,
|
|
50
|
+
StreamEvent extends Event,
|
|
51
|
+
> = {
|
|
52
|
+
command: CommandType;
|
|
53
|
+
context: Context;
|
|
54
|
+
eventStore: Store;
|
|
55
|
+
input: Input;
|
|
56
|
+
metadata: HandlerMetadata<Metadata>;
|
|
57
|
+
streamId?: string;
|
|
58
|
+
handleOptions?: HandleOptions<Store>;
|
|
59
|
+
run: (
|
|
60
|
+
streamId: string,
|
|
61
|
+
options?: {
|
|
62
|
+
handleOptions?: HandleOptions<Store>;
|
|
63
|
+
},
|
|
64
|
+
) => Promise<CommandHandlerResult<State, StreamEvent, Store>>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type CommandFlowHandler<
|
|
68
|
+
Context,
|
|
69
|
+
Store extends EventStore,
|
|
70
|
+
CommandType,
|
|
71
|
+
Input extends DefaultRecord,
|
|
72
|
+
Metadata extends DefaultCommandMetadata | undefined,
|
|
73
|
+
State,
|
|
74
|
+
StreamEvent extends Event,
|
|
75
|
+
ReturnType,
|
|
76
|
+
> = (
|
|
77
|
+
args: CommandFlowHandlerArgs<Context, Store, CommandType, Input, Metadata, State, StreamEvent>,
|
|
78
|
+
) => Promise<ReturnType> | ReturnType;
|
|
79
|
+
|
|
80
|
+
type CommandFlowExecutor<
|
|
81
|
+
Context,
|
|
82
|
+
Store extends EventStore,
|
|
83
|
+
Input extends DefaultRecord,
|
|
84
|
+
Metadata extends DefaultCommandMetadata | undefined,
|
|
85
|
+
ReturnType,
|
|
86
|
+
> = (options: CommandFlowExecuteOptions<Context, Store, Input, Metadata>) => Promise<ReturnType>;
|
|
87
|
+
|
|
88
|
+
export type CommandFlow<
|
|
89
|
+
Context,
|
|
90
|
+
Store extends EventStore,
|
|
91
|
+
CommandType extends string,
|
|
92
|
+
Input extends DefaultRecord,
|
|
93
|
+
Metadata extends DefaultCommandMetadata | undefined,
|
|
94
|
+
State,
|
|
95
|
+
StreamEvent extends Event,
|
|
96
|
+
ReturnType,
|
|
97
|
+
> = CommandFlowExecutor<Context, Store, Input, Metadata, ReturnType> & {
|
|
98
|
+
readonly commandType: CommandType;
|
|
99
|
+
readonly schemas: {
|
|
100
|
+
input: ZodTypeAny;
|
|
101
|
+
metadata?: ZodTypeAny;
|
|
102
|
+
event: ZodTypeAny;
|
|
103
|
+
state?: ZodTypeAny;
|
|
104
|
+
};
|
|
105
|
+
readonly decider: Decider<State, BuildCommand<CommandType, Input, Metadata>, StreamEvent>;
|
|
106
|
+
readonly createCommand: (
|
|
107
|
+
input: Input,
|
|
108
|
+
metadata?: HandlerMetadata<Metadata>,
|
|
109
|
+
) => BuildCommand<CommandType, Input, Metadata>;
|
|
110
|
+
readonly runCommand: (options: {
|
|
111
|
+
eventStore: Store;
|
|
112
|
+
command: BuildCommand<CommandType, Input, Metadata>;
|
|
113
|
+
streamId: string;
|
|
114
|
+
handleOptions?: HandleOptions<Store>;
|
|
115
|
+
}) => Promise<CommandHandlerResult<State, StreamEvent, Store>>;
|
|
116
|
+
readonly resolveStreamId?: StreamResolver<Context, BuildCommand<CommandType, Input, Metadata>>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export class CommandFlowBuilder<
|
|
120
|
+
Context,
|
|
121
|
+
Store extends EventStore,
|
|
122
|
+
CommandType extends string,
|
|
123
|
+
Input extends DefaultRecord = DefaultRecord,
|
|
124
|
+
Metadata extends DefaultCommandMetadata | undefined = undefined,
|
|
125
|
+
State = unknown,
|
|
126
|
+
StreamEvent extends Event = Event,
|
|
127
|
+
> {
|
|
128
|
+
private readonly commandType: CommandType;
|
|
129
|
+
private inputSchema?: ZodTypeAny;
|
|
130
|
+
private metadataSchema?: ZodTypeAny;
|
|
131
|
+
private eventSchema?: ZodTypeAny;
|
|
132
|
+
private stateSchema?: ZodTypeAny;
|
|
133
|
+
private currentDecider?: Decider<State, BuildCommand<CommandType, Input, Metadata>, StreamEvent>;
|
|
134
|
+
private streamResolver?: StreamResolver<Context, BuildCommand<CommandType, Input, Metadata>>;
|
|
135
|
+
private mapToStreamId?: (id: string) => string;
|
|
136
|
+
private retryOptions?: CommandHandlerRetryOptions;
|
|
137
|
+
|
|
138
|
+
constructor(commandType: CommandType) {
|
|
139
|
+
this.commandType = commandType;
|
|
140
|
+
debugCommand('Creating command flow builder for command: %s', commandType);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
input<Schema extends ZodType<DefaultRecord>>(
|
|
144
|
+
schema: Schema,
|
|
145
|
+
): CommandFlowBuilder<Context, Store, CommandType, z.infer<Schema>, Metadata, State, StreamEvent> {
|
|
146
|
+
this.inputSchema = schema;
|
|
147
|
+
debugCommand('[%s] Registered input schema', this.commandType);
|
|
148
|
+
return this as unknown as CommandFlowBuilder<
|
|
149
|
+
Context,
|
|
150
|
+
Store,
|
|
151
|
+
CommandType,
|
|
152
|
+
z.infer<Schema>,
|
|
153
|
+
Metadata,
|
|
154
|
+
State,
|
|
155
|
+
StreamEvent
|
|
156
|
+
>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
metadata<Schema extends ZodType<DefaultCommandMetadata>>(
|
|
160
|
+
schema: Schema,
|
|
161
|
+
): CommandFlowBuilder<Context, Store, CommandType, Input, z.infer<Schema>, State, StreamEvent> {
|
|
162
|
+
this.metadataSchema = schema;
|
|
163
|
+
debugCommand('[%s] Registered metadata schema', this.commandType);
|
|
164
|
+
return this as unknown as CommandFlowBuilder<
|
|
165
|
+
Context,
|
|
166
|
+
Store,
|
|
167
|
+
CommandType,
|
|
168
|
+
Input,
|
|
169
|
+
z.infer<Schema>,
|
|
170
|
+
State,
|
|
171
|
+
StreamEvent
|
|
172
|
+
>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
events<Schema extends ZodType<Event>>(
|
|
176
|
+
schema: Schema,
|
|
177
|
+
): CommandFlowBuilder<Context, Store, CommandType, Input, Metadata, State, z.infer<Schema>> {
|
|
178
|
+
this.eventSchema = schema;
|
|
179
|
+
debugCommand('[%s] Registered event schema', this.commandType);
|
|
180
|
+
return this as unknown as CommandFlowBuilder<Context, Store, CommandType, Input, Metadata, State, z.infer<Schema>>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
state<Schema extends ZodType>(
|
|
184
|
+
schema: Schema,
|
|
185
|
+
): CommandFlowBuilder<Context, Store, CommandType, Input, Metadata, z.infer<Schema>, StreamEvent> {
|
|
186
|
+
this.stateSchema = schema;
|
|
187
|
+
debugCommand('[%s] Registered state schema', this.commandType);
|
|
188
|
+
return this as unknown as CommandFlowBuilder<
|
|
189
|
+
Context,
|
|
190
|
+
Store,
|
|
191
|
+
CommandType,
|
|
192
|
+
Input,
|
|
193
|
+
Metadata,
|
|
194
|
+
z.infer<Schema>,
|
|
195
|
+
StreamEvent
|
|
196
|
+
>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
decider<NextState>(
|
|
200
|
+
decider: Decider<NextState, BuildCommand<CommandType, Input, Metadata>, StreamEvent>,
|
|
201
|
+
): CommandFlowBuilder<Context, Store, CommandType, Input, Metadata, NextState, StreamEvent> {
|
|
202
|
+
this.currentDecider = decider as unknown as Decider<State, BuildCommand<CommandType, Input, Metadata>, StreamEvent>;
|
|
203
|
+
debugCommand('[%s] Registered decider', this.commandType);
|
|
204
|
+
return this as unknown as CommandFlowBuilder<Context, Store, CommandType, Input, Metadata, NextState, StreamEvent>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
stream(resolver: StreamResolver<Context, BuildCommand<CommandType, Input, Metadata>>) {
|
|
208
|
+
this.streamResolver = resolver;
|
|
209
|
+
debugCommand('[%s] Registered stream resolver', this.commandType);
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mapStreamId(mapper: (id: string) => string) {
|
|
214
|
+
this.mapToStreamId = mapper;
|
|
215
|
+
debugCommand('[%s] Registered stream ID mapper', this.commandType);
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
retry(options: CommandHandlerRetryOptions) {
|
|
220
|
+
this.retryOptions = options;
|
|
221
|
+
debugCommand('[%s] Configured retry options', this.commandType);
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
handler<ReturnType = CommandHandlerResult<State, StreamEvent, Store>>(
|
|
226
|
+
handler?: CommandFlowHandler<
|
|
227
|
+
Context,
|
|
228
|
+
Store,
|
|
229
|
+
BuildCommand<CommandType, Input, Metadata>,
|
|
230
|
+
Input,
|
|
231
|
+
Metadata,
|
|
232
|
+
State,
|
|
233
|
+
StreamEvent,
|
|
234
|
+
ReturnType
|
|
235
|
+
>,
|
|
236
|
+
): CommandFlow<Context, Store, CommandType, Input, Metadata, State, StreamEvent, ReturnType> {
|
|
237
|
+
const inputSchema = this.ensureInputSchema() as ZodType<Input>;
|
|
238
|
+
const eventSchema = this.ensureEventSchema() as ZodType<StreamEvent>;
|
|
239
|
+
const metadataSchema = this.metadataSchema as Metadata extends undefined ? undefined : ZodType<Metadata>;
|
|
240
|
+
const stateSchema = this.stateSchema as ZodType<State> | undefined;
|
|
241
|
+
const decider = this.ensureDecider();
|
|
242
|
+
const streamResolver = this.streamResolver;
|
|
243
|
+
const mapToStreamId = this.mapToStreamId;
|
|
244
|
+
const retryOptions = this.retryOptions;
|
|
245
|
+
|
|
246
|
+
const validatedDecider: Decider<State, BuildCommand<CommandType, Input, Metadata>, StreamEvent> = {
|
|
247
|
+
decide: (command, state) => {
|
|
248
|
+
const safeState = stateSchema ? stateSchema.parse(state) : state;
|
|
249
|
+
const result = decider.decide(command, safeState);
|
|
250
|
+
|
|
251
|
+
const events = Array.isArray(result) ? result : [result];
|
|
252
|
+
const parsed = events.map(evt => eventSchema.parse(evt));
|
|
253
|
+
return Array.isArray(result) ? parsed : parsed[0]!;
|
|
254
|
+
},
|
|
255
|
+
evolve: (state, event) => {
|
|
256
|
+
const parsedEvent = eventSchema.parse(event);
|
|
257
|
+
const nextState = decider.evolve(state, parsedEvent);
|
|
258
|
+
return stateSchema ? stateSchema.parse(nextState) : nextState;
|
|
259
|
+
},
|
|
260
|
+
initialState: () => {
|
|
261
|
+
const initial = decider.initialState();
|
|
262
|
+
return stateSchema ? stateSchema.parse(initial) : initial;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const runner = DeciderCommandHandler<State, BuildCommand<CommandType, Input, Metadata>, StreamEvent>({
|
|
267
|
+
decide: validatedDecider.decide,
|
|
268
|
+
evolve: validatedDecider.evolve,
|
|
269
|
+
initialState: validatedDecider.initialState,
|
|
270
|
+
mapToStreamId,
|
|
271
|
+
retry: retryOptions,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const defaultHandler: CommandFlowHandler<
|
|
275
|
+
Context,
|
|
276
|
+
Store,
|
|
277
|
+
BuildCommand<CommandType, Input, Metadata>,
|
|
278
|
+
Input,
|
|
279
|
+
Metadata,
|
|
280
|
+
State,
|
|
281
|
+
StreamEvent,
|
|
282
|
+
CommandHandlerResult<State, StreamEvent, Store>
|
|
283
|
+
> = async ({ command, context, run, streamId, handleOptions }) => {
|
|
284
|
+
if (!streamResolver && !streamId) {
|
|
285
|
+
debugCommand('[%s] ERROR: No stream resolver or streamId provided', this.commandType);
|
|
286
|
+
throw new Error(
|
|
287
|
+
`No stream resolver provided for command "${this.commandType}". Call .stream(...) or provide streamId.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
debugCommand('[%s] Resolving stream ID', this.commandType);
|
|
292
|
+
const resolvedStream = streamId ?? streamResolver?.({ command, context });
|
|
293
|
+
if (!resolvedStream) {
|
|
294
|
+
debugCommand('[%s] ERROR: Failed to resolve stream ID', this.commandType);
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Unable to resolve stream id for command "${this.commandType}". Provide a streamId when executing.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
debugCommand('[%s] Stream resolved to: %s', this.commandType, resolvedStream);
|
|
301
|
+
return run(resolvedStream, { handleOptions });
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const finalHandler = (handler ?? defaultHandler) as CommandFlowHandler<
|
|
305
|
+
Context,
|
|
306
|
+
Store,
|
|
307
|
+
BuildCommand<CommandType, Input, Metadata>,
|
|
308
|
+
Input,
|
|
309
|
+
Metadata,
|
|
310
|
+
State,
|
|
311
|
+
StreamEvent,
|
|
312
|
+
ReturnType
|
|
313
|
+
>;
|
|
314
|
+
|
|
315
|
+
const flowExecutor: CommandFlowExecutor<Context, Store, Input, Metadata, ReturnType> = async options => {
|
|
316
|
+
const { context, eventStore, input, streamId, handleOptions } = options;
|
|
317
|
+
const metadataOption = options as MetadataInput<Metadata>;
|
|
318
|
+
|
|
319
|
+
debugCommand('[%s] Executing command flow with input: %O', this.commandType, input);
|
|
320
|
+
|
|
321
|
+
const parsedInput = inputSchema.parse(input);
|
|
322
|
+
debugCommand('[%s] Input validation passed', this.commandType);
|
|
323
|
+
let parsedMetadata: HandlerMetadata<Metadata>;
|
|
324
|
+
|
|
325
|
+
if (metadataSchema) {
|
|
326
|
+
if (!('metadata' in metadataOption)) {
|
|
327
|
+
debugCommand('[%s] ERROR: Metadata expected but not provided', this.commandType);
|
|
328
|
+
throw new Error(`Metadata expected for command "${this.commandType}" but none was provided.`);
|
|
329
|
+
}
|
|
330
|
+
const metadataValue = (metadataOption as { metadata: Metadata }).metadata;
|
|
331
|
+
parsedMetadata = metadataSchema.parse(metadataValue) as HandlerMetadata<Metadata>;
|
|
332
|
+
debugCommand('[%s] Metadata validation passed', this.commandType);
|
|
333
|
+
} else {
|
|
334
|
+
parsedMetadata = undefined as HandlerMetadata<Metadata>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const command = (metadataSchema
|
|
338
|
+
? {
|
|
339
|
+
type: this.commandType,
|
|
340
|
+
data: parsedInput,
|
|
341
|
+
metadata: parsedMetadata,
|
|
342
|
+
kind: 'Command' as const,
|
|
343
|
+
}
|
|
344
|
+
: {
|
|
345
|
+
type: this.commandType,
|
|
346
|
+
data: parsedInput,
|
|
347
|
+
kind: 'Command' as const,
|
|
348
|
+
}) as unknown as BuildCommand<CommandType, Input, Metadata>;
|
|
349
|
+
|
|
350
|
+
debugCommand('[%s] Command created successfully', this.commandType);
|
|
351
|
+
|
|
352
|
+
const run = (resolvedStreamId: string, runOptions?: { handleOptions?: HandleOptions<Store> }) => {
|
|
353
|
+
debugCommand('[%s] Running command on stream: %s', this.commandType, resolvedStreamId);
|
|
354
|
+
return runner(eventStore, resolvedStreamId, command, runOptions?.handleOptions ?? handleOptions);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const result = await finalHandler({
|
|
358
|
+
command,
|
|
359
|
+
context,
|
|
360
|
+
eventStore,
|
|
361
|
+
input: parsedInput,
|
|
362
|
+
metadata: parsedMetadata,
|
|
363
|
+
streamId,
|
|
364
|
+
handleOptions,
|
|
365
|
+
run,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
debugCommand('[%s] Command execution completed successfully', this.commandType);
|
|
369
|
+
return result;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const createCommand = (input: Input, metadataValue?: HandlerMetadata<Metadata>) => {
|
|
373
|
+
const parsedInput = inputSchema.parse(input);
|
|
374
|
+
|
|
375
|
+
if (metadataSchema) {
|
|
376
|
+
if (metadataValue === undefined) {
|
|
377
|
+
throw new Error(`Metadata expected for command "${this.commandType}" but none was provided.`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const parsedMetadata = metadataSchema.parse(metadataValue) as HandlerMetadata<Metadata>;
|
|
381
|
+
return {
|
|
382
|
+
type: this.commandType,
|
|
383
|
+
data: parsedInput,
|
|
384
|
+
metadata: parsedMetadata,
|
|
385
|
+
kind: 'Command' as const,
|
|
386
|
+
} as unknown as BuildCommand<CommandType, Input, Metadata>;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (metadataValue !== undefined) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
`Metadata value provided for command "${this.commandType}" without metadata schema. Add .metadata(...) to the builder.`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
type: this.commandType,
|
|
397
|
+
data: parsedInput,
|
|
398
|
+
kind: 'Command' as const,
|
|
399
|
+
} as unknown as BuildCommand<CommandType, Input, Metadata>;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const schemas = {
|
|
403
|
+
input: inputSchema,
|
|
404
|
+
...(metadataSchema ? { metadata: metadataSchema } : {}),
|
|
405
|
+
event: eventSchema,
|
|
406
|
+
...(stateSchema ? { state: stateSchema } : {}),
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const runCommand = ({
|
|
410
|
+
eventStore,
|
|
411
|
+
command,
|
|
412
|
+
streamId,
|
|
413
|
+
handleOptions,
|
|
414
|
+
}: {
|
|
415
|
+
eventStore: Store;
|
|
416
|
+
command: BuildCommand<CommandType, Input, Metadata>;
|
|
417
|
+
streamId: string;
|
|
418
|
+
handleOptions?: HandleOptions<Store>;
|
|
419
|
+
}) => {
|
|
420
|
+
if (!streamId) {
|
|
421
|
+
throw new Error(`Missing streamId when running command "${this.commandType}".`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return runner(eventStore, streamId, command, handleOptions);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const flow = Object.assign(flowExecutor, {
|
|
428
|
+
commandType: this.commandType,
|
|
429
|
+
schemas,
|
|
430
|
+
decider: validatedDecider,
|
|
431
|
+
createCommand,
|
|
432
|
+
runCommand,
|
|
433
|
+
}) as CommandFlow<Context, Store, CommandType, Input, Metadata, State, StreamEvent, ReturnType>;
|
|
434
|
+
|
|
435
|
+
if (streamResolver) {
|
|
436
|
+
Object.assign(flow, {
|
|
437
|
+
resolveStreamId: (resolverParams: { command: BuildCommand<CommandType, Input, Metadata>; context: Context }) =>
|
|
438
|
+
streamResolver(resolverParams),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
debugCommand('[%s] Command flow built successfully', this.commandType);
|
|
443
|
+
return flow;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private ensureInputSchema(): ZodTypeAny {
|
|
447
|
+
if (!this.inputSchema) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`Command "${this.commandType}" is missing an input schema. Call .input(...) before defining the handler.`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
return this.inputSchema;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private ensureEventSchema(): ZodTypeAny {
|
|
456
|
+
if (!this.eventSchema) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`Command "${this.commandType}" is missing an event schema. Call .events(...) before defining the handler.`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return this.eventSchema;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private ensureDecider(): Decider<State, BuildCommand<CommandType, Input, Metadata>, StreamEvent> {
|
|
465
|
+
if (!this.currentDecider) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Command "${this.commandType}" is missing a decider definition. Call .decider(...) before defining the handler.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return this.currentDecider;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
type Toolkit<Context, Store extends EventStore> = {
|
|
476
|
+
command<CommandType extends string>(
|
|
477
|
+
commandType: CommandType,
|
|
478
|
+
): CommandFlowBuilder<Context, Store, CommandType, DefaultRecord, undefined, unknown, Event>;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const createToolkit = <Context, Store extends EventStore>(): Toolkit<Context, Store> => ({
|
|
482
|
+
command: commandType => new CommandFlowBuilder<Context, Store, typeof commandType>(commandType),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
export const es = {
|
|
486
|
+
$context<Context, Store extends EventStore = EventStore>() {
|
|
487
|
+
return createToolkit<Context, Store>();
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
export const eventSourcing = es;
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Command, DeciderCommandHandler, Event } from '@event-driven-io/emmett';
|
|
2
|
+
|
|
3
|
+
import type { CommandHandler } from './builders/command-handler.js';
|
|
4
|
+
|
|
5
|
+
export { DeciderCommandHandler } from '@event-driven-io/emmett';
|
|
6
|
+
export type {
|
|
7
|
+
Command,
|
|
8
|
+
CommandBus,
|
|
9
|
+
CommandHandlerOptions,
|
|
10
|
+
CommandHandlerResult,
|
|
11
|
+
CommandHandlerRetryOptions,
|
|
12
|
+
CommandTypeOf,
|
|
13
|
+
DefaultCommandMetadata,
|
|
14
|
+
} from '@event-driven-io/emmett';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generic type for a command handler function
|
|
18
|
+
*/
|
|
19
|
+
export type CommandHandlerType<C extends Command, State, StreamEvent extends Event> = ReturnType<
|
|
20
|
+
typeof DeciderCommandHandler<State, C, StreamEvent>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Command handler configuration
|
|
25
|
+
*/
|
|
26
|
+
export type CommandHandlerConfig<C extends Command = Command, State = unknown, StreamEvent extends Event = Event> = {
|
|
27
|
+
commandType: string;
|
|
28
|
+
handler: CommandHandlerType<C, State, StreamEvent>;
|
|
29
|
+
getStreamName: (command: C) => string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Variance-friendly registration record describing a single command handler.
|
|
34
|
+
*
|
|
35
|
+
* Default type parameters are chosen so that `CommandHandlerRegistration[]`
|
|
36
|
+
* can hold handlers of many concrete command types:
|
|
37
|
+
*
|
|
38
|
+
* - `C = never` — contravariant (parameter position): `never` extends every
|
|
39
|
+
* specific command type, so any handler function is assignable.
|
|
40
|
+
* - `State = unknown` — covariant (return position): all aggregate states
|
|
41
|
+
* extend `unknown`.
|
|
42
|
+
* - `StreamEvent = Event` — covariant: all events extend the base `Event`.
|
|
43
|
+
*
|
|
44
|
+
* @template C The specific command type this handler processes.
|
|
45
|
+
* @template State The aggregate state type.
|
|
46
|
+
* @template StreamEvent The stream event type.
|
|
47
|
+
*/
|
|
48
|
+
export type CommandHandlerRegistration<C = never, State = unknown, StreamEvent extends Event = Event> = {
|
|
49
|
+
commandType: string;
|
|
50
|
+
handler: CommandHandler<State, C, StreamEvent>;
|
|
51
|
+
getStreamName: (command: C) => string;
|
|
52
|
+
};
|
package/src/debug.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { debug } from '@arki/log/debug';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Debug logger for event store operations
|
|
5
|
+
* Enable with: DEBUG=es:store
|
|
6
|
+
*/
|
|
7
|
+
export const debugStore = debug('es:store');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Debug logger for command flow operations
|
|
11
|
+
* Enable with: DEBUG=es:command
|
|
12
|
+
*/
|
|
13
|
+
export const debugCommand = debug('es:command');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Debug logger for projection operations
|
|
17
|
+
* Enable with: DEBUG=es:projection
|
|
18
|
+
*/
|
|
19
|
+
export const debugProjection = debug('es:projection');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Debug logger for process manager/saga operations
|
|
23
|
+
* Enable with: DEBUG=es:process
|
|
24
|
+
*/
|
|
25
|
+
export const debugProcess = debug('es:process');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Debug logger for builder operations
|
|
29
|
+
* Enable with: DEBUG=es:builder
|
|
30
|
+
*/
|
|
31
|
+
export const debugBuilder = debug('es:builder');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Enable all event sourcing debug logs
|
|
35
|
+
* Enable with: DEBUG=es:*
|
|
36
|
+
*/
|
package/src/decide.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export of Emmett's `Decider` type, which represents the behaviour contract
|
|
3
|
+
* of an event-sourced aggregate: `initialState`, `evolve`, and `decide`.
|
|
4
|
+
*
|
|
5
|
+
* Within this package we often refer to the same concept as an **aggregate**
|
|
6
|
+
* to emphasise that it owns state and command handling. Stick to `Decider` when
|
|
7
|
+
* interacting with upstream Emmett utilities, but feel free to adopt the
|
|
8
|
+
* aggregate terminology in your own domain code for clarity.
|
|
9
|
+
*/
|
|
10
|
+
export { type Decider } from '@event-driven-io/emmett';
|