@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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/chunk-24xjphpk.js +51 -0
- package/dist/chunk-24xjphpk.js.map +10 -0
- package/dist/chunk-3t1vzbab.js +88 -0
- package/dist/chunk-3t1vzbab.js.map +10 -0
- package/dist/chunk-6q8mr1sg.js +13 -0
- package/dist/chunk-6q8mr1sg.js.map +10 -0
- package/dist/chunk-9psg50cb.js +22 -0
- package/dist/chunk-9psg50cb.js.map +10 -0
- package/dist/chunk-dgzw1nj9.js +50 -0
- package/dist/chunk-dgzw1nj9.js.map +10 -0
- package/dist/chunk-gq80tgvf.js +70 -0
- package/dist/chunk-gq80tgvf.js.map +10 -0
- package/dist/chunk-jd4q8fcf.js +11 -0
- package/dist/chunk-jd4q8fcf.js.map +10 -0
- package/dist/chunk-m07vf8q9.js +210 -0
- package/dist/chunk-m07vf8q9.js.map +10 -0
- package/dist/chunk-ncpk70p4.js +313 -0
- package/dist/chunk-ncpk70p4.js.map +10 -0
- package/dist/chunk-pmfzjavb.js +49 -0
- package/dist/chunk-pmfzjavb.js.map +10 -0
- package/dist/chunk-qdczt68t.js +84 -0
- package/dist/chunk-qdczt68t.js.map +10 -0
- package/dist/chunk-tfy01fah.js +34 -0
- package/dist/chunk-tfy01fah.js.map +10 -0
- package/dist/chunk-tkvj6x8k.js +389 -0
- package/dist/chunk-tkvj6x8k.js.map +10 -0
- package/dist/chunk-ty9bhdvx.js +53 -0
- package/dist/chunk-ty9bhdvx.js.map +10 -0
- package/dist/chunk-wmz6a1jk.js +233 -0
- package/dist/chunk-wmz6a1jk.js.map +10 -0
- package/dist/chunk-x90g388y.js +22 -0
- package/dist/chunk-x90g388y.js.map +10 -0
- package/dist/chunk-z388cd25.js +91 -0
- package/dist/chunk-z388cd25.js.map +10 -0
- package/dist/chunk-zf3x573k.js +65 -0
- package/dist/chunk-zf3x573k.js.map +10 -0
- package/dist/createAggregateRoot.d.ts +191 -0
- package/dist/createAggregateRoot.d.ts.map +1 -0
- package/dist/createAggregateRoot.js +18 -0
- package/dist/createAggregateRoot.js.map +9 -0
- package/dist/createCheckpointApi.d.ts +38 -0
- package/dist/createCheckpointApi.d.ts.map +1 -0
- package/dist/createCheckpointApi.js +9 -0
- package/dist/createCheckpointApi.js.map +9 -0
- package/dist/createEventListener.d.ts +46 -0
- package/dist/createEventListener.d.ts.map +1 -0
- package/dist/createEventListener.js +12 -0
- package/dist/createEventListener.js.map +9 -0
- package/dist/createEventStore.d.ts +57 -0
- package/dist/createEventStore.d.ts.map +1 -0
- package/dist/createEventStore.js +20 -0
- package/dist/createEventStore.js.map +9 -0
- package/dist/createProcessManager.d.ts +70 -0
- package/dist/createProcessManager.d.ts.map +1 -0
- package/dist/createProcessManager.js +16 -0
- package/dist/createProcessManager.js.map +9 -0
- package/dist/createProjection.d.ts +120 -0
- package/dist/createProjection.d.ts.map +1 -0
- package/dist/createProjection.js +16 -0
- package/dist/createProjection.js.map +9 -0
- package/dist/drizzle/index.d.ts +449 -0
- package/dist/drizzle/index.d.ts.map +1 -0
- package/dist/drizzle/index.js +325 -0
- package/dist/drizzle/index.js.map +10 -0
- package/dist/drizzle/postgres.d.ts +686 -0
- package/dist/drizzle/postgres.d.ts.map +1 -0
- package/dist/drizzle/postgres.js +18 -0
- package/dist/drizzle/postgres.js.map +9 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +9 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +9 -0
- package/dist/lock.d.ts +6 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +8 -0
- package/dist/lock.js.map +9 -0
- package/dist/memory/index.d.ts +15 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +178 -0
- package/dist/memory/index.js.map +10 -0
- package/dist/redis/index.d.ts +2 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +8 -0
- package/dist/redis/index.js.map +9 -0
- package/dist/redis/redisLock.d.ts +4 -0
- package/dist/redis/redisLock.d.ts.map +1 -0
- package/dist/redis/redisLock.js +8 -0
- package/dist/redis/redisLock.js.map +9 -0
- package/dist/refresh.d.ts +4 -0
- package/dist/refresh.d.ts.map +1 -0
- package/dist/refresh.js +13 -0
- package/dist/refresh.js.map +9 -0
- package/dist/replay.d.ts +4 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +13 -0
- package/dist/replay.js.map +9 -0
- package/dist/snapshotGenerator.d.ts +79 -0
- package/dist/snapshotGenerator.d.ts.map +1 -0
- package/dist/snapshotGenerator.js +9 -0
- package/dist/snapshotGenerator.js.map +9 -0
- package/dist/superjson.d.ts +5 -0
- package/dist/superjson.d.ts.map +1 -0
- package/dist/superjson.js +12 -0
- package/dist/superjson.js.map +9 -0
- package/dist/transaction.d.ts +13 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +8 -0
- package/dist/transaction.js.map +9 -0
- package/dist/utilityTypes.d.js +2 -0
- package/dist/utilityTypes.d.js.map +9 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +9 -0
- package/dist/wrappedEventBuilder.d.ts +37 -0
- package/dist/wrappedEventBuilder.d.ts.map +1 -0
- package/dist/wrappedEventBuilder.js +12 -0
- package/dist/wrappedEventBuilder.js.map +9 -0
- package/package.json +87 -1
- package/src/createAggregateRoot.ts +902 -0
- package/src/createCheckpointApi.ts +106 -0
- package/src/createEventListener.ts +118 -0
- package/src/createEventStore.ts +446 -0
- package/src/createProcessManager.ts +363 -0
- package/src/createProjection.ts +577 -0
- package/src/drizzle/index.ts +449 -0
- package/src/drizzle/postgres.ts +120 -0
- package/src/errors.ts +85 -0
- package/src/index.ts +45 -0
- package/src/lock.ts +58 -0
- package/src/memory/index.ts +224 -0
- package/src/redis/index.ts +1 -0
- package/src/redis/redisLock.ts +61 -0
- package/src/refresh.ts +19 -0
- package/src/replay.ts +19 -0
- package/src/snapshotGenerator.ts +190 -0
- package/src/superjson.ts +11 -0
- package/src/tests/aggregateRoot.spec.ts +532 -0
- package/src/tests/eventListener.spec.ts +57 -0
- package/src/tests/eventStore.spec.ts +86 -0
- package/src/tests/processManager.spec.ts +185 -0
- package/src/tests/projection.spec.ts +511 -0
- package/src/tests/projectionProcessManager.spec.ts +203 -0
- package/src/tests/setup.ts +230 -0
- package/src/tests/transaction.spec.ts +185 -0
- package/src/transaction.ts +93 -0
- package/src/utilityTypes.d.ts +110 -0
- package/src/utils.ts +43 -0
- package/src/wrappedEventBuilder.ts +181 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import z from 'zod/v4'
|
|
2
|
+
|
|
3
|
+
import type { LoadEvents } from './createEventStore.ts'
|
|
4
|
+
import { ValidationError } from './errors.ts'
|
|
5
|
+
import type {
|
|
6
|
+
BaseOutputEvent,
|
|
7
|
+
MaybePromise,
|
|
8
|
+
StreamEvents,
|
|
9
|
+
} from './utilityTypes.js'
|
|
10
|
+
|
|
11
|
+
const checkpointSchema = z.object({
|
|
12
|
+
lastEventPosition: z.int(),
|
|
13
|
+
type: z.enum(['projection', 'processManager']),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
metadata: z.record(z.string(), z.any()),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export type Checkpoint = z.output<typeof checkpointSchema>
|
|
19
|
+
|
|
20
|
+
export type CheckpointMethods = {
|
|
21
|
+
get: (
|
|
22
|
+
type: 'projection' | 'processManager',
|
|
23
|
+
name: string,
|
|
24
|
+
) => MaybePromise<Checkpoint | null | undefined>
|
|
25
|
+
delete: (
|
|
26
|
+
type: 'projection' | 'processManager',
|
|
27
|
+
name: string,
|
|
28
|
+
) => MaybePromise<void>
|
|
29
|
+
upsert: (checkpoint: Checkpoint) => MaybePromise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createCheckpointApi(
|
|
33
|
+
params: CheckpointMethods & {
|
|
34
|
+
loadEvents: LoadEvents
|
|
35
|
+
relatedStreamEvents: StreamEvents[] | null
|
|
36
|
+
name: string
|
|
37
|
+
type: 'processManager' | 'projection'
|
|
38
|
+
apply: (event: BaseOutputEvent) => MaybePromise<void>
|
|
39
|
+
applyAfter?: (event: BaseOutputEvent) => MaybePromise<void>
|
|
40
|
+
},
|
|
41
|
+
) {
|
|
42
|
+
const api = {
|
|
43
|
+
relatedStreamEvents: params.relatedStreamEvents,
|
|
44
|
+
async clear() {
|
|
45
|
+
await params.delete(params.type, params.name)
|
|
46
|
+
},
|
|
47
|
+
async upsert(event: BaseOutputEvent, metadata: Record<string, any> = {}) {
|
|
48
|
+
await params.upsert({
|
|
49
|
+
lastEventPosition: event.position,
|
|
50
|
+
type: params.type,
|
|
51
|
+
name: params.name,
|
|
52
|
+
metadata,
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
async rehydrated(
|
|
56
|
+
options: {
|
|
57
|
+
onMetadata?: (metadata: Record<string, any>) => unknown
|
|
58
|
+
getMetadata?: () => Record<string, any>
|
|
59
|
+
onProgress?: (index: number, event: BaseOutputEvent) => void
|
|
60
|
+
} = {},
|
|
61
|
+
) {
|
|
62
|
+
const checkpoint = await params.get(params.type, params.name)
|
|
63
|
+
let startPosition = 0
|
|
64
|
+
if (checkpoint) {
|
|
65
|
+
const result = checkpointSchema.safeParse(checkpoint)
|
|
66
|
+
if (!result.success) {
|
|
67
|
+
throw new ValidationError(
|
|
68
|
+
`loaded checkpoint for ${params.type} ${params.name} does not comply with schema`,
|
|
69
|
+
result.error,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
startPosition = checkpoint.lastEventPosition + 1
|
|
73
|
+
options.onMetadata?.(checkpoint.metadata)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const events = params.loadEvents(params.relatedStreamEvents, {
|
|
77
|
+
from: startPosition,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
let index = 0
|
|
81
|
+
let lastEvent: BaseOutputEvent | null = null
|
|
82
|
+
for await (const event of events) {
|
|
83
|
+
lastEvent = event
|
|
84
|
+
await params.apply(event)
|
|
85
|
+
index++
|
|
86
|
+
options.onProgress?.(index, event)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!lastEvent) {
|
|
90
|
+
return // up to date
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (params.applyAfter) {
|
|
94
|
+
for await (const event of events) {
|
|
95
|
+
await params.applyAfter(event)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await api.upsert(lastEvent, options.getMetadata?.())
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return api
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type CheckpointApi = ReturnType<typeof createCheckpointApi>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AfterEffectHandlers,
|
|
5
|
+
AllEvents,
|
|
6
|
+
BaseOutputEvent,
|
|
7
|
+
EventHandlers,
|
|
8
|
+
EventTemplate,
|
|
9
|
+
Keys,
|
|
10
|
+
MaybePromise,
|
|
11
|
+
Merge,
|
|
12
|
+
} from './utilityTypes.js'
|
|
13
|
+
|
|
14
|
+
export type InternalEventListener = {
|
|
15
|
+
apply: (event: BaseOutputEvent) => Promise<void>
|
|
16
|
+
applyAfter: (event: BaseOutputEvent) => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type EventListenerInfo = {
|
|
20
|
+
name: string | null
|
|
21
|
+
event: BaseOutputEvent
|
|
22
|
+
}
|
|
23
|
+
const eventListenerInfoStore = new AsyncLocalStorage<EventListenerInfo>()
|
|
24
|
+
|
|
25
|
+
function withEventListenerInfo(
|
|
26
|
+
info: EventListenerInfo,
|
|
27
|
+
handler: () => Promise<void>,
|
|
28
|
+
) {
|
|
29
|
+
return eventListenerInfoStore.run(info, handler)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getEventListenerInfo() {
|
|
33
|
+
return eventListenerInfoStore.getStore()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isInsideEventListener() {
|
|
37
|
+
return Boolean(getEventListenerInfo())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type EventListenerApi<
|
|
41
|
+
Events extends EventTemplate = never,
|
|
42
|
+
Flags extends {
|
|
43
|
+
withEventHandlers: boolean
|
|
44
|
+
withGlobalEventHandler: boolean
|
|
45
|
+
withAfterEffects: boolean
|
|
46
|
+
withGlobalAfterEffect: boolean
|
|
47
|
+
} = {
|
|
48
|
+
withEventHandlers: false
|
|
49
|
+
withGlobalEventHandler: false
|
|
50
|
+
withAfterEffects: false
|
|
51
|
+
withGlobalAfterEffect: false
|
|
52
|
+
},
|
|
53
|
+
> = {
|
|
54
|
+
withEventHandlers: (
|
|
55
|
+
handlers: EventHandlers<Events>,
|
|
56
|
+
) => Omit<
|
|
57
|
+
EventListenerApi<Events, Merge<Flags, { withEventHandlers: true }>>,
|
|
58
|
+
Keys<Merge<Flags, { withEventHandlers: true }>>
|
|
59
|
+
>
|
|
60
|
+
withGlobalEventHandler: (
|
|
61
|
+
handler: (event: AllEvents<Events>) => MaybePromise<void>,
|
|
62
|
+
) => Omit<
|
|
63
|
+
EventListenerApi<Events, Merge<Flags, { withGlobalEventHandler: true }>>,
|
|
64
|
+
Keys<Merge<Flags, { withGlobalEventHandler: true }>>
|
|
65
|
+
>
|
|
66
|
+
withAfterEffects: (
|
|
67
|
+
handlers: AfterEffectHandlers<Events>,
|
|
68
|
+
) => Omit<
|
|
69
|
+
EventListenerApi<Events, Merge<Flags, { withAfterEffects: true }>>,
|
|
70
|
+
Keys<Merge<Flags, { withAfterEffects: true }>>
|
|
71
|
+
>
|
|
72
|
+
withGlobalAfterEffect: (
|
|
73
|
+
handler: (event: AllEvents<Events>) => MaybePromise<void>,
|
|
74
|
+
) => Omit<
|
|
75
|
+
EventListenerApi<Events, Merge<Flags, { withGlobalAfterEffect: true }>>,
|
|
76
|
+
Keys<Merge<Flags, { withGlobalAfterEffect: true }>>
|
|
77
|
+
>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createEventListener(
|
|
81
|
+
name: string | null = null,
|
|
82
|
+
): EventListenerApi {
|
|
83
|
+
let eventHandlers: EventHandlers<any> = {}
|
|
84
|
+
let afterEffectHandlers: AfterEffectHandlers<any> = {}
|
|
85
|
+
let globalEventHandler: (event: BaseOutputEvent) => MaybePromise<void>
|
|
86
|
+
let globalAfterEffect: (event: BaseOutputEvent) => MaybePromise<void>
|
|
87
|
+
const api = {
|
|
88
|
+
async apply(event) {
|
|
89
|
+
withEventListenerInfo({ name, event }, async () => {
|
|
90
|
+
await eventHandlers[`on${event.type}`]?.(event)
|
|
91
|
+
await globalEventHandler?.(event)
|
|
92
|
+
})
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async applyAfter(event) {
|
|
96
|
+
await afterEffectHandlers[`after${event.type}`]?.(event)
|
|
97
|
+
await globalAfterEffect?.(event)
|
|
98
|
+
},
|
|
99
|
+
withEventHandlers(handlers) {
|
|
100
|
+
eventHandlers = handlers
|
|
101
|
+
return api
|
|
102
|
+
},
|
|
103
|
+
withGlobalEventHandler(handler) {
|
|
104
|
+
globalEventHandler = handler as any
|
|
105
|
+
return api
|
|
106
|
+
},
|
|
107
|
+
withAfterEffects(handlers) {
|
|
108
|
+
afterEffectHandlers = handlers
|
|
109
|
+
return api
|
|
110
|
+
},
|
|
111
|
+
withGlobalAfterEffect(handler) {
|
|
112
|
+
globalAfterEffect = handler as any
|
|
113
|
+
return api
|
|
114
|
+
},
|
|
115
|
+
} as EventListenerApi & InternalEventListener
|
|
116
|
+
|
|
117
|
+
return api
|
|
118
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AggregateRootApi,
|
|
3
|
+
AggregateRootSnapshotMethods,
|
|
4
|
+
AnyAggregateRoot,
|
|
5
|
+
InternalAggregateRootApi,
|
|
6
|
+
} from './createAggregateRoot.ts'
|
|
7
|
+
import type { CheckpointMethods } from './createCheckpointApi.ts'
|
|
8
|
+
import {
|
|
9
|
+
type EventListenerApi,
|
|
10
|
+
type InternalEventListener,
|
|
11
|
+
createEventListener,
|
|
12
|
+
} from './createEventListener.ts'
|
|
13
|
+
import {
|
|
14
|
+
type InternalProcessManager,
|
|
15
|
+
type ProcessManagerApi,
|
|
16
|
+
buildProcessManagerCreator,
|
|
17
|
+
} from './createProcessManager.ts'
|
|
18
|
+
import {
|
|
19
|
+
type InternalProjection,
|
|
20
|
+
type ProjectionApi,
|
|
21
|
+
type ProjectionApiWithSnapshots,
|
|
22
|
+
type ProjectionSnapshotMethods,
|
|
23
|
+
buildProjectionCreator,
|
|
24
|
+
} from './createProjection.ts'
|
|
25
|
+
import lock, { type LockCreator } from './lock.ts'
|
|
26
|
+
import { withRefreshing } from './refresh.ts'
|
|
27
|
+
import createTransaction from './transaction.ts'
|
|
28
|
+
import type {
|
|
29
|
+
BaseInputEvent,
|
|
30
|
+
BaseOutputEvent,
|
|
31
|
+
EventHandlers,
|
|
32
|
+
EventTemplate,
|
|
33
|
+
MaybePromise,
|
|
34
|
+
Stream,
|
|
35
|
+
StreamEvents,
|
|
36
|
+
StreamId,
|
|
37
|
+
StreamIdEvents,
|
|
38
|
+
} from './utilityTypes.js'
|
|
39
|
+
import {
|
|
40
|
+
type WrappedEvent,
|
|
41
|
+
unwrapEvent,
|
|
42
|
+
wrappedEventBuilder,
|
|
43
|
+
} from './wrappedEventBuilder.ts'
|
|
44
|
+
|
|
45
|
+
export type LoadEvents = (
|
|
46
|
+
select:
|
|
47
|
+
| Stream
|
|
48
|
+
| StreamId
|
|
49
|
+
| Stream[]
|
|
50
|
+
| StreamId[]
|
|
51
|
+
| StreamEvents
|
|
52
|
+
| StreamEvents[]
|
|
53
|
+
| StreamIdEvents
|
|
54
|
+
| StreamIdEvents[]
|
|
55
|
+
| null,
|
|
56
|
+
range?: { from: number; to?: number },
|
|
57
|
+
) => AsyncGenerator<BaseOutputEvent>
|
|
58
|
+
|
|
59
|
+
type AppendEvents = (
|
|
60
|
+
streamId: StreamId,
|
|
61
|
+
events: BaseInputEvent[],
|
|
62
|
+
expectedVersion: number,
|
|
63
|
+
) => MaybePromise<BaseOutputEvent[]>
|
|
64
|
+
|
|
65
|
+
export type InternalAppendEvents = (
|
|
66
|
+
streamId: StreamId,
|
|
67
|
+
events: BaseInputEvent[],
|
|
68
|
+
expectedVersion: number,
|
|
69
|
+
) => MaybePromise<BaseOutputEvent[]>
|
|
70
|
+
|
|
71
|
+
export type EmitEvents = (events: BaseOutputEvent[]) => Promise<void>
|
|
72
|
+
|
|
73
|
+
export type EventsFromRoot<Root> =
|
|
74
|
+
Root extends Omit<AggregateRootApi<any, any, infer E>, any> ? E : never
|
|
75
|
+
|
|
76
|
+
export type OnProgress = {
|
|
77
|
+
begin?: (projectionName: string) => void
|
|
78
|
+
progress?: (
|
|
79
|
+
projectionName: string,
|
|
80
|
+
index: number,
|
|
81
|
+
event: BaseOutputEvent,
|
|
82
|
+
) => void
|
|
83
|
+
end?: (projectionName: string) => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface BaseEventStoreParams<Root extends AnyAggregateRoot> {
|
|
87
|
+
loadEvents: LoadEvents
|
|
88
|
+
appendEvents: AppendEvents
|
|
89
|
+
aggregateRoots: Root[]
|
|
90
|
+
checkpoint: CheckpointMethods
|
|
91
|
+
lock?: LockCreator
|
|
92
|
+
projectionSnapshot?: ProjectionSnapshotMethods
|
|
93
|
+
onProjectionReplay?: OnProgress
|
|
94
|
+
onProcessManagerRefresh?: OnProgress
|
|
95
|
+
autoInit?: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface EventStoreParamsWithAggregateRootSnapshots<
|
|
99
|
+
Root extends AnyAggregateRoot,
|
|
100
|
+
> extends BaseEventStoreParams<Root> {
|
|
101
|
+
aggregateRootSnapshot: AggregateRootSnapshotMethods
|
|
102
|
+
postProcessEvent?: (
|
|
103
|
+
event: WrappedEvent<EventsFromRoot<Root>>,
|
|
104
|
+
aggregateRoot: Root,
|
|
105
|
+
) => WrappedEvent
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface EventStoreParamsWithoutAggregateRootSnapshots<
|
|
109
|
+
Root extends AnyAggregateRoot,
|
|
110
|
+
> extends BaseEventStoreParams<Root> {
|
|
111
|
+
aggregateRoots: Root[]
|
|
112
|
+
aggregateRootSnapshot?: never
|
|
113
|
+
postProcessEvent?: (
|
|
114
|
+
event: WrappedEvent<EventsFromRoot<Root>>,
|
|
115
|
+
aggregateRoot: Root,
|
|
116
|
+
) => WrappedEvent
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type EventStore<
|
|
120
|
+
Event extends EventTemplate,
|
|
121
|
+
Root extends AnyAggregateRoot,
|
|
122
|
+
SupportsProjectionSnapshots = boolean,
|
|
123
|
+
> = {
|
|
124
|
+
createProjection(
|
|
125
|
+
name: string,
|
|
126
|
+
): SupportsProjectionSnapshots extends true
|
|
127
|
+
? ProjectionApiWithSnapshots<Event>
|
|
128
|
+
: ProjectionApi<Event>
|
|
129
|
+
createProcessManager(name: string): ProcessManagerApi<never, Event>
|
|
130
|
+
createEventListener(name: string): EventListenerApi<Event>
|
|
131
|
+
rebuild: () => Promise<void>
|
|
132
|
+
isReady: () => Promise<void>
|
|
133
|
+
init: () => Promise<EventStore<Event, Root, SupportsProjectionSnapshots>>
|
|
134
|
+
getAggregateRoot<S extends Root['type']>(
|
|
135
|
+
type: S,
|
|
136
|
+
): Root extends { type: S } ? Root : never
|
|
137
|
+
transaction<T extends () => any>(handler: T): Promise<Awaited<ReturnType<T>>>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createEventStore<const Root extends AnyAggregateRoot>(
|
|
141
|
+
params: EventStoreParamsWithoutAggregateRootSnapshots<Root>,
|
|
142
|
+
): EventStore<EventsFromRoot<Root>, Root, false>
|
|
143
|
+
export function createEventStore<const Root extends AnyAggregateRoot>(
|
|
144
|
+
params: EventStoreParamsWithAggregateRootSnapshots<Root>,
|
|
145
|
+
): EventStore<EventsFromRoot<Root>, Root, true>
|
|
146
|
+
export function createEventStore<const Root extends AnyAggregateRoot>(
|
|
147
|
+
params:
|
|
148
|
+
| EventStoreParamsWithoutAggregateRootSnapshots<Root>
|
|
149
|
+
| EventStoreParamsWithAggregateRootSnapshots<Root>,
|
|
150
|
+
) {
|
|
151
|
+
const transaction = createTransaction()
|
|
152
|
+
const { promise: initialized, resolve: resolveInitialized } =
|
|
153
|
+
Promise.withResolvers<void>()
|
|
154
|
+
const projections = new Map<string, InternalProjection>()
|
|
155
|
+
const processManagers = new Map<string, InternalProcessManager>()
|
|
156
|
+
const eventListeners = new Map<string, InternalEventListener>()
|
|
157
|
+
const aggregatRootsByEvents = new Map<
|
|
158
|
+
string,
|
|
159
|
+
{ aggregateRoot: InternalAggregateRootApi; eventType: string }
|
|
160
|
+
>()
|
|
161
|
+
|
|
162
|
+
let allReplaying = false
|
|
163
|
+
let initializing = false
|
|
164
|
+
|
|
165
|
+
const assertInitializing = () => {
|
|
166
|
+
if (!initializing && params.autoInit === false) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
'Cannot emit events because the event store is not initialized. Set autoInit to true, or explicitly call .init() before emitting events.',
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let exclusiveEmitEventsChain: Promise<any> = Promise.resolve()
|
|
174
|
+
const emitEvents: EmitEvents = async (events) => {
|
|
175
|
+
exclusiveEmitEventsChain = exclusiveEmitEventsChain.then(async () => {
|
|
176
|
+
for (const event of events) {
|
|
177
|
+
await Promise.all(
|
|
178
|
+
[
|
|
179
|
+
...processManagers.values(),
|
|
180
|
+
...projections.values(),
|
|
181
|
+
...eventListeners.values(),
|
|
182
|
+
].map((listener) => listener.apply(event)),
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const event of events) {
|
|
187
|
+
await Promise.all(
|
|
188
|
+
[...processManagers.values(), ...eventListeners.values()].map(
|
|
189
|
+
(processManager) => processManager.applyAfter(event),
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return exclusiveEmitEventsChain
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const loadEvents: LoadEvents = (select, range) => {
|
|
199
|
+
assertInitializing()
|
|
200
|
+
return params.loadEvents(select, range)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let exclusiveAppendEventsChain: Promise<any> = initialized
|
|
204
|
+
const appendEvents: InternalAppendEvents = async (
|
|
205
|
+
streamId,
|
|
206
|
+
events,
|
|
207
|
+
expectedVersion,
|
|
208
|
+
) => {
|
|
209
|
+
assertInitializing()
|
|
210
|
+
exclusiveAppendEventsChain = exclusiveAppendEventsChain.then(async () => {
|
|
211
|
+
if (events.length === 0) {
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
await new Promise((resolve) => setImmediate(resolve)) // edge case for sync setup, see 'can execute after effect' test
|
|
215
|
+
|
|
216
|
+
const processedEvents = events.map((rawEvent) => {
|
|
217
|
+
const event = structuredClone(rawEvent)
|
|
218
|
+
if (params.postProcessEvent) {
|
|
219
|
+
const wrappedEvent = wrappedEventBuilder(event) as WrappedEvent<any>
|
|
220
|
+
const { aggregateRoot } = aggregatRootsByEvents.get(event.type)!
|
|
221
|
+
const processedEvent = unwrapEvent(
|
|
222
|
+
params.postProcessEvent(
|
|
223
|
+
wrappedEvent,
|
|
224
|
+
aggregateRoot as unknown as Root,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
227
|
+
Object.assign(event, processedEvent)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return event
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return params.appendEvents(streamId, processedEvents, expectedVersion)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return exclusiveAppendEventsChain
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const aggregateRoot of params.aggregateRoots as unknown as InternalAggregateRootApi[]) {
|
|
240
|
+
aggregateRoot.eventTypes.forEach((eventType) => {
|
|
241
|
+
aggregatRootsByEvents.set(eventType, {
|
|
242
|
+
aggregateRoot,
|
|
243
|
+
eventType,
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
aggregateRoot.linkEventStore(
|
|
248
|
+
loadEvents,
|
|
249
|
+
appendEvents,
|
|
250
|
+
emitEvents,
|
|
251
|
+
transaction,
|
|
252
|
+
params.lock ?? lock(5000),
|
|
253
|
+
params.aggregateRootSnapshot,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const baseCreateProjection = buildProjectionCreator({
|
|
258
|
+
loadEvents,
|
|
259
|
+
checkpoint: params.checkpoint,
|
|
260
|
+
snapshot: params.projectionSnapshot,
|
|
261
|
+
onReplay: params.onProjectionReplay,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
const baseCreateProcessManager = buildProcessManagerCreator({
|
|
265
|
+
loadEvents,
|
|
266
|
+
checkpoint: params.checkpoint,
|
|
267
|
+
onRefresh: params.onProcessManagerRefresh,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const getRelatedStreamEvents = (
|
|
271
|
+
handlers: EventHandlers<any>,
|
|
272
|
+
): StreamEvents[] => {
|
|
273
|
+
const relatedStreamEvents = new Map<string, Set<string>>()
|
|
274
|
+
for (const handlerName of Object.keys(handlers)) {
|
|
275
|
+
const sanitizedHandlerName = handlerName.replace(/^(on|after)/, '')
|
|
276
|
+
if (!aggregatRootsByEvents.has(sanitizedHandlerName)) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`event handler ${handlerName} has no corresponding event in any aggregate root`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
const {
|
|
282
|
+
aggregateRoot: { type: aggregateRootName },
|
|
283
|
+
eventType,
|
|
284
|
+
} = aggregatRootsByEvents.get(sanitizedHandlerName)!
|
|
285
|
+
if (!relatedStreamEvents.has(aggregateRootName)) {
|
|
286
|
+
relatedStreamEvents.set(aggregateRootName, new Set())
|
|
287
|
+
}
|
|
288
|
+
relatedStreamEvents.get(aggregateRootName)!.add(eventType)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return Array.from(relatedStreamEvents, ([stream, events]) => ({
|
|
292
|
+
stream,
|
|
293
|
+
events: Array.from(events),
|
|
294
|
+
}))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const initialize = async () => {
|
|
298
|
+
initializing = true
|
|
299
|
+
await Promise.all(
|
|
300
|
+
projections.values().map((projection) => projection.init(true)),
|
|
301
|
+
)
|
|
302
|
+
await Promise.all(
|
|
303
|
+
processManagers
|
|
304
|
+
.values()
|
|
305
|
+
.map((processManager) => processManager.init(true)),
|
|
306
|
+
)
|
|
307
|
+
resolveInitialized()
|
|
308
|
+
}
|
|
309
|
+
setImmediate(() => {
|
|
310
|
+
if (allReplaying || params.autoInit === false) {
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
initialize()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const api = {
|
|
318
|
+
createProjection(name) {
|
|
319
|
+
if (projections.has(name)) {
|
|
320
|
+
throw new Error(`Projection ${name} is already registered`)
|
|
321
|
+
}
|
|
322
|
+
const api = baseCreateProjection(name)
|
|
323
|
+
const internalApi = api as unknown as InternalProjection
|
|
324
|
+
internalApi.setRelatedStreamEvents((handlers) =>
|
|
325
|
+
getRelatedStreamEvents(handlers),
|
|
326
|
+
)
|
|
327
|
+
projections.set(name, internalApi)
|
|
328
|
+
if (initializing) {
|
|
329
|
+
internalApi.init(true)
|
|
330
|
+
}
|
|
331
|
+
return api
|
|
332
|
+
},
|
|
333
|
+
createProcessManager(name) {
|
|
334
|
+
if (processManagers.has(name)) {
|
|
335
|
+
throw new Error(`Process Manager ${name} is already registered`)
|
|
336
|
+
}
|
|
337
|
+
const api = baseCreateProcessManager(name)
|
|
338
|
+
|
|
339
|
+
const internalApi = api as unknown as InternalProcessManager
|
|
340
|
+
internalApi.setRelatedStreamEvents((handlers) =>
|
|
341
|
+
getRelatedStreamEvents(handlers),
|
|
342
|
+
)
|
|
343
|
+
processManagers.set(name, internalApi)
|
|
344
|
+
if (initializing) {
|
|
345
|
+
internalApi.init(true)
|
|
346
|
+
}
|
|
347
|
+
return api
|
|
348
|
+
},
|
|
349
|
+
createEventListener(name) {
|
|
350
|
+
if (eventListeners.has(name)) {
|
|
351
|
+
throw new Error(`Event Listener ${name} is already registered`)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const api = createEventListener(name)
|
|
355
|
+
|
|
356
|
+
const internalApi = api as unknown as InternalEventListener
|
|
357
|
+
eventListeners.set(name, internalApi)
|
|
358
|
+
return api
|
|
359
|
+
},
|
|
360
|
+
async isReady() {
|
|
361
|
+
await initialized
|
|
362
|
+
await Promise.all([
|
|
363
|
+
...projections.values().map((projection) => projection.isReady()),
|
|
364
|
+
...processManagers.values().map((projection) => projection.isReady()),
|
|
365
|
+
])
|
|
366
|
+
},
|
|
367
|
+
async rebuild() {
|
|
368
|
+
allReplaying = true
|
|
369
|
+
initializing = true
|
|
370
|
+
const replayProjections = Array.from(projections.values()).filter(
|
|
371
|
+
(projection) => projection.canReplay(),
|
|
372
|
+
)
|
|
373
|
+
const replayProcessManagers = Array.from(processManagers.values()).filter(
|
|
374
|
+
(processManager) => processManager.hasState(),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
await Promise.all([
|
|
378
|
+
...replayProjections.map((projection) => projection.init(false)),
|
|
379
|
+
...replayProcessManagers.map((projection) => projection.init(false)),
|
|
380
|
+
...replayProjections.map((projection) => projection.beginReplay()),
|
|
381
|
+
...replayProcessManagers.map((processManager) =>
|
|
382
|
+
processManager.beginRefresh(),
|
|
383
|
+
),
|
|
384
|
+
])
|
|
385
|
+
|
|
386
|
+
await withRefreshing(async () => {
|
|
387
|
+
let index = 0
|
|
388
|
+
for await (const event of loadEvents(null)) {
|
|
389
|
+
index++
|
|
390
|
+
await Promise.all([
|
|
391
|
+
...replayProjections.map((projection) =>
|
|
392
|
+
projection.progressReplay(index, event),
|
|
393
|
+
),
|
|
394
|
+
...replayProcessManagers.map((processManager) =>
|
|
395
|
+
processManager.progressRefresh(index, event, false),
|
|
396
|
+
),
|
|
397
|
+
...Array.from(eventListeners.values(), (listener) =>
|
|
398
|
+
listener.apply(event),
|
|
399
|
+
),
|
|
400
|
+
])
|
|
401
|
+
await Promise.all([
|
|
402
|
+
...replayProcessManagers.map((processManager) =>
|
|
403
|
+
processManager.progressRefresh(index, event, true),
|
|
404
|
+
),
|
|
405
|
+
...Array.from(eventListeners.values(), (listener) =>
|
|
406
|
+
listener.applyAfter(event),
|
|
407
|
+
),
|
|
408
|
+
])
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
await Promise.all([
|
|
413
|
+
...replayProjections.map((projection) => projection.endReplay()),
|
|
414
|
+
...replayProcessManagers.map((processManager) =>
|
|
415
|
+
processManager.endRefresh(),
|
|
416
|
+
),
|
|
417
|
+
])
|
|
418
|
+
|
|
419
|
+
await Promise.all(
|
|
420
|
+
projections.values().map(async (projection) => {
|
|
421
|
+
if (!projection.canReplay()) {
|
|
422
|
+
await projection.init(true)
|
|
423
|
+
}
|
|
424
|
+
}),
|
|
425
|
+
)
|
|
426
|
+
resolveInitialized()
|
|
427
|
+
},
|
|
428
|
+
async init() {
|
|
429
|
+
if (initializing) {
|
|
430
|
+
return api
|
|
431
|
+
}
|
|
432
|
+
await initialize()
|
|
433
|
+
return api
|
|
434
|
+
},
|
|
435
|
+
getAggregateRoot(type) {
|
|
436
|
+
return params.aggregateRoots.find(
|
|
437
|
+
(aggregateRoot) => aggregateRoot.type === type,
|
|
438
|
+
) as any
|
|
439
|
+
},
|
|
440
|
+
async transaction(handler) {
|
|
441
|
+
return transaction.run(handler)
|
|
442
|
+
},
|
|
443
|
+
} as EventStore<any, Root>
|
|
444
|
+
|
|
445
|
+
return api
|
|
446
|
+
}
|