@requence/event-sourcing 1.1.1 → 1.3.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.
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  createAggregateRoot
3
- } from "./chunk-r601d5jm.js";
4
- import"./chunk-smy8mxvr.js";
3
+ } from "./chunk-wzw95hzk.js";
4
+ import"./chunk-fkzd741s.js";
5
5
  import"./chunk-6q8mr1sg.js";
6
- import"./chunk-6yrb30rx.js";
7
- import"./chunk-tfy01fah.js";
8
- import"./chunk-th14wxh8.js";
9
- import"./chunk-yn2sjcg3.js";
10
- import"./chunk-8qn1nqnh.js";
11
6
  import"./chunk-z388cd25.js";
7
+ import"./chunk-3ejt4h7x.js";
8
+ import"./chunk-8qn1nqnh.js";
9
+ import"./chunk-yn2sjcg3.js";
12
10
  import"./chunk-2fe8jjdc.js";
11
+ import"./chunk-tfy01fah.js";
12
+ import"./chunk-6yrb30rx.js";
13
13
  import"./chunk-391m4mfp.js";
14
14
  export {
15
15
  createAggregateRoot
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  createEventStore
3
- } from "./chunk-04wqxaqs.js";
4
- import"./chunk-gq80tgvf.js";
5
- import"./chunk-smy8mxvr.js";
6
- import"./chunk-6q8mr1sg.js";
7
- import"./chunk-6yrb30rx.js";
8
- import"./chunk-th14wxh8.js";
9
- import"./chunk-yn2sjcg3.js";
10
- import"./chunk-8qn1nqnh.js";
3
+ } from "./chunk-z2yp4rve.js";
4
+ import"./chunk-24xjphpk.js";
11
5
  import"./chunk-ty9bhdvx.js";
6
+ import"./chunk-fkzd741s.js";
7
+ import"./chunk-6q8mr1sg.js";
8
+ import"./chunk-gq80tgvf.js";
12
9
  import"./chunk-z388cd25.js";
13
- import"./chunk-24xjphpk.js";
10
+ import"./chunk-3ejt4h7x.js";
11
+ import"./chunk-8qn1nqnh.js";
12
+ import"./chunk-yn2sjcg3.js";
14
13
  import"./chunk-2fe8jjdc.js";
14
+ import"./chunk-6yrb30rx.js";
15
15
  import"./chunk-391m4mfp.js";
16
16
  export {
17
17
  createEventStore
@@ -2,10 +2,10 @@ import {
2
2
  buildProcessManagerCreator,
3
3
  getProcessManagerInfo,
4
4
  isInsideProcessManager
5
- } from "./chunk-smy8mxvr.js";
5
+ } from "./chunk-fkzd741s.js";
6
6
  import"./chunk-6q8mr1sg.js";
7
- import"./chunk-6yrb30rx.js";
8
7
  import"./chunk-yn2sjcg3.js";
8
+ import"./chunk-6yrb30rx.js";
9
9
  import"./chunk-391m4mfp.js";
10
10
  export {
11
11
  isInsideProcessManager,
@@ -2,9 +2,9 @@ import {
2
2
  buildProjectionCreator,
3
3
  getProjectionInfo,
4
4
  isInsideProjection
5
- } from "./chunk-th14wxh8.js";
6
- import"./chunk-yn2sjcg3.js";
5
+ } from "./chunk-3ejt4h7x.js";
7
6
  import"./chunk-8qn1nqnh.js";
7
+ import"./chunk-yn2sjcg3.js";
8
8
  import"./chunk-2fe8jjdc.js";
9
9
  import"./chunk-391m4mfp.js";
10
10
  export {
@@ -11,25 +11,25 @@ import {
11
11
  } from "../chunk-jd4q8fcf.js";
12
12
  import {
13
13
  createEventStore
14
- } from "../chunk-04wqxaqs.js";
15
- import"../chunk-gq80tgvf.js";
14
+ } from "../chunk-z2yp4rve.js";
15
+ import"../chunk-24xjphpk.js";
16
16
  import {
17
17
  createAggregateRoot
18
- } from "../chunk-r601d5jm.js";
19
- import"../chunk-smy8mxvr.js";
18
+ } from "../chunk-wzw95hzk.js";
19
+ import"../chunk-ty9bhdvx.js";
20
+ import"../chunk-fkzd741s.js";
20
21
  import {
21
22
  extendTypes,
22
23
  superjson
23
24
  } from "../chunk-6q8mr1sg.js";
24
- import"../chunk-6yrb30rx.js";
25
- import"../chunk-tfy01fah.js";
26
- import"../chunk-th14wxh8.js";
27
- import"../chunk-yn2sjcg3.js";
28
- import"../chunk-8qn1nqnh.js";
29
- import"../chunk-ty9bhdvx.js";
25
+ import"../chunk-gq80tgvf.js";
30
26
  import"../chunk-z388cd25.js";
