@kronos-ts/messaging 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 (266) hide show
  1. package/dist/command-bus.d.ts +30 -0
  2. package/dist/command-bus.d.ts.map +1 -0
  3. package/dist/command-bus.js +2 -0
  4. package/dist/command-bus.js.map +1 -0
  5. package/dist/command-handler.d.ts +58 -0
  6. package/dist/command-handler.d.ts.map +1 -0
  7. package/dist/command-handler.js +12 -0
  8. package/dist/command-handler.js.map +1 -0
  9. package/dist/command-handling-module.d.ts +53 -0
  10. package/dist/command-handling-module.d.ts.map +1 -0
  11. package/dist/command-handling-module.js +130 -0
  12. package/dist/command-handling-module.js.map +1 -0
  13. package/dist/correlation-data.d.ts +79 -0
  14. package/dist/correlation-data.d.ts.map +1 -0
  15. package/dist/correlation-data.js +133 -0
  16. package/dist/correlation-data.js.map +1 -0
  17. package/dist/dead-letter-queue.d.ts +134 -0
  18. package/dist/dead-letter-queue.d.ts.map +1 -0
  19. package/dist/dead-letter-queue.js +176 -0
  20. package/dist/dead-letter-queue.js.map +1 -0
  21. package/dist/dead-lettering-handler.d.ts +42 -0
  22. package/dist/dead-lettering-handler.d.ts.map +1 -0
  23. package/dist/dead-lettering-handler.js +67 -0
  24. package/dist/dead-lettering-handler.js.map +1 -0
  25. package/dist/descriptor.d.ts +135 -0
  26. package/dist/descriptor.d.ts.map +1 -0
  27. package/dist/descriptor.js +36 -0
  28. package/dist/descriptor.js.map +1 -0
  29. package/dist/emit-update.d.ts +22 -0
  30. package/dist/emit-update.d.ts.map +1 -0
  31. package/dist/emit-update.js +23 -0
  32. package/dist/emit-update.js.map +1 -0
  33. package/dist/event-bus.d.ts +29 -0
  34. package/dist/event-bus.d.ts.map +1 -0
  35. package/dist/event-bus.js +22 -0
  36. package/dist/event-bus.js.map +1 -0
  37. package/dist/event-criteria.d.ts +87 -0
  38. package/dist/event-criteria.d.ts.map +1 -0
  39. package/dist/event-criteria.js +90 -0
  40. package/dist/event-criteria.js.map +1 -0
  41. package/dist/event-gateway.d.ts +19 -0
  42. package/dist/event-gateway.d.ts.map +1 -0
  43. package/dist/event-gateway.js +22 -0
  44. package/dist/event-gateway.js.map +1 -0
  45. package/dist/event-handler.d.ts +30 -0
  46. package/dist/event-handler.d.ts.map +1 -0
  47. package/dist/event-handler.js +18 -0
  48. package/dist/event-handler.js.map +1 -0
  49. package/dist/event-processor-builder.d.ts +148 -0
  50. package/dist/event-processor-builder.d.ts.map +1 -0
  51. package/dist/event-processor-builder.js +175 -0
  52. package/dist/event-processor-builder.js.map +1 -0
  53. package/dist/event-processor.d.ts +10 -0
  54. package/dist/event-processor.d.ts.map +1 -0
  55. package/dist/event-processor.js +2 -0
  56. package/dist/event-processor.js.map +1 -0
  57. package/dist/event-sink.d.ts +23 -0
  58. package/dist/event-sink.d.ts.map +1 -0
  59. package/dist/event-sink.js +2 -0
  60. package/dist/event-sink.js.map +1 -0
  61. package/dist/event-source.d.ts +98 -0
  62. package/dist/event-source.d.ts.map +1 -0
  63. package/dist/event-source.js +191 -0
  64. package/dist/event-source.js.map +1 -0
  65. package/dist/gateway.d.ts +68 -0
  66. package/dist/gateway.d.ts.map +1 -0
  67. package/dist/gateway.js +62 -0
  68. package/dist/gateway.js.map +1 -0
  69. package/dist/handler-enhancer.d.ts +53 -0
  70. package/dist/handler-enhancer.d.ts.map +1 -0
  71. package/dist/handler-enhancer.js +17 -0
  72. package/dist/handler-enhancer.js.map +1 -0
  73. package/dist/handler.d.ts +51 -0
  74. package/dist/handler.d.ts.map +1 -0
  75. package/dist/handler.js +26 -0
  76. package/dist/handler.js.map +1 -0
  77. package/dist/index.d.ts +53 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +103 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/intercepting-command-bus.d.ts +17 -0
  82. package/dist/intercepting-command-bus.d.ts.map +1 -0
  83. package/dist/intercepting-command-bus.js +54 -0
  84. package/dist/intercepting-command-bus.js.map +1 -0
  85. package/dist/intercepting-event-bus.d.ts +8 -0
  86. package/dist/intercepting-event-bus.d.ts.map +1 -0
  87. package/dist/intercepting-event-bus.js +22 -0
  88. package/dist/intercepting-event-bus.js.map +1 -0
  89. package/dist/intercepting-query-bus.d.ts +17 -0
  90. package/dist/intercepting-query-bus.d.ts.map +1 -0
  91. package/dist/intercepting-query-bus.js +68 -0
  92. package/dist/intercepting-query-bus.js.map +1 -0
  93. package/dist/interceptor.d.ts +46 -0
  94. package/dist/interceptor.d.ts.map +1 -0
  95. package/dist/interceptor.js +2 -0
  96. package/dist/interceptor.js.map +1 -0
  97. package/dist/message-monitor-registry.d.ts +28 -0
  98. package/dist/message-monitor-registry.d.ts.map +1 -0
  99. package/dist/message-monitor-registry.js +37 -0
  100. package/dist/message-monitor-registry.js.map +1 -0
  101. package/dist/message-monitor.d.ts +36 -0
  102. package/dist/message-monitor.d.ts.map +1 -0
  103. package/dist/message-monitor.js +39 -0
  104. package/dist/message-monitor.js.map +1 -0
  105. package/dist/message.d.ts +42 -0
  106. package/dist/message.d.ts.map +1 -0
  107. package/dist/message.js +2 -0
  108. package/dist/message.js.map +1 -0
  109. package/dist/processing-state.d.ts +115 -0
  110. package/dist/processing-state.d.ts.map +1 -0
  111. package/dist/processing-state.js +205 -0
  112. package/dist/processing-state.js.map +1 -0
  113. package/dist/processor-configuration.d.ts +51 -0
  114. package/dist/processor-configuration.d.ts.map +1 -0
  115. package/dist/processor-configuration.js +2 -0
  116. package/dist/processor-configuration.js.map +1 -0
  117. package/dist/query-bus.d.ts +51 -0
  118. package/dist/query-bus.d.ts.map +1 -0
  119. package/dist/query-bus.js +2 -0
  120. package/dist/query-bus.js.map +1 -0
  121. package/dist/query-handler.d.ts +35 -0
  122. package/dist/query-handler.d.ts.map +1 -0
  123. package/dist/query-handler.js +19 -0
  124. package/dist/query-handler.js.map +1 -0
  125. package/dist/query-handling-module.d.ts +24 -0
  126. package/dist/query-handling-module.d.ts.map +1 -0
  127. package/dist/query-handling-module.js +32 -0
  128. package/dist/query-handling-module.js.map +1 -0
  129. package/dist/replay-token.d.ts +31 -0
  130. package/dist/replay-token.d.ts.map +1 -0
  131. package/dist/replay-token.js +37 -0
  132. package/dist/replay-token.js.map +1 -0
  133. package/dist/retrying-command-bus.d.ts +32 -0
  134. package/dist/retrying-command-bus.d.ts.map +1 -0
  135. package/dist/retrying-command-bus.js +58 -0
  136. package/dist/retrying-command-bus.js.map +1 -0
  137. package/dist/routing-strategy.d.ts +30 -0
  138. package/dist/routing-strategy.d.ts.map +1 -0
  139. package/dist/routing-strategy.js +37 -0
  140. package/dist/routing-strategy.js.map +1 -0
  141. package/dist/segment.d.ts +72 -0
  142. package/dist/segment.d.ts.map +1 -0
  143. package/dist/segment.js +103 -0
  144. package/dist/segment.js.map +1 -0
  145. package/dist/send.d.ts +28 -0
  146. package/dist/send.d.ts.map +1 -0
  147. package/dist/send.js +36 -0
  148. package/dist/send.js.map +1 -0
  149. package/dist/serializer.d.ts +40 -0
  150. package/dist/serializer.d.ts.map +1 -0
  151. package/dist/serializer.js +90 -0
  152. package/dist/serializer.js.map +1 -0
  153. package/dist/simple-command-bus.d.ts +23 -0
  154. package/dist/simple-command-bus.d.ts.map +1 -0
  155. package/dist/simple-command-bus.js +49 -0
  156. package/dist/simple-command-bus.js.map +1 -0
  157. package/dist/simple-query-bus.d.ts +16 -0
  158. package/dist/simple-query-bus.d.ts.map +1 -0
  159. package/dist/simple-query-bus.js +122 -0
  160. package/dist/simple-query-bus.js.map +1 -0
  161. package/dist/span-factory.d.ts +58 -0
  162. package/dist/span-factory.d.ts.map +1 -0
  163. package/dist/span-factory.js +19 -0
  164. package/dist/span-factory.js.map +1 -0
  165. package/dist/streaming-event-processor.d.ts +65 -0
  166. package/dist/streaming-event-processor.d.ts.map +1 -0
  167. package/dist/streaming-event-processor.js +239 -0
  168. package/dist/streaming-event-processor.js.map +1 -0
  169. package/dist/subscribing-event-processor.d.ts +57 -0
  170. package/dist/subscribing-event-processor.d.ts.map +1 -0
  171. package/dist/subscribing-event-processor.js +100 -0
  172. package/dist/subscribing-event-processor.js.map +1 -0
  173. package/dist/subscription-query.d.ts +63 -0
  174. package/dist/subscription-query.d.ts.map +1 -0
  175. package/dist/subscription-query.js +119 -0
  176. package/dist/subscription-query.js.map +1 -0
  177. package/dist/token-store.d.ts +83 -0
  178. package/dist/token-store.d.ts.map +1 -0
  179. package/dist/token-store.js +112 -0
  180. package/dist/token-store.js.map +1 -0
  181. package/dist/tracing-command-bus.d.ts +16 -0
  182. package/dist/tracing-command-bus.d.ts.map +1 -0
  183. package/dist/tracing-command-bus.js +44 -0
  184. package/dist/tracing-command-bus.js.map +1 -0
  185. package/dist/tracing-handler-enhancer.d.ts +11 -0
  186. package/dist/tracing-handler-enhancer.d.ts.map +1 -0
  187. package/dist/tracing-handler-enhancer.js +27 -0
  188. package/dist/tracing-handler-enhancer.js.map +1 -0
  189. package/dist/tracking-event-processor.d.ts +72 -0
  190. package/dist/tracking-event-processor.d.ts.map +1 -0
  191. package/dist/tracking-event-processor.js +223 -0
  192. package/dist/tracking-event-processor.js.map +1 -0
  193. package/dist/tracking-token.d.ts +120 -0
  194. package/dist/tracking-token.d.ts.map +1 -0
  195. package/dist/tracking-token.js +132 -0
  196. package/dist/tracking-token.js.map +1 -0
  197. package/dist/transaction.d.ts +60 -0
  198. package/dist/transaction.d.ts.map +1 -0
  199. package/dist/transaction.js +74 -0
  200. package/dist/transaction.js.map +1 -0
  201. package/dist/unit-of-work.d.ts +41 -0
  202. package/dist/unit-of-work.d.ts.map +1 -0
  203. package/dist/unit-of-work.js +96 -0
  204. package/dist/unit-of-work.js.map +1 -0
  205. package/dist/upcaster.d.ts +91 -0
  206. package/dist/upcaster.d.ts.map +1 -0
  207. package/dist/upcaster.js +114 -0
  208. package/dist/upcaster.js.map +1 -0
  209. package/dist/with-namespace.d.ts +59 -0
  210. package/dist/with-namespace.d.ts.map +1 -0
  211. package/dist/with-namespace.js +42 -0
  212. package/dist/with-namespace.js.map +1 -0
  213. package/package.json +65 -0
  214. package/src/command-bus.ts +34 -0
  215. package/src/command-handler.ts +116 -0
  216. package/src/command-handling-module.ts +183 -0
  217. package/src/correlation-data.ts +169 -0
  218. package/src/dead-letter-queue.ts +330 -0
  219. package/src/dead-lettering-handler.ts +109 -0
  220. package/src/descriptor.ts +176 -0
  221. package/src/emit-update.ts +35 -0
  222. package/src/event-bus.ts +45 -0
  223. package/src/event-criteria.ts +141 -0
  224. package/src/event-gateway.ts +42 -0
  225. package/src/event-handler.ts +44 -0
  226. package/src/event-processor-builder.ts +246 -0
  227. package/src/event-processor.ts +9 -0
  228. package/src/event-sink.ts +23 -0
  229. package/src/event-source.ts +301 -0
  230. package/src/gateway.ts +144 -0
  231. package/src/handler-enhancer.ts +70 -0
  232. package/src/handler.ts +133 -0
  233. package/src/index.ts +356 -0
  234. package/src/intercepting-command-bus.ts +73 -0
  235. package/src/intercepting-event-bus.ts +29 -0
  236. package/src/intercepting-query-bus.ts +104 -0
  237. package/src/interceptor.ts +48 -0
  238. package/src/message-monitor-registry.ts +64 -0
  239. package/src/message-monitor.ts +68 -0
  240. package/src/message.ts +41 -0
  241. package/src/processing-state.ts +258 -0
  242. package/src/processor-configuration.ts +59 -0
  243. package/src/query-bus.ts +69 -0
  244. package/src/query-handler.ts +49 -0
  245. package/src/query-handling-module.ts +44 -0
  246. package/src/replay-token.ts +53 -0
  247. package/src/retrying-command-bus.ts +80 -0
  248. package/src/routing-strategy.ts +59 -0
  249. package/src/segment.ts +136 -0
  250. package/src/send.ts +44 -0
  251. package/src/serializer.ts +122 -0
  252. package/src/simple-command-bus.ts +59 -0
  253. package/src/simple-query-bus.ts +158 -0
  254. package/src/span-factory.ts +81 -0
  255. package/src/streaming-event-processor.ts +351 -0
  256. package/src/subscribing-event-processor.ts +169 -0
  257. package/src/subscription-query.ts +173 -0
  258. package/src/token-store.ts +211 -0
  259. package/src/tracing-command-bus.ts +52 -0
  260. package/src/tracing-handler-enhancer.ts +34 -0
  261. package/src/tracking-event-processor.ts +336 -0
  262. package/src/tracking-token.ts +231 -0
  263. package/src/transaction.ts +98 -0
  264. package/src/unit-of-work.ts +138 -0
  265. package/src/upcaster.ts +174 -0
  266. package/src/with-namespace.ts +75 -0
