@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,330 @@
1
+ import type { EventMessage } from "./message.js"
2
+
3
+ /**
4
+ * A dead letter — an event that failed processing and was parked
5
+ * for later retry or manual intervention.
6
+ */
7
+ export interface DeadLetter {
8
+ /** The original event message that failed. */
9
+ readonly message: EventMessage
10
+ /** The error that caused the failure. */
11
+ readonly cause: Error
12
+ /** When this letter was first enqueued. */
13
+ readonly enqueuedAt: number
14
+ /** When this letter was last touched (enqueued or requeued). */
15
+ readonly lastTouched: number
16
+ /** Additional diagnostics metadata about the failure. */
17
+ readonly diagnostics: Record<string, unknown>
18
+ /** The sequence identifier this letter belongs to. */
19
+ readonly sequenceIdentifier: string
20
+ }
21
+
22
+ /**
23
+ * Decision on whether to enqueue a failed event as a dead letter.
24
+ */
25
+ export interface EnqueueDecision {
26
+ /** Whether to enqueue the failed event. */
27
+ readonly shouldEnqueue: boolean
28
+ /** Optional updated cause (may differ from original). */
29
+ readonly cause?: Error
30
+ /** Additional diagnostics to attach. */
31
+ readonly diagnostics?: Record<string, unknown>
32
+ }
33
+
34
+ /**
35
+ * Policy that determines whether a failed event should be dead-lettered.
36
+ */
37
+ export interface EnqueuePolicy {
38
+ /**
39
+ * Decide whether to enqueue a failed event.
40
+ * @param letter The dead letter candidate
41
+ * @param cause The error that caused the failure
42
+ */
43
+ decide(letter: DeadLetter, cause: Error): EnqueueDecision
44
+ }
45
+
46
+ /**
47
+ * Default policy: always enqueue with the original cause.
48
+ */
49
+ export function alwaysEnqueuePolicy(): EnqueuePolicy {
50
+ return {
51
+ decide() {
52
+ return { shouldEnqueue: true }
53
+ },
54
+ }
55
+ }
56
+
57
+ /**
58
+ * A sequenced dead letter queue that maintains ordering within sequences.
59
+ *
60
+ * Events in the same sequence (identified by `sequenceIdentifier`) are
61
+ * ordered by insertion. When a sequence has dead letters, subsequent
62
+ * events in that sequence are also dead-lettered to preserve ordering.
63
+ *
64
+ * Typical sequence identifier: aggregate ID or correlation ID.
65
+ *
66
+ * Database-backed implementations participate in the active UnitOfWork via
67
+ * the ALS-managed transaction (read through `getActiveTransaction()` /
68
+ * `getResource(TRANSACTION_KEY)`); no `ProcessingContext` parameter is
69
+ * threaded through the public surface.
70
+ */
71
+ export interface SequencedDeadLetterQueue {
72
+ /**
73
+ * Enqueue a dead letter into the given sequence.
74
+ */
75
+ enqueue(letter: DeadLetter): Promise<void>
76
+
77
+ /**
78
+ * Enqueue a dead letter only if the sequence already has dead letters.
79
+ * Used to block subsequent events in a failed sequence.
80
+ * The supplier is only called if the sequence exists (avoids creating
81
+ * unnecessary objects).
82
+ * Returns true if the letter was enqueued (sequence existed).
83
+ */
84
+ enqueueIfPresent(sequenceIdentifier: string, letterSupplier: () => DeadLetter): Promise<boolean>
85
+
86
+ /**
87
+ * Remove a dead letter from the queue (successfully reprocessed).
88
+ */
89
+ evict(sequenceIdentifier: string, letter: DeadLetter): Promise<void>
90
+
91
+ /**
92
+ * Re-insert a dead letter at the front of its sequence with updated properties.
93
+ */
94
+ requeue(
95
+ letter: DeadLetter,
96
+ update?: Partial<Pick<DeadLetter, "cause" | "diagnostics">>,
97
+ ): Promise<void>
98
+
99
+ /**
100
+ * Check if a sequence has any dead letters.
101
+ */
102
+ contains(sequenceIdentifier: string): Promise<boolean>
103
+
104
+ /**
105
+ * Get all dead letters in a sequence, in insertion order.
106
+ */
107
+ deadLetterSequence(sequenceIdentifier: string): Promise<DeadLetter[]>
108
+
109
+ /**
110
+ * Get all sequence identifiers that have dead letters.
111
+ */
112
+ sequenceIdentifiers(): Promise<string[]>
113
+
114
+ /**
115
+ * Process the oldest dead letter sequence matching the filter.
116
+ * For each letter in the sequence:
117
+ * - If processingTask returns `{ shouldEnqueue: false }`: letter is evicted, continue
118
+ * - If processingTask returns `{ shouldEnqueue: true }`: letter is requeued, stop
119
+ *
120
+ * Returns true if a sequence was processed.
121
+ */
122
+ process(
123
+ sequenceFilter: (sequenceId: string) => boolean,
124
+ processingTask: (letter: DeadLetter) => Promise<EnqueueDecision>,
125
+ ): Promise<boolean>
126
+
127
+ /** Total number of dead letters across all sequences. */
128
+ size(): number
129
+
130
+ /** Number of sequences with dead letters. */
131
+ amountOfSequences(): number
132
+
133
+ /** Clear all dead letters. */
134
+ clear(): Promise<void>
135
+
136
+ /**
137
+ * Check if the queue is full for the given sequence.
138
+ * Returns true if max sequences or max sequence size is reached.
139
+ */
140
+ isFull(sequenceIdentifier: string): boolean
141
+ }
142
+
143
+ /**
144
+ * Creates an in-memory dead letter queue.
145
+ *
146
+ * @param options.maxSequences Maximum number of sequences (default: 1024)
147
+ * @param options.maxSequenceSize Maximum letters per sequence (default: 1024)
148
+ */
149
+ export function createInMemoryDeadLetterQueue(options?: {
150
+ maxSequences?: number
151
+ maxSequenceSize?: number
152
+ }): SequencedDeadLetterQueue {
153
+ const maxSequences = options?.maxSequences ?? 1024
154
+ const maxSequenceSize = options?.maxSequenceSize ?? 1024
155
+
156
+ const sequences = new Map<string, DeadLetter[]>()
157
+ const processing = new Set<string>()
158
+
159
+ return {
160
+ async enqueue(letter) {
161
+ const seq = sequences.get(letter.sequenceIdentifier)
162
+ if (seq) {
163
+ if (seq.length >= maxSequenceSize) {
164
+ throw new DeadLetterQueueOverflowError(
165
+ `sequence "${letter.sequenceIdentifier}" has reached max size ${maxSequenceSize}`,
166
+ )
167
+ }
168
+ seq.push(letter)
169
+ } else {
170
+ if (sequences.size >= maxSequences) {
171
+ throw new DeadLetterQueueOverflowError(
172
+ `max sequences ${maxSequences} reached`,
173
+ )
174
+ }
175
+ sequences.set(letter.sequenceIdentifier, [letter])
176
+ }
177
+ },
178
+
179
+ async enqueueIfPresent(sequenceIdentifier, letterSupplier) {
180
+ const seq = sequences.get(sequenceIdentifier)
181
+ if (!seq) return false
182
+ if (seq.length >= maxSequenceSize) {
183
+ throw new DeadLetterQueueOverflowError(
184
+ `sequence "${sequenceIdentifier}" has reached max size ${maxSequenceSize}`,
185
+ )
186
+ }
187
+ seq.push(letterSupplier())
188
+ return true
189
+ },
190
+
191
+ async evict(sequenceIdentifier, letter) {
192
+ const seq = sequences.get(sequenceIdentifier)
193
+ if (!seq) return
194
+ const idx = seq.indexOf(letter)
195
+ if (idx >= 0) seq.splice(idx, 1)
196
+ if (seq.length === 0) sequences.delete(sequenceIdentifier)
197
+ },
198
+
199
+ async requeue(letter, update?) {
200
+ const seq = sequences.get(letter.sequenceIdentifier)
201
+ if (!seq) return
202
+
203
+ const updated: DeadLetter = {
204
+ ...letter,
205
+ lastTouched: Date.now(),
206
+ cause: update?.cause ?? letter.cause,
207
+ diagnostics: update?.diagnostics
208
+ ? { ...letter.diagnostics, ...update.diagnostics }
209
+ : letter.diagnostics,
210
+ }
211
+
212
+ const idx = seq.indexOf(letter)
213
+ if (idx >= 0) seq.splice(idx, 1)
214
+ seq.unshift(updated)
215
+ },
216
+
217
+ async contains(sequenceIdentifier) {
218
+ const seq = sequences.get(sequenceIdentifier)
219
+ return seq !== undefined && seq.length > 0
220
+ },
221
+
222
+ async deadLetterSequence(sequenceIdentifier) {
223
+ return sequences.get(sequenceIdentifier) ?? []
224
+ },
225
+
226
+ async sequenceIdentifiers() {
227
+ return [...sequences.keys()]
228
+ },
229
+
230
+ async process(sequenceFilter, processingTask) {
231
+ // Find oldest untaken sequence matching filter
232
+ let oldestId: string | undefined
233
+ let oldestTime = Infinity
234
+
235
+ for (const [id, letters] of sequences) {
236
+ if (processing.has(id)) continue
237
+ if (letters.length === 0) continue
238
+ if (!sequenceFilter(id)) continue
239
+
240
+ const firstTouched = letters[0]!.lastTouched
241
+ if (firstTouched < oldestTime) {
242
+ oldestTime = firstTouched
243
+ oldestId = id
244
+ }
245
+ }
246
+
247
+ if (!oldestId) return false
248
+
249
+ processing.add(oldestId)
250
+ try {
251
+ const letters = sequences.get(oldestId)
252
+ if (!letters) return false
253
+
254
+ // Process letters in order — take a snapshot of current letters
255
+ const snapshot = [...letters]
256
+ for (const letter of snapshot) {
257
+ const decision = await processingTask(letter)
258
+ if (decision.shouldEnqueue) {
259
+ // Requeue and stop — sequence is still blocked
260
+ await this.requeue(letter, {
261
+ cause: decision.cause,
262
+ diagnostics: decision.diagnostics,
263
+ })
264
+ return true
265
+ }
266
+ // Evict — successfully reprocessed
267
+ await this.evict(oldestId!, letter)
268
+ }
269
+ return true
270
+ } finally {
271
+ processing.delete(oldestId)
272
+ }
273
+ },
274
+
275
+ size() {
276
+ let total = 0
277
+ for (const letters of sequences.values()) {
278
+ total += letters.length
279
+ }
280
+ return total
281
+ },
282
+
283
+ amountOfSequences() {
284
+ return sequences.size
285
+ },
286
+
287
+ async clear() {
288
+ sequences.clear()
289
+ processing.clear()
290
+ },
291
+
292
+ isFull(sequenceIdentifier) {
293
+ const seq = sequences.get(sequenceIdentifier)
294
+ if (seq) {
295
+ return seq.length >= maxSequenceSize
296
+ }
297
+ return sequences.size >= maxSequences
298
+ },
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Thrown when the dead letter queue is full.
304
+ */
305
+ export class DeadLetterQueueOverflowError extends Error {
306
+ constructor(message: string) {
307
+ super(`Dead letter queue overflow: ${message}`)
308
+ this.name = "DeadLetterQueueOverflowError"
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Creates a DeadLetter from a failed event.
314
+ */
315
+ export function createDeadLetter(
316
+ message: EventMessage,
317
+ cause: Error,
318
+ sequenceIdentifier: string,
319
+ diagnostics?: Record<string, unknown>,
320
+ ): DeadLetter {
321
+ const now = Date.now()
322
+ return {
323
+ message,
324
+ cause,
325
+ enqueuedAt: now,
326
+ lastTouched: now,
327
+ diagnostics: diagnostics ?? {},
328
+ sequenceIdentifier,
329
+ }
330
+ }
@@ -0,0 +1,109 @@
1
+ import { qualifiedNameToString } from "@kronos-ts/common"
2
+ import type { EventMessage } from "./message.js"
3
+ import type { EventHandlerRegistration } from "./handler.js"
4
+ import type { SequencedEvent } from "./event-source.js"
5
+ import {
6
+ type SequencedDeadLetterQueue,
7
+ type EnqueuePolicy,
8
+ alwaysEnqueuePolicy,
9
+ createDeadLetter,
10
+ } from "./dead-letter-queue.js"
11
+
12
+ /**
13
+ * Options for dead-lettering event handler wrapper.
14
+ */
15
+ export interface DeadLetteringOptions {
16
+ /** The dead letter queue to use. */
17
+ queue: SequencedDeadLetterQueue
18
+ /** Policy deciding whether to dead-letter a failed event. Default: always. */
19
+ policy?: EnqueuePolicy
20
+ /**
21
+ * Extract a sequence identifier from an event. Events in the same
22
+ * sequence are ordered — if one fails, subsequent ones are blocked.
23
+ * Default: uses the first tag value or event name.
24
+ */
25
+ sequenceIdentifier?: (event: EventMessage) => string
26
+ }
27
+
28
+ /**
29
+ * Wraps event delivery with dead-letter support.
30
+ *
31
+ * When a handler fails:
32
+ * 1. Creates a DeadLetter and consults the EnqueuePolicy
33
+ * 2. If policy says enqueue: adds to DLQ, continues with next event
34
+ * 3. If policy says don't enqueue: error is swallowed
35
+ *
36
+ * When processing an event whose sequence already has dead letters:
37
+ * - The event is automatically dead-lettered (sequence is blocked)
38
+ * - This preserves ordering within the sequence
39
+ */
40
+ export function createDeadLetteringDelivery(options: DeadLetteringOptions) {
41
+ const {
42
+ queue,
43
+ policy = alwaysEnqueuePolicy(),
44
+ sequenceIdentifier = defaultSequenceIdentifier,
45
+ } = options
46
+
47
+ return {
48
+ /**
49
+ * Deliver an event to handlers, with dead-letter support.
50
+ *
51
+ * The DLQ participates in any active transaction via ALS — both the
52
+ * caller and the DLQ implementation read transactional state from the
53
+ * UnitOfWork ALS store, no explicit ProcessingContext is threaded.
54
+ */
55
+ async deliver(
56
+ sequencedEvent: SequencedEvent,
57
+ handlers: Array<EventHandlerRegistration<any>>,
58
+ ): Promise<void> {
59
+ const event = sequencedEvent.event
60
+ const seqId = sequenceIdentifier(event)
61
+
62
+ // If this sequence already has dead letters, block this event too
63
+ const blocked = await queue.enqueueIfPresent(
64
+ seqId,
65
+ () => createDeadLetter(
66
+ event,
67
+ new Error("Blocked: previous event in sequence failed"),
68
+ seqId,
69
+ { blocked: true, position: Number(sequencedEvent.sequence) },
70
+ ),
71
+ )
72
+ if (blocked) return
73
+
74
+ // Try to deliver to all handlers
75
+ for (const reg of handlers) {
76
+ try {
77
+ await reg.handler(event.payload, event.metadata)
78
+ } catch (err) {
79
+ const error = err instanceof Error ? err : new Error(String(err))
80
+ const letter = createDeadLetter(event, error, seqId, {
81
+ position: Number(sequencedEvent.sequence),
82
+ handlerName: qualifiedNameToString(reg.descriptor.name),
83
+ })
84
+
85
+ const decision = policy.decide(letter, error)
86
+ if (decision.shouldEnqueue) {
87
+ await queue.enqueue({
88
+ ...letter,
89
+ cause: decision.cause ?? letter.cause,
90
+ diagnostics: decision.diagnostics
91
+ ? { ...letter.diagnostics, ...decision.diagnostics }
92
+ : letter.diagnostics,
93
+ })
94
+ }
95
+ // Error is consumed by DLQ — don't propagate
96
+ return
97
+ }
98
+ }
99
+ },
100
+ }
101
+ }
102
+
103
+ function defaultSequenceIdentifier(event: EventMessage): string {
104
+ // Use first tag value if available, otherwise event name
105
+ if (event.tags && event.tags.length > 0) {
106
+ return event.tags[0]!.value
107
+ }
108
+ return qualifiedNameToString(event.name)
109
+ }
@@ -0,0 +1,176 @@
1
+ import type { z } from "zod"
2
+ import type { QualifiedName, Tag } from "@kronos-ts/common"
3
+ import { tagsFromRecord } from "@kronos-ts/common"
4
+
5
+ /**
6
+ * Describes a command message type — its name, payload schema,
7
+ * and optional result schema for typed gateway returns.
8
+ */
9
+ export interface CommandDescriptor<
10
+ P extends z.ZodType = z.ZodType,
11
+ R extends z.ZodType | undefined = undefined,
12
+ > {
13
+ readonly kind: "command"
14
+ readonly name: QualifiedName
15
+ /** Version of the command. Default: "1.0". */
16
+ readonly version: string
17
+ readonly payload: P
18
+ /** Optional result schema — enables typed return from `commandGateway.send()`. */
19
+ readonly result?: R
20
+ /**
21
+ * The payload field that contains the routing key for distributed command routing.
22
+ *
23
+ * Used by the command gateway to extract the routing key before dispatch.
24
+ * Commands with the same routing key are routed to the same handler instance.
25
+ */
26
+ readonly routingKey?: string
27
+ }
28
+
29
+ /**
30
+ * Describes an event message type — its name, payload schema, and tag derivation.
31
+ * Tags define how events are indexed for criteria-based sourcing.
32
+ */
33
+ export interface EventDescriptor<P extends z.ZodType = z.ZodType> {
34
+ readonly kind: "event"
35
+ readonly name: QualifiedName
36
+ readonly version: string
37
+ readonly payload: P
38
+ readonly tags?: (payload: z.infer<P>) => Tag[]
39
+ }
40
+
41
+ /**
42
+ * Describes a query message type — its name, payload schema,
43
+ * and optional result schema for typed gateway returns.
44
+ */
45
+ export interface QueryDescriptor<
46
+ P extends z.ZodType = z.ZodType,
47
+ R extends z.ZodType | undefined = undefined,
48
+ > {
49
+ readonly kind: "query"
50
+ readonly name: QualifiedName
51
+ /** Version of the query. Default: "1.0". */
52
+ readonly version: string
53
+ readonly payload: P
54
+ /** Optional result schema — enables typed return from `queryGateway.query()`. */
55
+ readonly result?: R
56
+ }
57
+
58
+ /** Any message descriptor. */
59
+ export type MessageDescriptor =
60
+ | CommandDescriptor
61
+ | EventDescriptor
62
+ | QueryDescriptor
63
+
64
+ /**
65
+ * Creates a command descriptor.
66
+ *
67
+ * Without result schema (void command):
68
+ * ```
69
+ * const CreateCourse = command({
70
+ * name: qn("university", "CreateCourse"),
71
+ * payload: z.object({ courseId: z.string(), name: z.string() }),
72
+ * routingKey: "courseId",
73
+ * })
74
+ * ```
75
+ *
76
+ * With result schema (typed return):
77
+ * ```
78
+ * const CreateCourse = command({
79
+ * name: qn("university", "CreateCourse"),
80
+ * payload: z.object({ courseId: z.string() }),
81
+ * result: z.object({ courseId: z.string() }),
82
+ * routingKey: "courseId",
83
+ * })
84
+ * // commandGateway.send(CreateCourse, { courseId: "cs-101" }) → Promise<{ courseId: string }>
85
+ * ```
86
+ */
87
+ export function command<P extends z.ZodType>(def: {
88
+ name: QualifiedName
89
+ version?: string
90
+ payload: P
91
+ routingKey?: string
92
+ }): CommandDescriptor<P, undefined>
93
+
94
+ export function command<P extends z.ZodType, R extends z.ZodType>(def: {
95
+ name: QualifiedName
96
+ version?: string
97
+ payload: P
98
+ result: R
99
+ routingKey?: string
100
+ }): CommandDescriptor<P, R>
101
+
102
+ export function command(def: any): CommandDescriptor {
103
+ return { kind: "command" as const, version: def.version ?? "1.0", ...def }
104
+ }
105
+
106
+ /**
107
+ * Creates an event descriptor.
108
+ *
109
+ * Tags can return `Tag[]` or a `Record<string, string>`:
110
+ * ```typescript
111
+ * event({
112
+ * name: qn("university", "CourseCreated"),
113
+ * payload: z.object({ courseId: z.string(), name: z.string() }),
114
+ * tags: (p) => ({ courseId: p.courseId }),
115
+ * })
116
+ * ```
117
+ */
118
+ export function event<P extends z.ZodType>(def: {
119
+ name: QualifiedName
120
+ version?: string
121
+ payload: P
122
+ tags?: (payload: z.infer<P>) => Tag[] | Record<string, string>
123
+ }): EventDescriptor<P> {
124
+ const rawTags = def.tags
125
+ const tags: ((payload: z.infer<P>) => Tag[]) | undefined = rawTags
126
+ ? (payload: z.infer<P>): Tag[] => {
127
+ const result = rawTags(payload)
128
+ return Array.isArray(result) ? result : tagsFromRecord(result)
129
+ }
130
+ : undefined
131
+ return {
132
+ kind: "event" as const,
133
+ name: def.name,
134
+ version: def.version ?? "1.0",
135
+ payload: def.payload,
136
+ ...(tags ? { tags } : {}),
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Creates a query descriptor.
142
+ *
143
+ * Without result schema:
144
+ * ```
145
+ * const GetCourse = query({
146
+ * name: qn("university", "GetCourseView"),
147
+ * payload: z.object({ courseId: z.string() }),
148
+ * })
149
+ * ```
150
+ *
151
+ * With result schema (typed return):
152
+ * ```
153
+ * const GetCourse = query({
154
+ * name: qn("university", "GetCourseView"),
155
+ * payload: z.object({ courseId: z.string() }),
156
+ * result: z.object({ courseId: z.string(), name: z.string() }),
157
+ * })
158
+ * // queryGateway.query(GetCourse, { courseId: "cs-101" }) → Promise<{ courseId: string, name: string }>
159
+ * ```
160
+ */
161
+ export function query<P extends z.ZodType>(def: {
162
+ name: QualifiedName
163
+ version?: string
164
+ payload: P
165
+ }): QueryDescriptor<P, undefined>
166
+
167
+ export function query<P extends z.ZodType, R extends z.ZodType>(def: {
168
+ name: QualifiedName
169
+ version?: string
170
+ payload: P
171
+ result: R
172
+ }): QueryDescriptor<P, R>
173
+
174
+ export function query(def: any): QueryDescriptor {
175
+ return { kind: "query" as const, version: def.version ?? "1.0", ...def }
176
+ }
@@ -0,0 +1,35 @@
1
+ import type { z } from "zod"
2
+ import { resourceKey, qualifiedNameToString, type ResourceKey } from "@kronos-ts/common"
3
+ import { requireInvocationPhase } from "./processing-state.js"
4
+ import type { QueryBus } from "./query-bus.js"
5
+ import type { QueryDescriptor } from "./descriptor.js"
6
+
7
+ /** Emit a subscription-query update from within the current processing context. */
8
+ export interface EmitUpdateFunction {
9
+ <Q extends z.ZodType>(
10
+ query: QueryDescriptor<Q>,
11
+ filter: (query: z.infer<Q>) => boolean,
12
+ update: unknown,
13
+ ): void
14
+ }
15
+
16
+ /**
17
+ * Resource key for the query bus component.
18
+ * Written by handling modules + processors at handler-invocation entry (D-44).
19
+ */
20
+ export const QUERY_BUS_KEY: ResourceKey<QueryBus> = resourceKey("queryBus")
21
+
22
+ /**
23
+ * Plan 04-01 (HDL-02 / D-42): module-level emitUpdate.
24
+ *
25
+ * Throws NoActiveUnitOfWork outside a UoW; throws WrongUoWPhase outside
26
+ * INVOCATION phase (D-43 mutator guard). Emits a subscription query update
27
+ * through the active query bus.
28
+ */
29
+ export const emitUpdate: EmitUpdateFunction = (queryDescriptor, filter, update) => {
30
+ const state = requireInvocationPhase() // D-43 mutator guard
31
+ const bus = state.resources.get(QUERY_BUS_KEY.symbol) as QueryBus | undefined
32
+ if (!bus) throw new Error("No query bus configured")
33
+ const queryName = qualifiedNameToString(queryDescriptor.name)
34
+ bus.emitUpdate(queryName, filter as (q: unknown) => boolean, update)
35
+ }
@@ -0,0 +1,45 @@
1
+ import type { EventMessage } from "./message.js"
2
+ import type { EventSink } from "./event-sink.js"
3
+
4
+ /**
5
+ * Subscribable source of events — push-based delivery.
6
+ */
7
+ export interface SubscribableEventSource {
8
+ /**
9
+ * Subscribe to events as they are published.
10
+ * Returns an unsubscribe function.
11
+ *
12
+ * @param handler Called with each batch of events as they are appended.
13
+ */
14
+ subscribe(handler: (events: ReadonlyArray<EventMessage>) => Promise<void>): () => void
15
+ }
16
+
17
+ /**
18
+ * The event bus — combines event publication (EventSink) with
19
+ * push-based subscription (SubscribableEventSource).
20
+ *
21
+ * In event sourcing setups, the EventStore serves as the EventBus.
22
+ * In non-event-sourcing setups, SimpleEventBus provides in-memory distribution.
23
+ */
24
+ export interface EventBus extends SubscribableEventSource, EventSink {}
25
+
26
+ /**
27
+ * In-memory event bus for non-event-sourcing scenarios.
28
+ * Publishes events directly to all subscribers.
29
+ */
30
+ export function createSimpleEventBus(): EventBus {
31
+ const subscribers = new Set<(events: ReadonlyArray<EventMessage>) => Promise<void>>()
32
+
33
+ return {
34
+ async publish(events) {
35
+ for (const subscriber of subscribers) {
36
+ try { await subscriber(events) } catch { /* ignore subscriber errors */ }
37
+ }
38
+ },
39
+
40
+ subscribe(handler) {
41
+ subscribers.add(handler)
42
+ return () => { subscribers.delete(handler) }
43
+ },
44
+ }
45
+ }