31
- import"../chunk-24xjphpk.js";
27
+ import"../chunk-3ejt4h7x.js";
28
+ import"../chunk-8qn1nqnh.js";
29
+ import"../chunk-yn2sjcg3.js";
32
30
  import"../chunk-2fe8jjdc.js";
31
+ import"../chunk-tfy01fah.js";
32
+ import"../chunk-6yrb30rx.js";
33
33
  import {
34
34
  ConcurrencyError
35
35
  } from "../chunk-391m4mfp.js";
@@ -330,4 +330,4 @@ export {
330
330
  createAggregateRoot
331
331
  };
332
332
 
333
- //# debugId=A994099EE4DAD7B764756E2164756E21
333
+ //# debugId=295E8F0129E030EF64756E2164756E21
@@ -5,6 +5,6 @@
5
5
  "import {\n type SQL,\n and,\n asc,\n desc,\n eq,\n gt,\n inArray,\n lte,\n sql,\n} from 'drizzle-orm'\nimport type { PgDatabase } from 'drizzle-orm/pg-core'\n\nimport type { AnyAggregateRoot } from '../createAggregateRoot.ts'\nimport {\n ConcurrencyError,\n createEventStore as createBaseEventStore,\n isStreamEvents,\n isStreamId,\n} from '../index.ts'\nimport {\n aggregateRootSnapshots,\n checkpoints,\n events,\n projectionAppliedEvents,\n projectionSnapshots,\n} from './postgres.ts'\nimport type {\n EventStore,\n EventStoreParamsWithAggregateRootSnapshots,\n EventsFromRoot,\n OnProgress,\n} from '../createEventStore.ts'\nimport type { LockCreator } from '../lock.ts'\nimport { superjson } from '../superjson.ts'\nimport type {\n BaseOutputEvent,\n MaybePromise,\n OutputEvent,\n} from '../utilityTypes.js'\n\nexport { createAggregateRoot } from '../index.ts'\nexport { extendTypes } from '../superjson.ts'\n\nexport type OnEventsAppendend = (\n events: BaseOutputEvent[],\n) => MaybePromise<void>\n\ninterface DrizzleEventStoreParams<Root extends AnyAggregateRoot> {\n database: PgDatabase<any, any> | (() => PgDatabase<any, any>)\n aggregateRoots: Root[]\n autoInit?: boolean\n lock?: LockCreator\n postProcessEvent?: EventStoreParamsWithAggregateRootSnapshots<Root>['postProcessEvent']\n onProjectionReplay?: OnProgress\n onProcessManagerRefresh?: OnProgress\n onEventsAppended?: OnEventsAppendend\n}\n\nfunction isPgUniqueViolation(\n err: unknown,\n): err is { cause: { code: string; constraint?: string } } {\n if (typeof err !== 'object' || err === null) {\n return false\n }\n\n const cause = (err as any).cause\n if (typeof cause !== 'object' || cause === null) {\n return false\n }\n\n if (cause.code !== '23505') {\n return false\n }\n\n return true\n}\n\nfunction createEventsQuery(database: PgDatabase<any, any>) {\n return database\n .select({\n position: events.position,\n type: events.type,\n schemaVersion: events.schemaVersion,\n payload: events.payload,\n streamId: events.streamId,\n streamType: events.streamType,\n streamVersion: events.streamVersion,\n createdAt: events.createdAt,\n actorId: events.actorId,\n correlationId: events.correlationId,\n causationId: events.causationId,\n metadata: events.metadata,\n })\n .from(events)\n .orderBy(asc(events.position))\n .$dynamic()\n}\n\ntype EventsQuery = ReturnType<typeof createEventsQuery>\n\nexport async function fetchEvents<E extends OutputEvent<any> = BaseOutputEvent>(\n database: PgDatabase<any, any>,\n modifyQuery: (query: EventsQuery, table: typeof events) => EventsQuery = (\n q,\n ) => q,\n) {\n const resolvedEvents = await modifyQuery(createEventsQuery(database), events)\n\n return resolvedEvents.map((resolvedEvent) => ({\n ...resolvedEvent,\n payload: superjson.parse(resolvedEvent.payload),\n metadata: superjson.deserialize(resolvedEvent.metadata),\n })) as E[]\n}\n\nexport function createEventStore<const Root extends AnyAggregateRoot>({\n database,\n aggregateRoots,\n postProcessEvent,\n onProjectionReplay,\n onProcessManagerRefresh,\n onEventsAppended,\n autoInit,\n lock,\n}: DrizzleEventStoreParams<Root>): EventStore<\n EventsFromRoot<Root>,\n Root,\n true\n> {\n const db = () => (typeof database === 'function' ? database() : database)\n\n return createBaseEventStore({\n aggregateRoots,\n postProcessEvent,\n onProjectionReplay,\n onProcessManagerRefresh,\n autoInit,\n lock,\n async *loadEvents(select, range) {\n const conditions = [] as SQL[]\n\n if (select) {\n const selects = Array.isArray(select) ? select : [select]\n const streamTypes = selects.map((select) => select.stream)\n const streamIds = selects\n .map((select) => (isStreamId(select) ? select.id : null))\n .filter(Boolean) as string[]\n const eventTypes = selects.flatMap((select) =>\n isStreamEvents(select) ? select.events : [],\n )\n conditions.push(inArray(events.streamType, streamTypes))\n\n if (streamIds.length > 0) {\n conditions.push(inArray(events.streamId, streamIds))\n }\n\n if (eventTypes.length > 0) {\n conditions.push(inArray(events.type, eventTypes))\n }\n }\n let lastPosition = 0\n\n if (range?.from) {\n lastPosition = range.from - 1\n }\n\n if (range?.to) {\n conditions.push(lte(events.position, range.to))\n }\n\n const chunkSize = 50\n\n while (true) {\n const resolvedEvents = await db()\n .select({\n position: events.position,\n type: events.type,\n schemaVersion: events.schemaVersion,\n payload: events.payload,\n streamId: events.streamId,\n streamType: events.streamType,\n streamVersion: events.streamVersion,\n createdAt: events.createdAt,\n actorId: events.actorId,\n correlationId: events.correlationId,\n causationId: events.causationId,\n metadata: events.metadata,\n })\n .from(events)\n .where(and(...conditions, gt(events.position, lastPosition)))\n .limit(chunkSize)\n .orderBy(asc(events.position))\n\n if (resolvedEvents.length === 0) {\n break\n }\n\n for (const resolvedEvent of resolvedEvents) {\n yield {\n ...resolvedEvent,\n payload: superjson.parse(resolvedEvent.payload),\n metadata: superjson.deserialize(resolvedEvent.metadata),\n }\n lastPosition = resolvedEvent.position\n }\n }\n },\n async appendEvents(streamId, newEvents, expectedVersion) {\n const mappedEvents = newEvents.map((event, index) => ({\n streamId: streamId.id,\n streamType: streamId.stream,\n streamVersion: expectedVersion + 1 + index,\n type: event.type,\n payload: superjson.stringify(event.payload),\n metadata: superjson.serialize(event.metadata),\n causationId: event.causationId,\n actorId: event.actorId,\n correlationId: event.correlationId,\n schemaVersion: event.schemaVersion,\n }))\n\n try {\n const persistedEvents = await db()\n .insert(events)\n .values(mappedEvents)\n .returning()\n\n const resolvedEvents = persistedEvents\n .map((persistendEvent) => ({\n ...persistendEvent,\n payload: superjson.parse(persistendEvent.payload),\n metadata: superjson.deserialize<\n Record<string, unknown> | undefined\n >(persistendEvent.metadata),\n }))\n .toSorted((eventA, eventB) => eventA.position - eventB.position)\n\n onEventsAppended?.(resolvedEvents)\n\n return resolvedEvents\n } catch (error) {\n if (isPgUniqueViolation(error)) {\n const [lastStreamEvent] = await db()\n .select({\n streamVersion: events.streamVersion,\n })\n .from(events)\n .where(\n and(\n eq(events.streamId, streamId.id),\n eq(events.streamType, streamId.stream),\n ),\n )\n .orderBy(desc(events.position))\n .limit(1)\n\n const currentVersion = lastStreamEvent?.streamVersion ?? 0\n throw new ConcurrencyError(streamId, expectedVersion, currentVersion)\n }\n throw error\n }\n },\n checkpoint: {\n async upsert(checkpoint, expectedVersion) {\n const metadata = superjson.stringify(checkpoint.metadata)\n // Compare-and-swap: on an existing row the DO UPDATE only fires when\n // the stored version still matches `expectedVersion`. A `null`\n // expectation means \"insert only\" — if the row already exists the\n // guard fails and no row is returned, signalling a conflict.\n const committed = await db()\n .insert(checkpoints)\n .values({\n lastEventPosition: checkpoint.lastEventPosition,\n name: checkpoint.name,\n type: checkpoint.type,\n metadata,\n version: checkpoint.version,\n })\n .onConflictDoUpdate({\n target: [checkpoints.type, checkpoints.name],\n set: {\n lastEventPosition: checkpoint.lastEventPosition,\n metadata,\n version: checkpoint.version,\n },\n setWhere:\n expectedVersion === null\n ? sql`false`\n : eq(checkpoints.version, expectedVersion),\n })\n .returning({ version: checkpoints.version })\n\n return committed.length > 0\n },\n async get(type, name) {\n const [checkpoint] = await db()\n .select({\n type: checkpoints.type,\n name: checkpoints.name,\n lastEventPosition: checkpoints.lastEventPosition,\n metadata: checkpoints.metadata,\n version: checkpoints.version,\n })\n .from(checkpoints)\n .where(and(eq(checkpoints.type, type), eq(checkpoints.name, name)))\n\n if (!checkpoint) {\n return null\n }\n\n return {\n ...checkpoint,\n metadata: superjson.parse(checkpoint.metadata),\n }\n },\n async delete(type, name) {\n await db()\n .delete(checkpoints)\n .where(and(eq(checkpoints.type, type), eq(checkpoints.name, name)))\n },\n },\n\n projectionSnapshot: {\n async put(snapshot) {\n await db()\n .insert(projectionSnapshots)\n .values({\n lastEventPosition: snapshot.lastEventPosition,\n projectionId: snapshot.projectionId,\n projectionType: snapshot.projectionType,\n schemaVersion: snapshot.schemaVersion,\n state: superjson.stringify(snapshot.state),\n metadata: superjson.stringify(snapshot.metadata),\n })\n },\n async get(select) {\n const [snapshot] = await db()\n .select({\n projectionId: projectionSnapshots.projectionId,\n projectionType: projectionSnapshots.projectionType,\n state: projectionSnapshots.state,\n lastEventPosition: projectionSnapshots.lastEventPosition,\n schemaVersion: projectionSnapshots.schemaVersion,\n createdAt: projectionSnapshots.createdAt,\n metadata: projectionSnapshots.metadata,\n })\n .from(projectionSnapshots)\n .where(\n and(\n eq(projectionSnapshots.projectionId, select.id),\n eq(projectionSnapshots.projectionType, select.projection),\n ),\n )\n .orderBy(desc(projectionSnapshots.lastEventPosition))\n\n if (!snapshot) {\n return null\n }\n\n return {\n ...snapshot,\n state: superjson.parse(snapshot.state),\n metadata: super.parse(snapshot.metadata),\n }\n },\n async delete(target, fromPosition) {\n const conditions = [\n eq(projectionSnapshots.projectionType, target.projection),\n ]\n\n if (target.id) {\n conditions.push(eq(projectionSnapshots.projectionId, target.id))\n }\n\n if (fromPosition) {\n conditions.push(\n gt(projectionSnapshots.lastEventPosition, fromPosition),\n )\n }\n\n await db()\n .delete(projectionSnapshots)\n .where(and(...conditions))\n },\n async incrementAppliedCount(select) {\n const [applied] = await db()\n .insert(projectionAppliedEvents)\n .values({\n projectionId: select.id,\n projectionType: select.projection,\n appliedEvents: 1,\n })\n .onConflictDoUpdate({\n target: [\n projectionAppliedEvents.projectionId,\n projectionAppliedEvents.projectionType,\n ],\n set: {\n appliedEvents: sql`${projectionAppliedEvents.appliedEvents} + 1`,\n },\n })\n .returning({\n appliedEvents: projectionAppliedEvents.appliedEvents,\n })\n\n return applied.appliedEvents\n },\n },\n aggregateRootSnapshot: {\n async put(snapshot) {\n await db()\n .insert(aggregateRootSnapshots)\n .values({\n lastEventPosition: snapshot.lastEventPosition,\n schemaVersion: snapshot.schemaVersion,\n state: superjson.stringify(snapshot.state),\n streamId: snapshot.streamId,\n streamType: snapshot.streamType,\n streamVersion: snapshot.streamVersion,\n metadata: superjson.stringify(snapshot.metadata),\n })\n },\n async get(select) {\n const [snapshot] = await db()\n .select({\n streamId: aggregateRootSnapshots.streamId,\n streamType: aggregateRootSnapshots.streamType,\n streamVersion: aggregateRootSnapshots.streamVersion,\n schemaVersion: aggregateRootSnapshots.schemaVersion,\n state: aggregateRootSnapshots.state,\n createdAt: aggregateRootSnapshots.createdAt,\n lastEventPosition: aggregateRootSnapshots.lastEventPosition,\n metadata: aggregateRootSnapshots.metadata,\n })\n .from(aggregateRootSnapshots)\n .where(\n and(\n eq(aggregateRootSnapshots.streamId, select.id),\n eq(aggregateRootSnapshots.streamType, select.stream),\n ),\n )\n .orderBy(desc(aggregateRootSnapshots.streamVersion))\n\n if (!snapshot) {\n return null\n }\n\n return {\n ...snapshot,\n state: superjson.parse(snapshot.state),\n metadata: superjson.parse(snapshot.metadata),\n }\n },\n async delete(target, fromStreamVersion) {\n await db()\n .delete(aggregateRootSnapshots)\n .where(\n and(\n eq(aggregateRootSnapshots.streamId, target.id),\n eq(aggregateRootSnapshots.streamType, target.stream),\n gt(aggregateRootSnapshots.streamVersion, fromStreamVersion),\n ),\n )\n },\n },\n }) as any\n}\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "A994099EE4DAD7B764756E2164756E21",
