@requence/event-sourcing 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/chunk-24xjphpk.js +51 -0
  4. package/dist/chunk-24xjphpk.js.map +10 -0
  5. package/dist/chunk-3t1vzbab.js +88 -0
  6. package/dist/chunk-3t1vzbab.js.map +10 -0
  7. package/dist/chunk-6q8mr1sg.js +13 -0
  8. package/dist/chunk-6q8mr1sg.js.map +10 -0
  9. package/dist/chunk-9psg50cb.js +22 -0
  10. package/dist/chunk-9psg50cb.js.map +10 -0
  11. package/dist/chunk-dgzw1nj9.js +50 -0
  12. package/dist/chunk-dgzw1nj9.js.map +10 -0
  13. package/dist/chunk-gq80tgvf.js +70 -0
  14. package/dist/chunk-gq80tgvf.js.map +10 -0
  15. package/dist/chunk-jd4q8fcf.js +11 -0
  16. package/dist/chunk-jd4q8fcf.js.map +10 -0
  17. package/dist/chunk-m07vf8q9.js +210 -0
  18. package/dist/chunk-m07vf8q9.js.map +10 -0
  19. package/dist/chunk-ncpk70p4.js +313 -0
  20. package/dist/chunk-ncpk70p4.js.map +10 -0
  21. package/dist/chunk-pmfzjavb.js +49 -0
  22. package/dist/chunk-pmfzjavb.js.map +10 -0
  23. package/dist/chunk-qdczt68t.js +84 -0
  24. package/dist/chunk-qdczt68t.js.map +10 -0
  25. package/dist/chunk-tfy01fah.js +34 -0
  26. package/dist/chunk-tfy01fah.js.map +10 -0
  27. package/dist/chunk-tkvj6x8k.js +389 -0
  28. package/dist/chunk-tkvj6x8k.js.map +10 -0
  29. package/dist/chunk-ty9bhdvx.js +53 -0
  30. package/dist/chunk-ty9bhdvx.js.map +10 -0
  31. package/dist/chunk-wmz6a1jk.js +233 -0
  32. package/dist/chunk-wmz6a1jk.js.map +10 -0
  33. package/dist/chunk-x90g388y.js +22 -0
  34. package/dist/chunk-x90g388y.js.map +10 -0
  35. package/dist/chunk-z388cd25.js +91 -0
  36. package/dist/chunk-z388cd25.js.map +10 -0
  37. package/dist/chunk-zf3x573k.js +65 -0
  38. package/dist/chunk-zf3x573k.js.map +10 -0
  39. package/dist/createAggregateRoot.d.ts +191 -0
  40. package/dist/createAggregateRoot.d.ts.map +1 -0
  41. package/dist/createAggregateRoot.js +18 -0
  42. package/dist/createAggregateRoot.js.map +9 -0
  43. package/dist/createCheckpointApi.d.ts +38 -0
  44. package/dist/createCheckpointApi.d.ts.map +1 -0
  45. package/dist/createCheckpointApi.js +9 -0
  46. package/dist/createCheckpointApi.js.map +9 -0
  47. package/dist/createEventListener.d.ts +46 -0
  48. package/dist/createEventListener.d.ts.map +1 -0
  49. package/dist/createEventListener.js +12 -0
  50. package/dist/createEventListener.js.map +9 -0
  51. package/dist/createEventStore.d.ts +57 -0
  52. package/dist/createEventStore.d.ts.map +1 -0
  53. package/dist/createEventStore.js +20 -0
  54. package/dist/createEventStore.js.map +9 -0
  55. package/dist/createProcessManager.d.ts +70 -0
  56. package/dist/createProcessManager.d.ts.map +1 -0
  57. package/dist/createProcessManager.js +16 -0
  58. package/dist/createProcessManager.js.map +9 -0
  59. package/dist/createProjection.d.ts +120 -0
  60. package/dist/createProjection.d.ts.map +1 -0
  61. package/dist/createProjection.js +16 -0
  62. package/dist/createProjection.js.map +9 -0
  63. package/dist/drizzle/index.d.ts +449 -0
  64. package/dist/drizzle/index.d.ts.map +1 -0
  65. package/dist/drizzle/index.js +325 -0
  66. package/dist/drizzle/index.js.map +10 -0
  67. package/dist/drizzle/postgres.d.ts +686 -0
  68. package/dist/drizzle/postgres.d.ts.map +1 -0
  69. package/dist/drizzle/postgres.js +18 -0
  70. package/dist/drizzle/postgres.js.map +9 -0
  71. package/dist/errors.d.ts +53 -0
  72. package/dist/errors.d.ts.map +1 -0
  73. package/dist/errors.js +26 -0
  74. package/dist/errors.js.map +9 -0
  75. package/dist/index.d.ts +15 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +73 -0
  78. package/dist/index.js.map +9 -0
  79. package/dist/lock.d.ts +6 -0
  80. package/dist/lock.d.ts.map +1 -0
  81. package/dist/lock.js +8 -0
  82. package/dist/lock.js.map +9 -0
  83. package/dist/memory/index.d.ts +15 -0
  84. package/dist/memory/index.d.ts.map +1 -0
  85. package/dist/memory/index.js +178 -0
  86. package/dist/memory/index.js.map +10 -0
  87. package/dist/redis/index.d.ts +2 -0
  88. package/dist/redis/index.d.ts.map +1 -0
  89. package/dist/redis/index.js +8 -0
  90. package/dist/redis/index.js.map +9 -0
  91. package/dist/redis/redisLock.d.ts +4 -0
  92. package/dist/redis/redisLock.d.ts.map +1 -0
  93. package/dist/redis/redisLock.js +8 -0
  94. package/dist/redis/redisLock.js.map +9 -0
  95. package/dist/refresh.d.ts +4 -0
  96. package/dist/refresh.d.ts.map +1 -0
  97. package/dist/refresh.js +13 -0
  98. package/dist/refresh.js.map +9 -0
  99. package/dist/replay.d.ts +4 -0
  100. package/dist/replay.d.ts.map +1 -0
  101. package/dist/replay.js +13 -0
  102. package/dist/replay.js.map +9 -0
  103. package/dist/snapshotGenerator.d.ts +79 -0
  104. package/dist/snapshotGenerator.d.ts.map +1 -0
  105. package/dist/snapshotGenerator.js +9 -0
  106. package/dist/snapshotGenerator.js.map +9 -0
  107. package/dist/superjson.d.ts +5 -0
  108. package/dist/superjson.d.ts.map +1 -0
  109. package/dist/superjson.js +12 -0
  110. package/dist/superjson.js.map +9 -0
  111. package/dist/transaction.d.ts +13 -0
  112. package/dist/transaction.d.ts.map +1 -0
  113. package/dist/transaction.js +8 -0
  114. package/dist/transaction.js.map +9 -0
  115. package/dist/utilityTypes.d.js +2 -0
  116. package/dist/utilityTypes.d.js.map +9 -0
  117. package/dist/utils.d.ts +4 -0
  118. package/dist/utils.d.ts.map +1 -0
  119. package/dist/utils.js +10 -0
  120. package/dist/utils.js.map +9 -0
  121. package/dist/wrappedEventBuilder.d.ts +37 -0
  122. package/dist/wrappedEventBuilder.d.ts.map +1 -0
  123. package/dist/wrappedEventBuilder.js +12 -0
  124. package/dist/wrappedEventBuilder.js.map +9 -0
  125. package/package.json +87 -1
  126. package/src/createAggregateRoot.ts +902 -0
  127. package/src/createCheckpointApi.ts +106 -0
  128. package/src/createEventListener.ts +118 -0
  129. package/src/createEventStore.ts +446 -0
  130. package/src/createProcessManager.ts +363 -0
  131. package/src/createProjection.ts +577 -0
  132. package/src/drizzle/index.ts +449 -0
  133. package/src/drizzle/postgres.ts +120 -0
  134. package/src/errors.ts +85 -0
  135. package/src/index.ts +45 -0
  136. package/src/lock.ts +58 -0
  137. package/src/memory/index.ts +224 -0
  138. package/src/redis/index.ts +1 -0
  139. package/src/redis/redisLock.ts +61 -0
  140. package/src/refresh.ts +19 -0
  141. package/src/replay.ts +19 -0
  142. package/src/snapshotGenerator.ts +190 -0
  143. package/src/superjson.ts +11 -0
  144. package/src/tests/aggregateRoot.spec.ts +532 -0
  145. package/src/tests/eventListener.spec.ts +57 -0
  146. package/src/tests/eventStore.spec.ts +86 -0
  147. package/src/tests/processManager.spec.ts +185 -0
  148. package/src/tests/projection.spec.ts +511 -0
  149. package/src/tests/projectionProcessManager.spec.ts +203 -0
  150. package/src/tests/setup.ts +230 -0
  151. package/src/tests/transaction.spec.ts +185 -0
  152. package/src/transaction.ts +93 -0
  153. package/src/utilityTypes.d.ts +110 -0
  154. package/src/utils.ts +43 -0
  155. package/src/wrappedEventBuilder.ts +181 -0
