@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,81 @@
1
+ import type { Message } from "./message.js"
2
+
3
+ /**
4
+ * A span representing a unit of tracing work.
5
+ * Start the span, do work, then end it.
6
+ *
7
+ */
8
+ export interface Span {
9
+ /** Start the span. Returns the span for chaining. */
10
+ start(): Span
11
+ /** End the span normally. */
12
+ end(): void
13
+ /** Record an error on the span and end it. */
14
+ recordException(error: Error): void
15
+ }
16
+
17
+ /**
18
+ * Provides custom attributes to add to spans.
19
+ *
20
+ */
21
+ export interface SpanAttributesProvider {
22
+ provideAttributes(message: Message): Record<string, string>
23
+ }
24
+
25
+ /**
26
+ * Factory for creating tracing spans around message processing.
27
+ *
28
+ * The SpanFactory is the core tracing abstraction. Implementations
29
+ * (e.g., OpenTelemetry) provide the actual span creation and context
30
+ * propagation. The framework calls these methods during dispatch and
31
+ * handling.
32
+ *
33
+ */
34
+ export interface SpanFactory {
35
+ /** Create a root trace span (no parent). */
36
+ createRootTrace(operationName: string): Span
37
+
38
+ /**
39
+ * Create a span for handling a message (consumer side).
40
+ * Extracts trace context from the parent message's metadata.
41
+ */
42
+ createHandlerSpan(operationName: string, parentMessage: Message): Span
43
+
44
+ /**
45
+ * Create a span for dispatching a message (producer side).
46
+ * Links to the parent message's trace context.
47
+ */
48
+ createDispatchSpan(operationName: string, parentMessage: Message): Span
49
+
50
+ /** Create a span for internal framework operations. */
51
+ createInternalSpan(operationName: string): Span
52
+
53
+ /**
54
+ * Inject trace context into a message's metadata so it propagates
55
+ * across message boundaries.
56
+ */
57
+ propagateContext<M extends Message>(message: M): M
58
+
59
+ /** Register a custom span attribute provider. */
60
+ registerSpanAttributeProvider(provider: SpanAttributesProvider): void
61
+ }
62
+
63
+ /**
64
+ * A no-op span factory. Default when no tracing is configured.
65
+ */
66
+ export function noOpSpanFactory(): SpanFactory {
67
+ const noOpSpan: Span = {
68
+ start() { return this },
69
+ end() {},
70
+ recordException() {},
71
+ }
72
+
73
+ return {
74
+ createRootTrace() { return noOpSpan },
75
+ createHandlerSpan() { return noOpSpan },
76
+ createDispatchSpan() { return noOpSpan },
77
+ createInternalSpan() { return noOpSpan },
78
+ propagateContext<M extends Message>(message: M) { return message },
79
+ registerSpanAttributeProvider() {},
80
+ }
81
+ }
@@ -0,0 +1,351 @@
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 { EventProcessingErrorHandler } from "./tracking-event-processor.js"
9
+ import { loggingErrorHandler } from "./tracking-event-processor.js"
10
+ import type { HandlerEnhancerDefinition } from "./handler-enhancer.js"
11
+ import type { TrackingToken } from "./tracking-token.js"
12
+ import {
13
+ globalSequenceToken,
14
+ replayToken,
15
+ isReplayToken,
16
+ isReplaying,
17
+ advanceToken,
18
+ } from "./tracking-token.js"
19
+ import { REPLAY_STATE_KEY } from "./replay-token.js"
20
+ import { setResource, onPrepareCommit } from "./processing-state.js"
21
+ import type { CommandBus } from "./command-bus.js"
22
+ import type { QueryBus } from "./query-bus.js"
23
+ import { STATE_MANAGER_KEY } from "@kronos-ts/eventsourcing"
24
+ import { COMMAND_BUS_KEY } from "./send.js"
25
+ import { QUERY_BUS_KEY } from "./emit-update.js"
26
+
27
+ /**
28
+ * A streaming event processor that uses push-based event delivery
29
+ * via {@link MessageStream}.
30
+ *
31
+ * Architecture:
32
+ * - Opens a MessageStream from the event source via {@link StreamableEventSource.open}
33
+ * - When events become available (via setCallback), pulls them
34
+ * - Processes batches within a UnitOfWork
35
+ * - Stores token at PREPARE_COMMIT (same transaction as handler work)
36
+ *
37
+ */
38
+ export interface EventProcessorStatus {
39
+ readonly segmentId: number
40
+ readonly position: bigint
41
+ readonly replaying: boolean
42
+ readonly caughtUp: boolean
43
+ readonly error?: Error
44
+ }
45
+
46
+ export interface StreamingEventProcessor {
47
+ readonly name: string
48
+ readonly running: boolean
49
+ readonly position: bigint
50
+ readonly replaying: boolean
51
+ /** Status per segment. */
52
+ processingStatus(): Map<number, EventProcessorStatus>
53
+ /** Whether this processor supports reset (always true if not running). */
54
+ supportsReset(): boolean
55
+ start(): Promise<void>
56
+ stop(): void
57
+ resetTokens(startPosition?: bigint, resetContext?: unknown): Promise<void>
58
+ splitSegment(segmentId: number): Promise<boolean>
59
+ mergeSegment(segmentId: number): Promise<boolean>
60
+ releaseSegment(segmentId: number): Promise<void>
61
+ }
62
+
63
+ export interface StreamingEventProcessorOptions {
64
+ name: string
65
+ eventSource: StreamableEventSource
66
+ eventHandlers: ReadonlyArray<EventHandlerDefinition>
67
+ /** State manager injected into ALS at handler-invocation entry (D-44). */
68
+ stateManager?: unknown
69
+ /** Command bus injected into ALS at handler-invocation entry (D-44). */
70
+ commandBus?: CommandBus
71
+ /** Query bus injected into ALS at handler-invocation entry (D-44). */
72
+ queryBus?: QueryBus
73
+ /** Optional per-event callback fired inside the UoW before handler invocation (e.g. monitoring). */
74
+ onEventDelivery?: () => void
75
+ unitOfWorkRunner?: UoWRunner
76
+ tokenStore?: TokenStore
77
+ batchSize?: number
78
+ errorHandler?: EventProcessingErrorHandler
79
+ /** Optional handler enhancer applied to all event handlers at setup time. */
80
+ handlerEnhancer?: HandlerEnhancerDefinition
81
+ /** Reset callback invoked from resetTokens(). */
82
+ onReset?: () => Promise<void> | void
83
+ }
84
+
85
+ export function createStreamingEventProcessor(
86
+ options: StreamingEventProcessorOptions,
87
+ ): StreamingEventProcessor {
88
+ const {
89
+ name,
90
+ eventSource,
91
+ eventHandlers,
92
+ stateManager,
93
+ commandBus,
94
+ queryBus,
95
+ onEventDelivery,
96
+ unitOfWorkRunner = runInNewUoW,
97
+ tokenStore,
98
+ batchSize = 100,
99
+ errorHandler = loggingErrorHandler(name),
100
+ handlerEnhancer,
101
+ onReset,
102
+ } = options
103
+
104
+ const segment = 0
105
+
106
+ const handlerMap = new Map<string, Array<EventHandlerRegistration<any>>>()
107
+ for (const reg of eventHandlers) {
108
+ const eventName = qualifiedNameToString(reg.descriptor.name)
109
+ if (!handlerMap.has(eventName)) {
110
+ handlerMap.set(eventName, [])
111
+ }
112
+ const enhanced = handlerEnhancer
113
+ ? {
114
+ ...reg,
115
+ handler: handlerEnhancer.wrapHandler(reg.handler, {
116
+ messageType: "event" as const,
117
+ messageName: eventName,
118
+ handlerGroup: name,
119
+ }),
120
+ }
121
+ : reg
122
+ handlerMap.get(eventName)!.push(enhanced as EventHandlerRegistration<any>)
123
+ }
124
+
125
+ let token: TrackingToken = globalSequenceToken(0n)
126
+ let isRunning = false
127
+ let stream: MessageStream<SequencedEvent> | null = null
128
+ let processTimer: ReturnType<typeof setTimeout> | null = null
129
+ let processing = false
130
+ let caughtUp = false
131
+ let lastError: Error | undefined
132
+
133
+ async function initialize() {
134
+ if (tokenStore) {
135
+ await tokenStore.initializeSegments(name, 1)
136
+ const stored = await tokenStore.get(name, segment)
137
+ if (stored !== undefined) {
138
+ token = stored
139
+ }
140
+ }
141
+ }
142
+
143
+ function openStream() {
144
+ stream = eventSource.open({ position: token.position() })
145
+ stream.setCallback(() => {
146
+ if (isRunning && !processing) {
147
+ scheduleImmediate()
148
+ }
149
+ })
150
+ }
151
+
152
+ async function processAvailable() {
153
+ if (!isRunning || processing) return
154
+ processing = true
155
+
156
+ try {
157
+ await processFromStream()
158
+ } catch (err) {
159
+ lastError = err instanceof Error ? err : new Error(String(err))
160
+ console.error(`Event processor "${name}" error:`, err)
161
+ } finally {
162
+ processing = false
163
+ }
164
+
165
+ if (isRunning && stream) {
166
+ if (stream.hasNextAvailable()) {
167
+ scheduleImmediate()
168
+ }
169
+ }
170
+ }
171
+
172
+ async function processFromStream() {
173
+ if (!stream) return
174
+
175
+ // Check for stream errors — reopen if needed
176
+ const streamError = stream.error()
177
+ if (streamError) {
178
+ console.error(`Event processor "${name}": stream error, reopening:`, streamError)
179
+ stream.close()
180
+ stream = null
181
+ openStream()
182
+ return
183
+ }
184
+
185
+ const batch: SequencedEvent[] = []
186
+ let event = stream.next()
187
+ while (event && batch.length < batchSize) {
188
+ batch.push(event)
189
+ if (batch.length < batchSize && stream.hasNextAvailable()) {
190
+ event = stream.next()
191
+ } else {
192
+ break
193
+ }
194
+ }
195
+
196
+ if (batch.length > 0) {
197
+ caughtUp = false
198
+ await processBatch(batch)
199
+ if (stream.hasNextAvailable()) {
200
+ scheduleImmediate()
201
+ }
202
+ } else {
203
+ caughtUp = true
204
+ if (isReplayToken(token)) {
205
+ token = globalSequenceToken(token.position())
206
+ if (tokenStore) {
207
+ await tokenStore.store(name, segment, token)
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ async function processBatch(batch: SequencedEvent[]) {
214
+ let batchEndToken: TrackingToken = token
215
+
216
+ await unitOfWorkRunner(emptyMetadata(), async () => {
217
+ for (const sequencedEvent of batch) {
218
+ setResource(REPLAY_STATE_KEY, { replaying: isReplaying(batchEndToken) })
219
+
220
+ await deliverEvent(sequencedEvent)
221
+
222
+ batchEndToken = advanceToken(batchEndToken, sequencedEvent.sequence + 1n)
223
+ }
224
+
225
+ if (tokenStore) {
226
+ onPrepareCommit(async () => {
227
+ await tokenStore.store(name, segment, batchEndToken)
228
+ // Extend claim to prevent expiry during long batches
229
+ await tokenStore.extendClaim(name, segment, name)
230
+ })
231
+ }
232
+ })
233
+
234
+ token = batchEndToken
235
+ }
236
+
237
+ async function deliverEvent(sequencedEvent: SequencedEvent) {
238
+ const event = sequencedEvent.event
239
+ const eventName = qualifiedNameToString(event.name)
240
+ const handlers = handlerMap.get(eventName)
241
+ if (!handlers || handlers.length === 0) return
242
+
243
+ // D-44 wiring: write framework components into ALS at per-event invocation entry.
244
+ if (stateManager !== undefined) setResource(STATE_MANAGER_KEY, stateManager as any)
245
+ if (commandBus !== undefined) setResource(COMMAND_BUS_KEY, commandBus)
246
+ if (queryBus !== undefined) setResource(QUERY_BUS_KEY, queryBus)
247
+ // Optional per-event callback (e.g. monitoring hooks registered inside the UoW).
248
+ if (onEventDelivery) onEventDelivery()
249
+
250
+ for (const reg of handlers) {
251
+ try {
252
+ await reg.handler(event.payload, event.metadata)
253
+ } catch (err) {
254
+ await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
255
+ }
256
+ }
257
+ }
258
+
259
+ function scheduleImmediate() {
260
+ if (processTimer !== null) {
261
+ clearTimeout(processTimer)
262
+ }
263
+ processTimer = setTimeout(processAvailable, 0)
264
+ }
265
+
266
+ return {
267
+ get name() { return name },
268
+ get running() { return isRunning },
269
+ get position() { return token.position() },
270
+ get replaying() { return isReplaying(token) },
271
+
272
+ processingStatus() {
273
+ const status = new Map<number, EventProcessorStatus>()
274
+ status.set(segment, {
275
+ segmentId: segment,
276
+ position: token.position(),
277
+ replaying: isReplaying(token),
278
+ caughtUp,
279
+ error: lastError,
280
+ })
281
+ return status
282
+ },
283
+
284
+ async start() {
285
+ if (isRunning) return
286
+ await initialize()
287
+ isRunning = true
288
+ openStream()
289
+ scheduleImmediate()
290
+ },
291
+
292
+ stop() {
293
+ isRunning = false
294
+ if (processTimer !== null) {
295
+ clearTimeout(processTimer)
296
+ processTimer = null
297
+ }
298
+ if (stream) {
299
+ stream.close()
300
+ stream = null
301
+ }
302
+ },
303
+
304
+ async resetTokens(startPosition: bigint = 0n, resetContext?: unknown) {
305
+ if (isRunning) {
306
+ throw new Error(`Processor "${name}" must be stopped before resetting tokens`)
307
+ }
308
+
309
+ const headPosition = await eventSource.getHeadPosition()
310
+
311
+ if (headPosition <= startPosition) {
312
+ token = globalSequenceToken(startPosition)
313
+ } else {
314
+ token = replayToken(
315
+ globalSequenceToken(headPosition),
316
+ globalSequenceToken(startPosition),
317
+ resetContext,
318
+ )
319
+ }
320
+
321
+ if (tokenStore) {
322
+ await tokenStore.store(name, segment, token)
323
+ }
324
+
325
+ if (onReset) {
326
+ await onReset()
327
+ }
328
+ },
329
+
330
+ async splitSegment(_segmentId: number): Promise<boolean> {
331
+ if (!tokenStore) return false
332
+ console.warn(`Processor "${name}": segment splitting requires multi-segment support (not yet implemented)`)
333
+ return false
334
+ },
335
+
336
+ async mergeSegment(_segmentId: number): Promise<boolean> {
337
+ if (!tokenStore) return false
338
+ console.warn(`Processor "${name}": segment merging requires multi-segment support (not yet implemented)`)
339
+ return false
340
+ },
341
+
342
+ async releaseSegment(_segmentId: number): Promise<void> {
343
+ if (!tokenStore) return
344
+ await tokenStore.releaseClaim(name, segment, name)
345
+ },
346
+
347
+ supportsReset() {
348
+ return !isRunning
349
+ },
350
+ }
351
+ }
@@ -0,0 +1,169 @@
1
+ import { emptyMetadata, qualifiedNameToString } from "@kronos-ts/common"
2
+ import type { EventMessage } from "./message.js"
3
+ import type { EventHandlerRegistration } from "./handler.js"
4
+ import type { EventHandlerDefinition } from "./event-handler.js"
5
+ import type { UoWRunner } from "./unit-of-work.js"
6
+ import { runInNewUoW } from "./unit-of-work.js"
7
+ import type { EventProcessingErrorHandler } from "./tracking-event-processor.js"
8
+ import { loggingErrorHandler } from "./tracking-event-processor.js"
9
+ import type { SubscribableEventSource } from "./event-bus.js"
10
+ import type { CommandBus } from "./command-bus.js"
11
+ import type { QueryBus } from "./query-bus.js"
12
+ import type { HandlerEnhancerDefinition } from "./handler-enhancer.js"
13
+ import { setResource } from "./processing-state.js"
14
+ import { STATE_MANAGER_KEY } from "@kronos-ts/eventsourcing"
15
+ import { COMMAND_BUS_KEY } from "./send.js"
16
+ import { QUERY_BUS_KEY } from "./emit-update.js"
17
+
18
+ // Re-export for backward compatibility
19
+ export type { SubscribableEventSource } from "./event-bus.js"
20
+
21
+ /**
22
+ * A push-based event processor that subscribes directly to an event source.
23
+ *
24
+ * Unlike tracking/streaming processors, the subscribing processor:
25
+ * - Does **not** use a token store (no position tracking)
26
+ * - Does **not** support replay or reset
27
+ * - Processes events synchronously with the publisher
28
+ * - Is suitable for in-memory projections that don't need persistence
29
+ *
30
+ */
31
+ export interface SubscribingEventProcessor {
32
+ readonly name: string
33
+ readonly running: boolean
34
+ start(): void
35
+ stop(): void
36
+ /** Always returns false — subscribing processors don't support reset. */
37
+ supportsReset(): boolean
38
+ }
39
+
40
+ export interface SubscribingEventProcessorOptions {
41
+ name: string
42
+ eventSource: SubscribableEventSource
43
+ eventHandlers: ReadonlyArray<EventHandlerDefinition>
44
+ /** State manager injected into ALS at handler-invocation entry (D-44). */
45
+ stateManager?: unknown
46
+ /** Command bus injected into ALS at handler-invocation entry (D-44). */
47
+ commandBus?: CommandBus
48
+ /** Query bus injected into ALS at handler-invocation entry (D-44). */
49
+ queryBus?: QueryBus
50
+ /** Optional per-event callback fired inside the UoW before handler invocation (e.g. monitoring). */
51
+ onEventDelivery?: () => void
52
+ unitOfWorkRunner?: UoWRunner
53
+ errorHandler?: EventProcessingErrorHandler
54
+ /**
55
+ * Plan 09-01: optional handler enhancer applied to each event handler at
56
+ * registration time. Symmetric to TrackingEventProcessor.handlerEnhancer.
57
+ * When set, each registered handler is wrapped via wrapHandler with
58
+ * messageType "event" and the group name as handlerGroup.
59
+ */
60
+ handlerEnhancer?: HandlerEnhancerDefinition
61
+ }
62
+
63
+ /**
64
+ * Creates a subscribing event processor.
65
+ *
66
+ * The processor subscribes to the event source and processes events
67
+ * within a UnitOfWork as they arrive. Events are delivered on the
68
+ * publisher's call stack (synchronous with append).
69
+ */
70
+ export function createSubscribingEventProcessor(
71
+ options: SubscribingEventProcessorOptions,
72
+ ): SubscribingEventProcessor {
73
+ const {
74
+ name,
75
+ eventSource,
76
+ eventHandlers,
77
+ stateManager,
78
+ commandBus,
79
+ queryBus,
80
+ onEventDelivery,
81
+ unitOfWorkRunner = runInNewUoW,
82
+ errorHandler = loggingErrorHandler(name),
83
+ handlerEnhancer,
84
+ } = options
85
+
86
+ // Build handler lookup: eventName → handler[]
87
+ // Plan 09-01: when a handlerEnhancer is supplied, wrap each handler at
88
+ // registration time symmetric to TrackingEventProcessor. Plan 11-02:
89
+ // handlerGroup is now the processor name (no separate group identity).
90
+ const handlerMap = new Map<string, EventHandlerRegistration<any>[]>()
91
+ for (const reg of eventHandlers) {
92
+ const eventName = qualifiedNameToString(reg.descriptor.name)
93
+ const enhanced = handlerEnhancer
94
+ ? {
95
+ ...reg,
96
+ handler: handlerEnhancer.wrapHandler(reg.handler, {
97
+ messageType: "event" as const,
98
+ messageName: eventName,
99
+ handlerGroup: name,
100
+ }),
101
+ }
102
+ : reg
103
+ const existing = handlerMap.get(eventName)
104
+ if (existing) {
105
+ existing.push(enhanced as EventHandlerRegistration<any>)
106
+ } else {
107
+ handlerMap.set(eventName, [enhanced as EventHandlerRegistration<any>])
108
+ }
109
+ }
110
+
111
+ let isRunning = false
112
+ let unsubscribe: (() => void) | null = null
113
+
114
+ async function handleEvents(events: ReadonlyArray<EventMessage>) {
115
+ if (!isRunning || events.length === 0) return
116
+
117
+ await unitOfWorkRunner(emptyMetadata(), async () => {
118
+ for (const event of events) {
119
+ await deliverEvent(event)
120
+ }
121
+ })
122
+ }
123
+
124
+ async function deliverEvent(event: EventMessage) {
125
+ const eventName = qualifiedNameToString(event.name)
126
+ const handlers = handlerMap.get(eventName)
127
+ if (!handlers || handlers.length === 0) return
128
+
129
+ // D-44 wiring: write framework components into ALS at per-event invocation entry.
130
+ if (stateManager !== undefined) setResource(STATE_MANAGER_KEY, stateManager as any)
131
+ if (commandBus !== undefined) setResource(COMMAND_BUS_KEY, commandBus)
132
+ if (queryBus !== undefined) setResource(QUERY_BUS_KEY, queryBus)
133
+ // Optional per-event callback (e.g. monitoring hooks registered inside the UoW).
134
+ if (onEventDelivery) onEventDelivery()
135
+
136
+ for (const reg of handlers) {
137
+ try {
138
+ await reg.handler(event.payload, event.metadata)
139
+ } catch (err) {
140
+ // SubscribingEventProcessor doesn't have position tracking,
141
+ // so pass -1n as position indicator
142
+ await errorHandler.handleError(err, eventName, -1n)
143
+ }
144
+ }
145
+ }
146
+
147
+ return {
148
+ get name() { return name },
149
+ get running() { return isRunning },
150
+
151
+ start() {
152
+ if (isRunning) return
153
+ isRunning = true
154
+ unsubscribe = eventSource.subscribe(handleEvents)
155
+ },
156
+
157
+ stop() {
158
+ isRunning = false
159
+ if (unsubscribe) {
160
+ unsubscribe()
161
+ unsubscribe = null
162
+ }
163
+ },
164
+
165
+ supportsReset() {
166
+ return false
167
+ },
168
+ }
169
+ }