8
+ "debugId": "295E8F0129E030EF64756E2164756E21",
9
9
  "names": []
10
10
  }
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export type { Checkpoint } from './createCheckpointApi.ts';
6
6
  export { isInsideProcessManager, getProcessManagerInfo, } from './createProcessManager.ts';
7
7
  export { isInsideEventListener, getEventListenerInfo, } from './createEventListener.ts';
8
8
  export * from './errors.ts';
9
- export { type Snapshot as AggregateRootSnapshot, type AnyAggregateRoot, type AnyStream, createAggregateRoot, } from './createAggregateRoot.ts';
9
+ export { type Snapshot as AggregateRootSnapshot, type AnyAggregateRoot, type AnyStream, type SettledOptions, createAggregateRoot, } from './createAggregateRoot.ts';
10
10
  export { skipRefreshing } from './refresh.ts';
11
11
  export { skipReplay } from './replay.ts';
12
12
  export declare function isStreamId(stream: Stream): stream is StreamId | StreamIdEvents;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,cAAc,EACf,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,YAAY,EAAE,eAAe,IAAI,KAAK,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,EACL,KAAK,QAAQ,IAAI,kBAAkB,EACnC,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,0BAA0B,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,OAAO,EACL,KAAK,QAAQ,IAAI,qBAAqB,EACtC,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,mBAAmB,GACpB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GACb,MAAM,IAAI,QAAQ,GAAG,cAAc,CAErC;AACD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,GACb,MAAM,IAAI,YAAY,GAAG,cAAc,CAEzC;AACD,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,cAAc,EACf,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,YAAY,EAAE,eAAe,IAAI,KAAK,EAAE,MAAM,qBAAqB,CAAA;AACnE,OAAO,EACL,KAAK,QAAQ,IAAI,kBAAkB,EACnC,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,0BAA0B,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,OAAO,EACL,KAAK,QAAQ,IAAI,qBAAqB,EACtC,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,mBAAmB,GACpB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,GACb,MAAM,IAAI,QAAQ,GAAG,cAAc,CAErC;AACD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,GACb,MAAM,IAAI,YAAY,GAAG,cAAc,CAEzC;AACD,YAAY,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA"}
