@requence/event-sourcing 1.0.4 → 1.1.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 (63) 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 +3 -1
  25. package/dist/drizzle/index.d.ts.map +1 -1
  26. package/dist/drizzle/index.js +27 -19
  27. package/dist/drizzle/index.js.map +3 -3
  28. package/dist/drizzle/postgres.d.ts +17 -0
  29. package/dist/drizzle/postgres.d.ts.map +1 -1
  30. package/dist/drizzle/postgres.js +1 -1
  31. package/dist/errors.d.ts +6 -0
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +5 -3
  34. package/dist/errors.js.map +1 -1
  35. package/dist/index.js +13 -11
  36. package/dist/index.js.map +1 -1
  37. package/dist/memory/index.d.ts +3 -1
  38. package/dist/memory/index.d.ts.map +1 -1
  39. package/dist/memory/index.js +20 -13
  40. package/dist/memory/index.js.map +3 -3
  41. package/dist/refresh.js +2 -2
  42. package/dist/replay.js +2 -2
  43. package/dist/snapshotGenerator.js +2 -2
  44. package/dist/tests/setup.d.ts +1 -0
  45. package/dist/tests/setup.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/src/createCheckpointApi.ts +86 -10
  48. package/src/createProcessManager.ts +71 -32
  49. package/src/drizzle/index.ts +23 -4
  50. package/src/drizzle/postgres.ts +3 -0
  51. package/src/errors.ts +12 -0
  52. package/src/memory/index.ts +14 -2
  53. package/src/tests/processManagerMultiInstance.spec.ts +86 -3
  54. package/src/tests/setup.ts +12 -2
  55. package/dist/chunk-8x4bedr4.js.map +0 -10
  56. package/dist/chunk-qdczt68t.js.map +0 -10
  57. package/dist/chunk-zf3x573k.js.map +0 -10
  58. /package/dist/{chunk-2qy4eb4h.js.map → chunk-04wqxaqs.js.map} +0 -0
  59. /package/dist/{chunk-x90g388y.js.map → chunk-2fe8jjdc.js.map} +0 -0
  60. /package/dist/{chunk-9psg50cb.js.map → chunk-6yrb30rx.js.map} +0 -0
  61. /package/dist/{chunk-pmfzjavb.js.map → chunk-8qn1nqnh.js.map} +0 -0
  62. /package/dist/{chunk-crc0araj.js.map → chunk-r601d5jm.js.map} +0 -0
  63. /package/dist/{chunk-kj36dz26.js.map → chunk-th14wxh8.js.map} +0 -0
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/memory/index.ts"],
4
4
  "sourcesContent": [
5
- "import type { AnyAggregateRoot } from '../createAggregateRoot.ts'\nimport type { Checkpoint } from '../createCheckpointApi.ts'\nimport {\n type AggregateRootSnapshot,\n ConcurrencyError,\n type Event,\n type ProjectionSnapshot,\n createEventStore as createBaseEventStore,\n isStreamEvents,\n isStreamId,\n} from '../index.ts'\nimport type {\n EventStore,\n EventStoreParamsWithAggregateRootSnapshots,\n EventsFromRoot,\n OnProgress,\n} from '../createEventStore.ts'\nimport type { BaseOutputEvent, MaybePromise } from '../utilityTypes.js'\n\nexport { createAggregateRoot } from '../index.ts'\n\nexport type OnEventsAppended = (\n events: BaseOutputEvent[],\n) => MaybePromise<void>\n\ninterface MemoryEventStoreParams<Root extends AnyAggregateRoot> {\n aggregateRoots: Root[]\n autoInit?: boolean\n postProcessEvent?: EventStoreParamsWithAggregateRootSnapshots<Root>['postProcessEvent']\n onProjectionReplay?: OnProgress\n onProcessManagerRefresh?: OnProgress\n onEventsAppended?: OnEventsAppended\n}\n\nexport function createEventStore<const Root extends AnyAggregateRoot>({\n aggregateRoots,\n postProcessEvent,\n onProjectionReplay,\n onProcessManagerRefresh,\n onEventsAppended,\n autoInit,\n}: MemoryEventStoreParams<Root>): EventStore<\n EventsFromRoot<Root>,\n Root,\n true\n> {\n const events: Event[] = []\n const checkpoints = new Set<Checkpoint>()\n const aggregateRootSnapshots = new Map<string, AggregateRootSnapshot>()\n const projectionSnapshots = new Set<ProjectionSnapshot>()\n const projectionAppliedCount = new Map<string, number>()\n\n return createBaseEventStore({\n aggregateRoots,\n postProcessEvent,\n onProjectionReplay,\n onProcessManagerRefresh,\n autoInit,\n\n async *loadEvents(select, range) {\n let streamTypes: string[] | null = null\n let streamIds: string[] | null = null\n let eventTypes: string[] | null = null\n\n if (select) {\n const selects = Array.isArray(select) ? select : [select]\n streamTypes = selects.map((select) => select.stream)\n streamIds = selects\n .map((select) => (isStreamId(select) ? select.id : null))\n .filter(Boolean) as string[]\n eventTypes = selects.flatMap((select) =>\n isStreamEvents(select) ? select.events : [],\n )\n }\n\n const filteredEvents = events.filter((event) => {\n if (streamTypes?.length && !streamTypes.includes(event.streamType)) {\n return false\n }\n\n if (streamIds?.length && !streamIds.includes(event.streamId)) {\n return false\n }\n\n if (eventTypes?.length && !eventTypes.includes(event.type)) {\n return false\n }\n\n if (range?.from && event.position < range.from) {\n return false\n }\n\n if (range?.to && event.position > range.to) {\n return false\n }\n\n return true\n })\n\n for (const event of filteredEvents) {\n yield event\n }\n },\n\n appendEvents({ id, stream }, newEvents, expectedVersion) {\n const lastEvent = events.findLast(\n (e) => e.streamId === id && e.streamType === stream,\n )\n\n const currentVersion = lastEvent?.streamVersion ?? 0\n\n if (currentVersion !== expectedVersion) {\n throw new ConcurrencyError(\n { id, stream },\n expectedVersion,\n currentVersion,\n )\n }\n\n const extendedEvents: Event[] = newEvents.map((event, index) => ({\n ...event,\n createdAt: new Date(),\n position: events.length + index,\n streamId: id,\n streamType: stream,\n streamVersion: expectedVersion + index + 1,\n }))\n\n events.push(...extendedEvents)\n onEventsAppended?.(extendedEvents)\n return extendedEvents\n },\n\n checkpoint: {\n get(type, name) {\n for (const checkpoint of checkpoints) {\n if (checkpoint.type === type && checkpoint.name === name) {\n return checkpoint\n }\n }\n },\n upsert(checkpoint) {\n let persisted: Checkpoint | undefined\n for (const c of checkpoints) {\n if (c.type === checkpoint.type && c.name === checkpoint.name) {\n persisted = c\n break\n }\n }\n\n if (persisted) {\n persisted.lastEventPosition = checkpoint.lastEventPosition\n persisted.metadata = checkpoint.metadata\n } else {\n checkpoints.add(checkpoint)\n }\n },\n delete(type, name) {\n for (const checkpoint of checkpoints) {\n if (checkpoint.type === type && checkpoint.name === name) {\n checkpoints.delete(checkpoint)\n }\n }\n },\n },\n\n projectionSnapshot: {\n put(snapshot) {\n projectionSnapshots.add(snapshot)\n },\n get(select) {\n return Array.from(projectionSnapshots)\n .toReversed()\n .find(\n (snapshot) =>\n snapshot.projectionId === select.id &&\n snapshot.projectionType === select.projection,\n )\n },\n delete(target, fromPosition) {\n for (const snapshot of projectionSnapshots) {\n if (target.id && snapshot.projectionId !== target.id) {\n continue\n }\n\n if (snapshot.projectionType !== target.projection) {\n continue\n }\n\n if (fromPosition && snapshot.lastEventPosition < fromPosition) {\n continue\n }\n\n projectionSnapshots.delete(snapshot)\n }\n },\n incrementAppliedCount(select) {\n const key = `${select.projection}-${select.id}`\n const nextCount = (projectionAppliedCount.get(key) ?? 0) + 1\n projectionAppliedCount.set(key, nextCount)\n return nextCount\n },\n },\n\n aggregateRootSnapshot: {\n put(snapshot) {\n aggregateRootSnapshots.set(\n `${snapshot.streamType}:${snapshot.streamId}`,\n snapshot,\n )\n },\n get(select) {\n return aggregateRootSnapshots.get(`${select.stream}:${select.id}`)\n },\n delete(target, fromStreamVersion) {\n const key = `${target.stream}:${target.id}`\n const snapshot = aggregateRootSnapshots.get(key)\n if (snapshot && snapshot.streamVersion > fromStreamVersion) {\n aggregateRootSnapshots.delete(key)\n }\n },\n },\n }) as any\n}\n"
5
+ "import type { AnyAggregateRoot } from '../createAggregateRoot.ts'\nimport type { Checkpoint } from '../createCheckpointApi.ts'\nimport {\n type AggregateRootSnapshot,\n ConcurrencyError,\n type Event,\n type ProjectionSnapshot,\n createEventStore as createBaseEventStore,\n isStreamEvents,\n isStreamId,\n} from '../index.ts'\nimport type {\n EventStore,\n EventStoreParamsWithAggregateRootSnapshots,\n EventsFromRoot,\n OnProgress,\n} from '../createEventStore.ts'\nimport type { LockCreator } from '../lock.ts'\nimport type { BaseOutputEvent, MaybePromise } from '../utilityTypes.js'\n\nexport { createAggregateRoot } from '../index.ts'\n\nexport type OnEventsAppended = (\n events: BaseOutputEvent[],\n) => MaybePromise<void>\n\ninterface MemoryEventStoreParams<Root extends AnyAggregateRoot> {\n aggregateRoots: Root[]\n autoInit?: boolean\n lock?: LockCreator\n postProcessEvent?: EventStoreParamsWithAggregateRootSnapshots<Root>['postProcessEvent']\n onProjectionReplay?: OnProgress\n onProcessManagerRefresh?: OnProgress\n onEventsAppended?: OnEventsAppended\n}\n\nexport function createEventStore<const Root extends AnyAggregateRoot>({\n aggregateRoots,\n postProcessEvent,\n onProjectionReplay,\n onProcessManagerRefresh,\n onEventsAppended,\n autoInit,\n lock,\n}: MemoryEventStoreParams<Root>): EventStore<\n EventsFromRoot<Root>,\n Root,\n true\n> {\n const events: Event[] = []\n const checkpoints = new Set<Checkpoint>()\n const aggregateRootSnapshots = new Map<string, AggregateRootSnapshot>()\n const projectionSnapshots = new Set<ProjectionSnapshot>()\n const projectionAppliedCount = new Map<string, number>()\n\n return createBaseEventStore({\n aggregateRoots,\n postProcessEvent,\n onProjectionReplay,\n onProcessManagerRefresh,\n autoInit,\n lock,\n\n async *loadEvents(select, range) {\n let streamTypes: string[] | null = null\n let streamIds: string[] | null = null\n let eventTypes: string[] | null = null\n\n if (select) {\n const selects = Array.isArray(select) ? select : [select]\n streamTypes = selects.map((select) => select.stream)\n streamIds = selects\n .map((select) => (isStreamId(select) ? select.id : null))\n .filter(Boolean) as string[]\n eventTypes = selects.flatMap((select) =>\n isStreamEvents(select) ? select.events : [],\n )\n }\n\n const filteredEvents = events.filter((event) => {\n if (streamTypes?.length && !streamTypes.includes(event.streamType)) {\n return false\n }\n\n if (streamIds?.length && !streamIds.includes(event.streamId)) {\n return false\n }\n\n if (eventTypes?.length && !eventTypes.includes(event.type)) {\n return false\n }\n\n if (range?.from && event.position < range.from) {\n return false\n }\n\n if (range?.to && event.position > range.to) {\n return false\n }\n\n return true\n })\n\n for (const event of filteredEvents) {\n yield event\n }\n },\n\n appendEvents({ id, stream }, newEvents, expectedVersion) {\n const lastEvent = events.findLast(\n (e) => e.streamId === id && e.streamType === stream,\n )\n\n const currentVersion = lastEvent?.streamVersion ?? 0\n\n if (currentVersion !== expectedVersion) {\n throw new ConcurrencyError(\n { id, stream },\n expectedVersion,\n currentVersion,\n )\n }\n\n const extendedEvents: Event[] = newEvents.map((event, index) => ({\n ...event,\n createdAt: new Date(),\n position: events.length + index,\n streamId: id,\n streamType: stream,\n streamVersion: expectedVersion + index + 1,\n }))\n\n events.push(...extendedEvents)\n onEventsAppended?.(extendedEvents)\n return extendedEvents\n },\n\n checkpoint: {\n get(type, name) {\n for (const checkpoint of checkpoints) {\n if (checkpoint.type === type && checkpoint.name === name) {\n return checkpoint\n }\n }\n },\n upsert(checkpoint, expectedVersion) {\n let persisted: Checkpoint | undefined\n for (const c of checkpoints) {\n if (c.type === checkpoint.type && c.name === checkpoint.name) {\n persisted = c\n break\n }\n }\n\n if (persisted) {\n // Compare-and-swap: reject the write when another writer advanced the\n // checkpoint, or when the caller expected to create a fresh one.\n if (expectedVersion === null || persisted.version !== expectedVersion) {\n return false\n }\n persisted.lastEventPosition = checkpoint.lastEventPosition\n persisted.metadata = checkpoint.metadata\n persisted.version = checkpoint.version\n } else {\n checkpoints.add({ ...checkpoint })\n }\n\n return true\n },\n delete(type, name) {\n for (const checkpoint of checkpoints) {\n if (checkpoint.type === type && checkpoint.name === name) {\n checkpoints.delete(checkpoint)\n }\n }\n },\n },\n\n projectionSnapshot: {\n put(snapshot) {\n projectionSnapshots.add(snapshot)\n },\n get(select) {\n return Array.from(projectionSnapshots)\n .toReversed()\n .find(\n (snapshot) =>\n snapshot.projectionId === select.id &&\n snapshot.projectionType === select.projection,\n )\n },\n delete(target, fromPosition) {\n for (const snapshot of projectionSnapshots) {\n if (target.id && snapshot.projectionId !== target.id) {\n continue\n }\n\n if (snapshot.projectionType !== target.projection) {\n continue\n }\n\n if (fromPosition && snapshot.lastEventPosition < fromPosition) {\n continue\n }\n\n projectionSnapshots.delete(snapshot)\n }\n },\n incrementAppliedCount(select) {\n const key = `${select.projection}-${select.id}`\n const nextCount = (projectionAppliedCount.get(key) ?? 0) + 1\n projectionAppliedCount.set(key, nextCount)\n return nextCount\n },\n },\n\n aggregateRootSnapshot: {\n put(snapshot) {\n aggregateRootSnapshots.set(\n `${snapshot.streamType}:${snapshot.streamId}`,\n snapshot,\n )\n },\n get(select) {\n return aggregateRootSnapshots.get(`${select.stream}:${select.id}`)\n },\n delete(target, fromStreamVersion) {\n const key = `${target.stream}:${target.id}`\n const snapshot = aggregateRootSnapshots.get(key)\n if (snapshot && snapshot.streamVersion > fromStreamVersion) {\n aggregateRootSnapshots.delete(key)\n }\n },\n },\n }) as any\n}\n"
6
6
  ],
