@kronos-ts/eventsourcing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/append-condition.d.ts +20 -0
  2. package/dist/append-condition.d.ts.map +1 -0
  3. package/dist/append-condition.js +7 -0
  4. package/dist/append-condition.js.map +1 -0
  5. package/dist/append.d.ts +33 -0
  6. package/dist/append.d.ts.map +1 -0
  7. package/dist/append.js +65 -0
  8. package/dist/append.js.map +1 -0
  9. package/dist/consistency-marker.d.ts +28 -0
  10. package/dist/consistency-marker.d.ts.map +1 -0
  11. package/dist/consistency-marker.js +28 -0
  12. package/dist/consistency-marker.js.map +1 -0
  13. package/dist/event-sourced-repository.d.ts +23 -0
  14. package/dist/event-sourced-repository.d.ts.map +1 -0
  15. package/dist/event-sourced-repository.js +105 -0
  16. package/dist/event-sourced-repository.js.map +1 -0
  17. package/dist/event-storage-engine.d.ts +60 -0
  18. package/dist/event-storage-engine.d.ts.map +1 -0
  19. package/dist/event-storage-engine.js +2 -0
  20. package/dist/event-storage-engine.js.map +1 -0
  21. package/dist/event-store-transaction.d.ts +31 -0
  22. package/dist/event-store-transaction.d.ts.map +1 -0
  23. package/dist/event-store-transaction.js +28 -0
  24. package/dist/event-store-transaction.js.map +1 -0
  25. package/dist/event-store.d.ts +26 -0
  26. package/dist/event-store.d.ts.map +1 -0
  27. package/dist/event-store.js +2 -0
  28. package/dist/event-store.js.map +1 -0
  29. package/dist/in-memory-event-store.d.ts +14 -0
  30. package/dist/in-memory-event-store.d.ts.map +1 -0
  31. package/dist/in-memory-event-store.js +225 -0
  32. package/dist/in-memory-event-store.js.map +1 -0
  33. package/dist/index.d.ts +16 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +16 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/intercepting-event-store.d.ts +11 -0
  38. package/dist/intercepting-event-store.d.ts.map +1 -0
  39. package/dist/intercepting-event-store.js +47 -0
  40. package/dist/intercepting-event-store.js.map +1 -0
  41. package/dist/load.d.ts +43 -0
  42. package/dist/load.d.ts.map +1 -0
  43. package/dist/load.js +36 -0
  44. package/dist/load.js.map +1 -0
  45. package/dist/snapshot-policy.d.ts +45 -0
  46. package/dist/snapshot-policy.d.ts.map +1 -0
  47. package/dist/snapshot-policy.js +34 -0
  48. package/dist/snapshot-policy.js.map +1 -0
  49. package/dist/snapshot-store.d.ts +42 -0
  50. package/dist/snapshot-store.d.ts.map +1 -0
  51. package/dist/snapshot-store.js +23 -0
  52. package/dist/snapshot-store.js.map +1 -0
  53. package/dist/sourcing-condition.d.ts +14 -0
  54. package/dist/sourcing-condition.d.ts.map +1 -0
  55. package/dist/sourcing-condition.js +7 -0
  56. package/dist/sourcing-condition.js.map +1 -0
  57. package/dist/tag-resolver.d.ts +30 -0
  58. package/dist/tag-resolver.d.ts.map +1 -0
  59. package/dist/tag-resolver.js +46 -0
  60. package/dist/tag-resolver.js.map +1 -0
  61. package/package.json +58 -0
  62. package/src/append-condition.ts +23 -0
  63. package/src/append.ts +99 -0
  64. package/src/consistency-marker.ts +43 -0
  65. package/src/event-sourced-repository.ts +141 -0
  66. package/src/event-storage-engine.ts +69 -0
  67. package/src/event-store-transaction.ts +58 -0
  68. package/src/event-store.ts +26 -0
  69. package/src/in-memory-event-store.ts +268 -0
  70. package/src/index.ts +73 -0
  71. package/src/intercepting-event-store.ts +70 -0
  72. package/src/load.ts +70 -0
  73. package/src/snapshot-policy.ts +73 -0
  74. package/src/snapshot-store.ts +67 -0
  75. package/src/sourcing-condition.ts +17 -0
  76. package/src/tag-resolver.ts +62 -0