@@ -0,0 +1,336 @@
1
+ import { emptyMetadata, qualifiedNameToString } from "@kronos-ts/common"
2
+ import type { EventHandlerRegistration } from "./handler.js"
3
+ import type { EventHandlerDefinition } from "./event-handler.js"
4
+ import type { StreamableEventSource, MessageStream, SequencedEvent } from "./event-source.js"
5
+ import type { UoWRunner } from "./unit-of-work.js"
6
+ import { runInNewUoW } from "./unit-of-work.js"
7
+ import type { TokenStore } from "./token-store.js"
8
+ import type { TrackingToken } from "./tracking-token.js"
9
+ import {
10
+ globalSequenceToken,
11
+ replayToken,
12
+ isReplayToken,
13
+ isReplaying,
14
+ advanceToken,
15
+ } from "./tracking-token.js"
16
+ import { REPLAY_STATE_KEY } from "./replay-token.js"
17
+ import { setResource, onPrepareCommit } from "./processing-state.js"
18
+ import type { HandlerEnhancerDefinition } from "./handler-enhancer.js"
19
+ import type { CommandBus } from "./command-bus.js"
20
+ import type { QueryBus } from "./query-bus.js"
21
+ import { STATE_MANAGER_KEY } from "@kronos-ts/eventsourcing"
22
+ import { COMMAND_BUS_KEY } from "./send.js"
23
+ import { QUERY_BUS_KEY } from "./emit-update.js"
24
+
25
+ /**
26
+ * A tracking event processor that reads events from a streamable event source
27
+ * and delivers them to registered event handlers.
28
+ *
29
+ * Uses {@link StreamableEventSource.open} to get a persistent {@link MessageStream}.
30
+ * Each batch of events is processed within a UnitOfWork, enabling
31
+ * transactional event processing and coordinated token updates.
32
+ *
33
+ * Supports replay via {@link resetTokens} — the processor can be stopped,
34
+ * reset to a starting position, and restarted.
35
+ */
36
+ export interface TrackingEventProcessor {
37
+ readonly name: string
38
+ readonly running: boolean
39
+ /** Current effective position in the event stream. */
40
+ readonly position: bigint
41
+ /** Whether the processor is currently replaying events. */
42
+ readonly replaying: boolean
43
+ start(): Promise<void>
44
+ stop(): void
45
+ /**
46
+ * Reset the processor to replay events from a starting position.
47
+ * The processor must be stopped before calling this.
48
+ */
49
+ resetTokens(startPosition?: bigint, resetContext?: unknown): Promise<void>
50
+ }
51
+
52
+ export interface TrackingEventProcessorOptions {
53
+ name: string
54
+ eventSource: StreamableEventSource
55
+ eventHandlers: ReadonlyArray<EventHandlerDefinition>
56
+ /** State manager injected into ALS at handler-invocation entry (D-44). */
57
+ stateManager?: unknown
58
+ /** Command bus injected into ALS at handler-invocation entry (D-44). */
59
+ commandBus?: CommandBus
60
+ /** Query bus injected into ALS at handler-invocation entry (D-44). */
61
+ queryBus?: QueryBus
62
+ /** Optional per-event callback fired inside the UoW before handler invocation (e.g. monitoring). */
63
+ onEventDelivery?: () => void
64
+ unitOfWorkRunner?: UoWRunner
65
+ tokenStore?: TokenStore
66
+ /** Polling interval when no events are available (ms). Default: 500. */
67
+ pollingIntervalMs?: number
68
+ batchSize?: number
69
+ errorHandler?: EventProcessingErrorHandler
70
+ /** Optional handler enhancer applied to all event handlers at setup time. */
71
+ handlerEnhancer?: HandlerEnhancerDefinition
72
+ /** Reset callback invoked from resetTokens(). */
73
+ onReset?: () => Promise<void> | void
74
+ }
75
+
76
+ /**
77
+ * Determines what happens when an event handler fails.
78
+ */
79
+ export interface EventProcessingErrorHandler {
80
+ handleError(error: unknown, eventName: string, position: bigint): void | Promise<void>
81
+ }
82
+
83
+ /**
84
+ * Logs errors and continues processing. Default behavior.
85
+ */
86
+ export function loggingErrorHandler(processorName: string): EventProcessingErrorHandler {
87
+ return {
88
+ handleError(error, eventName, position) {
89
+ console.error(
90
+ `Event processor "${processorName}": handler failed for "${eventName}" at position ${position}:`,
91
+ error,
92
+ )
93
+ },
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Rethrows errors, aborting the current batch and triggering rollback.
99
+ */
100
+ export function propagatingErrorHandler(): EventProcessingErrorHandler {
101
+ return {
102
+ handleError(error) {
103
+ throw error
104
+ },
105
+ }
106
+ }
107
+
108
+ export function createTrackingEventProcessor(
109
+ options: TrackingEventProcessorOptions,
110
+ ): TrackingEventProcessor {
111
+ const {
112
+ name,
113
+ eventSource,
114
+ eventHandlers,
115
+ stateManager,
116
+ commandBus,
117
+ queryBus,
118
+ onEventDelivery,
119
+ unitOfWorkRunner = runInNewUoW,
120
+ tokenStore,
121
+ pollingIntervalMs = 500,
122
+ batchSize = 100,
123
+ errorHandler = loggingErrorHandler(name),
124
+ handlerEnhancer,
125
+ onReset,
126
+ } = options
127
+
128
+ const segment = 0
129
+
130
+ const handlerMap = new Map<string, Array<EventHandlerRegistration<any>>>()
131
+ for (const reg of eventHandlers) {
132
+ const eventName = qualifiedNameToString(reg.descriptor.name)
133
+ if (!handlerMap.has(eventName)) {
134
+ handlerMap.set(eventName, [])
135
+ }
136
+ const enhanced = handlerEnhancer
137
+ ? {
138
+ ...reg,
139
+ handler: handlerEnhancer.wrapHandler(reg.handler, {
140
+ messageType: "event" as const,
141
+ messageName: eventName,
142
+ handlerGroup: name,
143
+ }),
144
+ }
145
+ : reg
146
+ handlerMap.get(eventName)!.push(enhanced as EventHandlerRegistration<any>)
147
+ }
148
+
149
+ let token: TrackingToken = globalSequenceToken(0n)
150
+ let isRunning = false
151
+ let stream: MessageStream<SequencedEvent> | null = null
152
+ let pollTimer: ReturnType<typeof setTimeout> | null = null
153
+ let processing = false
154
+
155
+ async function initialize() {
156
+ if (tokenStore) {
157
+ await tokenStore.initializeSegments(name, 1)
158
+ const stored = await tokenStore.get(name, segment)
159
+ if (stored !== undefined) {
160
+ token = stored
161
+ }
162
+ }
163
+ }
164
+
165
+ function openStream() {
166
+ stream = eventSource.open({ position: token.position() })
167
+ stream.setCallback(() => {
168
+ if (isRunning && !processing) {
169
+ scheduleImmediate()
170
+ }
171
+ })
172
+ }
173
+
174
+ async function poll() {
175
+ if (!isRunning || processing) return
176
+ processing = true
177
+
178
+ try {
179
+ if (!stream) {
180
+ openStream()
181
+ }
182
+
183
+ // Check for stream errors — reopen if needed
184
+ if (stream!.error()) {
185
+ console.error(`Event processor "${name}": stream error, reopening:`, stream!.error())
186
+ stream!.close()
187
+ stream = null
188
+ openStream()
189
+ processing = false
190
+ return
191
+ }
192
+
193
+ const batch: SequencedEvent[] = []
194
+ let event = stream!.next()
195
+ while (event && batch.length < batchSize) {
196
+ batch.push(event)
197
+ if (batch.length < batchSize && stream!.hasNextAvailable()) {
198
+ event = stream!.next()
199
+ } else {
200
+ break
201
+ }
202
+ }
203
+
204
+ if (batch.length > 0) {
205
+ await processBatch(batch)
206
+ if (isRunning) {
207
+ if (stream!.hasNextAvailable()) {
208
+ scheduleImmediate()
209
+ }
210
+ // else: stream callback will wake us when events arrive
211
+ }
212
+ } else {
213
+ // If replay is done and no more events, unwrap
214
+ if (isReplayToken(token)) {
215
+ token = globalSequenceToken(token.position())
216
+ if (tokenStore) {
217
+ await tokenStore.store(name, segment, token)
218
+ }
219
+ }
220
+ // No events available — wait for callback or poll again after interval
221
+ if (isRunning) {
222
+ pollTimer = setTimeout(poll, pollingIntervalMs)
223
+ }
224
+ }
225
+ } catch (err) {
226
+ console.error(`Event processor "${name}" error during poll:`, err)
227
+ if (isRunning) pollTimer = setTimeout(poll, pollingIntervalMs * 2)
228
+ } finally {
229
+ processing = false
230
+ }
231
+ }
232
+
233
+ async function processBatch(batch: SequencedEvent[]) {
234
+ let batchEndToken: TrackingToken = token
235
+
236
+ await unitOfWorkRunner(emptyMetadata(), async () => {
237
+ for (const sequencedEvent of batch) {
238
+ setResource(REPLAY_STATE_KEY, { replaying: isReplaying(batchEndToken) })
239
+
240
+ await deliverEvent(sequencedEvent)
241
+
242
+ batchEndToken = advanceToken(batchEndToken, sequencedEvent.sequence + 1n)
243
+ }
244
+
245
+ if (tokenStore) {
246
+ onPrepareCommit(async () => {
247
+ await tokenStore.store(name, segment, batchEndToken)
248
+ await tokenStore.extendClaim(name, segment, name)
249
+ })
250
+ }
251
+ })
252
+
253
+ token = batchEndToken
254
+ }
255
+
256
+ async function deliverEvent(sequencedEvent: SequencedEvent) {
257
+ const event = sequencedEvent.event
258
+ const eventName = qualifiedNameToString(event.name)
259
+ const handlers = handlerMap.get(eventName)
260
+ if (!handlers || handlers.length === 0) return
261
+
262
+ // D-44 wiring: write framework components into ALS at per-event invocation entry.
263
+ if (stateManager !== undefined) setResource(STATE_MANAGER_KEY, stateManager as any)
264
+ if (commandBus !== undefined) setResource(COMMAND_BUS_KEY, commandBus)
265
+ if (queryBus !== undefined) setResource(QUERY_BUS_KEY, queryBus)
266
+ // Optional per-event callback (e.g. monitoring hooks registered inside the UoW).
267
+ if (onEventDelivery) onEventDelivery()
268
+
269
+ for (const reg of handlers) {
270
+ try {
271
+ await reg.handler(event.payload, event.metadata)
272
+ } catch (err) {
273
+ await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
274
+ }
275
+ }
276
+ }
277
+
278
+ function scheduleImmediate() {
279
+ if (pollTimer !== null) {
280
+ clearTimeout(pollTimer)
281
+ }
282
+ pollTimer = setTimeout(poll, 0)
283
+ }
284
+
285
+ return {
286
+ get name() { return name },
287
+ get running() { return isRunning },
288
+ get position() { return token.position() },
289
+ get replaying() { return isReplaying(token) },
290
+
291
+ async start() {
292
+ if (isRunning) return
293
+ await initialize()
294
+ isRunning = true
295
+ poll()
296
+ },
297
+
298
+ stop() {
299
+ isRunning = false
300
+ if (pollTimer !== null) {
301
+ clearTimeout(pollTimer)
302
+ pollTimer = null
303
+ }
304
+ if (stream) {
305
+ stream.close()
306
+ stream = null
307
+ }
308
+ },
309
+
310
+ async resetTokens(startPosition: bigint = 0n, resetContext?: unknown) {
311
+ if (isRunning) {
312
+ throw new Error(`Processor "${name}" must be stopped before resetting tokens`)
313
+ }
314
+
315
+ const headPosition = await eventSource.getHeadPosition()
316
+
317
+ if (headPosition <= startPosition) {
318
+ token = globalSequenceToken(startPosition)
319
+ } else {
320
+ token = replayToken(
321
+ globalSequenceToken(headPosition),
322
+ globalSequenceToken(startPosition),
323
+ resetContext,
324
+ )
325
+ }
326
+
327
+ if (tokenStore) {
328
+ await tokenStore.store(name, segment, token)
329
+ }
330
+
331
+ if (onReset) {
332
+ await onReset()
333
+ }
334
+ },
335
+ }
336
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * A tracking token represents a processor's position in an event stream.
3
+ *
4
+ * This interface is extensible — event store implementations can define
5
+ * their own token types (e.g., a PostgreSQL extension might track
6
+ * commit timestamps alongside sequence positions).
7
+ *
8
+ * The framework ships two built-in implementations:
9
+ * - `GlobalSequenceToken` — for sequential event stores (in-memory, Axon Server)
10
+ * - `ReplayToken` — wraps any token to mark replay-in-progress state
11
+ *
12
+ * Tokens are immutable. All operations return new instances.
13
+ */
14
+ export interface TrackingToken {
15
+ /**
16
+ * Discriminant for type narrowing and serialization.
17
+ * Each token implementation has a unique kind string.
18
+ */
19
+ readonly kind: string
20
+
21
+ /**
22
+ * The effective read position in the event stream.
23
+ * Used by the event source to know where to start reading.
24
+ */
25
+ position(): bigint
26
+
27
+ /**
28
+ * Does this token's position include all events covered by the other?
29
+ * Returns true if every event that `other` has seen, this token has also seen.
30
+ *
31
+ * Used for replay completion detection and segment merging.
32
+ */
33
+ covers(other: TrackingToken): boolean
34
+
35
+ /**
36
+ * Create a token representing the lower bound of this and another token.
37
+ * When opening a shared stream for multiple segments, use the lower bound
38
+ * to ensure no events are missed by any segment.
39
+ */
40
+ lowerBound(other: TrackingToken): TrackingToken
41
+
42
+ /**
43
+ * Create a token representing the upper bound of this and another token.
44
+ * Represents the furthest position — used for segment merging.
45
+ */
46
+ upperBound(other: TrackingToken): TrackingToken
47
+
48
+ /**
49
+ * Check if this token represents the same position as the other.
50
+ * Default: `this.covers(other) && other.covers(this)`.
51
+ */
52
+ samePositionAs(other: TrackingToken): boolean
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // GlobalSequenceToken
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export interface GlobalSequenceToken extends TrackingToken {
60
+ readonly kind: "global-sequence"
61
+ readonly sequence: bigint
62
+ }
63
+
64
+ /**
65
+ * Creates a token for a global-sequence event store (monotonically increasing positions).
66
+ * This is the default token type for in-memory stores and Axon Server.
67
+ */
68
+ export function globalSequenceToken(sequence: bigint): GlobalSequenceToken {
69
+ return {
70
+ kind: "global-sequence",
71
+ sequence,
72
+ position: () => sequence,
73
+ covers: (other) => sequence >= other.position(),
74
+ lowerBound: (other) => globalSequenceToken(sequence < other.position() ? sequence : other.position()),
75
+ upperBound: (other) => globalSequenceToken(sequence > other.position() ? sequence : other.position()),
76
+ samePositionAs: (other) => sequence === other.position(),
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Sentinel token representing the beginning of the event stream.
82
+ * A processor starting with FIRST_TOKEN will read from position 0.
83
+ */
84
+ export const FIRST_TOKEN: TrackingToken = globalSequenceToken(0n)
85
+
86
+ /**
87
+ * Sentinel token representing the tail of the event stream.
88
+ * A processor starting with LATEST_TOKEN will skip all existing events
89
+ * and only process new events appended after startup.
90
+ */
91
+ export const LATEST_TOKEN: TrackingToken = {
92
+ kind: "latest",
93
+ position: () => BigInt(Number.MAX_SAFE_INTEGER),
94
+ covers: () => true,
95
+ lowerBound: (other) => other,
96
+ upperBound: () => LATEST_TOKEN,
97
+ samePositionAs: (other) => other === LATEST_TOKEN || (other.kind === "latest"),
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // ReplayToken
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export interface ReplayToken extends TrackingToken {
105
+ readonly kind: "replay"
106
+ /** The wrapped token representing current progress during replay. */
107
+ readonly currentToken: TrackingToken
108
+ /** The token representing the position when reset was triggered. Events before this are replayed. */
109
+ readonly tokenAtReset: TrackingToken
110
+ /** Optional user-provided context (e.g., reason for the reset). */
111
+ readonly resetContext?: unknown
112
+ }
113
+
114
+ /**
115
+ * Creates a replay token that wraps another token to mark replay-in-progress.
116
+ *
117
+ * During replay, events are re-delivered from `currentToken` up to `tokenAtReset`.
118
+ * Once `currentToken.covers(tokenAtReset)`, the replay is complete and the
119
+ * token unwraps to the current position.
120
+ *
121
+ * @param tokenAtReset The head position when reset was triggered
122
+ * @param currentToken Where replay is currently reading from
123
+ * @param resetContext Optional user-provided context
124
+ */
125
+ export function replayToken(
126
+ tokenAtReset: TrackingToken,
127
+ currentToken: TrackingToken,
128
+ resetContext?: unknown,
129
+ ): ReplayToken {
130
+ return {
131
+ kind: "replay",
132
+ currentToken,
133
+ tokenAtReset,
134
+ resetContext,
135
+
136
+ position: () => currentToken.position(),
137
+
138
+ covers: (other) => currentToken.covers(other),
139
+
140
+ lowerBound: (other) => {
141
+ const inner = currentToken.lowerBound(other)
142
+ // If the lower bound is still within replay range, keep the replay wrapper
143
+ if (!inner.covers(tokenAtReset)) {
144
+ return replayToken(tokenAtReset, inner, resetContext)
145
+ }
146
+ return inner
147
+ },
148
+
149
+ upperBound: (other) => {
150
+ const inner = currentToken.upperBound(other)
151
+ // If the upper bound has passed the reset point, replay is done
152
+ if (inner.covers(tokenAtReset)) {
153
+ return inner
154
+ }
155
+ return replayToken(tokenAtReset, inner, resetContext)
156
+ },
157
+
158
+ samePositionAs: (other) => currentToken.samePositionAs(other),
159
+ }
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Type guards
164
+ // ---------------------------------------------------------------------------
165
+
166
+ export function isReplayToken(token: TrackingToken): token is ReplayToken {
167
+ return token.kind === "replay"
168
+ }
169
+
170
+ export function isGlobalSequenceToken(token: TrackingToken): token is GlobalSequenceToken {
171
+ return token.kind === "global-sequence"
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Token operations
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /**
179
+ * Advance a token to a new position. If the token is a ReplayToken and
180
+ * the new position covers the reset point, unwraps to a plain token.
181
+ */
182
+ export function advanceToken(token: TrackingToken, newPosition: bigint): TrackingToken {
183
+ const advanced = globalSequenceToken(newPosition)
184
+
185
+ if (!isReplayToken(token)) {
186
+ return advanced
187
+ }
188
+
189
+ // Check if replay is complete
190
+ if (advanced.covers(token.tokenAtReset)) {
191
+ return advanced
192
+ }
193
+
194
+ // Still replaying — wrap the advanced position
195
+ return replayToken(token.tokenAtReset, advanced, token.resetContext)
196
+ }
197
+
198
+ /**
199
+ * Check whether the given token represents a replay in progress.
200
+ * Returns true if the token is a ReplayToken AND the current position
201
+ * has not yet passed the reset position.
202
+ *
203
+ * A ReplayToken that has been fully replayed (current covers reset)
204
+ * would have been unwrapped by advanceToken — so any ReplayToken
205
+ * that still exists is mid-replay.
206
+ */
207
+ export function isReplaying(token: TrackingToken): boolean {
208
+ return isReplayToken(token)
209
+ }
210
+
211
+ /**
212
+ * Unwrap a token to its innermost non-replay token.
213
+ * If the token is not a ReplayToken, returns it unchanged.
214
+ */
215
+ export function unwrapToken(token: TrackingToken): TrackingToken {
216
+ if (isReplayToken(token)) {
217
+ return unwrapToken(token.currentToken)
218
+ }
219
+ return token
220
+ }
221
+
222
+ /**
223
+ * Check if an event at the given position was already processed before
224
+ * the reset (i.e., it's a replayed event, not a new one).
225
+ *
226
+ * Uses the token's `covers()` semantics: if the reset token covers
227
+ * the event position, then the event existed before the reset.
228
+ */
229
+ export function wasProcessedBeforeReset(token: ReplayToken, eventPosition: bigint): boolean {
230
+ return token.tokenAtReset.covers(globalSequenceToken(eventPosition))
231
+ }
@@ -0,0 +1,98 @@
1
+ import { resourceKey, type ResourceKey } from "@kronos-ts/common"
2
+ import {
3
+ processingStateStorage,
4
+ setResource,
5
+ on,
6
+ onError,
7
+ Phase,
8
+ } from "./processing-state.js"
9
+ import type { UoWRunner } from "./unit-of-work.js"
10
+
11
+ /**
12
+ * Manages transaction lifecycle. Users provide an implementation
13
+ * for their specific database/ORM.
14
+ *
15
+ * The framework calls begin/commit/rollback — users never touch these directly.
16
+ */
17
+ export interface TransactionManager<T = unknown> {
18
+ begin(): Promise<T>
19
+ commit(tx: T): Promise<void>
20
+ rollback(tx: T): Promise<void>
21
+ }
22
+
23
+ /**
24
+ * A no-op transaction manager for when no database transactions are needed.
25
+ */
26
+ export function noTransactionManager(): TransactionManager<void> {
27
+ return {
28
+ begin: async () => {},
29
+ commit: async () => {},
30
+ rollback: async () => {},
31
+ }
32
+ }
33
+
34
+ /** Resource key for storing the active transaction in the active UnitOfWork's ALS-backed resources. */
35
+ export const TRANSACTION_KEY: ResourceKey<unknown> = resourceKey("transaction")
36
+
37
+ /**
38
+ * Get the active transaction from the active UnitOfWork's ALS-backed resources.
39
+ * Returns undefined if no UnitOfWork is active or no transaction has been stored.
40
+ *
41
+ * ORM integrations use this to participate in the framework's transaction:
42
+ * ```
43
+ * const db = drizzle(pool, {
44
+ * transaction: () => getActiveTransaction(),
45
+ * })
46
+ * ```
47
+ *
48
+ * Permissive undefined-return preserved (D-12 / D-23): callers outside a UoW
49
+ * get `undefined`, NOT a NoActiveUnitOfWork throw — this is an ORM escape hatch,
50
+ * not a framework-internal accessor.
51
+ */
52
+ export function getActiveTransaction<T = unknown>(): T | undefined {
53
+ const state = processingStateStorage.getStore()
54
+ if (!state) return undefined
55
+ return state.resources.get(TRANSACTION_KEY.symbol) as T | undefined
56
+ }
57
+
58
+ /**
59
+ * Wraps a delegate runner with transaction management. The transaction is:
60
+ * - Started before the delegate's action runs
61
+ * - Available via `getActiveTransaction()` throughout
62
+ * - Committed in the COMMIT phase
63
+ * - Rolled back on error
64
+ *
65
+ * The transaction is stored as a resource on the active UoW's ALS state,
66
+ * so all phases (PRE_INVOCATION through AFTER_COMMIT) and any code calling
67
+ * `getActiveTransaction()` inside the UoW see it.
68
+ *
69
+ * ```
70
+ * const txRunner = transactionalUnitOfWorkFactory(runInUoW, myTransactionManager)
71
+ * await txRunner(metadata, async () => { ... })
72
+ * ```
73
+ *
74
+ * Plan 03-04 (CTX-04 / D-34): rewritten as a composable runner wrapper.
75
+ * Previously took/returned `UnitOfWorkFactory`; the UoW interface and
76
+ * factory are gone, so this now takes/returns `UoWRunner`. The name is
77
+ * preserved despite the shape change — public docs and extension code
78
+ * import `transactionalUnitOfWorkFactory` by name. Phase 9 (Extension
79
+ * Migration) can rename if the kronos() app API warrants.
80
+ */
81
+ export function transactionalUnitOfWorkFactory<T>(
82
+ delegate: UoWRunner,
83
+ txManager: TransactionManager<T>,
84
+ ): UoWRunner {
85
+ return async (metadata, action) => {
86
+ const tx = await txManager.begin()
87
+ return delegate(metadata, async () => {
88
+ setResource(TRANSACTION_KEY, tx)
89
+ on(Phase.COMMIT, async () => {
90
+ await txManager.commit(tx)
91
+ })
92
+ onError(async () => {
93
+ await txManager.rollback(tx)
94
+ })
95
+ return action()
96
+ })
97
+ }
98
+ }