7
- "mappings": "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",
8
- "debugId": "C07C59508B45971C64756E2164756E21",
7
+ "mappings": "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",
8
+ "debugId": "44866831ADFEF6FD64756E2164756E21",
9
9
  "names": []
10
10
  }
package/dist/refresh.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  isRefreshing,
3
3
  skipRefreshing,
4
4
  withRefreshing
5
- } from "./chunk-9psg50cb.js";
6
- import"./chunk-qdczt68t.js";
5
+ } from "./chunk-6yrb30rx.js";
6
+ import"./chunk-391m4mfp.js";
7
7
  export {
8
8
  withRefreshing,
9
9
  skipRefreshing,
package/dist/replay.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  isReplaying,
3
3
  skipReplay,
4
4
  withReplaying
5
- } from "./chunk-x90g388y.js";
6
- import"./chunk-qdczt68t.js";
5
+ } from "./chunk-2fe8jjdc.js";
6
+ import"./chunk-391m4mfp.js";
7
7
  export {
8
8
  withReplaying,
9
9
  skipReplay,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createSnapshotGenerator
3
- } from "./chunk-pmfzjavb.js";
4
- import"./chunk-qdczt68t.js";
3
+ } from "./chunk-8qn1nqnh.js";
4
+ import"./chunk-391m4mfp.js";
5
5
  export {
6
6
  createSnapshotGenerator as default
7
7
  };