@@ -0,0 +1,30 @@
1
+ import type { Tag } from "@kronos-ts/common";
2
+ import type { EventMessage } from "@kronos-ts/messaging";
3
+ /**
4
+ * Resolves tags from an event message. Tags are metadata markers attached
5
+ * to events for filtering, categorization, and criteria-based sourcing.
6
+ *
7
+ * By default, tags are derived from the event descriptor's `tags` function
8
+ * at event creation time. The TagResolver runs before storage and can enrich
9
+ * events with additional tags from metadata, context, etc.
10
+ */
11
+ export interface TagResolver {
12
+ resolve(event: EventMessage): Tag[];
13
+ }
14
+ /**
15
+ * Default tag resolver — passes through tags already on the event.
16
+ *
17
+ * Events are created with descriptor-derived tags. This resolver simply
18
+ * returns those existing tags unchanged.
19
+ */
20
+ export declare function descriptorBasedTagResolver(): TagResolver;
21
+ /**
22
+ * Resolves additional tags from event metadata. For each configured key,
23
+ * if the metadata contains that key, a tag is created.
24
+ */
25
+ export declare function metadataBasedTagResolver(...metadataKeys: string[]): TagResolver;
26
+ /**
27
+ * Combines multiple tag resolvers. Tags from all resolvers are merged.
28
+ */
29
+ export declare function multiTagResolver(...resolvers: TagResolver[]): TagResolver;
30
+ //# sourceMappingURL=tag-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-resolver.d.ts","sourceRoot":"","sources":["../src/tag-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAExD;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,GAAG,EAAE,CAAA;CACpC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,IAAI,WAAW,CAMxD;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,YAAY,EAAE,MAAM,EAAE,GAAG,WAAW,CAa/E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,SAAS,EAAE,WAAW,EAAE,GAAG,WAAW,CAUzE"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Default tag resolver — passes through tags already on the event.
3
+ *
4
+ * Events are created with descriptor-derived tags. This resolver simply
5
+ * returns those existing tags unchanged.
6
+ */
7
+ export function descriptorBasedTagResolver() {
8
+ return {
9
+ resolve(event) {
10
+ return [...event.tags];
11
+ },
12
+ };
13
+ }
14
+ /**
15
+ * Resolves additional tags from event metadata. For each configured key,
16
+ * if the metadata contains that key, a tag is created.
17
+ */
18
+ export function metadataBasedTagResolver(...metadataKeys) {
19
+ return {
20
+ resolve(event) {
21
+ const tags = [];
22
+ for (const key of metadataKeys) {
23
+ const value = event.metadata[key];
24
+ if (value != null) {
25
+ tags.push({ key, value: String(value) });
26
+ }
27
+ }
28
+ return tags;
29
+ },
30
+ };
31
+ }
32
+ /**
33
+ * Combines multiple tag resolvers. Tags from all resolvers are merged.
34
+ */
35
+ export function multiTagResolver(...resolvers) {
36
+ return {
37
+ resolve(event) {
38
+ const tags = [];
39
+ for (const resolver of resolvers) {
40
+ tags.push(...resolver.resolve(event));
41
+ }
42
+ return tags;
43
+ },
44
+ };
45
+ }
46
+ //# sourceMappingURL=tag-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-resolver.js","sourceRoot":"","sources":["../src/tag-resolver.ts"],"names":[],"mappings":"AAeA;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO;QACL,OAAO,CAAC,KAAmB;YACzB,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAG,YAAsB;IAChE,OAAO;QACL,OAAO,CAAC,KAAmB;YACzB,MAAM,IAAI,GAAU,EAAE,CAAA;YACtB,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;gBAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;gBACjC,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;oBAClB,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;gBAC1C,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;KACF,CAAA;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAG,SAAwB;IAC1D,OAAO;QACL,OAAO,CAAC,KAAmB;YACzB,MAAM,IAAI,GAAU,EAAE,CAAA;YACtB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;gBACjC,IAAI,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA;YACvC,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@kronos-ts/eventsourcing",
3
+ "version": "0.1.0",
4
+ "description": "Event sourcing for Kronos — dynamic-consistency-boundary event store with load/append.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "author": "Theo Emanuelsson",
8
+ "homepage": "https://github.com/KronosDB/kronos-ts/tree/main/packages/eventsourcing#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/KronosDB/kronos-ts.git",
12
+ "directory": "packages/eventsourcing"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/KronosDB/kronos-ts/issues"
16
+ },
17
+ "keywords": [
18
+ "kronos",
19
+ "event-sourcing",
20
+ "cqrs",
21
+ "dcb",
22
+ "typescript"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "src/index.ts",
26
+ "types": "src/index.ts",
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "!src/**/__tests__",
31
+ "!src/**/*.test.ts",
32
+ "!src/**/*.bench.ts"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "clean": "rm -rf dist *.tsbuildinfo"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "main": "./dist/index.js",
41
+ "types": "./dist/index.d.ts",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "default": "./dist/index.js"
46
+ }
47
+ }
48
+ },
49
+ "dependencies": {
50
+ "@kronos-ts/common": "workspace:*",
51
+ "@kronos-ts/messaging": "workspace:*",
52
+ "@kronos-ts/modelling": "workspace:*",
53
+ "zod": "^4.3.6"
54
+ },
55
+ "devDependencies": {
56
+ "@kronos-ts/app": "workspace:*"
57
+ }
58
+ }
@@ -0,0 +1,23 @@
1
+ import type { EventCriteria } from "@kronos-ts/messaging"
2
+ import type { ConsistencyMarker } from "./consistency-marker.js"
3
+
4
+ /**
5
+ * Defines the consistency boundary for appending events.
6
+ *
7
+ * By default, the append condition matches the sourcing condition —
8
+ * guaranteeing that no conflicting events were appended since the state was loaded.
9
+ *
10
+ * Can be overridden per command handler for cases where less strict consistency
11
+ * is valid (e.g. a bank debit that doesn't conflict with credits).
12
+ */
13
+ export interface AppendCondition {
14
+ readonly criteria: EventCriteria
15
+ readonly marker: ConsistencyMarker
16
+ }
17
+
18
+ /**
19
+ * Create an append condition from criteria and a consistency marker.
20
+ */
21
+ export function appendCondition(criteria: EventCriteria, marker: ConsistencyMarker): AppendCondition {
22
+ return { criteria, marker }
23
+ }
package/src/append.ts ADDED
@@ -0,0 +1,99 @@
1
+ import {
2
+ resourceKey,
3
+ qualifiedNameToString,
4
+ generateIdentifier,
5
+ type Metadata,
6
+ type ResourceKey,
7
+ } from "@kronos-ts/common"
8
+ import {
9
+ getResource,
10
+ computeIfAbsent,
11
+ requireInvocationPhase,
12
+ } from "@kronos-ts/messaging/processing-state"
13
+ import type { z } from "zod"
14
+ import type { EventDescriptor, EventMessage, EventCriteria } from "@kronos-ts/messaging"
15
+
16
+ /** Append events to the active unit of work, buffered until commit. */
17
+ export interface AppendFunction {
18
+ <P extends z.ZodType>(event: EventDescriptor<P>, payload: z.infer<P>): void
19
+ <P extends z.ZodType>(event: EventDescriptor<P>, payload: z.infer<P>, metadata: Metadata): void
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Resource keys (owned by append — open-question #1 resolved: keys live with
24
+ // the helper that writes them)
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** Buffered events waiting to be flushed at PREPARE_COMMIT. */
28
+ export const BUFFERED_EVENTS_KEY: ResourceKey<EventMessage[]> = resourceKey("bufferedEvents")
29
+
30
+ /** Sourcing info from load() calls, used to build append condition. */
31
+ export const SOURCING_INFOS_KEY: ResourceKey<Array<{ criteria: EventCriteria; markerPosition: bigint }>> =
32
+ resourceKey("sourcingInfos")
33
+
34
+ /** State cache: prevents duplicate load() calls within same UnitOfWork. */
35
+ export const STATE_CACHE_KEY: ResourceKey<Map<string, Promise<unknown>>> = resourceKey("stateCache")
36
+
37
+ /** State module references keyed by cache key, used to apply evolvers on append. */
38
+ export const STATE_MODULES_KEY: ResourceKey<Map<string, { module: any; id: unknown }>> =
39
+ resourceKey("stateModules")
40
+
41
+ /**
42
+ * Plan 04-01 (HDL-02 / D-42): module-level append.
43
+ *
44
+ * Throws NoActiveUnitOfWork outside a UoW (D-43 fail-fast on no-UoW).
45
+ * Throws WrongUoWPhase outside INVOCATION phase (D-43 mutator guard).
46
+ *
47
+ * Buffers events in BUFFERED_EVENTS_KEY; updates cached state via
48
+ * matching evolvers (same logic as command-handling-module.ts appendFn).
49
+ */
50
+ export const append: AppendFunction = ((
51
+ eventDescriptor: EventDescriptor<any>,
52
+ eventPayload: unknown,
53
+ eventMetadata?: Metadata,
54
+ ) => {
55
+ const state = requireInvocationPhase() // D-43 mutator guard
56
+ const events = computeIfAbsent(BUFFERED_EVENTS_KEY, () => [])
57
+ const tags = eventDescriptor.tags ? eventDescriptor.tags(eventPayload) : []
58
+ const eventMessage: EventMessage = {
59
+ identifier: generateIdentifier(),
60
+ name: eventDescriptor.name,
61
+ version: eventDescriptor.version,
62
+ payload: eventPayload,
63
+ metadata: eventMetadata ?? state.metadata, // fallback to UoW metadata when caller omits
64
+ timestamp: Date.now(),
65
+ tags,
66
+ }
67
+ events.push(eventMessage)
68
+
69
+ // Update cached state by applying matching evolvers.
70
+ // Verbatim copy of command-handling-module.ts:101-123 logic — moved here so
71
+ // both command-handling-module (via delegation) and direct module-level callers
72
+ // get identical behaviour.
73
+ const cache = getResource(STATE_CACHE_KEY)
74
+ const modules = getResource(STATE_MODULES_KEY)
75
+ if (cache && modules) {
76
+ const eventType = qualifiedNameToString(eventDescriptor.name)
77
+ for (const [cacheKey, { module, id }] of modules) {
78
+ const cachedPromise = cache.get(cacheKey)
79
+ if (!cachedPromise) continue
80
+ const evolvers = (module as any).evolvers as ReadonlyArray<{
81
+ descriptor: { name: any }
82
+ evolve: (s: any, e: any, id: any) => any
83
+ }> | undefined
84
+ if (!evolvers) continue
85
+ for (const evolver of evolvers) {
86
+ if (qualifiedNameToString(evolver.descriptor.name) === eventType) {
87
+ cache.set(
88
+ cacheKey,
89
+ cachedPromise.then((result: any) => ({
90
+ ...result,
91
+ state: evolver.evolve(result.state, eventPayload, id),
92
+ })),
93
+ )
94
+ break
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }) as AppendFunction
@@ -0,0 +1,43 @@
1
+ import { resourceKey, type ResourceKey } from "@kronos-ts/common"
2
+
3
+ /**
4
+ * An opaque marker representing a position in the event store.
5
+ * Used by append conditions to guarantee consistency —
6
+ * "no conflicting events were appended since this marker."
7
+ */
8
+ export interface ConsistencyMarker {
9
+ readonly position: bigint
10
+ }
11
+
12
+ /** Marker representing the origin (before any events). */
13
+ export const ORIGIN: ConsistencyMarker = { position: -1n }
14
+
15
+ /** Marker representing infinity (no consistency check needed). */
16
+ export const INFINITY: ConsistencyMarker = { position: BigInt(Number.MAX_SAFE_INTEGER) }
17
+
18
+ /** Resource key for storing a ConsistencyMarker in a ProcessingContext. */
19
+ export const MARKER_RESOURCE_KEY: ResourceKey<ConsistencyMarker> = resourceKey("consistencyMarker")
20
+
21
+ export function noMarker(): ConsistencyMarker {
22
+ return ORIGIN
23
+ }
24
+
25
+ export function markerAt(position: bigint): ConsistencyMarker {
26
+ return { position }
27
+ }
28
+
29
+ /**
30
+ * Returns the lower bound of two markers — the most restrictive position.
31
+ * When checking consistency across multiple sourcing operations,
32
+ * use the lower bound to ensure no events are missed.
33
+ */
34
+ export function markerLowerBound(a: ConsistencyMarker, b: ConsistencyMarker): ConsistencyMarker {
35
+ return a.position < b.position ? a : b
36
+ }
37
+
38
+ /**
39
+ * Returns the upper bound of two markers — the least restrictive position.
40
+ */
41
+ export function markerUpperBound(a: ConsistencyMarker, b: ConsistencyMarker): ConsistencyMarker {
42
+ return a.position > b.position ? a : b
43
+ }
@@ -0,0 +1,141 @@
1
+ import { qualifiedNameToString } from "@kronos-ts/common"
2
+ import type { EventMessage } from "@kronos-ts/messaging"
3
+ import type { StateModule, StateRepository, LoadResult } from "@kronos-ts/modelling"
4
+ import type { EventStore } from "./event-store.js"
5
+ import { sourcingCondition } from "./sourcing-condition.js"
6
+ import type { SnapshotStore, Snapshot } from "./snapshot-store.js"
7
+ import type { SnapshotPolicy, EvolutionResult } from "./snapshot-policy.js"
8
+
9
+ export interface EventSourcedRepositoryOptions<Id, S> {
10
+ state: StateModule<Id, S>
11
+ eventStore: EventStore
12
+ snapshotStore?: SnapshotStore
13
+ snapshotPolicy?: SnapshotPolicy
14
+ }
15
+
16
+ /**
17
+ * Creates a repository for a state module sourced from events.
18
+ *
19
+ * When `load(id)` is called, the repository:
20
+ * 1. Checks the snapshot store for a cached state (if configured)
21
+ * 2. Resolves the sourcing criteria from the state module + id
22
+ * 3. Sources matching events from the event store (from snapshot position if available)
23
+ * 4. Starts from snapshot state (or `create()`) and folds events through matching evolvers
24
+ * 5. Optionally creates a new snapshot if the policy triggers
25
+ * 6. Returns the state AND sourcing info (criteria + marker)
26
+ */
27
+ export function createEventSourcedRepository<Id, S>(
28
+ module: StateModule<Id, S>,
29
+ eventStore: EventStore,
30
+ snapshotStore?: SnapshotStore,
31
+ snapshotPolicy?: SnapshotPolicy,
32
+ ): StateRepository<Id, S> {
33
+ async function doLoad(id: Id): Promise<LoadResult<S>> {
34
+ const startTime = performance.now()
35
+ const criteria = module.criteria(id)
36
+
37
+ // Try to load snapshot
38
+ let state = module.create(id)
39
+ let startPosition: bigint | undefined
40
+ let snapshot: Snapshot | undefined
41
+
42
+ if (snapshotStore) {
43
+ try {
44
+ snapshot = await snapshotStore.load(module.name, id)
45
+ if (snapshot) {
46
+ state = snapshot.payload as S
47
+ // Source events AFTER the snapshot position
48
+ startPosition = snapshot.position + 1n
49
+ }
50
+ } catch (err) {
51
+ console.warn(`Failed to load snapshot for ${module.name}:${String(id)}, falling back to full replay:`, err)
52
+ // Fall back to full replay from the beginning
53
+ }
54
+ }
55
+
56
+ const condition = sourcingCondition(criteria, startPosition)
57
+ const { events, marker } = await eventStore.source(condition)
58
+
59
+ const lifecycle = module.lifecycle
60
+ let isFirstEvent = !snapshot // first event only if no snapshot
61
+ let wasDeleted = lifecycle?.isDeleted?.(state) ?? false
62
+
63
+ let eventsApplied = 0
64
+ for (const event of events) {
65
+ const previousState = state
66
+ state = await applyEvent(module, state, event, id)
67
+ eventsApplied++
68
+
69
+ // Lifecycle hooks
70
+ if (lifecycle && state !== previousState) {
71
+ // onCreate: first event transitions from initial state
72
+ if (isFirstEvent && eventsApplied === 1) {
73
+ await lifecycle.onCreate?.(state, id)
74
+ }
75
+
76
+ // onStateChange: after each evolving event
77
+ await lifecycle.onStateChange?.(previousState, state, event, id)
78
+
79
+ // onDelete: when isDeleted transitions from false to true
80
+ if (lifecycle.isDeleted) {
81
+ const nowDeleted = lifecycle.isDeleted(state)
82
+ if (nowDeleted && !wasDeleted) {
83
+ await lifecycle.onDelete?.(state, id)
84
+ }
85
+ wasDeleted = nowDeleted
86
+ }
87
+ }
88
+ }
89
+
90
+ const sourcingTimeMs = performance.now() - startTime
91
+
92
+ // Check if we should create a new snapshot (fire-and-forget)
93
+ if (snapshotStore && snapshotPolicy && eventsApplied > 0) {
94
+ const result: EvolutionResult = { eventsApplied, sourcingTimeMs }
95
+ if (snapshotPolicy.shouldSnapshot(result)) {
96
+ snapshotStore
97
+ .store(module.name, id, {
98
+ position: marker.position,
99
+ payload: state,
100
+ timestamp: Date.now(),
101
+ metadata: {},
102
+ })
103
+ .catch((err) => {
104
+ console.warn(`Failed to store snapshot for ${module.name}:${String(id)}:`, err)
105
+ })
106
+ }
107
+ }
108
+
109
+ return {
110
+ state,
111
+ sourcingInfo: {
112
+ criteria,
113
+ markerPosition: marker.position,
114
+ },
115
+ }
116
+ }
117
+
118
+ return {
119
+ stateName: module.name,
120
+ load: doLoad,
121
+ loadOrCreate: doLoad, // Same implementation — create() always provides initial state
122
+ }
123
+ }
124
+
125
+ async function applyEvent<Id, S>(
126
+ module: StateModule<Id, S>,
127
+ state: S,
128
+ event: EventMessage,
129
+ id: Id,
130
+ ): Promise<S> {
131
+ const eventType = qualifiedNameToString(event.name)
132
+
133
+ for (const evolver of module.evolvers) {
134
+ const evolverType = qualifiedNameToString(evolver.descriptor.name)
135
+ if (evolverType === eventType) {
136
+ return await evolver.evolve(state, event.payload, id)
137
+ }
138
+ }
139
+
140
+ return state
141
+ }
@@ -0,0 +1,69 @@
1
+ import type { EventMessage, StreamableEventSource } from "@kronos-ts/messaging"
2
+ import type { SourcingCondition } from "./sourcing-condition.js"
3
+ import type { SourcingResult } from "./event-store.js"
4
+ import type { AppendCondition } from "./append-condition.js"
5
+ import type { ConsistencyMarker } from "./consistency-marker.js"
6
+
7
+ /**
8
+ * A transactional handle for an append operation.
9
+ *
10
+ * The two-phase pattern allows the processing lifecycle to control when
11
+ * events become visible:
12
+ *
13
+ * 1. {@link appendEvents} stages events and returns an AppendTransaction
14
+ * 2. {@link commit} makes events visible to consumers
15
+ * 3. {@link afterCommit} returns the consistency marker
16
+ * 4. {@link rollback} discards staged events on failure
17
+ */
18
+ export interface AppendTransaction {
19
+ /** Make appended events visible to consumers. */
20
+ commit(): Promise<void>
21
+ /** Get the consistency marker after a successful commit. */
22
+ afterCommit(): Promise<ConsistencyMarker>
23
+ /** Discard staged events. */
24
+ rollback(): void
25
+ }
26
+
27
+ /**
28
+ * Infrastructure-level abstraction for event persistence.
29
+ *
30
+ * This is the raw storage mechanism — append, source, and stream events.
31
+ * Database extensions (drizzle, knex, prisma, etc.) implement this interface
32
+ * to provide persistent event storage.
33
+ *
34
+ * Not intended for direct use by application code. The {@link EventStore}
35
+ * composes an EventStorageEngine with event distribution (EventSink) and
36
+ * tag resolution (TagResolver).
37
+ */
38
+ export interface EventStorageEngine extends StreamableEventSource {
39
+ /**
40
+ * Source events matching the given condition (criteria-based, for state sourcing).
41
+ * Returns the matching events and a consistency marker.
42
+ */
43
+ source(condition: SourcingCondition): Promise<SourcingResult>
44
+
45
+ /**
46
+ * Append events to the store.
47
+ * If an append condition is provided, the engine verifies that no conflicting
48
+ * events were written since the marker before appending.
49
+ *
50
+ * Returns an {@link AppendTransaction} for two-phase commit control.
51
+ * For simple cases, use the convenience form that auto-commits:
52
+ * ```typescript
53
+ * const marker = await store.append(events, condition)
54
+ * ```
55
+ */
56
+ appendEvents(
57
+ events: ReadonlyArray<EventMessage>,
58
+ condition?: AppendCondition,
59
+ ): Promise<AppendTransaction>
60
+
61
+ /**
62
+ * Convenience method that appends events and auto-commits in one step.
63
+ * Equivalent to calling appendEvents() followed by commit() and afterCommit().
64
+ */
65
+ append(
66
+ events: ReadonlyArray<EventMessage>,
67
+ condition?: AppendCondition,
68
+ ): Promise<ConsistencyMarker>
69
+ }
@@ -0,0 +1,58 @@
1
+ import type { EventMessage } from "@kronos-ts/messaging"
2
+
3
+ /**
4
+ * A transaction scope for event store operations within a UnitOfWork.
5
+ *
6
+ * Events are buffered during the transaction and only persisted when
7
+ * the UnitOfWork commits. The `onAppend` hook enables state cache
8
+ * updates as events are buffered (before persistence).
9
+ */
10
+ export interface EventStoreTransaction {
11
+ /**
12
+ * Buffer an event for append. The event is not yet persisted —
13
+ * it will be written to the store at PREPARE_COMMIT.
14
+ */
15
+ appendEvent(event: EventMessage): void
16
+
17
+ /**
18
+ * Register a callback invoked each time an event is buffered via `appendEvent`.
19
+ * Used by the state cache to apply events to cached state immediately,
20
+ * keeping the cache consistent within the same UnitOfWork.
21
+ */
22
+ onAppend(callback: (event: EventMessage) => void): void
23
+
24
+ /**
25
+ * Get all buffered events (not yet committed).
26
+ */
27
+ readonly bufferedEvents: ReadonlyArray<EventMessage>
28
+ }
29
+
30
+ /**
31
+ * Creates an EventStoreTransaction that buffers events and notifies
32
+ * registered callbacks.
33
+ */
34
+ export function createEventStoreTransaction(): EventStoreTransaction {
35
+ const events: EventMessage[] = []
36
+ const appendCallbacks: Array<(event: EventMessage) => void> = []
37
+
38
+ return {
39
+ appendEvent(event: EventMessage): void {
40
+ events.push(event)
41
+ for (const callback of appendCallbacks) {
42
+ try {
43
+ callback(event)
44
+ } catch (e) {
45
+ console.warn("EventStoreTransaction: onAppend callback threw an exception:", e)
46
+ }
47
+ }
48
+ },
49
+
50
+ onAppend(callback: (event: EventMessage) => void): void {
51
+ appendCallbacks.push(callback)
52
+ },
53
+
54
+ get bufferedEvents(): ReadonlyArray<EventMessage> {
55
+ return events
56
+ },
57
+ }
58
+ }
@@ -0,0 +1,26 @@
1
+ import type { EventMessage, EventBus } from "@kronos-ts/messaging"
2
+ import type { EventStorageEngine } from "./event-storage-engine.js"
3
+ import type { ConsistencyMarker } from "./consistency-marker.js"
4
+
5
+ /**
6
+ * Result of sourcing events — the events plus a consistency marker
7
+ * representing the position up to which events were read.
8
+ */
9
+ export interface SourcingResult {
10
+ readonly events: ReadonlyArray<EventMessage>
11
+ readonly marker: ConsistencyMarker
12
+ }
13
+
14
+ /**
15
+ * The event store — dual-role component that combines event storage
16
+ * with event distribution.
17
+ *
18
+ * Extends:
19
+ * - `EventStorageEngine` — raw storage (source, append, stream)
20
+ * - `EventBus` — event publication + push-based subscription
21
+ *
22
+ * In an event sourcing context, the EventStore persists events durably while
23
+ * simultaneously distributing them to subscribed event handlers, eliminating
24
+ * the need for a separate EventBus component.
25
+ */
26
+ export interface EventStore extends EventStorageEngine, EventBus {}