@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.
- package/README.md +1 -1
- package/dist/{chunk-2qy4eb4h.js → chunk-04wqxaqs.js} +3 -3
- package/dist/{chunk-3t1vzbab.js → chunk-263c07sn.js} +3 -2
- package/dist/{chunk-3t1vzbab.js.map → chunk-263c07sn.js.map} +3 -3
- package/dist/{chunk-x90g388y.js → chunk-2fe8jjdc.js} +1 -1
- package/dist/{chunk-qdczt68t.js → chunk-391m4mfp.js} +14 -2
- package/dist/chunk-391m4mfp.js.map +10 -0
- package/dist/{chunk-9psg50cb.js → chunk-6yrb30rx.js} +1 -1
- package/dist/{chunk-pmfzjavb.js → chunk-8qn1nqnh.js} +1 -1
- package/dist/{chunk-crc0araj.js → chunk-r601d5jm.js} +5 -5
- package/dist/{chunk-8x4bedr4.js → chunk-smy8mxvr.js} +35 -21
- package/dist/chunk-smy8mxvr.js.map +10 -0
- package/dist/{chunk-kj36dz26.js → chunk-th14wxh8.js} +4 -4
- package/dist/{chunk-zf3x573k.js → chunk-yn2sjcg3.js} +36 -11
- package/dist/chunk-yn2sjcg3.js.map +10 -0
- package/dist/createAggregateRoot.js +8 -8
- package/dist/createCheckpointApi.d.ts +37 -2
- package/dist/createCheckpointApi.d.ts.map +1 -1
- package/dist/createCheckpointApi.js +2 -2
- package/dist/createEventStore.js +8 -8
- package/dist/createProcessManager.d.ts.map +1 -1
- package/dist/createProcessManager.js +4 -4
- package/dist/createProjection.js +5 -5
- package/dist/drizzle/index.d.ts +3 -1
- package/dist/drizzle/index.d.ts.map +1 -1
- package/dist/drizzle/index.js +27 -19
- package/dist/drizzle/index.js.map +3 -3
- package/dist/drizzle/postgres.d.ts +17 -0
- package/dist/drizzle/postgres.d.ts.map +1 -1
- package/dist/drizzle/postgres.js +1 -1
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/memory/index.d.ts +3 -1
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +20 -13
- package/dist/memory/index.js.map +3 -3
- package/dist/refresh.js +2 -2
- package/dist/replay.js +2 -2
- package/dist/snapshotGenerator.js +2 -2
- package/dist/tests/setup.d.ts +1 -0
- package/dist/tests/setup.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/createCheckpointApi.ts +86 -10
- package/src/createProcessManager.ts +71 -32
- package/src/drizzle/index.ts +23 -4
- package/src/drizzle/postgres.ts +3 -0
- package/src/errors.ts +12 -0
- package/src/memory/index.ts +14 -2
- package/src/tests/processManagerMultiInstance.spec.ts +86 -3
- package/src/tests/setup.ts +12 -2
- package/dist/chunk-8x4bedr4.js.map +0 -10
- package/dist/chunk-qdczt68t.js.map +0 -10
- package/dist/chunk-zf3x573k.js.map +0 -10
- /package/dist/{chunk-2qy4eb4h.js.map → chunk-04wqxaqs.js.map} +0 -0
- /package/dist/{chunk-x90g388y.js.map → chunk-2fe8jjdc.js.map} +0 -0
- /package/dist/{chunk-9psg50cb.js.map → chunk-6yrb30rx.js.map} +0 -0
- /package/dist/{chunk-pmfzjavb.js.map → chunk-8qn1nqnh.js.map} +0 -0
- /package/dist/{chunk-crc0araj.js.map → chunk-r601d5jm.js.map} +0 -0
- /package/dist/{chunk-kj36dz26.js.map → chunk-th14wxh8.js.map} +0 -0
package/dist/memory/index.js.map
CHANGED
|
@@ -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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "44866831ADFEF6FD64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/refresh.js
CHANGED
package/dist/replay.js
CHANGED
package/dist/tests/setup.d.ts
CHANGED
|
@@ -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
|
|
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,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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
214
|
+
if (!(error instanceof Error)) {
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
201
217
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 =
|
|
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)
|
package/src/drizzle/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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)))
|
package/src/drizzle/postgres.ts
CHANGED
|
@@ -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,
|
package/src/memory/index.ts
CHANGED
|
@@ -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) {
|