package/dist/index.js CHANGED
@@ -4,35 +4,35 @@ import {
4
4
  } from "./chunk-jd4q8fcf.js";
5
5
  import {
6
6
  createEventStore
7
- } from "./chunk-04wqxaqs.js";
8
- import"./chunk-gq80tgvf.js";
7
+ } from "./chunk-z2yp4rve.js";
8
+ import {
9
+ getEventListenerInfo,
10
+ isInsideEventListener
11
+ } from "./chunk-24xjphpk.js";
9
12
  import {
10
13
  createAggregateRoot
11
- } from "./chunk-r601d5jm.js";
14
+ } from "./chunk-wzw95hzk.js";
15
+ import"./chunk-ty9bhdvx.js";
12
16
  import {
13
17
  getProcessManagerInfo,
14
18
  isInsideProcessManager
15
- } from "./chunk-smy8mxvr.js";
19
+ } from "./chunk-fkzd741s.js";
16
20
  import"./chunk-6q8mr1sg.js";
17
- import {
18
- skipRefreshing
19
- } from "./chunk-6yrb30rx.js";
20
- import"./chunk-tfy01fah.js";
21
+ import"./chunk-gq80tgvf.js";
22
+ import"./chunk-z388cd25.js";
21
23
  import {
22
24
  getProjectionInfo,
23
25
  isInsideProjection
24
- } from "./chunk-th14wxh8.js";
25
- import"./chunk-yn2sjcg3.js";
26
+ } from "./chunk-3ejt4h7x.js";
26
27
  import"./chunk-8qn1nqnh.js";
