@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,577 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
|
+
|
|
3
|
+
import z from 'zod/v4'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type CheckpointApi,
|
|
7
|
+
type CheckpointMethods,
|
|
8
|
+
createCheckpointApi,
|
|
9
|
+
} from './createCheckpointApi.ts'
|
|
10
|
+
import type { LoadEvents, OnProgress } from './createEventStore.ts'
|
|
11
|
+
import {
|
|
12
|
+
ProjectionEventHandlerExecutionError,
|
|
13
|
+
ProjectionReplayLoopError,
|
|
14
|
+
ReplaySkipError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
} from './errors.ts'
|
|
17
|
+
import { isReplaying, withReplaying } from './replay.ts'
|
|
18
|
+
import type {
|
|
19
|
+
CompleteSnapshotGenerator,
|
|
20
|
+
InternalSnapshotGenerator,
|
|
21
|
+
SnapshotGenerator,
|
|
22
|
+
} from './snapshotGenerator.ts'
|
|
23
|
+
import createSnapshotGenerator from './snapshotGenerator.ts'
|
|
24
|
+
import type {
|
|
25
|
+
AllEvents,
|
|
26
|
+
BaseOutputEvent,
|
|
27
|
+
EventHandlers,
|
|
28
|
+
EventTemplate,
|
|
29
|
+
Keys,
|
|
30
|
+
MaybePromise,
|
|
31
|
+
Merge,
|
|
32
|
+
OutputEvent,
|
|
33
|
+
StreamEvents,
|
|
34
|
+
} from './utilityTypes.d.ts'
|
|
35
|
+
|
|
36
|
+
const hydratingStore = new AsyncLocalStorage<boolean>()
|
|
37
|
+
|
|
38
|
+
function isHydrating() {
|
|
39
|
+
return hydratingStore.getStore() ?? false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function withHydrating(handler: () => Promise<void>) {
|
|
43
|
+
await hydratingStore.run(true, handler)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ProjectionInfo = {
|
|
47
|
+
name: string
|
|
48
|
+
event: BaseOutputEvent
|
|
49
|
+
}
|
|
50
|
+
const projectionInfoStore = new AsyncLocalStorage<ProjectionInfo>()
|
|
51
|
+
|
|
52
|
+
function withProjectionInfo(
|
|
53
|
+
info: ProjectionInfo,
|
|
54
|
+
handler: () => Promise<void>,
|
|
55
|
+
) {
|
|
56
|
+
return projectionInfoStore.run(info, handler)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getProjectionInfo() {
|
|
60
|
+
return projectionInfoStore.getStore()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isInsideProjection() {
|
|
64
|
+
return Boolean(getProjectionInfo())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type ReplayConfig = {
|
|
68
|
+
deleteOne?: (id: string) => MaybePromise<void>
|
|
69
|
+
deleteAll?: () => MaybePromise<void>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type ProjectionEventHandlers<Events extends EventTemplate> = EventHandlers<
|
|
73
|
+
Events,
|
|
74
|
+
{
|
|
75
|
+
replaying: boolean
|
|
76
|
+
replayUntil: (
|
|
77
|
+
position: number,
|
|
78
|
+
filterEvents?: (event: OutputEvent<Events>) => boolean,
|
|
79
|
+
) => Promise<void>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
|
|
83
|
+
type ReplayProjection<Events extends EventTemplate> = {
|
|
84
|
+
replayOne: (streamId: string) => Promise<void>
|
|
85
|
+
replayUntil: (
|
|
86
|
+
streamId: string,
|
|
87
|
+
position: number,
|
|
88
|
+
filterEvents?: (event: OutputEvent<Events>) => boolean,
|
|
89
|
+
) => Promise<void>
|
|
90
|
+
replay: () => Promise<void>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type InternalProjection = {
|
|
94
|
+
apply: (event: BaseOutputEvent) => Promise<void>
|
|
95
|
+
setRelatedStreamEvents: (
|
|
96
|
+
generateRelatedStreamEvents: (
|
|
97
|
+
eventHandlers: EventHandlers<EventTemplate>,
|
|
98
|
+
) => StreamEvents[],
|
|
99
|
+
) => void
|
|
100
|
+
isReady(): Promise<void>
|
|
101
|
+
canReplay(): boolean
|
|
102
|
+
beginReplay(): Promise<void>
|
|
103
|
+
progressReplay(index: number, event: BaseOutputEvent): Promise<void>
|
|
104
|
+
endReplay(): void
|
|
105
|
+
init(catchUp: boolean): Promise<void>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type ProjectionApi<
|
|
109
|
+
Events extends EventTemplate = never,
|
|
110
|
+
RConfig extends ReplayConfig = never,
|
|
111
|
+
Flags extends {
|
|
112
|
+
withEventHandlers: boolean
|
|
113
|
+
withGlobalEventHandler: boolean
|
|
114
|
+
concurrent: boolean
|
|
115
|
+
withReplay: boolean
|
|
116
|
+
withSnapshots: boolean
|
|
117
|
+
} = {
|
|
118
|
+
withEventHandlers: false
|
|
119
|
+
withGlobalEventHandler: false
|
|
120
|
+
concurrent: false
|
|
121
|
+
withReplay: false
|
|
122
|
+
withSnapshots: true
|
|
123
|
+
},
|
|
124
|
+
> = {
|
|
125
|
+
withEventHandlers: (
|
|
126
|
+
handlers: ProjectionEventHandlers<Events>,
|
|
127
|
+
) => Omit<
|
|
128
|
+
ProjectionApi<Events, RConfig, Merge<Flags, { withEventHandlers: true }>>,
|
|
129
|
+
Keys<Merge<Flags, { withEventHandlers: true }>>
|
|
130
|
+
>
|
|
131
|
+
withGlobalEventHandler: (
|
|
132
|
+
handler: (
|
|
133
|
+
event: AllEvents<Events>,
|
|
134
|
+
methods: ReplayProjection<Events>,
|
|
135
|
+
) => MaybePromise<void>,
|
|
136
|
+
) => Omit<
|
|
137
|
+
ProjectionApi<
|
|
138
|
+
Events,
|
|
139
|
+
RConfig,
|
|
140
|
+
Merge<Flags, { withGlobalEventHandler: true }>
|
|
141
|
+
>,
|
|
142
|
+
Keys<Merge<Flags, { withGlobalEventHandler: true }>>
|
|
143
|
+
>
|
|
144
|
+
concurrent: () => Omit<
|
|
145
|
+
ProjectionApi<Events, RConfig, Merge<Flags, { concurrent: true }>>,
|
|
146
|
+
Keys<Merge<Flags, { concurrent: true }>>
|
|
147
|
+
>
|
|
148
|
+
withReplay: <Config extends ReplayConfig>(
|
|
149
|
+
config: Config,
|
|
150
|
+
) => Omit<
|
|
151
|
+
ProjectionApi<Events, Config, Merge<Flags, { withReplay: true }>>,
|
|
152
|
+
Keys<Merge<Flags, { withReplay: true }>>
|
|
153
|
+
>
|
|
154
|
+
withSnapshots: (
|
|
155
|
+
handler: (
|
|
156
|
+
generator: SnapshotGenerator<Omit<Snapshot, 'schemaVersion' | 'state'>>,
|
|
157
|
+
) => CompleteSnapshotGenerator,
|
|
158
|
+
) => Omit<
|
|
159
|
+
ProjectionApi<Events, RConfig, Merge<Flags, { withSnapshots: true }>>,
|
|
160
|
+
Keys<Merge<Flags, { withSnapshots: true }>>
|
|
161
|
+
>
|
|
162
|
+
isReady(): Promise<void>
|
|
163
|
+
} & ('deleteOne' extends keyof RConfig
|
|
164
|
+
? Pick<ReplayProjection<Events>, 'replayOne' | 'replayUntil'>
|
|
165
|
+
: {}) &
|
|
166
|
+
('deleteAll' extends keyof RConfig
|
|
167
|
+
? Pick<ReplayProjection<Events>, 'replay'>
|
|
168
|
+
: {})
|
|
169
|
+
|
|
170
|
+
export type ProjectionApiWithSnapshots<
|
|
171
|
+
Events extends EventTemplate = never,
|
|
172
|
+
RConfig extends ReplayConfig = never,
|
|
173
|
+
Flags extends {
|
|
174
|
+
withEventHandlers: boolean
|
|
175
|
+
withGlobalEventHandler: boolean
|
|
176
|
+
concurrent: boolean
|
|
177
|
+
withReplay: boolean
|
|
178
|
+
withSnapshots: boolean
|
|
179
|
+
} = {
|
|
180
|
+
withEventHandlers: false
|
|
181
|
+
withGlobalEventHandler: false
|
|
182
|
+
concurrent: false
|
|
183
|
+
withReplay: false
|
|
184
|
+
withSnapshots: false
|
|
185
|
+
},
|
|
186
|
+
> = ProjectionApi<Events, RConfig, Flags>
|
|
187
|
+
|
|
188
|
+
const snapshotSchema = z.object({
|
|
189
|
+
projectionId: z.string(),
|
|
190
|
+
projectionType: z.string(),
|
|
191
|
+
lastEventPosition: z.int(),
|
|
192
|
+
state: z.unknown(),
|
|
193
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
194
|
+
schemaVersion: z.int(),
|
|
195
|
+
createdAt: z.date(),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
export type Snapshot = z.output<typeof snapshotSchema>
|
|
199
|
+
|
|
200
|
+
export type ProjectionSnapshotMethods = {
|
|
201
|
+
get: (select: {
|
|
202
|
+
projection: string
|
|
203
|
+
id: string
|
|
204
|
+
}) => MaybePromise<Snapshot | null | undefined>
|
|
205
|
+
put: (snapshot: Snapshot) => MaybePromise<void>
|
|
206
|
+
delete: (
|
|
207
|
+
target: { id?: string; projection: string },
|
|
208
|
+
fromPosition?: number,
|
|
209
|
+
) => MaybePromise<void>
|
|
210
|
+
incrementAppliedCount: (target: {
|
|
211
|
+
projection: string
|
|
212
|
+
id: string
|
|
213
|
+
}) => MaybePromise<number>
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function buildProjectionCreator(params: {
|
|
217
|
+
loadEvents: LoadEvents
|
|
218
|
+
checkpoint: CheckpointMethods
|
|
219
|
+
onReplay?: OnProgress
|
|
220
|
+
snapshot?: ProjectionSnapshotMethods
|
|
221
|
+
}): (name: string) => ProjectionApi {
|
|
222
|
+
return (name) => {
|
|
223
|
+
let idle = true
|
|
224
|
+
let replaying = false
|
|
225
|
+
let handlers: ProjectionEventHandlers<any> = {}
|
|
226
|
+
let globalEventHandler:
|
|
227
|
+
| ((event: BaseOutputEvent) => MaybePromise<void>)
|
|
228
|
+
| null = null
|
|
229
|
+
let generateRelatedStreamEvents: (
|
|
230
|
+
eventHandlers: EventHandlers<any>,
|
|
231
|
+
) => StreamEvents[]
|
|
232
|
+
let relatedStreamEvents: StreamEvents[] | null = null
|
|
233
|
+
let exclusive = true
|
|
234
|
+
let checkpointApi: CheckpointApi
|
|
235
|
+
let lastEventPosition = -1
|
|
236
|
+
|
|
237
|
+
let replayConfig: ReplayConfig | null = null
|
|
238
|
+
|
|
239
|
+
let snapshotting:
|
|
240
|
+
| (CompleteSnapshotGenerator & InternalSnapshotGenerator)
|
|
241
|
+
| null = null
|
|
242
|
+
|
|
243
|
+
const { promise: hydrated, resolve: resolveHydrated } =
|
|
244
|
+
Promise.withResolvers<void>()
|
|
245
|
+
|
|
246
|
+
const locks = new Map<string, Promise<void>>()
|
|
247
|
+
const apply = async (event: BaseOutputEvent) => {
|
|
248
|
+
const handler = handlers[`on${event.type}`]
|
|
249
|
+
if (!handler && !globalEventHandler) {
|
|
250
|
+
return Promise.resolve()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (isHydrating()) {
|
|
254
|
+
lastEventPosition = event.position
|
|
255
|
+
} else {
|
|
256
|
+
await hydrated
|
|
257
|
+
// event was already applied through hydration
|
|
258
|
+
if (event.position <= lastEventPosition) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const unlocked = !exclusive
|
|
264
|
+
? Promise.resolve()
|
|
265
|
+
: (locks.get(event.streamId)?.catch(() => {}) ?? Promise.resolve())
|
|
266
|
+
|
|
267
|
+
const runner = (async () => {
|
|
268
|
+
await unlocked
|
|
269
|
+
await withProjectionInfo({ name, event }, async () => {
|
|
270
|
+
try {
|
|
271
|
+
await handler?.(event, {
|
|
272
|
+
replaying,
|
|
273
|
+
async replayUntil(position, filterEvents) {
|
|
274
|
+
if (position >= event.position) {
|
|
275
|
+
throw new ProjectionReplayLoopError(
|
|
276
|
+
`Event handler on${event.type} of projection ${name} tried to replay to the future`,
|
|
277
|
+
{ name, eventHandler: `on${event.type}` },
|
|
278
|
+
event,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
locks.delete(event.streamId)
|
|
283
|
+
return api.replayUntil(event.streamId, position, filterEvents)
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof ReplaySkipError) {
|
|
288
|
+
if (isReplaying()) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw new Error(
|
|
293
|
+
'ReplaySkipError should never be thrown outside replay',
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
!(error instanceof Error) ||
|
|
299
|
+
error instanceof ProjectionReplayLoopError
|
|
300
|
+
) {
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
throw new ProjectionEventHandlerExecutionError(
|
|
304
|
+
name,
|
|
305
|
+
`on${event.type}`,
|
|
306
|
+
event,
|
|
307
|
+
error,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
await globalEventHandler?.(event)
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (!(error instanceof Error)) {
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
throw new ProjectionEventHandlerExecutionError(
|
|
318
|
+
name,
|
|
319
|
+
'GLOBAL',
|
|
320
|
+
event,
|
|
321
|
+
error,
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
if (params.snapshot && snapshotting) {
|
|
327
|
+
const appliedEvents = await params.snapshot.incrementAppliedCount({
|
|
328
|
+
projection: name,
|
|
329
|
+
id: event.streamId,
|
|
330
|
+
})
|
|
331
|
+
const snapshot = await snapshotting.internalCapture(
|
|
332
|
+
event,
|
|
333
|
+
appliedEvents,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if (snapshot) {
|
|
337
|
+
await params.snapshot.put({
|
|
338
|
+
projectionType: name,
|
|
339
|
+
projectionId: event.streamId,
|
|
340
|
+
lastEventPosition: event.position,
|
|
341
|
+
state: snapshot.state,
|
|
342
|
+
schemaVersion: snapshot.version,
|
|
343
|
+
createdAt: new Date(),
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await checkpointApi.upsert(event)
|
|
349
|
+
})()
|
|
350
|
+
|
|
351
|
+
if (exclusive) {
|
|
352
|
+
locks.set(event.streamId, runner)
|
|
353
|
+
|
|
354
|
+
runner
|
|
355
|
+
.finally(() => {
|
|
356
|
+
if (locks.get(event.streamId) === runner) {
|
|
357
|
+
locks.delete(event.streamId)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.catch(() => {})
|
|
361
|
+
return runner
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return Promise.resolve()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let resolveReplay: null | (() => void) = null
|
|
368
|
+
let resolveReplayOne: null | (() => void) = null
|
|
369
|
+
let replayOneStreamId: null | string = null
|
|
370
|
+
|
|
371
|
+
const api = {
|
|
372
|
+
name: 'projection ' + name,
|
|
373
|
+
async isReady() {
|
|
374
|
+
await hydrated
|
|
375
|
+
},
|
|
376
|
+
withEventHandlers(h) {
|
|
377
|
+
handlers = h
|
|
378
|
+
return api
|
|
379
|
+
},
|
|
380
|
+
withGlobalEventHandler(handler) {
|
|
381
|
+
globalEventHandler = handler as any
|
|
382
|
+
return api
|
|
383
|
+
},
|
|
384
|
+
concurrent() {
|
|
385
|
+
exclusive = false
|
|
386
|
+
return api
|
|
387
|
+
},
|
|
388
|
+
setRelatedStreamEvents(generate) {
|
|
389
|
+
generateRelatedStreamEvents = generate
|
|
390
|
+
},
|
|
391
|
+
withReplay(config) {
|
|
392
|
+
replayConfig = config
|
|
393
|
+
return api as any
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
withSnapshots(handler) {
|
|
397
|
+
snapshotting = handler(createSnapshotGenerator() as any) as any
|
|
398
|
+
return api
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
async apply(event) {
|
|
402
|
+
await apply(event)
|
|
403
|
+
},
|
|
404
|
+
|
|
405
|
+
async replayOne(streamId: string) {
|
|
406
|
+
if (!idle) {
|
|
407
|
+
await replayOne(streamId)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const { promise, resolve } = Promise.withResolvers<void>()
|
|
412
|
+
resolveReplayOne = resolve
|
|
413
|
+
replayOneStreamId = streamId
|
|
414
|
+
return promise
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
async replay() {
|
|
418
|
+
if (!idle) {
|
|
419
|
+
await replay()
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
const { promise, resolve } = Promise.withResolvers<void>()
|
|
423
|
+
resolveReplay = resolve
|
|
424
|
+
return promise
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
canReplay() {
|
|
428
|
+
return Boolean(replayConfig)
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
async beginReplay() {
|
|
432
|
+
if (!replayConfig?.deleteAll) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
'this projection does not support replay all. withReplay({ deleteAll }) not implemented.',
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
lastEventPosition = -1
|
|
439
|
+
params.onReplay?.begin?.(name)
|
|
440
|
+
await params.snapshot?.delete({ projection: name })
|
|
441
|
+
await replayConfig.deleteAll()
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
async progressReplay(index, event) {
|
|
445
|
+
await withReplaying(() => withHydrating(() => apply(event)))
|
|
446
|
+
if (
|
|
447
|
+
!checkpointApi.relatedStreamEvents ||
|
|
448
|
+
checkpointApi.relatedStreamEvents.some((streamEvent) =>
|
|
449
|
+
streamEvent.events.includes(event.type),
|
|
450
|
+
)
|
|
451
|
+
) {
|
|
452
|
+
params.onReplay?.progress?.(name, index, event)
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
endReplay() {
|
|
457
|
+
params.onReplay?.end?.(name)
|
|
458
|
+
resolveHydrated()
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
async replayUntil(id, position, filterEvents = () => true) {
|
|
462
|
+
let startPosition = 0
|
|
463
|
+
if (snapshotting) {
|
|
464
|
+
await params.snapshot?.delete({ id, projection: name }, position)
|
|
465
|
+
const snapshot = await params.snapshot?.get({ id, projection: name })
|
|
466
|
+
if (snapshot) {
|
|
467
|
+
const result = snapshotSchema.safeParse(snapshot)
|
|
468
|
+
|
|
469
|
+
if (!result.success) {
|
|
470
|
+
throw new ValidationError(
|
|
471
|
+
'loaded projection snapshot does not comply with schema',
|
|
472
|
+
result.error,
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
snapshotting.internalApply(result.data)
|
|
476
|
+
startPosition = result.data.lastEventPosition + 1
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const events = params.loadEvents(
|
|
481
|
+
relatedStreamEvents?.map((relatedStreamEvent) => ({
|
|
482
|
+
...relatedStreamEvent,
|
|
483
|
+
id,
|
|
484
|
+
})) ?? null,
|
|
485
|
+
{
|
|
486
|
+
from: startPosition,
|
|
487
|
+
to: position,
|
|
488
|
+
},
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
replaying = true
|
|
492
|
+
await withReplaying(async () => {
|
|
493
|
+
for await (const event of events) {
|
|
494
|
+
if ((filterEvents as (event: BaseOutputEvent) => boolean)(event)) {
|
|
495
|
+
await apply(event)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
replaying = false
|
|
500
|
+
},
|
|
501
|
+
async init(catchUp) {
|
|
502
|
+
idle = false
|
|
503
|
+
relatedStreamEvents = globalEventHandler
|
|
504
|
+
? null
|
|
505
|
+
: generateRelatedStreamEvents(handlers as EventHandlers<any>)
|
|
506
|
+
checkpointApi = createCheckpointApi({
|
|
507
|
+
...params.checkpoint,
|
|
508
|
+
name,
|
|
509
|
+
relatedStreamEvents,
|
|
510
|
+
type: 'projection',
|
|
511
|
+
loadEvents: params.loadEvents,
|
|
512
|
+
apply: (event) => withHydrating(() => apply(event)),
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
if (resolveReplay) {
|
|
516
|
+
await replay()
|
|
517
|
+
resolveReplay()
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (resolveReplayOne && replayOneStreamId) {
|
|
522
|
+
await replayOne(replayOneStreamId)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (!catchUp) {
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
await checkpointApi.rehydrated()
|
|
530
|
+
resolveHydrated()
|
|
531
|
+
resolveReplayOne?.()
|
|
532
|
+
},
|
|
533
|
+
} as ProjectionApi & InternalProjection & ReplayProjection<any>
|
|
534
|
+
|
|
535
|
+
async function replayOne(id: string) {
|
|
536
|
+
if (!replayConfig?.deleteOne) {
|
|
537
|
+
throw new Error(
|
|
538
|
+
'this projection does not support replay one. withReplay({ deleteOne }) not implemented.',
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
await params.snapshot?.delete({ id, projection: name })
|
|
542
|
+
await replayConfig.deleteOne(id)
|
|
543
|
+
const streamIds = relatedStreamEvents
|
|
544
|
+
? relatedStreamEvents.map((relatedStreamEvent) => ({
|
|
545
|
+
...relatedStreamEvent,
|
|
546
|
+
id,
|
|
547
|
+
}))
|
|
548
|
+
: null
|
|
549
|
+
const events = params.loadEvents(streamIds)
|
|
550
|
+
|
|
551
|
+
replaying = true
|
|
552
|
+
await withReplaying(async () => {
|
|
553
|
+
for await (const event of events) {
|
|
554
|
+
await withHydrating(() => apply(event))
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
replaying = false
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function replay() {
|
|
561
|
+
await api.beginReplay()
|
|
562
|
+
const events = params.loadEvents(relatedStreamEvents)
|
|
563
|
+
let index = 0
|
|
564
|
+
replaying = true
|
|
565
|
+
await withReplaying(async () => {
|
|
566
|
+
for await (const event of events) {
|
|
567
|
+
await api.progressReplay(index, event)
|
|
568
|
+
index++
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
replaying = false
|
|
572
|
+
api.endReplay()
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return api
|
|
576
|
+
}
|
|
577
|
+
}
|