@@ -45,6 +45,7 @@ export declare function setupEventStore<const Root extends AnyAggregateRoot>(agg
45
45
  type: "projection" | "processManager";
46
46
  name: string;
47
47
  metadata: Record<string, any>;
48
+ version: number;
48
49
  }>;
49
50
  pseudoEventStore: Event[];
50
51
  pseudoAggregateRootSnapshotStore: Map<string, {
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/tests/setup.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AAE1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAEjE,OAAO,EAGL,KAAK,KAAK,EAMX,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,wBAAgB,mBAAmB;;;;;;;;;;;;;;;;;iBAehB,MAAM;gBAGP,MAAM;;;;;;;;;;;;;IAIvB;AAED,wBAAgB,eAAe,CAAC,KAAK,CAAC,IAAI,SAAS,gBAAgB,EACjE,aAAa,EAAE,IAAI,GAAG,IAAI,EAAE,EAC5B,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyL9E"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/tests/setup.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAA;AAE1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAEjE,OAAO,EAGL,KAAK,KAAK,EAMX,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,wBAAgB,mBAAmB;;;;;;;;;;;;;;;;;iBAehB,MAAM;gBAGP,MAAM;;;;;;;;;;;;;IAIvB;AAED,wBAAgB,eAAe,CAAC,KAAK,CAAC,IAAI,SAAS,gBAAgB,EACjE,aAAa,EAAE,IAAI,GAAG,IAAI,EAAE,EAC5B,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmM9E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@requence/event-sourcing",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Event sourcing primitives — aggregate roots, projections, process managers, and event listeners",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -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)
@@ -31,6 +31,7 @@ import type {
31
31
  EventsFromRoot,
32
32
  OnProgress,
33
33
  } from '../createEventStore.ts'
34
+ import type { LockCreator } from '../lock.ts'
34
35
  import { superjson } from '../superjson.ts'
35
36
  import type {
36
37
  BaseOutputEvent,
@@ -49,6 +50,7 @@ interface DrizzleEventStoreParams<Root extends AnyAggregateRoot> {
49
50
  database: PgDatabase<any, any> | (() => PgDatabase<any, any>)
50
51
  aggregateRoots: Root[]
51
52
  autoInit?: boolean
53
+ lock?: LockCreator
52
54
  postProcessEvent?: EventStoreParamsWithAggregateRootSnapshots<Root>['postProcessEvent']
53
55
  onProjectionReplay?: OnProgress
54
56
  onProcessManagerRefresh?: OnProgress
@@ -120,6 +122,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
120
122
  onProcessManagerRefresh,
121
123
  onEventsAppended,
122
124
  autoInit,
125
+ lock,
123
126
  }: DrizzleEventStoreParams<Root>): EventStore<
124
127
  EventsFromRoot<Root>,
125
128
  Root,
@@ -133,6 +136,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
133
136
  onProjectionReplay,
134
137
  onProcessManagerRefresh,
135
138
  autoInit,
139
+ lock,
136
140
  async *loadEvents(select, range) {
137
141
  const conditions = [] as SQL[]
138
142
 
@@ -258,22 +262,36 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
258
262
  }
259
263
  },
