@arki/event-sourcing 0.1.1 → 0.1.2

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.
@@ -0,0 +1,153 @@
1
+ import type { z } from '@arki/contracts';
2
+
3
+ import type { Event } from '../event.js';
4
+ import { debugBuilder } from '../debug.js';
5
+
6
+ /**
7
+ * Type definition for CanHandle predicate
8
+ * Used to filter events that a process manager can handle
9
+ */
10
+ type CanHandle<EventType extends Event> = EventType['type'][];
11
+
12
+ /**
13
+ * Fluent builder for Emmett process managers (sagas).
14
+ *
15
+ * Process managers sit outside an aggregate and react to streams of events in order to
16
+ * coordinate long-running workflows. This builder helps you describe that behaviour with
17
+ * explicit event filters, a validated execution context, and a terminal `.handler(...)`
18
+ * that produces the configuration object Emmett expects.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const orderSaga = defineProcessManager<OrderEvent>()
23
+ * .name('order-fulfillment')
24
+ * .eventTypes(['OrderPlaced', 'PaymentProcessed', 'OrderShipped'])
25
+ * .contextSchema(z.object({ messageBus: z.any(), repo: z.any() }))
26
+ * .handler(async (events, context) => {
27
+ * for (const event of events) {
28
+ * if (event.type === 'OrderPlaced') {
29
+ * await context.messageBus.send({ type: 'ProcessPayment', data: event.data });
30
+ * } else if (event.type === 'PaymentProcessed') {
31
+ * await context.messageBus.send({ type: 'ShipOrder', data: event.data });
32
+ * }
33
+ * }
34
+ * });
35
+ * ```
36
+ */
37
+ class ProcessManagerBuilder<EventType extends Event = never, Context = never> {
38
+ private _name?: string;
39
+ private _eventTypes?: EventType['type'][];
40
+ private _contextSchema?: z.ZodType<Context>;
41
+ private _handlerFn?: (events: EventType[], context: Context) => Promise<void>;
42
+
43
+ /**
44
+ * Creates a deep copy of the builder to ensure immutability
45
+ * @private
46
+ */
47
+ private clone(): ProcessManagerBuilder<EventType, Context> {
48
+ const builder = new ProcessManagerBuilder<EventType, Context>();
49
+ builder._name = this._name;
50
+ builder._eventTypes = this._eventTypes;
51
+ builder._contextSchema = this._contextSchema;
52
+ builder._handlerFn = this._handlerFn;
53
+ return builder;
54
+ }
55
+
56
+ /**
57
+ * Sets the process manager name
58
+ * @param name - Unique identifier for the process manager
59
+ * @returns A new ProcessManagerBuilder instance with the name set
60
+ */
61
+ name(name: string): ProcessManagerBuilder<EventType, Context> {
62
+ const builder = this.clone();
63
+ builder._name = name;
64
+ debugBuilder('[process-manager] Set name: %s', name);
65
+ return builder;
66
+ }
67
+
68
+ /**
69
+ * Sets the event types this process manager can handle
70
+ * @param types - Array of event type strings
71
+ * @returns A new ProcessManagerBuilder instance with the event types set
72
+ */
73
+ eventTypes(types: EventType['type'][]): ProcessManagerBuilder<EventType, Context> {
74
+ const builder = this.clone();
75
+ builder._eventTypes = types;
76
+ debugBuilder('[process-manager:%s] Set event types: %o', this._name ?? 'unnamed', types);
77
+ return builder;
78
+ }
79
+
80
+ /**
81
+ * Sets the context schema for validation
82
+ * @param schema - Zod schema for validating the context
83
+ * @returns A new ProcessManagerBuilder instance with the context schema set
84
+ */
85
+ contextSchema<TNewContext>(schema: z.ZodType<TNewContext>): ProcessManagerBuilder<EventType, TNewContext> {
86
+ const builder = new ProcessManagerBuilder<EventType, TNewContext>();
87
+ builder._name = this._name;
88
+ builder._eventTypes = this._eventTypes;
89
+ builder._contextSchema = schema;
90
+ builder._handlerFn = this._handlerFn as ((events: EventType[], context: TNewContext) => Promise<void>) | undefined;
91
+ debugBuilder('[process-manager:%s] Set context schema', this._name ?? 'unnamed');
92
+ return builder;
93
+ }
94
+
95
+ /**
96
+ * Terminal method that creates the process manager
97
+ *
98
+ * Validates that all required properties are set and returns an object
99
+ * with three properties:
100
+ * - processorId: The process manager identifier
101
+ * - canHandle: Array of event types to filter events
102
+ * - eachBatch: Function that validates context and processes event batches
103
+ *
104
+ * @param fn - Function that processes event batches to coordinate workflows
105
+ * @returns An object containing the process manager configuration
106
+ * @throws Error if name, eventTypes, or contextSchema are not defined
107
+ */
108
+ handler(fn: (events: EventType[], context: Context) => Promise<void>): {
109
+ processorId: string;
110
+ canHandle: CanHandle<EventType>;
111
+ eachBatch: (events: EventType[], context: unknown) => Promise<void>;
112
+ } {
113
+ if (!this._name || !this._eventTypes || !this._contextSchema) {
114
+ debugBuilder('[process-manager] ERROR: Missing required properties');
115
+ throw new Error('Process manager must have name, eventTypes, and contextSchema defined');
116
+ }
117
+
118
+ const name = this._name;
119
+ const eventTypes = this._eventTypes;
120
+ const contextSchema = this._contextSchema;
121
+
122
+ debugBuilder('[process-manager:%s] Building handler for event types: %o', name, eventTypes);
123
+
124
+ return {
125
+ processorId: name,
126
+ canHandle: eventTypes,
127
+ eachBatch: async (events: EventType[], context: unknown) => {
128
+ debugBuilder('[process-manager:%s] Validating context', name);
129
+ const validatedContext = contextSchema.parse(context);
130
+ debugBuilder('[process-manager:%s] Processing batch of %d events', name, events.length);
131
+ await fn(events, validatedContext);
132
+ debugBuilder('[process-manager:%s] Batch processed successfully', name);
133
+ },
134
+ };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Factory function to create a new ProcessManagerBuilder
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const builder = defineProcessManager<MyEvent>()
144
+ * .name('my-process-manager')
145
+ * .eventTypes(['EventTypeA', 'EventTypeB'])
146
+ * .contextSchema(myContextSchema)
147
+ * .handler(async (events, context) => { ... });
148
+ * ```
149
+ */
150
+ export function defineProcessManager<EventType extends Event = never>(): ProcessManagerBuilder<EventType, never> {
151
+ debugBuilder('[process-manager] Creating new process manager builder');
152
+ return new ProcessManagerBuilder<EventType, never>();
153
+ }
@@ -0,0 +1,151 @@
1
+ import type { z } from '@arki/contracts';
2
+
3
+ import type { AnyEvent } from '../event.js';
4
+ import { debugBuilder } from '../debug.js';
5
+
6
+ /**
7
+ * Type definition for CanHandle predicate
8
+ * Used to filter events that a projection can handle
9
+ */
10
+ type CanHandle<EventType extends AnyEvent> = EventType['type'][];
11
+
12
+ /**
13
+ * Fluent builder for read-model projections.
14
+ *
15
+ * Projections consume event streams and populate query-side views. This builder captures
16
+ * the essentials—projection name, handled event types, validated context—and terminates
17
+ * in a `.handler(...)` call that returns the shape required by Emmett’s projection
18
+ * registration utilities.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const userProjection = defineProjection<UserEvent>()
23
+ * .name('user-projection')
24
+ * .eventTypes(['UserCreated', 'UserUpdated'])
25
+ * .contextSchema(z.object({ repo: z.any() }))
26
+ * .handler(async (events, context) => {
27
+ * for (const event of events) {
28
+ * if (event.type === 'UserCreated') {
29
+ * await context.repo.users.create(event.data);
30
+ * }
31
+ * }
32
+ * });
33
+ * ```
34
+ */
35
+ class ProjectionBuilder<EventType extends AnyEvent = never, Context = never> {
36
+ private _name?: string;
37
+ private _eventTypes?: EventType['type'][];
38
+ private _contextSchema?: z.ZodType<Context>;
39
+ private _handlerFn?: (events: EventType[], context: Context) => Promise<void>;
40
+
41
+ /**
42
+ * Creates a deep copy of the builder to ensure immutability
43
+ * @private
44
+ */
45
+ private clone(): ProjectionBuilder<EventType, Context> {
46
+ const builder = new ProjectionBuilder<EventType, Context>();
47
+ builder._name = this._name;
48
+ builder._eventTypes = this._eventTypes;
49
+ builder._contextSchema = this._contextSchema;
50
+ builder._handlerFn = this._handlerFn;
51
+ return builder;
52
+ }
53
+
54
+ /**
55
+ * Sets the projection name
56
+ * @param name - Unique identifier for the projection
57
+ * @returns A new ProjectionBuilder instance with the name set
58
+ */
59
+ name(name: string): ProjectionBuilder<EventType, Context> {
60
+ const builder = this.clone();
61
+ builder._name = name;
62
+ debugBuilder('[projection] Set name: %s', name);
63
+ return builder;
64
+ }
65
+
66
+ /**
67
+ * Sets the event types this projection can handle
68
+ * @param types - Array of event type strings
69
+ * @returns A new ProjectionBuilder instance with the event types set
70
+ */
71
+ eventTypes(types: EventType['type'][]): ProjectionBuilder<EventType, Context> {
72
+ const builder = this.clone();
73
+ builder._eventTypes = types;
74
+ debugBuilder('[projection:%s] Set event types: %o', this._name ?? 'unnamed', types);
75
+ return builder;
76
+ }
77
+
78
+ /**
79
+ * Sets the context schema for validation
80
+ * @param schema - Zod schema for validating the context
81
+ * @returns A new ProjectionBuilder instance with the context schema set
82
+ */
83
+ contextSchema<TNewContext>(schema: z.ZodType<TNewContext>): ProjectionBuilder<EventType, TNewContext> {
84
+ const builder = new ProjectionBuilder<EventType, TNewContext>();
85
+ builder._name = this._name;
86
+ builder._eventTypes = this._eventTypes;
87
+ builder._contextSchema = schema;
88
+ builder._handlerFn = this._handlerFn as ((events: EventType[], context: TNewContext) => Promise<void>) | undefined;
89
+ debugBuilder('[projection:%s] Set context schema', this._name ?? 'unnamed');
90
+ return builder;
91
+ }
92
+
93
+ /**
94
+ * Terminal method that creates the projection
95
+ *
96
+ * Validates that all required properties are set and returns an object
97
+ * with three properties:
98
+ * - name: The projection name
99
+ * - canHandle: Array of event types to filter events
100
+ * - handle: Function that validates context and processes events
101
+ *
102
+ * @param fn - Function that processes events and updates read models
103
+ * @returns An object containing the projection configuration
104
+ * @throws Error if name, eventTypes, or contextSchema are not defined
105
+ */
106
+ handler(fn: (events: EventType[], context: Context) => Promise<void>): {
107
+ name: string;
108
+ canHandle: CanHandle<EventType>;
109
+ handle: (events: EventType[], context: unknown) => Promise<void>;
110
+ } {
111
+ if (!this._name || !this._eventTypes || !this._contextSchema) {
112
+ debugBuilder('[projection] ERROR: Missing required properties');
113
+ throw new Error('Projection must have name, eventTypes, and contextSchema defined');
114
+ }
115
+
116
+ const name = this._name;
117
+ const eventTypes = this._eventTypes;
118
+ const contextSchema = this._contextSchema;
119
+
120
+ debugBuilder('[projection:%s] Building handler for event types: %o', name, eventTypes);
121
+
122
+ return {
123
+ name,
124
+ canHandle: eventTypes,
125
+ handle: async (events: EventType[], context: unknown) => {
126
+ debugBuilder('[projection:%s] Validating context', name);
127
+ const validatedContext = contextSchema.parse(context);
128
+ debugBuilder('[projection:%s] Processing %d events', name, events.length);
129
+ await fn(events, validatedContext);
130
+ debugBuilder('[projection:%s] Events processed successfully', name);
131
+ },
132
+ };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Factory function to create a new ProjectionBuilder
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const builder = defineProjection<MyEvent>()
142
+ * .name('my-projection')
143
+ * .eventTypes(['EventTypeA', 'EventTypeB'])
144
+ * .contextSchema(myContextSchema)
145
+ * .handler(async (events, context) => { ... });
146
+ * ```
147
+ */
148
+ export function defineProjection<EventType extends AnyEvent = never>(): ProjectionBuilder<EventType, never> {
149
+ debugBuilder('[projection] Creating new projection builder');
150
+ return new ProjectionBuilder<EventType, never>();
151
+ }
@@ -0,0 +1,338 @@
1
+ import type { z } from '@arki/contracts';
2
+
3
+ import type { Event } from '../event.js';
4
+ import { debugBuilder } from '../debug.js';
5
+
6
+ /**
7
+ * Type definition for CanHandle predicate
8
+ * Used to filter events that a stateful process manager can handle
9
+ */
10
+ type CanHandle<EventType extends Event> = EventType['type'][];
11
+
12
+ /**
13
+ * StatefulProcessManagerBuilder - Fluent API for building type-safe stateful process managers (sagas)
14
+ *
15
+ * Provides an immutable builder pattern for creating stateful process managers with:
16
+ * - Type safety through TypeScript generics
17
+ * - Runtime validation through Zod schemas
18
+ * - ORPC-style API with .handler() as terminal method
19
+ * - Persistent state management with load/save operations
20
+ *
21
+ * A Stateful Process Manager (Saga) maintains state across events to coordinate complex,
22
+ * long-running business processes by:
23
+ * - name: Unique identifier for the stateful process manager
24
+ * - eventTypes: Array of event types this process manager handles
25
+ * - stateSchema: Schema for validating process manager state
26
+ * - contextSchema: Schema for validating execution context
27
+ * - getStateId: Function to extract state identifier from events
28
+ * - loadState: Function to load existing state from storage
29
+ * - saveState: Function to persist state to storage
30
+ * - handler: Function that processes individual events and returns updated state
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * type OrderState = {
35
+ * orderId: string;
36
+ * status: 'pending' | 'paid' | 'shipped' | 'completed';
37
+ * paymentId?: string;
38
+ * trackingId?: string;
39
+ * };
40
+ *
41
+ * const orderProcessManager = defineStatefulProcessManager<OrderEvent, OrderState>()
42
+ * .name('order-fulfillment')
43
+ * .eventTypes(['OrderPlaced', 'PaymentProcessed', 'OrderShipped'])
44
+ * .stateSchema(z.object({
45
+ * orderId: z.string(),
46
+ * status: z.enum(['pending', 'paid', 'shipped', 'completed']),
47
+ * paymentId: z.string().optional(),
48
+ * trackingId: z.string().optional(),
49
+ * }))
50
+ * .contextSchema(z.object({ messageBus: z.any(), repo: z.any() }))
51
+ * .getStateId(event => `order-${event.data.orderId}`)
52
+ * .loadState(async (stateId, context) => {
53
+ * return await context.repo.orderSagas.findById(stateId);
54
+ * })
55
+ * .saveState(async (stateId, state, context) => {
56
+ * await context.repo.orderSagas.upsert({ id: stateId, ...state });
57
+ * })
58
+ * .handler(async (event, state, context) => {
59
+ * if (event.type === 'OrderPlaced') {
60
+ * // Initialize state for new order
61
+ * return {
62
+ * orderId: event.data.orderId,
63
+ * status: 'pending',
64
+ * };
65
+ * } else if (event.type === 'PaymentProcessed') {
66
+ * // Update state after payment
67
+ * return {
68
+ * ...state!,
69
+ * status: 'paid',
70
+ * paymentId: event.data.paymentId,
71
+ * };
72
+ * } else if (event.type === 'OrderShipped') {
73
+ * // Update state after shipping
74
+ * return {
75
+ * ...state!,
76
+ * status: 'shipped',
77
+ * trackingId: event.data.trackingId,
78
+ * };
79
+ * }
80
+ * return state!;
81
+ * });
82
+ * ```
83
+ */
84
+ class StatefulProcessManagerBuilder<EventType extends Event = never, State = never, Context = never> {
85
+ private _name?: string;
86
+ private _eventTypes?: EventType['type'][];
87
+ private _stateSchema?: z.ZodType<State>;
88
+ private _contextSchema?: z.ZodType<Context>;
89
+ private _getStateIdFn?: (event: EventType) => string;
90
+ private _loadStateFn?: (stateId: string, context: Context) => Promise<State | null>;
91
+ private _saveStateFn?: (stateId: string, state: State, context: Context) => Promise<void>;
92
+ private _handlerFn?: (event: EventType, state: State | null, context: Context) => Promise<State>;
93
+
94
+ /**
95
+ * Creates a deep copy of the builder to ensure immutability
96
+ * @private
97
+ */
98
+ private clone(): StatefulProcessManagerBuilder<EventType, State, Context> {
99
+ const builder = new StatefulProcessManagerBuilder<EventType, State, Context>();
100
+ builder._name = this._name;
101
+ builder._eventTypes = this._eventTypes;
102
+ builder._stateSchema = this._stateSchema;
103
+ builder._contextSchema = this._contextSchema;
104
+ builder._getStateIdFn = this._getStateIdFn;
105
+ builder._loadStateFn = this._loadStateFn;
106
+ builder._saveStateFn = this._saveStateFn;
107
+ builder._handlerFn = this._handlerFn;
108
+ return builder;
109
+ }
110
+
111
+ /**
112
+ * Sets the stateful process manager name
113
+ * @param name - Unique identifier for the stateful process manager
114
+ * @returns A new StatefulProcessManagerBuilder instance with the name set
115
+ */
116
+ name(name: string): StatefulProcessManagerBuilder<EventType, State, Context> {
117
+ const builder = this.clone();
118
+ builder._name = name;
119
+ debugBuilder('[stateful-process-manager] Set name: %s', name);
120
+ return builder;
121
+ }
122
+
123
+ /**
124
+ * Sets the event types this stateful process manager can handle
125
+ * @param types - Array of event type strings
126
+ * @returns A new StatefulProcessManagerBuilder instance with the event types set
127
+ */
128
+ eventTypes(types: EventType['type'][]): StatefulProcessManagerBuilder<EventType, State, Context> {
129
+ const builder = this.clone();
130
+ builder._eventTypes = types;
131
+ debugBuilder('[stateful-process-manager:%s] Set event types: %o', this._name ?? 'unnamed', types);
132
+ return builder;
133
+ }
134
+
135
+ /**
136
+ * Sets the state schema for validation
137
+ * @param schema - Zod schema for validating the stateful process manager state
138
+ * @returns A new StatefulProcessManagerBuilder instance with the state schema set
139
+ */
140
+ stateSchema<TNewState>(schema: z.ZodType<TNewState>): StatefulProcessManagerBuilder<EventType, TNewState, Context> {
141
+ const builder = new StatefulProcessManagerBuilder<EventType, TNewState, Context>();
142
+ builder._name = this._name;
143
+ builder._eventTypes = this._eventTypes;
144
+ builder._stateSchema = schema;
145
+ builder._contextSchema = this._contextSchema;
146
+ builder._getStateIdFn = this._getStateIdFn as ((event: EventType) => string) | undefined;
147
+ builder._loadStateFn = this._loadStateFn as
148
+ | ((stateId: string, context: Context) => Promise<TNewState | null>)
149
+ | undefined;
150
+ builder._saveStateFn = this._saveStateFn as
151
+ | ((stateId: string, state: TNewState, context: Context) => Promise<void>)
152
+ | undefined;
153
+ builder._handlerFn = this._handlerFn as
154
+ | ((event: EventType, state: TNewState | null, context: Context) => Promise<TNewState>)
155
+ | undefined;
156
+ debugBuilder('[stateful-process-manager:%s] Set state schema', this._name ?? 'unnamed');
157
+ return builder;
158
+ }
159
+
160
+ /**
161
+ * Sets the context schema for validation
162
+ * @param schema - Zod schema for validating the execution context
163
+ * @returns A new StatefulProcessManagerBuilder instance with the context schema set
164
+ */
165
+ contextSchema<TNewContext>(
166
+ schema: z.ZodType<TNewContext>,
167
+ ): StatefulProcessManagerBuilder<EventType, State, TNewContext> {
168
+ const builder = new StatefulProcessManagerBuilder<EventType, State, TNewContext>();
169
+ builder._name = this._name;
170
+ builder._eventTypes = this._eventTypes;
171
+ builder._stateSchema = this._stateSchema;
172
+ builder._contextSchema = schema;
173
+ builder._getStateIdFn = this._getStateIdFn;
174
+ builder._loadStateFn = this._loadStateFn as
175
+ | ((stateId: string, context: TNewContext) => Promise<State | null>)
176
+ | undefined;
177
+ builder._saveStateFn = this._saveStateFn as
178
+ | ((stateId: string, state: State, context: TNewContext) => Promise<void>)
179
+ | undefined;
180
+ builder._handlerFn = this._handlerFn as
181
+ | ((event: EventType, state: State | null, context: TNewContext) => Promise<State>)
182
+ | undefined;
183
+ debugBuilder('[stateful-process-manager:%s] Set context schema', this._name ?? 'unnamed');
184
+ return builder;
185
+ }
186
+
187
+ /**
188
+ * Sets the function to extract state identifier from events
189
+ * @param fn - Function that extracts a unique state identifier from an event
190
+ * @returns A new StatefulProcessManagerBuilder instance with the getStateId function set
191
+ */
192
+ getStateId(fn: (event: EventType) => string): StatefulProcessManagerBuilder<EventType, State, Context> {
193
+ const builder = this.clone();
194
+ builder._getStateIdFn = fn;
195
+ debugBuilder('[stateful-process-manager:%s] Set getStateId function', this._name ?? 'unnamed');
196
+ return builder;
197
+ }
198
+
199
+ /**
200
+ * Sets the function to load existing state from storage
201
+ * @param fn - Function that loads state by identifier, returns null if state doesn't exist
202
+ * @returns A new StatefulProcessManagerBuilder instance with the loadState function set
203
+ */
204
+ loadState(
205
+ fn: (stateId: string, context: Context) => Promise<State | null>,
206
+ ): StatefulProcessManagerBuilder<EventType, State, Context> {
207
+ const builder = this.clone();
208
+ builder._loadStateFn = fn;
209
+ debugBuilder('[stateful-process-manager:%s] Set loadState function', this._name ?? 'unnamed');
210
+ return builder;
211
+ }
212
+
213
+ /**
214
+ * Sets the function to persist state to storage
215
+ * @param fn - Function that saves state by identifier
216
+ * @returns A new StatefulProcessManagerBuilder instance with the saveState function set
217
+ */
218
+ saveState(
219
+ fn: (stateId: string, state: State, context: Context) => Promise<void>,
220
+ ): StatefulProcessManagerBuilder<EventType, State, Context> {
221
+ const builder = this.clone();
222
+ builder._saveStateFn = fn;
223
+ debugBuilder('[stateful-process-manager:%s] Set saveState function', this._name ?? 'unnamed');
224
+ return builder;
225
+ }
226
+
227
+ /**
228
+ * Terminal method that creates the stateful process manager
229
+ *
230
+ * Validates that all required configuration is set and returns an object
231
+ * with three properties:
232
+ * - processorId: The stateful process manager identifier
233
+ * - canHandle: Array of event types to filter events
234
+ * - eachBatch: Function that validates context, loads state for each event,
235
+ * processes it through the handler, and saves the updated state
236
+ *
237
+ * @param fn - Function that processes individual events with their state and returns updated state
238
+ * @returns An object containing the stateful process manager configuration
239
+ * @throws Error if name, eventTypes, stateSchema, contextSchema, getStateId, loadState, or saveState are not defined
240
+ */
241
+ handler(fn: (event: EventType, state: State | null, context: Context) => Promise<State>): {
242
+ processorId: string;
243
+ canHandle: CanHandle<EventType>;
244
+ eachBatch: (events: EventType[], context: unknown) => Promise<void>;
245
+ } {
246
+ if (
247
+ !this._name ||
248
+ !this._eventTypes ||
249
+ !this._stateSchema ||
250
+ !this._contextSchema ||
251
+ !this._getStateIdFn ||
252
+ !this._loadStateFn ||
253
+ !this._saveStateFn
254
+ ) {
255
+ debugBuilder('[stateful-process-manager] ERROR: Missing required properties');
256
+ throw new Error('Stateful process manager must have all configuration methods called');
257
+ }
258
+
259
+ const name = this._name;
260
+ const eventTypes = this._eventTypes;
261
+ const stateSchema = this._stateSchema;
262
+ const contextSchema = this._contextSchema;
263
+ const getStateIdFn = this._getStateIdFn;
264
+ const loadStateFn = this._loadStateFn;
265
+ const saveStateFn = this._saveStateFn;
266
+
267
+ debugBuilder('[stateful-process-manager:%s] Building handler for event types: %o', name, eventTypes);
268
+
269
+ return {
270
+ processorId: name,
271
+ canHandle: eventTypes,
272
+ eachBatch: async (events: EventType[], context: unknown) => {
273
+ debugBuilder('[stateful-process-manager:%s] Validating context', name);
274
+ const validatedContext = contextSchema.parse(context);
275
+ debugBuilder('[stateful-process-manager:%s] Processing batch of %d events', name, events.length);
276
+
277
+ for (const event of events) {
278
+ const stateId = getStateIdFn(event);
279
+ debugBuilder('[stateful-process-manager:%s] Processing event with stateId: %s', name, stateId);
280
+
281
+ // Load state
282
+ debugBuilder('[stateful-process-manager:%s] Loading state for: %s', name, stateId);
283
+ const state = await loadStateFn(stateId, validatedContext);
284
+ const validatedState = state ? stateSchema.parse(state) : null;
285
+ debugBuilder(
286
+ '[stateful-process-manager:%s] State loaded: %s',
287
+ name,
288
+ validatedState ? 'existing state' : 'no existing state',
289
+ );
290
+
291
+ // Handle event
292
+ debugBuilder('[stateful-process-manager:%s] Handling event', name);
293
+ const newState = await fn(event, validatedState, validatedContext);
294
+ const validatedNewState = stateSchema.parse(newState);
295
+ debugBuilder('[stateful-process-manager:%s] Event handled, new state computed', name);
296
+
297
+ // Save state
298
+ debugBuilder('[stateful-process-manager:%s] Saving state for: %s', name, stateId);
299
+ await saveStateFn(stateId, validatedNewState, validatedContext);
300
+ debugBuilder('[stateful-process-manager:%s] State saved successfully', name);
301
+ }
302
+
303
+ debugBuilder('[stateful-process-manager:%s] Batch processed successfully', name);
304
+ },
305
+ };
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Factory function to create a new StatefulProcessManagerBuilder
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const builder = defineStatefulProcessManager<MyEvent, MyState>()
315
+ * .name('my-stateful-process-manager')
316
+ * .eventTypes(['EventTypeA', 'EventTypeB'])
317
+ * .stateSchema(myStateSchema)
318
+ * .contextSchema(myContextSchema)
319
+ * .getStateId(event => `state-${event.data.id}`)
320
+ * .loadState(async (stateId, context) => {
321
+ * return await context.repo.findById(stateId);
322
+ * })
323
+ * .saveState(async (stateId, state, context) => {
324
+ * await context.repo.upsert({ id: stateId, ...state });
325
+ * })
326
+ * .handler(async (event, state, context) => {
327
+ * // Process event and return updated state
328
+ * return { ...state, updated: true };
329
+ * });
330
+ * ```
331
+ */
332
+ export function defineStatefulProcessManager<
333
+ EventType extends Event = never,
334
+ State = never,
335
+ >(): StatefulProcessManagerBuilder<EventType, State, never> {
336
+ debugBuilder('[stateful-process-manager] Creating new stateful process manager builder');
337
+ return new StatefulProcessManagerBuilder<EventType, State, never>();
338
+ }
package/src/bus.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { getInMemoryMessageBus } from '@event-driven-io/emmett';
2
+
3
+ export type { CommandBus, EventBus, MessageBus } from '@event-driven-io/emmett';