27
- import"./chunk-ty9bhdvx.js";
28
- import"./chunk-z388cd25.js";
29
- import {
30
- getEventListenerInfo,
31
- isInsideEventListener
32
- } from "./chunk-24xjphpk.js";
28
+ import"./chunk-yn2sjcg3.js";
33
29
  import {
34
30
  skipReplay
35
31
  } from "./chunk-2fe8jjdc.js";
32
+ import"./chunk-tfy01fah.js";
33
+ import {
34
+ skipRefreshing
35
+ } from "./chunk-6yrb30rx.js";
36
36
  import {
37
37
  CheckpointConcurrencyError,
38
38
  CommandError,
@@ -72,4 +72,4 @@ export {
72
72
  CheckpointConcurrencyError
73
73
  };
74
74
 
75
- //# debugId=6C4703510F84795D64756E2164756E21
75
+ //# debugId=C43CA0108CEF7D7664756E2164756E21
package/dist/index.js.map CHANGED
@@ -4,6 +4,6 @@
4
4
  "sourcesContent": [
5
5
  ],
6
6
  "mappings": "",
7
- "debugId": "6C4703510F84795D64756E2164756E21",
7
+ "debugId": "C43CA0108CEF7D7664756E2164756E21",
8
8
  "names": []
9
9
  }
@@ -4,22 +4,22 @@ import {
4
4
  } from "../chunk-jd4q8fcf.js";