260
264
  checkpoint: {
261
- async upsert(checkpoint) {
262
- await db()
265
+ async upsert(checkpoint, expectedVersion) {
266
+ const metadata = superjson.stringify(checkpoint.metadata)
267
+ // Compare-and-swap: on an existing row the DO UPDATE only fires when
268
+ // the stored version still matches `expectedVersion`. A `null`
269
+ // expectation means "insert only" — if the row already exists the
270
+ // guard fails and no row is returned, signalling a conflict.
271
+ const committed = await db()
263
272
  .insert(checkpoints)
264
273
  .values({
265
274
  lastEventPosition: checkpoint.lastEventPosition,
266
275
  name: checkpoint.name,
267
276
  type: checkpoint.type,
268
- metadata: superjson.stringify(checkpoint.metadata),
277
+ metadata,
278
+ version: checkpoint.version,
269
279
  })
270
280
  .onConflictDoUpdate({
271
281
  target: [checkpoints.type, checkpoints.name],
272
282
  set: {
273
283
  lastEventPosition: checkpoint.lastEventPosition,
274
- metadata: superjson.stringify(checkpoint.metadata),
284
+ metadata,
285
+ version: checkpoint.version,
275
286
  },
287
+ setWhere:
288
+ expectedVersion === null
289
+ ? sql`false`
290
+ : eq(checkpoints.version, expectedVersion),
276
291
  })
292
+ .returning({ version: checkpoints.version })
293
+
294
+ return committed.length > 0
277
295
  },
278
296
  async get(type, name) {
279
297
  const [checkpoint] = await db()
@@ -282,6 +300,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
282
300
  name: checkpoints.name,
283
301
  lastEventPosition: checkpoints.lastEventPosition,
284
302
  metadata: checkpoints.metadata,
303
+ version: checkpoints.version,
285
304
  })
286
305
  .from(checkpoints)
287
306
  .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,
@@ -15,6 +15,7 @@ import type {
15
15
  EventsFromRoot,
16
16
  OnProgress,
17
17
  } from '../createEventStore.ts'
18
+ import type { LockCreator } from '../lock.ts'
18
19
  import type { BaseOutputEvent, MaybePromise } from '../utilityTypes.js'
19
20
 
20
21
  export { createAggregateRoot } from '../index.ts'
@@ -26,6 +27,7 @@ export type OnEventsAppended = (
26
27
  interface MemoryEventStoreParams<Root extends AnyAggregateRoot> {
27
28
  aggregateRoots: Root[]
28
29
  autoInit?: boolean
30
+ lock?: LockCreator
29
31
  postProcessEvent?: EventStoreParamsWithAggregateRootSnapshots<Root>['postProcessEvent']
30
32
  onProjectionReplay?: OnProgress
31
33
  onProcessManagerRefresh?: OnProgress
@@ -39,6 +41,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
39
41
  onProcessManagerRefresh,
40
42
  onEventsAppended,
41
43
  autoInit,
44
+ lock,
42
45
  }: MemoryEventStoreParams<Root>): EventStore<
43
46
  EventsFromRoot<Root>,
44
47
  Root,
@@ -56,6 +59,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
56
59
  onProjectionReplay,
57
60
  onProcessManagerRefresh,
58
61
  autoInit,
62
+ lock,
59
63
 
60
64
  async *loadEvents(select, range) {
61
65
  let streamTypes: string[] | null = null
@@ -139,7 +143,7 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
139
143
  }
140
144
  }
141
145
  },
142
- upsert(checkpoint) {
146
+ upsert(checkpoint, expectedVersion) {
143
147
  let persisted: Checkpoint | undefined
144
148
  for (const c of checkpoints) {
145
149
  if (c.type === checkpoint.type && c.name === checkpoint.name) {
@@ -149,11 +153,19 @@ export function createEventStore<const Root extends AnyAggregateRoot>({
149
153
  }
150
154
 
151
155
  if (persisted) {
156
+ // Compare-and-swap: reject the write when another writer advanced the
157
+ // checkpoint, or when the caller expected to create a fresh one.
158
+ if (expectedVersion === null || persisted.version !== expectedVersion) {
159
+ return false
160
+ }
152
161
  persisted.lastEventPosition = checkpoint.lastEventPosition
153
162
  persisted.metadata = checkpoint.metadata
163
+ persisted.version = checkpoint.version
154
164
  } else {
155
- checkpoints.add(checkpoint)
165
+ checkpoints.add({ ...checkpoint })
156
166
  }
167
+
168
+ return true
157
169
  },
158
170
  delete(type, name) {
159
171
  for (const checkpoint of checkpoints) {