@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,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
+ }