5
5
  import {
6
6
  createEventStore
7
- } from "../chunk-04wqxaqs.js";
8
- import"../chunk-gq80tgvf.js";
7
+ } from "../chunk-z2yp4rve.js";
8
+ import"../chunk-24xjphpk.js";
9
9
  import {
10
10
  createAggregateRoot
11
- } from "../chunk-r601d5jm.js";
12
- import"../chunk-smy8mxvr.js";
13
- import"../chunk-6q8mr1sg.js";
14
- import"../chunk-6yrb30rx.js";
15
- import"../chunk-tfy01fah.js";
16
- import"../chunk-th14wxh8.js";
17
- import"../chunk-yn2sjcg3.js";
18
- import"../chunk-8qn1nqnh.js";
11
+ } from "../chunk-wzw95hzk.js";
19
12
  import"../chunk-ty9bhdvx.js";
13
+ import"../chunk-fkzd741s.js";
14
+ import"../chunk-6q8mr1sg.js";
15
+ import"../chunk-gq80tgvf.js";
20
16
  import"../chunk-z388cd25.js";
21
- import"../chunk-24xjphpk.js";
17
+ import"../chunk-3ejt4h7x.js";
18
+ import"../chunk-8qn1nqnh.js";
19
+ import"../chunk-yn2sjcg3.js";
22
20
  import"../chunk-2fe8jjdc.js";
21
+ import"../chunk-tfy01fah.js";
22
+ import"../chunk-6yrb30rx.js";
23
23
  import {
24
24
  ConcurrencyError
25
25
  } from "../chunk-391m4mfp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@requence/event-sourcing",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Event sourcing primitives — aggregate roots, projections, process managers, and event listeners",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -12,6 +12,7 @@ import { getProcessManagerInfo } from './createProcessManager.ts'
12
12
  import { getProjectionInfo } from './createProjection.ts'
13
13
  import {
14
14
  CommandError,
15
+ ConcurrencyError,
15
16
  EventValidationError,
16
17
  InfiniteLoopError,
17
18
  ValidationError,
@@ -83,7 +84,12 @@ type InternalEvent<Event extends EventTemplate> = Prettify<
83
84
  [K in Event as K['version']]: Omit<
84
85
  BaseInternalEvent<K['type'], K['version'], z.output<K['schema']>>,
85
86
  'createdAt' | 'position' | 'streamVersion'
86
- >
87
+ > & {
88
+ // only set when folding persisted events (stream loading/replay);
89
+ // events yielded by a command fold before they are appended and have
90
+ // no position yet
91
+ position?: number
92
+ }
87
93
  }[Event['version']]
88
94
  >
89
95
 
@@ -130,12 +136,25 @@ type VersionBuilder = <V extends number, S extends z.ZodType>(
130
136
  schema: S,
131
137
  ) => { schema: S; version: V }
132
138
 
