@requence/event-sourcing 1.0.4 → 1.1.0

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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-2qy4eb4h.js → chunk-04wqxaqs.js} +3 -3
  3. package/dist/{chunk-3t1vzbab.js → chunk-263c07sn.js} +3 -2
  4. package/dist/{chunk-3t1vzbab.js.map → chunk-263c07sn.js.map} +3 -3
  5. package/dist/{chunk-x90g388y.js → chunk-2fe8jjdc.js} +1 -1
  6. package/dist/{chunk-qdczt68t.js → chunk-391m4mfp.js} +14 -2
  7. package/dist/chunk-391m4mfp.js.map +10 -0
  8. package/dist/{chunk-9psg50cb.js → chunk-6yrb30rx.js} +1 -1
  9. package/dist/{chunk-pmfzjavb.js → chunk-8qn1nqnh.js} +1 -1
  10. package/dist/{chunk-crc0araj.js → chunk-r601d5jm.js} +5 -5
  11. package/dist/{chunk-8x4bedr4.js → chunk-smy8mxvr.js} +35 -21
  12. package/dist/chunk-smy8mxvr.js.map +10 -0
  13. package/dist/{chunk-kj36dz26.js → chunk-th14wxh8.js} +4 -4
  14. package/dist/{chunk-zf3x573k.js → chunk-yn2sjcg3.js} +36 -11
  15. package/dist/chunk-yn2sjcg3.js.map +10 -0
  16. package/dist/createAggregateRoot.js +8 -8
  17. package/dist/createCheckpointApi.d.ts +37 -2
  18. package/dist/createCheckpointApi.d.ts.map +1 -1
  19. package/dist/createCheckpointApi.js +2 -2
  20. package/dist/createEventStore.js +8 -8
  21. package/dist/createProcessManager.d.ts.map +1 -1
  22. package/dist/createProcessManager.js +4 -4
  23. package/dist/createProjection.js +5 -5
  24. package/dist/drizzle/index.d.ts.map +1 -1
  25. package/dist/drizzle/index.js +24 -18
  26. package/dist/drizzle/index.js.map +3 -3
  27. package/dist/drizzle/postgres.d.ts +17 -0
  28. package/dist/drizzle/postgres.d.ts.map +1 -1
  29. package/dist/drizzle/postgres.js +1 -1
  30. package/dist/errors.d.ts +6 -0
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +5 -3
  33. package/dist/errors.js.map +1 -1
  34. package/dist/index.js +13 -11
  35. package/dist/index.js.map +1 -1
  36. package/dist/memory/index.d.ts.map +1 -1
  37. package/dist/memory/index.js +17 -12
  38. package/dist/memory/index.js.map +3 -3
  39. package/dist/refresh.js +2 -2
  40. package/dist/replay.js +2 -2
  41. package/dist/snapshotGenerator.js +2 -2
  42. package/dist/tests/setup.d.ts +1 -0
  43. package/dist/tests/setup.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/createCheckpointApi.ts +86 -10
  46. package/src/createProcessManager.ts +71 -32
  47. package/src/drizzle/index.ts +19 -4
  48. package/src/drizzle/postgres.ts +3 -0
  49. package/src/errors.ts +12 -0
  50. package/src/memory/index.ts +10 -2
  51. package/src/tests/processManagerMultiInstance.spec.ts +86 -3
  52. package/src/tests/setup.ts +12 -2
  53. package/dist/chunk-8x4bedr4.js.map +0 -10
  54. package/dist/chunk-qdczt68t.js.map +0 -10
  55. package/dist/chunk-zf3x573k.js.map +0 -10
  56. /package/dist/{chunk-2qy4eb4h.js.map → chunk-04wqxaqs.js.map} +0 -0
  57. /package/dist/{chunk-x90g388y.js.map → chunk-2fe8jjdc.js.map} +0 -0
  58. /package/dist/{chunk-9psg50cb.js.map → chunk-6yrb30rx.js.map} +0 -0
  59. /package/dist/{chunk-pmfzjavb.js.map → chunk-8qn1nqnh.js.map} +0 -0
  60. /package/dist/{chunk-crc0araj.js.map → chunk-r601d5jm.js.map} +0 -0
  61. /package/dist/{chunk-kj36dz26.js.map → chunk-th14wxh8.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import z from 'zod/v4'
2
2
 
3
3
  import type { LoadEvents } from './createEventStore.ts'
4
- import { ValidationError } from './errors.ts'
4
+ import { CheckpointConcurrencyError, ValidationError } from './errors.ts'
5
5
  import type {
6
6
  BaseOutputEvent,
7
7
  MaybePromise,
@@ -13,6 +13,7 @@ const checkpointSchema = z.object({
13
13
  type: z.enum(['projection', 'processManager']),
14
14
  name: z.string(),
15
15
  metadata: z.record(z.string(), z.any()),
16
+ version: z.int(),
16
17
  })
17
18
 
18
19
  export type Checkpoint = z.output<typeof checkpointSchema>
@@ -26,7 +27,19 @@ export type CheckpointMethods = {
26
27
  type: 'projection' | 'processManager',
27
28
  name: string,
28
29
  ) => MaybePromise<void>
29
- upsert: (checkpoint: Checkpoint) => MaybePromise<void>
30
+ /**
31
+ * Persist a checkpoint using optimistic concurrency control.
32
+ *
33
+ * The write must only succeed if the currently stored version equals
34
+ * `expectedVersion` (or, when `expectedVersion` is `null`, if no checkpoint
35
+ * exists yet). `checkpoint.version` holds the version to store on success.
36
+ * Return `true` when the write was applied and `false` when it was rejected
37
+ * because another writer had advanced the checkpoint in the meantime.
38
+ */
39
+ upsert: (
40
+ checkpoint: Checkpoint,
41
+ expectedVersion: number | null,
42
+ ) => MaybePromise<boolean>
30
43
  }
31
44
 
32
45
  export function createCheckpointApi(
@@ -39,18 +52,81 @@ export function createCheckpointApi(
39
52
  applyAfter?: (event: BaseOutputEvent) => MaybePromise<void>
40
53
  },
41
54
  ) {
55
+ // In-memory view of the persisted checkpoint version. `null` means "no
56
+ // checkpoint has been written yet". It is kept in sync with the store on
57
+ // every load/upsert so optimistic-concurrency writes can detect when another
58
+ // instance has advanced the same checkpoint.
59
+ let version: number | null = null
60
+
42
61
  const api = {
43
62
  relatedStreamEvents: params.relatedStreamEvents,
44
63
  async clear() {
45
64
  await params.delete(params.type, params.name)
65
+ version = null
46
66
  },
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
- })
67
+ /**
68
+ * Load the persisted checkpoint and sync the local version. Returns the
69
+ * raw checkpoint (or `null`), so callers can rehydrate their own state
70
+ * (e.g. a process manager reloading its folded state) from it.
71
+ */
72
+ async load() {
73
+ const checkpoint = await params.get(params.type, params.name)
74
+ version = checkpoint?.version ?? null
75
+ return checkpoint ?? null
76
+ },
77
+ /**
78
+ * Persist the checkpoint position (and optional metadata) for the given
79
+ * event.
80
+ *
81
+ * - `'cas'` (compare-and-swap): the write must land on top of the exact
82
+ * version we last observed. If another writer advanced the checkpoint,
83
+ * the local version is refreshed and a {@link CheckpointConcurrencyError}
84
+ * is thrown so the caller can re-fold its state and retry. Used for
85
+ * process managers that fold state into the checkpoint.
86
+ * - `'monotonic'` (default): retries on conflict, skipping the write once
87
+ * the stored position already covers this event. Safe for cursors with
88
+ * no derived state (projections, stateless/after-effect writes).
89
+ */
90
+ async upsert(
91
+ event: BaseOutputEvent,
92
+ metadata: Record<string, any> = {},
93
+ mode: 'cas' | 'monotonic' = 'monotonic',
94
+ ) {
95
+ while (true) {
96
+ const expectedVersion = version
97
+ const nextVersion = (expectedVersion ?? -1) + 1
98
+ const committed = await params.upsert(
99
+ {
100
+ lastEventPosition: event.position,
101
+ type: params.type,
102
+ name: params.name,
103
+ metadata,
104
+ version: nextVersion,
105
+ },
106
+ expectedVersion,
107
+ )
108
+
109
+ if (committed) {
110
+ version = nextVersion
111
+ return
112
+ }
113
+
114
+ // Another writer advanced this checkpoint. Refresh our view of it.
115
+ const current = await api.load()
116
+
117
+ if (mode === 'cas') {
118
+ throw new CheckpointConcurrencyError(
119
+ params.type,
120
+ params.name,
121
+ expectedVersion,
122
+ )
123
+ }
124
+
125
+ // monotonic: the stored cursor already covers this event, nothing to do.
126
+ if (current && current.lastEventPosition >= event.position) {
127
+ return
128
+ }
129
+ }
54
130
  },
55
131
  async rehydrated(
56
132
  options: {
@@ -59,7 +135,7 @@ export function createCheckpointApi(
59
135
  onProgress?: (index: number, event: BaseOutputEvent) => void
60
136
  } = {},
61
137
  ) {
62
- const checkpoint = await params.get(params.type, params.name)
138
+ const checkpoint = await api.load()
63
139
  let startPosition = 0
64
140
  if (checkpoint) {
65
141
  const result = checkpointSchema.safeParse(checkpoint)
@@ -7,6 +7,7 @@ import {
7
7
  } from './createCheckpointApi.ts'
8
8
  import type { LoadEvents, OnProgress } from './createEventStore.ts'
9
9
  import {
10
+ CheckpointConcurrencyError,
10
11
  ProcessManagerEventHandlerExecutionError,
11
12
  RefreshingSkipError,
12
13
  } from './errors.ts'
@@ -180,39 +181,75 @@ export function buildProcessManagerCreator(params: {
180
181
  const runner = (async () => {
181
182
  await unlocked
182
183
  const refreshing = isRefreshing()
183
- await withProcessManagerInfo(
184
- {
185
- name,
186
- event,
187
- },
188
- async () => {
189
- try {
190
- await handler(event, { refreshing })
191
- } catch (error) {
192
- if (error instanceof RefreshingSkipError) {
193
- if (refreshing) {
194
- return
184
+ // Fold state into the process-manager checkpoint with optimistic
185
+ // concurrency control. Only the stateful `on<Event>` fold needs
186
+ // compare-and-swap — after-effects and stateless managers persist a
187
+ // plain cursor, which is safe to write monotonically. When a concurrent
188
+ // instance advances the checkpoint between our read and write, reload
189
+ // the latest state and re-apply this event on top of it.
190
+ const useCas = hasState && !after && !hydrating && !refreshing
191
+ while (true) {
192
+ const currentHandler = after
193
+ ? createAfterEffects(state)[`after${event.type}`]
194
+ : createEventHandlers(state)[`on${event.type}`]
195
+ await withProcessManagerInfo(
196
+ {
197
+ name,
198
+ event,
199
+ },
200
+ async () => {
201
+ try {
202
+ await currentHandler!(event, { refreshing })
203
+ } catch (error) {
204
+ if (error instanceof RefreshingSkipError) {
205
+ if (refreshing) {
206
+ return
207
+ }
208
+
209
+ throw new Error(
210
+ 'RefreshingSkipError should never be thrown outside refresh',
211
+ )
195
212
  }
196
213
 
197
- throw new Error(
198
- 'RefreshingSkipError should never be thrown outside refresh',
199
- )
200
- }
214
+ if (!(error instanceof Error)) {
215
+ throw error
216
+ }
201
217
 
202
- if (!(error instanceof Error)) {
203
- throw error
218
+ throw new ProcessManagerEventHandlerExecutionError(
219
+ name,
220
+ after ? `after${event.type}` : `on${event.type}`,
221
+ event,
222
+ error,
223
+ )
204
224
  }
225
+ },
226
+ )
205
227
 
206
- throw new ProcessManagerEventHandlerExecutionError(
207
- name,
208
- after ? `after${event.type}` : `on${event.type}`,
209
- event,
210
- error,
211
- )
228
+ try {
229
+ await checkpointApi.upsert(
230
+ event,
231
+ { state },
232
+ useCas ? 'cas' : 'monotonic',
233
+ )
234
+ break
235
+ } catch (error) {
236
+ if (!(error instanceof CheckpointConcurrencyError)) {
237
+ throw error
212
238
  }
213
- },
214
- )
215
- await checkpointApi.upsert(event, { state })
239
+
240
+ // A concurrent writer advanced the checkpoint. Reload the latest
241
+ // folded state (and version) and re-apply this event on top of it.
242
+ // This event is emitted only by this instance, so it is never
243
+ // already part of the reloaded state — we must always re-fold it
244
+ // (do not skip on position: events are processed out of global
245
+ // order across instances).
246
+ const checkpoint = await checkpointApi.load()
247
+ state =
248
+ checkpoint?.metadata?.state != null
249
+ ? clone(checkpoint.metadata.state)
250
+ : clone(initialState)
251
+ }
252
+ }
216
253
  })()
217
254
 
218
255
  if (exclusive) {
@@ -243,12 +280,14 @@ export function buildProcessManagerCreator(params: {
243
280
  },
244
281
  async beginSession() {
245
282
  if (!hasState) return
246
- const checkpoint = await params.checkpoint.get(
247
- 'processManager',
248
- name,
249
- )
283
+ // Load through the checkpoint API so the local version is synced with
284
+ // the store; the subsequent fold relies on it for compare-and-swap.
285
+ const checkpoint = await checkpointApi.load()
250
286
  if (checkpoint) {
251
- state = checkpoint.metadata?.state ?? clone(initialState)
287
+ state =
288
+ checkpoint.metadata?.state != null
289
+ ? clone(checkpoint.metadata.state)
290
+ : clone(initialState)
252
291
  lastEventPosition = checkpoint.lastEventPosition
253
292
  } else {
254
293
  state = clone(initialState)
@@ -258,22 +258,36 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
258
258
  }
259
259
  },
260
260
  checkpoint: {
261
- async upsert(checkpoint) {
262
- await db()
261
+ async upsert(checkpoint, expectedVersion) {
262
+ const metadata = superjson.stringify(checkpoint.metadata)
263
+ // Compare-and-swap: on an existing row the DO UPDATE only fires when
264
+ // the stored version still matches `expectedVersion`. A `null`
265
+ // expectation means "insert only" — if the row already exists the
266
+ // guard fails and no row is returned, signalling a conflict.
267
+ const committed = await db()
263
268
  .insert(checkpoints)
264
269
  .values({
265
270
  lastEventPosition: checkpoint.lastEventPosition,
266
271
  name: checkpoint.name,
267
272
  type: checkpoint.type,
268
- metadata: superjson.stringify(checkpoint.metadata),
273
+ metadata,
274
+ version: checkpoint.version,
269
275
  })
270
276
  .onConflictDoUpdate({
271
277
  target: [checkpoints.type, checkpoints.name],
272
278
  set: {
273
279
  lastEventPosition: checkpoint.lastEventPosition,
274
- metadata: superjson.stringify(checkpoint.metadata),
280
+ metadata,
281
+ version: checkpoint.version,
275
282
  },
283
+ setWhere:
284
+ expectedVersion === null
285
+ ? sql`false`
286
+ : eq(checkpoints.version, expectedVersion),
276
287
  })
288
+ .returning({ version: checkpoints.version })
289
+
290
+ return committed.length > 0
277
291
  },
278
292
  async get(type, name) {
279
293
  const [checkpoint] = await db()
@@ -282,6 +296,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
282
296
  name: checkpoints.name,
283
297
  lastEventPosition: checkpoints.lastEventPosition,
284
298
  metadata: checkpoints.metadata,
299
+ version: checkpoints.version,
285
300
  })
286
301
  .from(checkpoints)
287
302
  .where(and(eq(checkpoints.type, type), eq(checkpoints.name, name)))
@@ -99,6 +99,9 @@ export const checkpoints = eventSourcing.table(
99
99
  name: text().notNull(),
100
100
  lastEventPosition: bigint({ mode: 'number' }).notNull(),
101
101
  metadata: text().notNull(),
102
+ // Optimistic-concurrency guard: bumped on every write so concurrent
103
+ // instances can detect when they raced on the same checkpoint.
104
+ version: bigint({ mode: 'number' }).notNull().default(0),
102
105
  },
103
106
  (table) => [
104
107
  unique().on(table.type, table.name),
package/src/errors.ts CHANGED
@@ -14,6 +14,18 @@ export class ConcurrencyError extends Error {
14
14
  }
15
15
  }
16
16
 
17
+ export class CheckpointConcurrencyError extends Error {
18
+ constructor(
19
+ public readonly checkpointType: 'projection' | 'processManager',
20
+ public readonly name: string,
21
+ public readonly expectedVersion: number | null,
22
+ ) {
23
+ super(
24
+ `Concurrency error on ${checkpointType} checkpoint "${name}": expected version ${expectedVersion}, but it was modified by another writer`,
25
+ )
26
+ }
27
+ }
28
+
17
29
  export class ValidationError extends Error {
18
30
  constructor(
19
31
  message: string,
@@ -139,7 +139,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
139
139
  }
140
140
  }
141
141
  },
142
- upsert(checkpoint) {
142
+ upsert(checkpoint, expectedVersion) {
143
143
  let persisted: Checkpoint | undefined
144
144
  for (const c of checkpoints) {
145
145
  if (c.type === checkpoint.type && c.name === checkpoint.name) {
@@ -149,11 +149,19 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
149
149
  }
150
150
 
151
151
  if (persisted) {
152
+ // Compare-and-swap: reject the write when another writer advanced the
153
+ // checkpoint, or when the caller expected to create a fresh one.
154
+ if (expectedVersion === null || persisted.version !== expectedVersion) {
155
+ return false
156
+ }
152
157
  persisted.lastEventPosition = checkpoint.lastEventPosition
153
158
  persisted.metadata = checkpoint.metadata
159
+ persisted.version = checkpoint.version
154
160
  } else {
155
- checkpoints.add(checkpoint)
161
+ checkpoints.add({ ...checkpoint })
156
162
  }
163
+
164
+ return true
157
165
  },
158
166
  delete(type, name) {
159
167
  for (const checkpoint of checkpoints) {
@@ -88,7 +88,7 @@ function createSharedDatabase() {
88
88
  if (cp.type === type && cp.name === name) return cp
89
89
  }
90
90
  },
91
- upsert(checkpoint: Checkpoint) {
91
+ upsert(checkpoint: Checkpoint, expectedVersion: number | null) {
92
92
  let persisted: Checkpoint | undefined
93
93
  for (const c of checkpoints) {
94
94
  if (c.type === checkpoint.type && c.name === checkpoint.name) {
@@ -97,11 +97,20 @@ function createSharedDatabase() {
97
97
  }
98
98
  }
99
99
  if (persisted) {
100
+ // Optimistic-concurrency guard — reject stale writes.
101
+ if (
102
+ expectedVersion === null ||
103
+ persisted.version !== expectedVersion
104
+ ) {
105
+ return false
106
+ }
100
107
  persisted.lastEventPosition = checkpoint.lastEventPosition
101
108
  persisted.metadata = checkpoint.metadata
109
+ persisted.version = checkpoint.version
102
110
  } else {
103
- checkpoints.add(checkpoint)
111
+ checkpoints.add({ ...checkpoint })
104
112
  }
113
+ return true
105
114
  },
106
115
  delete(type: string, name: string) {
107
116
  for (const cp of checkpoints) {
@@ -123,6 +132,9 @@ function createInstance(
123
132
  sharedDb: ReturnType<typeof createSharedDatabase>,
124
133
  opts?: {
125
134
  onEventsEmitted?: (events: BaseOutputEvent[]) => MaybePromise<void>
135
+ // Hook invoked before every checkpoint write — lets a test pause one
136
+ // instance mid-flight to force a concurrent-write interleaving.
137
+ beforeCheckpointUpsert?: () => MaybePromise<void>
126
138
  },
127
139
  ) {
128
140
  const aggregate = createAggregateRoot('counters')
@@ -135,6 +147,14 @@ function createInstance(
135
147
  },
136
148
  }))
137
149
 
150
+ const checkpoint = {
151
+ ...sharedDb.checkpoint,
152
+ async upsert(cp: Checkpoint, expectedVersion: number | null) {
153
+ await opts?.beforeCheckpointUpsert?.()
154
+ return sharedDb.checkpoint.upsert(cp, expectedVersion)
155
+ },
156
+ }
157
+
138
158
  const eventStore = createEventStore({
139
159
  aggregateRoots: [aggregate],
140
160
  autoInit: false,
@@ -145,7 +165,7 @@ function createInstance(
145
165
  return appended
146
166
  },
147
167
  loadEvents: sharedDb.loadEvents.bind(sharedDb),
148
- checkpoint: sharedDb.checkpoint,
168
+ checkpoint,
149
169
  })
150
170
 
151
171
  return { aggregate, eventStore }
@@ -211,4 +231,67 @@ describe('ProcessManager Multi-Instance', () => {
211
231
  // Instance A also sees the correct state (loaded from shared checkpoint)
212
232
  expect(await pmA.state()).toEqual({ total: 15 })
213
233
  })
234
+
235
+ it('does not lose updates when two instances fold state concurrently', async () => {
236
+ const sharedDb = createSharedDatabase()
237
+
238
+ // Gate instance A's checkpoint write so we can deterministically force the
239
+ // race: A reads the (empty) checkpoint and folds its event, then parks
240
+ // right before persisting. While it is parked, B reads the same empty
241
+ // checkpoint, folds its own event and commits. When A is released its write
242
+ // is now stale — without optimistic concurrency it would clobber B's
243
+ // update and lose it.
244
+ const aAtGate = Promise.withResolvers<void>()
245
+ const releaseA = Promise.withResolvers<void>()
246
+ let aParked = false
247
+
248
+ const instanceA = createInstance(sharedDb, {
249
+ async beforeCheckpointUpsert() {
250
+ if (!aParked) {
251
+ aParked = true
252
+ aAtGate.resolve()
253
+ await releaseA.promise
254
+ }
255
+ },
256
+ })
257
+ const pmA = instanceA.eventStore
258
+ .createProcessManager('counter-pm')
259
+ .withState({ total: 0 })
260
+ .withEventHandlers((state) => ({
261
+ async onCounterIncremented({ payload }) {
262
+ state.total += payload.amount
263
+ },
264
+ }))
265
+ await instanceA.eventStore.init()
266
+ await instanceA.eventStore.isReady()
267
+
268
+ const instanceB = createInstance(sharedDb)
269
+ const pmB = instanceB.eventStore
270
+ .createProcessManager('counter-pm')
271
+ .withState({ total: 0 })
272
+ .withEventHandlers((state) => ({
273
+ async onCounterIncremented({ payload }) {
274
+ state.total += payload.amount
275
+ },
276
+ }))
277
+ await instanceB.eventStore.init()
278
+ await instanceB.eventStore.isReady()
279
+
280
+ // A starts processing but parks before persisting its checkpoint.
281
+ const aSettled = instanceA.aggregate.newStream().increment(10).settled()
282
+ await aAtGate.promise
283
+
284
+ // B reads the still-empty checkpoint and commits { total: 5 }.
285
+ await instanceB.aggregate.newStream().increment(5).settled()
286
+
287
+ // Release A — its stale write must be rejected, forcing it to reload B's
288
+ // state and re-fold on top of it.
289
+ releaseA.resolve()
290
+ await aSettled
291
+
292
+ const checkpoint = sharedDb.checkpoint.get('processManager', 'counter-pm')!
293
+ expect(checkpoint.metadata).toEqual({ state: { total: 15 } })
294
+ expect(await pmA.state()).toEqual({ total: 15 })
295
+ expect(await pmB.state()).toEqual({ total: 15 })
296
+ })
214
297
  })
@@ -142,7 +142,7 @@ export function setupEventStore<const Root extends AnyAggregateRoot>(
142
142
  }
143
143
  }
144
144
  },
145
- upsert(checkpoint) {
145
+ upsert(checkpoint, expectedVersion) {
146
146
  let persistedCheckpoint: Checkpoint | undefined
147
147
  for (const c of pseudoCheckpointStore) {
148
148
  if (c.type === checkpoint.type && c.name === checkpoint.name) {
@@ -152,10 +152,20 @@ export function setupEventStore<const Root extends AnyAggregateRoot>(
152
152
  }
153
153
 
154
154
  if (persistedCheckpoint) {
155
+ if (
156
+ expectedVersion === null ||
157
+ persistedCheckpoint.version !== expectedVersion
158
+ ) {
159
+ return false
160
+ }
155
161
  persistedCheckpoint.lastEventPosition = checkpoint.lastEventPosition
162
+ persistedCheckpoint.metadata = checkpoint.metadata
163
+ persistedCheckpoint.version = checkpoint.version
156
164
  } else {
157
- pseudoCheckpointStore.add(checkpoint)
165
+ pseudoCheckpointStore.add({ ...checkpoint })
158
166
  }
167
+
168
+ return true
159
169
  },
160
170
  delete(type, name) {
161
171
  for (const checkpoint of pseudoCheckpointStore) {
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/createProcessManager.ts"],
4
- "sourcesContent": [
5
- "import { AsyncLocalStorage } from 'node:async_hooks'\n\nimport {\n type CheckpointApi,\n type CheckpointMethods,\n createCheckpointApi,\n} from './createCheckpointApi.ts'\nimport type { LoadEvents, OnProgress } from './createEventStore.ts'\nimport {\n ProcessManagerEventHandlerExecutionError,\n RefreshingSkipError,\n} from './errors.ts'\nimport { isRefreshing, withRefreshing } from './refresh.ts'\nimport { clone } from './superjson.ts'\nimport type {\n AfterEffectHandlers,\n BaseOutputEvent,\n EventHandlers,\n EventTemplate,\n Keys,\n Merge,\n StreamEvents,\n} from './utilityTypes.js'\n\ntype ProcessManagerInfo = {\n name: string\n event: BaseOutputEvent\n}\nconst processManagerInfoStore = new AsyncLocalStorage<ProcessManagerInfo>()\n\nfunction withProcessManagerInfo(\n info: ProcessManagerInfo,\n handler: () => Promise<void>,\n) {\n return processManagerInfoStore.run(info, handler)\n}\n\nexport function getProcessManagerInfo() {\n return processManagerInfoStore.getStore()\n}\n\nexport function isInsideProcessManager() {\n return Boolean(getProcessManagerInfo())\n}\n\ntype ProcessManagerEventHandlers<Events extends EventTemplate> = EventHandlers<\n Events,\n { refreshing: boolean }\n>\n\ntype ProcessManagerAfterEffectHandlers<Events extends EventTemplate> =\n AfterEffectHandlers<Events, { refreshing: boolean }>\n\nexport type InternalProcessManager = {\n apply: (event: BaseOutputEvent) => Promise<void>\n applyAfter: (event: BaseOutputEvent) => Promise<void>\n beginSession: () => Promise<void>\n endSession: () => void\n setRelatedStreamEvents: (\n generateRelatedStreamEvents: (\n eventHandlers: EventHandlers<EventTemplate>,\n ) => StreamEvents[],\n ) => void\n isReady(): Promise<void>\n beginRefresh(): Promise<void>\n progressRefresh(\n index: number,\n event: BaseOutputEvent,\n after: boolean,\n ): Promise<void>\n endRefresh(): void\n init(catchUp: boolean): Promise<void>\n hasState(): boolean\n}\n\nexport type ProcessManagerApi<\n State = never,\n Events extends EventTemplate = never,\n Flags extends {\n withState: boolean\n withEventHandlers: boolean\n concurrent: boolean\n withAfterEffects: boolean\n } = {\n withState: false\n withEventHandlers: false\n concurrent: false\n withAfterEffects: false\n },\n> = {\n withState: <NextState>(\n state: NextState,\n ) => Omit<\n ProcessManagerApi<NextState, Events, Merge<Flags, { withState: true }>>,\n Keys<Merge<Flags, { withState: true }>>\n >\n withEventHandlers: (\n handlers: [State] extends [never]\n ? ProcessManagerEventHandlers<Events>\n : (state: State) => ProcessManagerEventHandlers<Events>,\n ) => Omit<\n ProcessManagerApi<State, Events, Merge<Flags, { withEventHandlers: true }>>,\n Keys<Merge<Flags, { withEventHandlers: true }>>\n >\n withAfterEffects: (\n handlers: [State] extends [never]\n ? ProcessManagerAfterEffectHandlers<Events>\n : (state: State) => ProcessManagerAfterEffectHandlers<Events>,\n ) => Omit<\n ProcessManagerApi<State, Events, Merge<Flags, { withAfterEffects: true }>>,\n Keys<Merge<Flags, { withAfterEffects: true }>>\n >\n concurrent: () => Omit<\n ProcessManagerApi<State, Events, Merge<Flags, { concurrent: true }>>,\n Keys<Merge<Flags, { concurrent: true }>>\n >\n isReady(): Promise<void>\n $inferState: State\n} & (Flags['withState'] extends true\n ? {\n refreshState: () => Promise<void>\n state: () => Promise<State>\n }\n : {})\n\nexport function buildProcessManagerCreator(params: {\n loadEvents: LoadEvents\n checkpoint: CheckpointMethods\n onRefresh?: OnProgress\n}) {\n return (name: string): ProcessManagerApi => {\n let idle = true\n let createEventHandlers: (\n state: any,\n ) => ProcessManagerEventHandlers<any> = () => ({})\n let createAfterEffects: (\n state: any,\n ) => ProcessManagerAfterEffectHandlers<any> = () => ({})\n\n let hasState = false\n let state: any = null\n let initialState: any = null\n let exclusive = true\n let sessionActive = false\n let generateRelatedStreamEvents: (\n eventHandlers: EventHandlers<any>,\n ) => StreamEvents[]\n let checkpointApi: CheckpointApi\n let lastEventPosition = -1\n const { promise: hydrated, resolve: resolveHydrated } =\n Promise.withResolvers<void>()\n\n const locks = new Map<string, Promise<void>>()\n const apply = async (\n event: BaseOutputEvent,\n after: boolean,\n hydrating = false,\n ) => {\n const handler = after\n ? createAfterEffects(state)[`after${event.type}`]\n : createEventHandlers(state)[`on${event.type}`]\n\n if (!handler) {\n return Promise.resolve()\n }\n\n if (hydrating) {\n lastEventPosition = event.position\n } else {\n await hydrated\n // event was already applied through hydration\n if (event.position <= lastEventPosition) {\n return\n }\n }\n const unlocked = !exclusive\n ? Promise.resolve()\n : (locks.get(event.streamId)?.catch(() => {}) ?? Promise.resolve())\n\n const runner = (async () => {\n await unlocked\n const refreshing = isRefreshing()\n await withProcessManagerInfo(\n {\n name,\n event,\n },\n async () => {\n try {\n await handler(event, { refreshing })\n } catch (error) {\n if (error instanceof RefreshingSkipError) {\n if (refreshing) {\n return\n }\n\n throw new Error(\n 'RefreshingSkipError should never be thrown outside refresh',\n )\n }\n\n if (!(error instanceof Error)) {\n throw error\n }\n\n throw new ProcessManagerEventHandlerExecutionError(\n name,\n after ? `after${event.type}` : `on${event.type}`,\n event,\n error,\n )\n }\n },\n )\n await checkpointApi.upsert(event, { state })\n })()\n\n if (exclusive) {\n locks.set(event.streamId, runner)\n\n runner\n .finally(() => {\n if (locks.get(event.streamId) === runner) {\n locks.delete(event.streamId)\n }\n })\n .catch(() => {})\n return runner\n }\n\n return Promise.resolve()\n }\n\n let resolveRefresh: null | (() => void) = null\n\n const api = {\n name,\n hasState() {\n return hasState\n },\n async isReady() {\n await hydrated\n },\n async beginSession() {\n if (!hasState) return\n const checkpoint = await params.checkpoint.get(\n 'processManager',\n name,\n )\n if (checkpoint) {\n state = checkpoint.metadata?.state ?? clone(initialState)\n lastEventPosition = checkpoint.lastEventPosition\n } else {\n state = clone(initialState)\n lastEventPosition = -1\n }\n sessionActive = true\n },\n endSession() {\n if (!hasState) return\n state = null\n sessionActive = false\n },\n async apply(event) {\n await apply(event, false)\n },\n async applyAfter(event) {\n await apply(event, true)\n },\n withState(nextState) {\n state = clone(nextState)\n initialState = clone(nextState)\n hasState = true\n return api as any\n },\n concurrent() {\n exclusive = false\n return api\n },\n withEventHandlers(handlers) {\n createEventHandlers =\n typeof handlers === 'function' ? (handlers as any) : () => handlers\n\n return api\n },\n withAfterEffects(handlers) {\n createAfterEffects =\n typeof handlers === 'function' ? (handlers as any) : () => handlers\n\n return api\n },\n setRelatedStreamEvents(generate) {\n generateRelatedStreamEvents = generate\n },\n async beginRefresh() {\n params.onRefresh?.begin?.(name)\n await checkpointApi.clear()\n lastEventPosition = -1\n state = initialState\n },\n async progressRefresh(index, event, after) {\n await apply(event, after, true)\n\n if (\n !checkpointApi.relatedStreamEvents ||\n checkpointApi.relatedStreamEvents.some((streamEvent) =>\n streamEvent.events.includes(event.type),\n )\n ) {\n params.onRefresh?.progress?.(name, index, event)\n }\n },\n endRefresh() {\n params.onRefresh?.end?.(name)\n resolveHydrated()\n },\n async refreshState() {\n if (!idle) {\n await refresh()\n return\n }\n const { promise, resolve } = Promise.withResolvers<void>()\n resolveRefresh = resolve\n return promise\n },\n async state() {\n await hydrated\n if (!sessionActive && hasState) {\n const checkpoint = await params.checkpoint.get(\n 'processManager',\n name,\n )\n return checkpoint?.metadata?.state ?? clone(initialState)\n }\n return state\n },\n async init(catchUp) {\n idle = false\n const relatedStreamEvents = generateRelatedStreamEvents({\n ...createEventHandlers(null),\n ...createAfterEffects(null),\n })\n\n checkpointApi = createCheckpointApi({\n ...params.checkpoint,\n name,\n relatedStreamEvents,\n type: 'processManager',\n loadEvents: params.loadEvents,\n apply: (event) => apply(event, false, true),\n applyAfter: (event) => apply(event, true, true),\n })\n\n if (resolveRefresh) {\n await refresh()\n return\n }\n if (!catchUp) {\n return\n }\n\n await checkpointApi.rehydrated({\n getMetadata() {\n return { state }\n },\n onMetadata(metadata) {\n state = metadata.state ?? state\n },\n })\n\n // Release state after hydration — it will be loaded\n // from the checkpoint on the next beginSession call\n state = null\n\n resolveHydrated()\n },\n $inferState: null as never,\n } as ProcessManagerApi & InternalProcessManager\n\n async function refresh() {\n await withRefreshing(async () => {\n await api.beginRefresh()\n await checkpointApi.rehydrated({\n getMetadata() {\n return { state }\n },\n onProgress(index, event) {\n params.onRefresh?.progress?.(name, index, event)\n },\n })\n })\n api.endRefresh()\n }\n\n return api\n }\n}\n"
6
- ],
7
- "mappings": "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",
8
- "debugId": "973C14BE668A8E0964756E2164756E21",
9
- "names": []
10
- }
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/errors.ts"],
4
- "sourcesContent": [
5
- "import z from 'zod/v4'\n\nimport type { BaseOutputEvent, OutputEvent } from './utilityTypes.js'\n\nexport class ConcurrencyError extends Error {\n constructor(\n streamId: { stream: string; id: string },\n expectedVersion: number,\n actualVersion: number,\n ) {\n super(\n `Concurrency error on ${streamId.stream}(${streamId.id}): expected v${expectedVersion}, actual v${actualVersion}`,\n )\n }\n}\n\nexport class ValidationError extends Error {\n constructor(\n message: string,\n public readonly zodError: z.ZodError,\n ) {\n super(`${message}: ${z.prettifyError(zodError)}`)\n }\n}\n\nexport class EventValidationError extends ValidationError {}\n\nexport class InfiniteLoopError extends Error {}\n\nexport class CommandError<T extends Error> extends Error {\n constructor(\n public readonly aggregateRootType: string,\n public readonly commandName: string,\n public readonly streamId: string,\n public readonly originalError: T,\n ) {\n super(\n `Error executing command \"${commandName}\" on aggregate root \"${aggregateRootType}\" for stream id \"${streamId}\": ${originalError.message}`,\n )\n }\n}\n\nexport class ProjectionReplayLoopError extends Error {\n constructor(\n message: string,\n public readonly projection: { name: string; eventHandler: string },\n public readonly event: OutputEvent<any>,\n ) {\n super(message)\n }\n}\n\nexport class RefreshingSkipError extends Error {}\n\nexport class ReplaySkipError extends Error {}\n\nexport class ProjectionEventHandlerExecutionError<\n T extends Error,\n> extends Error {\n constructor(\n public readonly projection: string,\n public readonly eventHandler: string,\n public readonly event: BaseOutputEvent,\n public readonly originalError: T,\n ) {\n super(\n `Error execution event handler \"${eventHandler}\" in projection \"${projection}\": ${originalError.message}`,\n )\n }\n}\n\nexport class ProcessManagerEventHandlerExecutionError<\n T extends Error,\n> extends Error {\n constructor(\n public readonly processManager: string,\n public readonly eventHandler: string,\n public readonly event: BaseOutputEvent,\n public readonly originalError: T,\n ) {\n super(\n `Error execution event handler \"${eventHandler}\" in process manager \"${processManager}\": ${originalError.message}`,\n )\n }\n}\n"
6
- ],
7
- "mappings": ";AAAA;AAAA;AAIO,MAAM,yBAAyB,MAAM;AAAA,EAC1C,WAAW,CACT,UACA,iBACA,eACA;AAAA,IACA,MACE,wBAAwB,SAAS,UAAU,SAAS,kBAAkB,4BAA4B,eACpG;AAAA;AAEJ;AAAA;AAEO,MAAM,wBAAwB,MAAM;AAAA,EAGvB;AAAA,EAFlB,WAAW,CACT,SACgB,UAChB;AAAA,IACA,MAAM,GAAG,YAAY,EAAE,cAAc,QAAQ,GAAG;AAAA,IAFhC;AAAA;AAIpB;AAAA;AAEO,MAAM,6BAA6B,gBAAgB;AAAC;AAAA;AAEpD,MAAM,0BAA0B,MAAM;AAAC;AAAA;AAEvC,MAAM,qBAAsC,MAAM;AAAA,EAErC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAJlB,WAAW,CACO,mBACA,aACA,UACA,eAChB;AAAA,IACA,MACE,4BAA4B,mCAAmC,qCAAqC,cAAc,cAAc,SAClI;AAAA,IAPgB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAMpB;AAAA;AAEO,MAAM,kCAAkC,MAAM;AAAA,EAGjC;AAAA,EACA;AAAA,EAHlB,WAAW,CACT,SACgB,YACA,OAChB;AAAA,IACA,MAAM,OAAO;AAAA,IAHG;AAAA,IACA;AAAA;AAIpB;AAAA;AAEO,MAAM,4BAA4B,MAAM;AAAC;AAAA;AAEzC,MAAM,wBAAwB,MAAM;AAAC;AAAA;AAErC,MAAM,6CAEH,MAAM;AAAA,EAEI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAJlB,WAAW,CACO,YACA,cACA,OACA,eAChB;AAAA,IACA,MACE,kCAAkC,gCAAgC,gBAAgB,cAAc,SAClG;AAAA,IAPgB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAMpB;AAAA;AAEO,MAAM,iDAEH,MAAM;AAAA,EAEI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAJlB,WAAW,CACO,gBACA,cACA,OACA,eAChB;AAAA,IACA,MACE,kCAAkC,qCAAqC,oBAAoB,cAAc,SAC3G;AAAA,IAPgB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAMpB;",
8
- "debugId": "7F0D85CC4F4ACE1E64756E2164756E21",
9
- "names": []
10
- }
@@ -1,10 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/createCheckpointApi.ts"],
4
- "sourcesContent": [
5
- "import z from 'zod/v4'\n\nimport type { LoadEvents } from './createEventStore.ts'\nimport { ValidationError } from './errors.ts'\nimport type {\n BaseOutputEvent,\n MaybePromise,\n StreamEvents,\n} from './utilityTypes.ts'\n\nconst checkpointSchema = z.object({\n lastEventPosition: z.int(),\n type: z.enum(['projection', 'processManager']),\n name: z.string(),\n metadata: z.record(z.string(), z.any()),\n})\n\nexport type Checkpoint = z.output<typeof checkpointSchema>\n\nexport type CheckpointMethods = {\n get: (\n type: 'projection' | 'processManager',\n name: string,\n ) => MaybePromise<Checkpoint | null | undefined>\n delete: (\n type: 'projection' | 'processManager',\n name: string,\n ) => MaybePromise<void>\n upsert: (checkpoint: Checkpoint) => MaybePromise<void>\n}\n\nexport function createCheckpointApi(\n params: CheckpointMethods & {\n loadEvents: LoadEvents\n relatedStreamEvents: StreamEvents[] | null\n name: string\n type: 'processManager' | 'projection'\n apply: (event: BaseOutputEvent) => MaybePromise<void>\n applyAfter?: (event: BaseOutputEvent) => MaybePromise<void>\n },\n) {\n const api = {\n relatedStreamEvents: params.relatedStreamEvents,\n async clear() {\n await params.delete(params.type, params.name)\n },\n async upsert(event: BaseOutputEvent, metadata: Record<string, any> = {}) {\n await params.upsert({\n lastEventPosition: event.position,\n type: params.type,\n name: params.name,\n metadata,\n })\n },\n async rehydrated(\n options: {\n onMetadata?: (metadata: Record<string, any>) => unknown\n getMetadata?: () => Record<string, any>\n onProgress?: (index: number, event: BaseOutputEvent) => void\n } = {},\n ) {\n const checkpoint = await params.get(params.type, params.name)\n let startPosition = 0\n if (checkpoint) {\n const result = checkpointSchema.safeParse(checkpoint)\n if (!result.success) {\n throw new ValidationError(\n `loaded checkpoint for ${params.type} ${params.name} does not comply with schema`,\n result.error,\n )\n }\n startPosition = checkpoint.lastEventPosition + 1\n options.onMetadata?.(checkpoint.metadata)\n }\n\n const events = params.loadEvents(params.relatedStreamEvents, {\n from: startPosition,\n })\n\n let index = 0\n let lastEvent: BaseOutputEvent | null = null\n for await (const event of events) {\n lastEvent = event\n await params.apply(event)\n index++\n options.onProgress?.(index, event)\n }\n\n if (!lastEvent) {\n return // up to date\n }\n\n if (params.applyAfter) {\n for await (const event of events) {\n await params.applyAfter(event)\n }\n }\n\n await api.upsert(lastEvent, options.getMetadata?.())\n },\n }\n\n return api\n}\n\nexport type CheckpointApi = ReturnType<typeof createCheckpointApi>\n"
6
- ],
7
- "mappings": ";;;;;AAAA;AAUA,IAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,mBAAmB,EAAE,IAAI;AAAA,EACzB,MAAM,EAAE,KAAK,CAAC,cAAc,gBAAgB,CAAC;AAAA,EAC7C,MAAM,EAAE,OAAO;AAAA,EACf,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,IAAI,CAAC;AACxC,CAAC;AAgBM,SAAS,mBAAmB,CACjC,QAQA;AAAA,EACA,MAAM,MAAM;AAAA,IACV,qBAAqB,OAAO;AAAA,SACtB,MAAK,GAAG;AAAA,MACZ,MAAM,OAAO,OAAO,OAAO,MAAM,OAAO,IAAI;AAAA;AAAA,SAExC,OAAM,CAAC,OAAwB,WAAgC,CAAC,GAAG;AAAA,MACvE,MAAM,OAAO,OAAO;AAAA,QAClB,mBAAmB,MAAM;AAAA,QACzB,MAAM,OAAO;AAAA,QACb,MAAM,OAAO;AAAA,QACb;AAAA,MACF,CAAC;AAAA;AAAA,SAEG,WAAU,CACd,UAII,CAAC,GACL;AAAA,MACA,MAAM,aAAa,MAAM,OAAO,IAAI,OAAO,MAAM,OAAO,IAAI;AAAA,MAC5D,IAAI,gBAAgB;AAAA,MACpB,IAAI,YAAY;AAAA,QACd,MAAM,SAAS,iBAAiB,UAAU,UAAU;AAAA,QACpD,IAAI,CAAC,OAAO,SAAS;AAAA,UACnB,MAAM,IAAI,gBACR,yBAAyB,OAAO,QAAQ,OAAO,oCAC/C,OAAO,KACT;AAAA,QACF;AAAA,QACA,gBAAgB,WAAW,oBAAoB;AAAA,QAC/C,QAAQ,aAAa,WAAW,QAAQ;AAAA,MAC1C;AAAA,MAEA,MAAM,SAAS,OAAO,WAAW,OAAO,qBAAqB;AAAA,QAC3D,MAAM;AAAA,MACR,CAAC;AAAA,MAED,IAAI,QAAQ;AAAA,MACZ,IAAI,YAAoC;AAAA,MACxC,iBAAiB,SAAS,QAAQ;AAAA,QAChC,YAAY;AAAA,QACZ,MAAM,OAAO,MAAM,KAAK;AAAA,QACxB;AAAA,QACA,QAAQ,aAAa,OAAO,KAAK;AAAA,MACnC;AAAA,MAEA,IAAI,CAAC,WAAW;AAAA,QACd;AAAA,MACF;AAAA,MAEA,IAAI,OAAO,YAAY;AAAA,QACrB,iBAAiB,SAAS,QAAQ;AAAA,UAChC,MAAM,OAAO,WAAW,KAAK;AAAA,QAC/B;AAAA,MACF;AAAA,MAEA,MAAM,IAAI,OAAO,WAAW,QAAQ,cAAc,CAAC;AAAA;AAAA,EAEvD;AAAA,EAEA,OAAO;AAAA;",
8
- "debugId": "EDA59BA1E9DE8B8764756E2164756E21",
9
- "names": []
10
- }