@@ -0,0 +1,902 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+
3
+ import { deepEqual } from 'fast-equals'
4
+ import { z } from 'zod/v4'
5
+
6
+ import type {
7
+ EmitEvents,
8
+ InternalAppendEvents,
9
+ LoadEvents,
10
+ } from './createEventStore.ts'
11
+ import { getProcessManagerInfo } from './createProcessManager.ts'
12
+ import { getProjectionInfo } from './createProjection.ts'
13
+ import {
14
+ CommandError,
15
+ EventValidationError,
16
+ InfiniteLoopError,
17
+ ValidationError,
18
+ } from './errors.ts'
19
+ import type { LockCreator } from './lock.ts'
20
+ import { isRefreshing } from './refresh.ts'
21
+ import createSnapshotGenerator, {
22
+ type CompleteSnapshotGenerator,
23
+ type InternalSnapshotGenerator,
24
+ type SnapshotGenerator,
25
+ } from './snapshotGenerator.ts'
26
+ import { clone } from './superjson.ts'
27
+ import type { Transaction, TransactionDelay } from './transaction.ts'
28
+ import type {
29
+ BaseInputEvent,
30
+ BaseOutputEvent,
31
+ EventTemplate,
32
+ Keys,
33
+ MaybePromise,
34
+ Merge,
35
+ OutputEvents,
36
+ Prettify,
37
+ } from './utilityTypes.ts'
38
+ import { awaitAll, normalizeArray } from './utils.ts'
39
+ import {
40
+ type WrappedEvent,
41
+ type WrappedEventBuilder,
42
+ handleEventError,
43
+ unwrapEvent,
44
+ wrappedEventBuilder,
45
+ } from './wrappedEventBuilder.ts'
46
+
47
+ type UppercaseRecord<T extends Record<string, any>> = {
48
+ [K in keyof T]: K extends Capitalize<K & string>
49
+ ? any
50
+ : `Error: Key '${K & string}' must be Capitalized`
51
+ }
52
+
53
+ type BaseEventMap = Record<
54
+ string,
55
+ z.ZodType | (z.ZodType | { schema: z.ZodType; version: number })[]
56
+ >
57
+
58
+ type DownstreamRootPromise = () => Promise<unknown>
59
+ const downstreamRootsPromiseStore = new AsyncLocalStorage<
60
+ DownstreamRootPromise[]
61
+ >()
62
+
63
+ function registerRootPromiseToUpstream(rootPromise: DownstreamRootPromise) {
64
+ downstreamRootsPromiseStore.getStore()?.push(rootPromise)
65
+ }
66
+
67
+ async function collectDownstreamRootPromise(handler: () => Promise<void>) {
68
+ const rootPromises: DownstreamRootPromise[] = []
69
+ await downstreamRootsPromiseStore.run(rootPromises, handler)
70
+ return async () => {
71
+ await Promise.all(rootPromises.map((promise) => promise()))
72
+ }
73
+ }
74
+
75
+ type BaseInternalEvent<
76
+ Type extends string,
77
+ Version extends number,
78
+ Payload = unknown,
79
+ > = BaseOutputEvent<Type, Version, Payload>
80
+
81
+ type InternalEvent<Event extends EventTemplate> = Prettify<
82
+ {
83
+ [K in Event as K['version']]: Omit<
84
+ BaseInternalEvent<K['type'], K['version'], z.output<K['schema']>>,
85
+ 'createdAt' | 'position' | 'streamVersion'
86
+ >
87
+ }[Event['version']]
88
+ >
89
+
90
+ type InternalEventHandlers<Events extends EventTemplate> = {
91
+ [K in Events as `on${K['type']}`]?: (
92
+ event: InternalEvent<K>,
93
+ ) => MaybePromise<void>
94
+ }
95
+
96
+ type EventsFromMap<EventMap extends BaseEventMap> = Prettify<
97
+ {
98
+ [K in keyof EventMap]: { type: K & string } & (EventMap[K] extends z.ZodType
99
+ ? { schema: EventMap[K]; version: 0 }
100
+ : EventMap[K] extends { schema: infer S; version: infer V }
101
+ ? S extends z.ZodType
102
+ ? { schema: S; version: V }
103
+ : never
104
+ : EventMap[K] extends Array<infer I>
105
+ ? I extends z.ZodType
106
+ ? { schema: I; version: 0 }
107
+ : I extends { schema: infer S; version: infer V }
108
+ ? { schema: S; version: V }
109
+ : never
110
+ : never)
111
+ }[keyof EventMap]
112
+ >
113
+
114
+ type Constructor<T> = new (...args: any[]) => T
115
+
116
+ type Command = (
117
+ ...args: any[]
118
+ ) =>
119
+ | MaybePromise<void | WrappedEvent | WrappedEvent[]>
120
+ | Iterable<WrappedEvent>
121
+ | AsyncIterable<WrappedEvent>
122
+
123
+ type Utils = {
124
+ reset: () => void
125
+ replayUntil: (streamVersion: number) => Promise<void>
126
+ }
127
+
128
+ type VersionBuilder = <V extends number, S extends z.ZodType>(
129
+ version: V,
130
+ schema: S,
131
+ ) => { schema: S; version: V }
132
+
133
+ type Stream<Commands extends Record<string, Command>, State> = {
134
+ [A in keyof Commands]: (
135
+ ...args: Parameters<Commands[A]>
136
+ ) => Stream<Commands, State>
137
+ } & {
138
+ settled: () => Promise<Stream<Commands, State>>
139
+ state: () => Promise<State>
140
+ transformError: <T extends Error>(
141
+ error: Constructor<T>,
142
+ errorHandler: (commandError: CommandError<T>) => Error,
143
+ ) => Stream<Commands, State>
144
+ readonly streamId: string
145
+ }
146
+
147
+ export type AnyStream = {
148
+ settled: () => Promise<AnyStream>
149
+ state: () => Promise<unknown>
150
+ transformError: <T extends Error>(
151
+ error: Constructor<T>,
152
+ errorHandler: (error: CommandError<T>) => Error,
153
+ ) => AnyStream
154
+ }
155
+
156
+ export type AnyAggregateRoot = {
157
+ type: string
158
+ loadStream: (
159
+ id: string,
160
+ filter?: (event: BaseOutputEvent) => MaybePromise<boolean>,
161
+ ) => AnyStream
162
+ newStream: () => AnyStream
163
+ $inferState: any
164
+ $inferEvents: any
165
+ $inferEvent: any
166
+ $inferCommandParameters: any
167
+ }
168
+
169
+ type EventHandlerOptions = {
170
+ streamId: string
171
+ extendLock: (ms?: number) => Promise<boolean>
172
+ }
173
+
174
+ export type AggregateRootApi<
175
+ Name,
176
+ State = never,
177
+ Events extends EventTemplate = never,
178
+ Commands extends Record<string, Command> = Record<string, Command>,
179
+ Flags extends {
180
+ withInitialState: boolean
181
+ withEvents: boolean
182
+ withCommands: boolean
183
+ withEventHandlers: boolean
184
+ withSnapshots: boolean
185
+ } = {
186
+ withInitialState: false
187
+ withEvents: false
188
+ withCommands: true
189
+ withEventHandlers: true
190
+ withSnapshots: false
191
+ },
192
+ > = {
193
+ withInitialState: <NextState extends Record<string, unknown>>(
194
+ state: NextState,
195
+ ) => Omit<
196
+ AggregateRootApi<
197
+ Name,
198
+ NextState,
199
+ Events,
200
+ Commands,
201
+ Merge<Flags, { withInitialState: true; withEventHandlers: false }>
202
+ >,
203
+ Keys<Merge<Flags, { withInitialState: true; withEventHandlers: false }>>
204
+ >
205
+ withCommands: <
206
+ NextCommands extends Record<
207
+ Exclude<string, 'state' | 'settled' | 'transformError' | 'streamId'>,
208
+ Command
209
+ >,
210
+ >(
211
+ handlers: [State] extends [never]
212
+ ? (
213
+ event: WrappedEventBuilder<Events>,
214
+ options: EventHandlerOptions,
215
+ ) => NextCommands
216
+ : (
217
+ state: Readonly<State>,
218
+ event: WrappedEventBuilder<Events>,
219
+ options: EventHandlerOptions,
220
+ ) => NextCommands,
221
+ ) => Omit<
222
+ AggregateRootApi<
223
+ Name,
224
+ State,
225
+ Events,
226
+ NextCommands,
227
+ Merge<
228
+ Flags,
229
+ { withInitialState: true; withCommands: true; withEvents: true }
230
+ >
231
+ >,
232
+ Keys<
233
+ Merge<
234
+ Flags,
235
+ { withInitialState: true; withCommands: true; withEvents: true }
236
+ >
237
+ >
238
+ >
239
+ withEvents: {
240
+ <EventMap extends BaseEventMap>(
241
+ handler: EventMap & UppercaseRecord<EventMap>,
242
+ ): Omit<
243
+ AggregateRootApi<
244
+ Name,
245
+ State,
246
+ EventsFromMap<EventMap>,
247
+ Commands,
248
+ Merge<
249
+ Flags,
250
+ {
251
+ withInitialState: true
252
+ withEvents: true
253
+ withCommands: false
254
+ }
255
+ >
256
+ >,
257
+ Keys<
258
+ Merge<
259
+ Flags,
260
+ {
261
+ withInitialState: true
262
+ withEvents: true
263
+ withCommands: false
264
+ }
265
+ >
266
+ >
267
+ >
268
+ <EventMap extends BaseEventMap>(
269
+ handler: (utils: {
270
+ version: VersionBuilder
271
+ z: typeof z
272
+ }) => EventMap & UppercaseRecord<EventMap>,
273
+ ): Omit<
274
+ AggregateRootApi<
275
+ Name,
276
+ State,
277
+ EventsFromMap<EventMap>,
278
+ Commands,
279
+ Merge<
280
+ Flags,
281
+ {
282
+ withInitialState: true
283
+ withEvents: true
284
+ withCommands: false
285
+ }
286
+ >
287
+ >,
288
+ Keys<
289
+ Merge<
290
+ Flags,
291
+ {
292
+ withInitialState: true
293
+ withEvents: true
294
+ withCommands: false
295
+ }
296
+ >
297
+ >
298
+ >
299
+ }
300
+ withEventHandlers: (
301
+ handler: (state: State, utils: Utils) => InternalEventHandlers<Events>,
302
+ ) => Omit<
303
+ AggregateRootApi<
304
+ Name,
305
+ State,
306
+ Events,
307
+ Commands,
308
+ Merge<
309
+ Flags,
310
+ { withInitialState: true; withEventHandlers: true; withEvents: true }
311
+ >
312
+ >,
313
+ Keys<
314
+ Merge<
315
+ Flags,
316
+ { withInitialState: true; withEventHandlers: true; withEvents: true }
317
+ >
318
+ >
319
+ >
320
+ withSnapshots: (
321
+ handler: (
322
+ state: State,
323
+ generate: SnapshotGenerator<Omit<Snapshot, 'schemaVersion' | 'state'>>,
324
+ utils: { z: typeof z },
325
+ ) => CompleteSnapshotGenerator,
326
+ ) => Omit<
327
+ AggregateRootApi<
328
+ Name,
329
+ State,
330
+ Events,
331
+ Commands,
332
+ Merge<Flags, { withSnapshots: true }>
333
+ >,
334
+ Keys<Merge<Flags, { withSnapshots: true }>>
335
+ >
336
+ loadStream: (
337
+ id: string,
338
+ filter?: (event: OutputEvents<Events>) => MaybePromise<boolean>,
339
+ ) => Stream<Commands, State>
340
+ newStream: (id?: string) => Stream<Commands, State>
341
+
342
+ $inferEvents: OutputEvents<Events>
343
+ $inferEvent: OutputEvents<Events> extends BaseOutputEvent
344
+ ? { [E in OutputEvents<Events> as E['type']]: E }
345
+ : never
346
+ $inferState: State
347
+ $inferCommandParameters: {
348
+ [CommandName in keyof Commands]: Parameters<Commands[CommandName]>
349
+ }
350
+ readonly type: Name
351
+ }
352
+
353
+ export type InternalAggregateRootApi = {
354
+ linkEventStore: (
355
+ loadEvents: LoadEvents,
356
+ appendEvents: InternalAppendEvents,
357
+ emitEvents: EmitEvents,
358
+ transaction: Transaction,
359
+ lock: LockCreator,
360
+ snapshots?: AggregateRootSnapshotMethods,
361
+ ) => void
362
+ eventTypes: string[]
363
+ readonly type: string
364
+ }
365
+
366
+ const snapshotSchema = z.object({
367
+ streamId: z.string(),
368
+ schemaVersion: z.int(),
369
+ state: z.unknown(),
370
+ createdAt: z.date(),
371
+ streamVersion: z.int(),
372
+ streamType: z.string(),
373
+ lastEventPosition: z.int(),
374
+ metadata: z.record(z.string(), z.unknown()).optional(),
375
+ })
376
+
377
+ export type Snapshot = z.output<typeof snapshotSchema>
378
+
379
+ export type AggregateRootSnapshotMethods = {
380
+ get: (select: {
381
+ stream: string
382
+ id: string
383
+ }) => MaybePromise<Snapshot | null | undefined>
384
+ put: (snapshot: Snapshot) => MaybePromise<void>
385
+ delete: (
386
+ target: {
387
+ stream: string
388
+ id: string
389
+ },
390
+ fromPosition: number,
391
+ ) => MaybePromise<void>
392
+ }
393
+
394
+ export function createAggregateRoot<Name extends string>(type: Name) {
395
+ let initialState: any = null
396
+ let createEventHandlers: (
397
+ state: any,
398
+ ) => InternalEventHandlers<any> = () => ({})
399
+
400
+ let loadEvents!: LoadEvents
401
+ let appendEvents!: InternalAppendEvents
402
+ let emitEvents!: EmitEvents
403
+ let snapshots: AggregateRootSnapshotMethods | undefined
404
+ let createLock!: LockCreator
405
+ let transaction!: Transaction
406
+
407
+ let createCommands: (
408
+ state: any,
409
+ eventBuilder: (name: string, payload: unknown) => WrappedEvent,
410
+ options: EventHandlerOptions,
411
+ ) => Record<string, Command> = () => ({})
412
+
413
+ const registeredEvents = new Map<string, Map<number, z.ZodType>>()
414
+ let createSnapshotApi:
415
+ | ((
416
+ state: any,
417
+ generate: SnapshotGenerator<any>,
418
+ utils: { z: typeof z },
419
+ ) => CompleteSnapshotGenerator & InternalSnapshotGenerator)
420
+ | null = null
421
+
422
+ const loadUntil = async (
423
+ state: any,
424
+ snapshotApi:
425
+ | (CompleteSnapshotGenerator & InternalSnapshotGenerator)
426
+ | undefined,
427
+ id: string,
428
+ untilPosition?: number,
429
+ filter?: (event: BaseOutputEvent) => MaybePromise<boolean>,
430
+ ) => {
431
+ let position = 0
432
+ let version = 0
433
+ if (snapshotApi) {
434
+ const snapshot = await snapshots?.get({
435
+ stream: type,
436
+ id,
437
+ })
438
+ if (snapshot) {
439
+ const result = snapshotSchema.safeParse(snapshot)
440
+
441
+ if (!result.success) {
442
+ throw new ValidationError(
443
+ 'loaded aggregate root snapshot does not comply with schema',
444
+ result.error,
445
+ )
446
+ }
447
+
448
+ snapshotApi.internalApply(result.data)
449
+ position = snapshot.lastEventPosition
450
+ version = snapshot.streamVersion
451
+ }
452
+ }
453
+
454
+ const events = loadEvents(
455
+ { stream: type, id },
456
+ {
457
+ from: position,
458
+ to: untilPosition,
459
+ },
460
+ )
461
+ const eventHandlers = createEventHandlers(state)
462
+ for await (const event of events) {
463
+ version += 1
464
+ if ((await filter?.(event)) === false) {
465
+ continue
466
+ }
467
+ await (eventHandlers as any)[`on${event.type}`]?.(event)
468
+ }
469
+
470
+ return version
471
+ }
472
+
473
+ const createStream = (state: any, streamId: string) => {
474
+ let version = 0
475
+ const snapshotApi = createSnapshotApi?.(
476
+ state,
477
+ createSnapshotGenerator() as any,
478
+ { z },
479
+ )
480
+ const errorHandlers = new Map<
481
+ Constructor<Error>,
482
+ (error: CommandError<Error>) => Error
483
+ >()
484
+ let downstreamSettledPromise = () => Promise.resolve()
485
+ let releaseLock!: () => Promise<void>
486
+ let extendLock!: EventHandlerOptions['extendLock']
487
+
488
+ const warnReleaseLock = () => {
489
+ if (releasedAfterChain) {
490
+ console.warn(`aggregate root for ${type}:${streamId} was open too long`)
491
+ }
492
+ releaseLock()
493
+ }
494
+
495
+ const processEvents = async (events: BaseInputEvent[]) => {
496
+ if (!appendEvents) {
497
+ throw new Error(
498
+ `aggregate root ${type} is not linked to an event store`,
499
+ )
500
+ }
501
+
502
+ const persistedEvents = await appendEvents(
503
+ { stream: type, id: streamId },
504
+ events,
505
+ version,
506
+ )
507
+
508
+ warnReleaseLock()
509
+
510
+ for (const persistedEvent of persistedEvents) {
511
+ const snapshot = await snapshotApi?.internalCapture(
512
+ persistedEvent,
513
+ persistedEvent.streamVersion,
514
+ )
515
+
516
+ if (snapshot) {
517
+ await snapshots?.put({
518
+ state: snapshot.state,
519
+ lastEventPosition: persistedEvent.position,
520
+ streamId,
521
+ streamVersion: version,
522
+ streamType: type,
523
+ schemaVersion: snapshot.version,
524
+ createdAt: new Date(),
525
+ })
526
+ }
527
+ }
528
+
529
+ version += events.length
530
+
531
+ downstreamSettledPromise = await collectDownstreamRootPromise(() =>
532
+ emitEvents(persistedEvents),
533
+ )
534
+ }
535
+
536
+ let commandChain = createLock([type, streamId]).then((lock) => {
537
+ releaseLock = lock.release
538
+ extendLock = lock.extend
539
+ return [] as Error | BaseInputEvent[]
540
+ })
541
+ let commandChainTouched = false
542
+ let releasedAfterChain = false
543
+ let transactionDelay: TransactionDelay | undefined
544
+
545
+ const stateReady = Promise.withResolvers<Error | undefined>()
546
+
547
+ const rawCommands = createCommands(state, wrappedEventBuilder, {
548
+ streamId,
549
+ extendLock,
550
+ })
551
+
552
+ const commands = Object.fromEntries(
553
+ Object.entries(rawCommands).map(([commandName, commandFn]) => [
554
+ commandName,
555
+ (...args: any[]) => {
556
+ // don't create new events when refreshing the process manager
557
+ if (isRefreshing()) {
558
+ return api.commands
559
+ }
560
+
561
+ const commandId = crypto.randomUUID()
562
+ transactionDelay ??= transaction.delay()
563
+ commandChainTouched = true
564
+ commandChain = commandChain
565
+ .then(async (previousEvents) => {
566
+ if (previousEvents instanceof Error) {
567
+ return previousEvents
568
+ }
569
+ const events = (
570
+ await normalizeArray(
571
+ commandFn.apply(rawCommands, args),
572
+ () => extendLock(),
573
+ )
574
+ ).filter((event) => Boolean(event))
575
+
576
+ const baseInputEvents = await Promise.all(
577
+ events.map(async (event) => {
578
+ event.withAdditionalMetadata({ commandName })
579
+ event.withDefaultCausationId(commandId)
580
+
581
+ const unwrappedEvent = unwrapEvent(event)
582
+
583
+ const eventVersions = registeredEvents.get(
584
+ unwrappedEvent.type,
585
+ )
586
+ if (!eventVersions) {
587
+ throw new Error(
588
+ `cannot append unknown event ${unwrappedEvent.type}`,
589
+ )
590
+ }
591
+ const latestVersion = Math.max(
592
+ ...Array.from(eventVersions.keys()),
593
+ )
594
+ const schema = eventVersions.get(latestVersion)!
595
+
596
+ const result = schema.safeParse(unwrappedEvent.payload)
597
+ if (!result.success) {
598
+ const error = new EventValidationError(
599
+ `payload does not comply with schema for event ${unwrappedEvent.type}@${latestVersion} at stream ${streamId}`,
600
+ result.error,
601
+ )
602
+
603
+ await handleEventError(event, error)
604
+ throw error
605
+ }
606
+
607
+ unwrappedEvent.payload = result.data
608
+
609
+ return {
610
+ ...unwrappedEvent,
611
+ schemaVersion: latestVersion,
612
+ }
613
+ }),
614
+ )
615
+
616
+ const eventHandlers = createEventHandlers(state)
617
+ for (const event of baseInputEvents) {
618
+ const processManagerInfo = getProcessManagerInfo()
619
+ if (processManagerInfo) {
620
+ if (
621
+ processManagerInfo.event.streamId === streamId &&
622
+ processManagerInfo.event.streamType === type &&
623
+ processManagerInfo.event.type === event.type &&
624
+ deepEqual(processManagerInfo.event.payload, event.payload)
625
+ ) {
626
+ return new InfiniteLoopError(
627
+ `Infinite loop detected. Process Manager "${processManagerInfo.name}" listens and emits same event "${event.type}" for stream ${type}:${streamId}, causing a recurring event cycle`,
628
+ )
629
+ }
630
+ }
631
+
632
+ await eventHandlers[`on${event.type}`]?.({
633
+ ...event,
634
+ streamId,
635
+ streamType: type,
636
+ })
637
+ }
638
+
639
+ return [...previousEvents, ...baseInputEvents]
640
+ })
641
+ .catch((error) => {
642
+ if (!(error instanceof Error)) {
643
+ throw error
644
+ }
645
+
646
+ const commandError = new CommandError(
647
+ type,
648
+ commandName,
649
+ streamId,
650
+ error,
651
+ )
652
+ for (const [ErrorType, handler] of errorHandlers) {
653
+ if (error instanceof ErrorType) {
654
+ return new CommandError(
655
+ type,
656
+ commandName,
657
+ streamId,
658
+ handler(commandError),
659
+ )
660
+ }
661
+ }
662
+
663
+ return commandError
664
+ })
665
+
666
+ const currentCommandChain = commandChain
667
+ currentCommandChain.then(async (allEvents) => {
668
+ if (
669
+ // we are the last command
670
+ currentCommandChain === commandChain
671
+ ) {
672
+ stateReady.resolve(
673
+ allEvents instanceof Error ? allEvents : undefined,
674
+ )
675
+
676
+ commandChain = commandChain.then(async () => {
677
+ // no Error in chain
678
+ if (Array.isArray(allEvents)) {
679
+ transactionDelay?.resolve()
680
+ try {
681
+ await transactionDelay?.commit()
682
+ } catch (error) {
683
+ if (error instanceof Error) {
684
+ warnReleaseLock()
685
+ return error
686
+ }
687
+ }
688
+
689
+ await processEvents(allEvents)
690
+ return []
691
+ }
692
+
693
+ warnReleaseLock()
694
+ transactionDelay?.reject(allEvents)
695
+ if (transaction.active) {
696
+ return []
697
+ }
698
+
699
+ throw allEvents
700
+ })
701
+ }
702
+ })
703
+
704
+ return api.commands
705
+ },
706
+ ]),
707
+ )
708
+
709
+ setImmediate(() => {
710
+ if (!commandChainTouched) {
711
+ releasedAfterChain = true
712
+ commandChain = commandChain.then((v) => {
713
+ releaseLock()
714
+ return v
715
+ })
716
+ }
717
+ })
718
+
719
+ const api = {
720
+ load(filter?: (event: BaseOutputEvent) => MaybePromise<boolean>) {
721
+ // ensure commands are executed after load is finished
722
+ commandChain = commandChain
723
+ .then(() =>
724
+ loadUntil(state, snapshotApi, streamId, undefined, filter),
725
+ )
726
+ .then((loadedVersion) => {
727
+ version = loadedVersion
728
+ return []
729
+ })
730
+ },
731
+ commands: {
732
+ ...commands,
733
+ async state() {
734
+ const maybeError = await Promise.race([
735
+ stateReady.promise,
736
+ awaitAll(() => commandChain),
737
+ ])
738
+
739
+ if (maybeError) {
740
+ throw maybeError
741
+ }
742
+
743
+ return state
744
+ },
745
+ async settled() {
746
+ const parentProcessManager = getProcessManagerInfo()
747
+ if (parentProcessManager) {
748
+ const error = new Error(
749
+ `aggregateRoot.settled() cannot be called inside a process manager. Attempted on aggregate stream "${type}" within Process Manager ${parentProcessManager.name}.`,
750
+ )
751
+ commandChain = commandChain.then(() => error)
752
+ throw error
753
+ }
754
+
755
+ if (transaction.active) {
756
+ const error = new Error(
757
+ `aggregateRoot.settled() cannot be called inside a transaction. Attempted on aggregate stream "${type}".`,
758
+ )
759
+ commandChain = commandChain.then(() => error)
760
+ throw error
761
+ }
762
+
763
+ await awaitAll(() => commandChain)
764
+ await downstreamSettledPromise()
765
+ return api.commands
766
+ },
767
+ transformError(
768
+ error: Constructor<Error>,
769
+ handler: (error: Error) => never,
770
+ ) {
771
+ errorHandlers.set(error, handler)
772
+ return api.commands
773
+ },
774
+ streamId,
775
+ },
776
+ }
777
+
778
+ registerRootPromiseToUpstream(() => api.commands.settled())
779
+ transaction.after(() => api.commands.settled())
780
+ return api
781
+ }
782
+
783
+ const api: AggregateRootApi<Name> & InternalAggregateRootApi = {
784
+ type,
785
+ withInitialState: (state) => {
786
+ initialState = clone(state)
787
+ return api as any
788
+ },
789
+
790
+ withCommands(handlers) {
791
+ createCommands = (state, eventBuilder, options) => {
792
+ if (state === null) {
793
+ return (handlers as any)(eventBuilder, options)
794
+ }
795
+
796
+ return (handlers as any)(state, eventBuilder, options)
797
+ }
798
+
799
+ return api as any
800
+ },
801
+
802
+ withEvents(handler) {
803
+ const eventMap: BaseEventMap =
804
+ typeof handler === 'function'
805
+ ? handler({ version: (version, schema) => ({ version, schema }), z })
806
+ : handler
807
+
808
+ for (const [type, config] of Object.entries(eventMap)) {
809
+ const versions = Array.isArray(config) ? config : [config]
810
+ for (const v of versions) {
811
+ let schema: z.ZodType
812
+ let version: number
813
+ if (typeof v === 'object' && 'version' in v) {
814
+ schema = v.schema
815
+ version = v.version
816
+ } else {
817
+ schema = v
818
+ version = 0
819
+ }
820
+
821
+ if (registeredEvents.get(type)?.has(version)) {
822
+ throw new Error(
823
+ `event ${type} is already registered in version ${version}`,
824
+ )
825
+ }
826
+
827
+ if (!registeredEvents.has(type)) {
828
+ registeredEvents.set(type, new Map())
829
+ }
830
+ registeredEvents.get(type)!.set(version, schema)
831
+ }
832
+ }
833
+ return api as any
834
+ },
835
+
836
+ withEventHandlers(handler) {
837
+ createEventHandlers = handler as any
838
+ return api as any
839
+ },
840
+
841
+ withSnapshots: (handler) => {
842
+ createSnapshotApi = handler as any
843
+ return api as any
844
+ },
845
+
846
+ newStream(id) {
847
+ if (!loadEvents) {
848
+ throw new Error(
849
+ `aggregate root ${type} is not linked to an event store`,
850
+ )
851
+ }
852
+
853
+ const parentProjectionName = getProjectionInfo()?.name
854
+ if (parentProjectionName) {
855
+ throw new Error(
856
+ `aggregateRoot.newStream() cannot be called inside a projection. Attempted on aggregate stream ${type} within Projection ${parentProjectionName}.`,
857
+ )
858
+ }
859
+ return createStream(clone(initialState), id ?? crypto.randomUUID())
860
+ .commands as any
861
+ },
862
+
863
+ loadStream(id, filter) {
864
+ if (!loadEvents) {
865
+ throw new Error(
866
+ `aggregate root ${type} is not linked to an event store`,
867
+ )
868
+ }
869
+
870
+ const parentProjectionName = getProjectionInfo()?.name
871
+ if (parentProjectionName) {
872
+ throw new Error(
873
+ `aggregateRoot.loadStream("${id}") cannot be called inside a projection. Attempted on aggregate stream ${type} within Projection ${parentProjectionName}.`,
874
+ )
875
+ }
876
+ const root = createStream(clone(initialState), id)
877
+ root.load(filter as any)
878
+ return root.commands as any
879
+ },
880
+
881
+ linkEventStore(load, append, emit, trans, lock, snap) {
882
+ loadEvents = load
883
+ appendEvents = append
884
+ emitEvents = emit
885
+ transaction = trans
886
+ createLock = lock
887
+ snapshots = snap
888
+ },
889
+ get eventTypes() {
890
+ return Array.from(registeredEvents.keys())
891
+ },
892
+ $inferEvents: null as never,
893
+ $inferEvent: null as never,
894
+ $inferState: null as never,
895
+ $inferCommandParameters: null as never,
896
+ }
897
+
898
+ return api as Omit<
899
+ AggregateRootApi<Name>,
900
+ 'withCommands' | 'withEventHandlers'
901
+ >
902
+ }