139
+ /**
140
+ * Options for {@link Stream.settled}.
141
+ *
142
+ * `maxRetries` — how many times to reload the stream and re-apply the dispatched
143
+ * commands when the append loses the optimistic-concurrency race
144
+ * ({@link ConcurrencyError}). Defaults to `0` (no retry — surface the error, the
145
+ * historical behaviour). Only the append is retried; command handlers must be
146
+ * pure functions of loaded state and their arguments for retries to be safe.
147
+ */
148
+ export type SettledOptions = {
149
+ maxRetries?: number
150
+ }
151
+
133
152
  type Stream<Commands extends Record<string, Command>, State> = {
134
153
  [A in keyof Commands]: (
135
154
  ...args: Parameters<Commands[A]>
136
155
  ) => Stream<Commands, State>
137
156
  } & {
138
- settled: () => Promise<Stream<Commands, State>>
157
+ settled: (options?: SettledOptions) => Promise<Stream<Commands, State>>
139
158
  state: () => Promise<State>
140
159
  transformError: <T extends Error>(
141
160
  error: Constructor<T>,
@@ -145,7 +164,7 @@ type Stream<Commands extends Record<string, Command>, State> = {
145
164
  }
146
165
 
147
166
  export type AnyStream = {
148
- settled: () => Promise<AnyStream>
167
+ settled: (options?: SettledOptions) => Promise<AnyStream>
149
168
  state: () => Promise<unknown>
150
169
  transformError: <T extends Error>(
151
170
  error: Constructor<T>,
@@ -470,7 +489,29 @@ export function createAggregateRoot<Name extends string>(type: Name) {
470
489
  return version
471
490
  }
472
491
 
473
- const createStream = (state: any, streamId: string) => {
492
+ // A command as dispatched by the caller, recorded so it can be re-applied
493
+ // against a freshly-loaded stream when a settle() retries a ConcurrencyError.
494
+ type RecordedCommand = { name: string; args: any[] }
495
+
496
+ type CreateStreamConfig = {
497
+ // Whether to load the existing stream before commands run. `false` for
498
+ // newStream (expects version 0); `true` for loadStream and every retry.
499
+ load?: boolean
500
+ filter?: (event: BaseOutputEvent) => MaybePromise<boolean>
501
+ // Seeded on a retry so replayed commands re-apply automatically, and
502
+ // transformError handlers registered before settle() carry over.
503
+ replayCommands?: RecordedCommand[]
504
+ errorHandlerEntries?: [
505
+ Constructor<Error>,
506
+ (error: CommandError<Error>) => Error,
507
+ ][]
508
+ }
509
+
510
+ const createStream = (
511
+ state: any,
512
+ streamId: string,
513
+ config: CreateStreamConfig = {},
514
+ ) => {
474
515
  let version = 0
475
516
  const snapshotApi = createSnapshotApi?.(
476
517
  state,
@@ -480,7 +521,8 @@ export function createAggregateRoot<Name extends string>(type: Name) {
480
521
  const errorHandlers = new Map<
481
522
  Constructor<Error>,
482
523
  (error: CommandError<Error>) => Error
483
- >()
524
+ >(config.errorHandlerEntries)
525
+ const dispatchedCommands: RecordedCommand[] = []
484
526
  let downstreamSettledPromise = () => Promise.resolve()
485
527
  let releaseLock!: () => Promise<void>
486
528
  let extendLock!: EventHandlerOptions['extendLock']
@@ -558,6 +600,9 @@ export function createAggregateRoot<Name extends string>(type: Name) {
558
600
  return api.commands
559
601
  }
560
602
 
603
+ // Record for a potential settle() retry (reload + re-apply).
604
+ dispatchedCommands.push({ name: commandName, args })
605
+
561
606
  const commandId = crypto.randomUUID()
562
607
  transactionDelay ??= transaction.delay()
563
608
  commandChainTouched = true
@@ -742,7 +787,7 @@ export function createAggregateRoot<Name extends string>(type: Name) {
742
787
 
743
788
  return state
744
789
  },
745
- async settled() {
790
+ async settled(options?: SettledOptions): Promise<any> {
746
791
  const parentProcessManager = getProcessManagerInfo()
747
792
  if (parentProcessManager) {
748
793
  const error = new Error(
@@ -760,7 +805,36 @@ export function createAggregateRoot<Name extends string>(type: Name) {
760
805
  throw error
761
806
  }
762
807
 
763
- await awaitAll(() => commandChain)
808
+ const maxRetries = options?.maxRetries ?? 0
809
+
810
+ try {
811
+ await awaitAll(() => commandChain)
812
+ } catch (error) {
813
+ // Optimistic-concurrency recovery: another writer advanced the
814
+ // stream between load and append. Reload and re-apply the same
815
+ // commands against the fresh version. A failed append leaves this
816
+ // attempt's lock held (it never reached releaseLock), so release it
817
+ // before the retry re-acquires — otherwise the retry blocks until
818
+ // the lock's TTL lapses.
819
+ if (maxRetries > 0 && error instanceof ConcurrencyError) {
820
+ await releaseLock?.()
821
+
822
+ const retryStream = createStream(clone(initialState), streamId, {
823
+ load: true,
824
+ filter: config.filter,
825
+ replayCommands: dispatchedCommands,
826
+ errorHandlerEntries: Array.from(errorHandlers),
827
+ })
828
+
829
+ return retryStream.commands.settled({
830
+ ...options,
831
+ maxRetries: maxRetries - 1,
832
+ })
833
+ }
834
+
835
+ throw error
836
+ }
837
+
764
838
  await downstreamSettledPromise()
765
839
  return api.commands
766
840
  },
@@ -777,6 +851,18 @@ export function createAggregateRoot<Name extends string>(type: Name) {
777
851
 
778
852
  registerRootPromiseToUpstream(() => api.commands.settled())
779
853
  transaction.after(() => api.commands.settled())
854
+
855
+ // Fold existing events before commands run (loadStream, and every retry).
856
+ if (config.load) {
857
+ api.load(config.filter)
858
+ }
859
+
860
+ // Re-apply the caller's commands on a retry so the fresh, reloaded stream
861
+ // reproduces the original intent against the current version.
862
+ for (const { name, args } of config.replayCommands ?? []) {
863
+ ;(api.commands as any)[name](...args)
864
+ }
865
+
780
866
  return api
781
867
  }
782
868
 
@@ -856,8 +942,9 @@ export function createAggregateRoot<Name extends string>(type: Name) {
856
942
  `aggregateRoot.newStream() cannot be called inside a projection. Attempted on aggregate stream ${type} within Projection ${parentProjectionName}.`,
857
943
  )
858
944
  }
859
- return createStream(clone(initialState), id ?? crypto.randomUUID())
860
- .commands as any
945
+ return createStream(clone(initialState), id ?? crypto.randomUUID(), {
946
+ load: false,
947
+ }).commands as any
861
948
  },
862
949
 
863
950
  loadStream(id, filter) {
@@ -873,9 +960,10 @@ export function createAggregateRoot<Name extends string>(type: Name) {
873
960
  `aggregateRoot.loadStream("${id}") cannot be called inside a projection. Attempted on aggregate stream ${type} within Projection ${parentProjectionName}.`,
874
961
  )
875
962
  }
876
- const root = createStream(clone(initialState), id)
877
- root.load(filter as any)
878
- return root.commands as any
963
+ return createStream(clone(initialState), id, {
964
+ load: true,
965
+ filter: filter as any,
966
+ }).commands as any
879
967
  },
880
968
 
881
969
  linkEventStore(load, append, emit, trans, lock, snap) {
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export {
26
26
  type Snapshot as AggregateRootSnapshot,
27
27
  type AnyAggregateRoot,
28
28
  type AnyStream,
29
+ type SettledOptions,
29
30
  createAggregateRoot,
30
31
  } from './createAggregateRoot.ts'
31
32
 
@@ -336,6 +336,53 @@ describe('AggregateRoot', () => {
336
336
  expect(pseudoEventStore.length).toBe(3)
337
337
  })
338
338
 
339
+ it('passes the persisted position to event handlers when loading a stream', async () => {
340
+ const foldedPositions: (number | undefined)[] = []
341
+
342
+ const aggregateRoot = createAggregateRoot('test')
343
+ .withInitialState({
344
+ lastDeletePosition: undefined as number | undefined,
345
+ })
346
+ .withEvents(({ z }) => ({
347
+ Created: z.null(),
348
+ Deleted: z.null(),
349
+ }))
350
+ .withEventHandlers((state) => ({
351
+ onCreated(event) {
352
+ foldedPositions.push(event.position)
353
+ },
354
+ onDeleted(event) {
355
+ foldedPositions.push(event.position)
356
+ state.lastDeletePosition = event.position
357
+ },
358
+ }))
359
+ .withCommands((_state, event) => ({
360
+ create() {
361
+ return event('Created', null)
362
+ },
363
+ remove() {
364
+ return event('Deleted', null)
365
+ },
366
+ }))
367
+
368
+ const { pseudoEventStore } = setupEventStore(aggregateRoot)
369
+
370
+ const root = await aggregateRoot.newStream().create().remove().settled()
371
+ // events yielded by commands fold before they are appended — no position yet
372
+ expect(foldedPositions).toEqual([undefined, undefined])
373
+ expect((await root.state()).lastDeletePosition).toBeUndefined()
374
+
375
+ foldedPositions.length = 0
376
+ const { streamId } = pseudoEventStore[0]
377
+ const state = await aggregateRoot.loadStream(streamId).state()
378
+ // persisted events carry their global log position when folded
379
+ expect(foldedPositions).toEqual([
380
+ pseudoEventStore[0].position,
381
+ pseudoEventStore[1].position,
382
+ ])
383
+ expect(state.lastDeletePosition).toBe(pseudoEventStore[1].position)
384
+ })
385
+
339
386
  it('resolves state before side effects', async () => {
340
387
  const aggregateRoot = createAggregateRoot('test')
341
388
  .withInitialState({
@@ -493,6 +540,48 @@ describe('hotAggregateRoot', () => {
493
540
  expect(name).toBe('B') // 'A' never made it
494
541
  })
495
542
 
543
+ it('retries the losing write on ConcurrencyError when maxRetries is set', async () => {
544
+ const slowAggregateRoot = createAggregateRoot('slow')
545
+ .withEvents(({ z }) => ({
546
+ Created: z.string(),
547
+ }))
548
+ .withCommands((event) => ({
549
+ async create(delay: number, name: string) {
550
+ await setTimeout(delay)
551
+ return event('Created', name)
552
+ },
553
+ }))
554
+
555
+ const { eventStore, pseudoEventStore } = setupEventStore(slowAggregateRoot, {
556
+ lock: lock(40), // 40ms
557
+ })
558
+
559
+ const called: string[] = []
560
+ eventStore.createEventListener('test').withEventHandlers({
561
+ onCreated(event) {
562
+ called.push(event.payload)
563
+ },
564
+ })
565
+
566
+ // Same race as the test above: A locks stream:a, the lock auto-releases
567
+ // after 40ms while A is still in its 100ms command, so B slips in and
568
+ // advances the stream to v1. A then loses the append race — but with
569
+ // maxRetries it reloads (now seeing B's event) and re-appends at v2 instead
570
+ // of surfacing the ConcurrencyError.
571
+ await Promise.all([
572
+ slowAggregateRoot
573
+ .loadStream('a')
574
+ .create(100, 'A')
575
+ .settled({ maxRetries: 3 }),
576
+ slowAggregateRoot.loadStream('a').create(5, 'B').settled(),
577
+ ])
578
+
579
+ const persisted = pseudoEventStore.filter((e) => e.streamType === 'slow')
580
+ expect(persisted.map((e) => e.payload)).toEqual(['B', 'A'])
581
+ expect(persisted.map((e) => e.streamVersion)).toEqual([1, 2])
582
+ expect(called).toEqual(['B', 'A'])
583
+ })
584
+
496
585
  it('auto extends locks', async () => {
497
586
  const aggregateRoot = createAggregateRoot('slow')
498
587
  .withEvents(({ z }) => ({