@kronos-ts/messaging 0.3.1 → 0.4.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.
- package/dist/dead-letter-listener.d.ts +36 -0
- package/dist/dead-letter-listener.d.ts.map +1 -0
- package/dist/dead-letter-listener.js +69 -0
- package/dist/dead-letter-listener.js.map +1 -0
- package/dist/dead-letter-queue.d.ts +5 -7
- package/dist/dead-letter-queue.d.ts.map +1 -1
- package/dist/dead-letter-queue.js +3 -13
- package/dist/dead-letter-queue.js.map +1 -1
- package/dist/dead-letter-reprocessor.d.ts +44 -0
- package/dist/dead-letter-reprocessor.d.ts.map +1 -0
- package/dist/dead-letter-reprocessor.js +48 -0
- package/dist/dead-letter-reprocessor.js.map +1 -0
- package/dist/dead-lettering-handler.d.ts +7 -4
- package/dist/dead-lettering-handler.d.ts.map +1 -1
- package/dist/dead-lettering-handler.js +36 -15
- package/dist/dead-lettering-handler.js.map +1 -1
- package/dist/enqueue-policy.d.ts +79 -0
- package/dist/enqueue-policy.d.ts.map +1 -0
- package/dist/enqueue-policy.js +95 -0
- package/dist/enqueue-policy.js.map +1 -0
- package/dist/event-processor-builder.d.ts +42 -3
- package/dist/event-processor-builder.d.ts.map +1 -1
- package/dist/event-processor-builder.js +49 -2
- package/dist/event-processor-builder.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/processor-configuration.d.ts +1 -1
- package/dist/processor-configuration.d.ts.map +1 -1
- package/dist/sequencing-policy.d.ts +41 -0
- package/dist/sequencing-policy.d.ts.map +1 -0
- package/dist/sequencing-policy.js +39 -0
- package/dist/sequencing-policy.js.map +1 -0
- package/dist/streaming-event-processor.d.ts +24 -0
- package/dist/streaming-event-processor.d.ts.map +1 -1
- package/dist/streaming-event-processor.js +76 -1
- package/dist/streaming-event-processor.js.map +1 -1
- package/dist/subscribing-event-processor.d.ts +6 -0
- package/dist/subscribing-event-processor.d.ts.map +1 -1
- package/dist/subscribing-event-processor.js.map +1 -1
- package/dist/tracking-event-processor.d.ts +27 -0
- package/dist/tracking-event-processor.d.ts.map +1 -1
- package/dist/tracking-event-processor.js +79 -1
- package/dist/tracking-event-processor.js.map +1 -1
- package/package.json +3 -3
- package/src/dead-letter-listener.ts +97 -0
- package/src/dead-letter-queue.ts +8 -17
- package/src/dead-letter-reprocessor.ts +95 -0
- package/src/dead-lettering-handler.ts +50 -26
- package/src/enqueue-policy.ts +118 -0
- package/src/event-processor-builder.ts +67 -3
- package/src/index.ts +33 -1
- package/src/processor-configuration.ts +1 -1
- package/src/sequencing-policy.ts +55 -0
- package/src/streaming-event-processor.ts +112 -1
- package/src/subscribing-event-processor.ts +6 -0
- package/src/tracking-event-processor.ts +118 -1
|
@@ -5,6 +5,14 @@ import type { StreamableEventSource, MessageStream, SequencedEvent } from "./eve
|
|
|
5
5
|
import type { UoWRunner } from "./unit-of-work.js"
|
|
6
6
|
import { runInNewUoW } from "./unit-of-work.js"
|
|
7
7
|
import type { TokenStore } from "./token-store.js"
|
|
8
|
+
import type { SequencedDeadLetterQueue, EnqueuePolicy, DeadLetter } from "./dead-letter-queue.js"
|
|
9
|
+
import type { SequencingPolicy } from "./sequencing-policy.js"
|
|
10
|
+
import { createDeadLetteringDelivery } from "./dead-lettering-handler.js"
|
|
11
|
+
import { type DeadLetterListener, noOpDeadLetterListener } from "./dead-letter-listener.js"
|
|
12
|
+
import {
|
|
13
|
+
type DeadLetterReprocessor,
|
|
14
|
+
createDeadLetterReprocessor,
|
|
15
|
+
} from "./dead-letter-reprocessor.js"
|
|
8
16
|
import type { EventProcessingErrorHandler } from "./tracking-event-processor.js"
|
|
9
17
|
import { loggingErrorHandler } from "./tracking-event-processor.js"
|
|
10
18
|
import type { HandlerEnhancerDefinition } from "./handler-enhancer.js"
|
|
@@ -55,6 +63,9 @@ export interface StreamingEventProcessor {
|
|
|
55
63
|
start(): Promise<void>
|
|
56
64
|
stop(): void
|
|
57
65
|
resetTokens(startPosition?: bigint, resetContext?: unknown): Promise<void>
|
|
66
|
+
/** Replay parked dead letters back through the handlers (oldest matching
|
|
67
|
+
* sequence). No-op returning false when no DLQ is configured. */
|
|
68
|
+
reprocessDeadLetters(filter?: (sequenceId: string) => boolean): Promise<boolean>
|
|
58
69
|
splitSegment(segmentId: number): Promise<boolean>
|
|
59
70
|
mergeSegment(segmentId: number): Promise<boolean>
|
|
60
71
|
releaseSegment(segmentId: number): Promise<void>
|
|
@@ -74,6 +85,24 @@ export interface StreamingEventProcessorOptions {
|
|
|
74
85
|
onEventDelivery?: () => void
|
|
75
86
|
unitOfWorkRunner?: UoWRunner
|
|
76
87
|
tokenStore?: TokenStore
|
|
88
|
+
/**
|
|
89
|
+
* Dead letter queue for poison-pill handling. When set, a handler failure
|
|
90
|
+
* parks the event in the DLQ (per {@link enqueuePolicy}) and the batch
|
|
91
|
+
* commits so the token advances past it — instead of redelivering the batch
|
|
92
|
+
* forever. The enqueue runs inside the batch UnitOfWork, so it commits in the
|
|
93
|
+
* same transaction as the token update.
|
|
94
|
+
*/
|
|
95
|
+
deadLetterQueue?: SequencedDeadLetterQueue
|
|
96
|
+
/** Decides whether a failed event is enqueued. Default: always enqueue. */
|
|
97
|
+
enqueuePolicy?: EnqueuePolicy
|
|
98
|
+
/** Decides each event's ordered sequence for the DLQ. Default: first tag value. */
|
|
99
|
+
sequencingPolicy?: SequencingPolicy
|
|
100
|
+
/** Observability hook for dead-letter lifecycle events. Default: no-op. */
|
|
101
|
+
deadLetterListener?: DeadLetterListener
|
|
102
|
+
/** When true, resetTokens() also clears this processor's DLQ (Axon allowReset). Default: false. */
|
|
103
|
+
resetClearsDeadLetters?: boolean
|
|
104
|
+
/** When set, automatically drains the DLQ on this interval (ms). Off by default. */
|
|
105
|
+
dlqRetryIntervalMs?: number
|
|
77
106
|
batchSize?: number
|
|
78
107
|
/** Delay before retrying after a batch failure, in ms. Backs off to avoid hot-looping a deterministic failure. */
|
|
79
108
|
errorBackoffMs?: number
|
|
@@ -97,7 +126,13 @@ export function createStreamingEventProcessor(
|
|
|
97
126
|
onEventDelivery,
|
|
98
127
|
unitOfWorkRunner = runInNewUoW,
|
|
99
128
|
tokenStore,
|
|
100
|
-
|
|
129
|
+
deadLetterQueue,
|
|
130
|
+
enqueuePolicy,
|
|
131
|
+
sequencingPolicy,
|
|
132
|
+
deadLetterListener = noOpDeadLetterListener(),
|
|
133
|
+
resetClearsDeadLetters = false,
|
|
134
|
+
dlqRetryIntervalMs,
|
|
135
|
+
batchSize = 1,
|
|
101
136
|
errorBackoffMs = 1000,
|
|
102
137
|
errorHandler = loggingErrorHandler(name),
|
|
103
138
|
handlerEnhancer,
|
|
@@ -106,6 +141,31 @@ export function createStreamingEventProcessor(
|
|
|
106
141
|
|
|
107
142
|
const segment = 0
|
|
108
143
|
|
|
144
|
+
// Option A: when a DLQ is configured, handler failures are caught and parked
|
|
145
|
+
// (not propagated), so the batch commits and the token advances past the
|
|
146
|
+
// poison pill. Built once; invoked inside the batch UnitOfWork by deliverEvent.
|
|
147
|
+
const deadLetterDelivery = deadLetterQueue
|
|
148
|
+
? createDeadLetteringDelivery({
|
|
149
|
+
queue: deadLetterQueue,
|
|
150
|
+
policy: enqueuePolicy,
|
|
151
|
+
sequencingPolicy,
|
|
152
|
+
listener: deadLetterListener,
|
|
153
|
+
})
|
|
154
|
+
: undefined
|
|
155
|
+
|
|
156
|
+
// Reprocessor: replays a parked letter through the same handlers, with the
|
|
157
|
+
// same ALS resources as live delivery, so dependencies resolve identically.
|
|
158
|
+
const reprocessor: DeadLetterReprocessor | undefined = deadLetterQueue
|
|
159
|
+
? createDeadLetterReprocessor({
|
|
160
|
+
queue: deadLetterQueue,
|
|
161
|
+
policy: enqueuePolicy,
|
|
162
|
+
unitOfWorkRunner,
|
|
163
|
+
listener: deadLetterListener,
|
|
164
|
+
replay: replayDeadLetter,
|
|
165
|
+
})
|
|
166
|
+
: undefined
|
|
167
|
+
let dlqRetryTimer: ReturnType<typeof setInterval> | null = null
|
|
168
|
+
|
|
109
169
|
const handlerMap = new Map<string, Array<EventHandlerRegistration<any>>>()
|
|
110
170
|
for (const reg of eventHandlers) {
|
|
111
171
|
const eventName = qualifiedNameToString(reg.descriptor.name)
|
|
@@ -268,6 +328,14 @@ export function createStreamingEventProcessor(
|
|
|
268
328
|
// Optional per-event callback (e.g. monitoring hooks registered inside the UoW).
|
|
269
329
|
if (onEventDelivery) onEventDelivery()
|
|
270
330
|
|
|
331
|
+
// DLQ path: park failures, keep the batch committable (Option A). The DLQ
|
|
332
|
+
// delivery enforces per-sequence ordering and never propagates, so the
|
|
333
|
+
// errorHandler / batch-redelivery path is bypassed while a DLQ is active.
|
|
334
|
+
if (deadLetterDelivery) {
|
|
335
|
+
await deadLetterDelivery.deliver(sequencedEvent, handlers)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
271
339
|
for (const reg of handlers) {
|
|
272
340
|
try {
|
|
273
341
|
await reg.handler({ ...event, sequence: sequencedEvent.sequence })
|
|
@@ -277,6 +345,27 @@ export function createStreamingEventProcessor(
|
|
|
277
345
|
}
|
|
278
346
|
}
|
|
279
347
|
|
|
348
|
+
// Replay a parked dead letter through the handlers, re-establishing the same
|
|
349
|
+
// ALS resources as live delivery. Throws on the first handler failure so the
|
|
350
|
+
// reprocessor can requeue the letter (delivery is at-least-once → handlers
|
|
351
|
+
// must be idempotent).
|
|
352
|
+
async function replayDeadLetter(letter: DeadLetter): Promise<void> {
|
|
353
|
+
const event = letter.message
|
|
354
|
+
const eventName = qualifiedNameToString(event.name)
|
|
355
|
+
const handlers = handlerMap.get(eventName)
|
|
356
|
+
if (!handlers || handlers.length === 0) return
|
|
357
|
+
|
|
358
|
+
if (stateManager !== undefined) setResource(STATE_MANAGER_KEY, stateManager as any)
|
|
359
|
+
if (commandBus !== undefined) setResource(COMMAND_BUS_KEY, commandBus)
|
|
360
|
+
if (queryBus !== undefined) setResource(QUERY_BUS_KEY, queryBus)
|
|
361
|
+
|
|
362
|
+
const position =
|
|
363
|
+
typeof letter.diagnostics.position === "number" ? BigInt(letter.diagnostics.position) : 0n
|
|
364
|
+
for (const reg of handlers) {
|
|
365
|
+
await reg.handler({ ...event, sequence: position })
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
280
369
|
function scheduleImmediate() {
|
|
281
370
|
if (processTimer !== null) {
|
|
282
371
|
clearTimeout(processTimer)
|
|
@@ -308,6 +397,13 @@ export function createStreamingEventProcessor(
|
|
|
308
397
|
isRunning = true
|
|
309
398
|
openStream()
|
|
310
399
|
scheduleImmediate()
|
|
400
|
+
if (reprocessor && dlqRetryIntervalMs && dlqRetryIntervalMs > 0) {
|
|
401
|
+
dlqRetryTimer = setInterval(() => {
|
|
402
|
+
void reprocessor.reprocessAll().catch((err) => {
|
|
403
|
+
console.error(`Event processor "${name}": scheduled DLQ drain failed:`, err)
|
|
404
|
+
})
|
|
405
|
+
}, dlqRetryIntervalMs)
|
|
406
|
+
}
|
|
311
407
|
},
|
|
312
408
|
|
|
313
409
|
stop() {
|
|
@@ -316,12 +412,21 @@ export function createStreamingEventProcessor(
|
|
|
316
412
|
clearTimeout(processTimer)
|
|
317
413
|
processTimer = null
|
|
318
414
|
}
|
|
415
|
+
if (dlqRetryTimer !== null) {
|
|
416
|
+
clearInterval(dlqRetryTimer)
|
|
417
|
+
dlqRetryTimer = null
|
|
418
|
+
}
|
|
319
419
|
if (stream) {
|
|
320
420
|
stream.close()
|
|
321
421
|
stream = null
|
|
322
422
|
}
|
|
323
423
|
},
|
|
324
424
|
|
|
425
|
+
async reprocessDeadLetters(filter?: (sequenceId: string) => boolean) {
|
|
426
|
+
if (!reprocessor) return false
|
|
427
|
+
return reprocessor.reprocess(filter)
|
|
428
|
+
},
|
|
429
|
+
|
|
325
430
|
async resetTokens(startPosition: bigint = 0n, resetContext?: unknown) {
|
|
326
431
|
if (isRunning) {
|
|
327
432
|
throw new Error(`Processor "${name}" must be stopped before resetting tokens`)
|
|
@@ -343,6 +448,12 @@ export function createStreamingEventProcessor(
|
|
|
343
448
|
await tokenStore.store(name, segment, token)
|
|
344
449
|
}
|
|
345
450
|
|
|
451
|
+
// Axon allowReset: only clear parked letters when opted in (a replay
|
|
452
|
+
// re-derives view state, making prior dead letters meaningless).
|
|
453
|
+
if (resetClearsDeadLetters && deadLetterQueue) {
|
|
454
|
+
await deadLetterQueue.clear()
|
|
455
|
+
}
|
|
456
|
+
|
|
346
457
|
if (onReset) {
|
|
347
458
|
await onReset()
|
|
348
459
|
}
|
|
@@ -27,6 +27,12 @@ export type { SubscribableEventSource } from "./event-bus.js"
|
|
|
27
27
|
* - Processes events synchronously with the publisher
|
|
28
28
|
* - Is suitable for in-memory projections that don't need persistence
|
|
29
29
|
*
|
|
30
|
+
* It also does **not** support a dead-letter queue — deliberately. A DLQ exists
|
|
31
|
+
* to let a processor advance its token past a poison pill and reprocess later;
|
|
32
|
+
* a subscribing processor has no token and runs in the publisher's call stack,
|
|
33
|
+
* so a failure must surface to the publisher (via the error handler), not be
|
|
34
|
+
* silently parked. Use a tracking/streaming processor with `.deadLetterQueue()`
|
|
35
|
+
* when you need dead-lettering.
|
|
30
36
|
*/
|
|
31
37
|
export interface SubscribingEventProcessor {
|
|
32
38
|
readonly name: string
|
|
@@ -5,6 +5,14 @@ import type { StreamableEventSource, MessageStream, SequencedEvent } from "./eve
|
|
|
5
5
|
import type { UoWRunner } from "./unit-of-work.js"
|
|
6
6
|
import { runInNewUoW } from "./unit-of-work.js"
|
|
7
7
|
import type { TokenStore } from "./token-store.js"
|
|
8
|
+
import type { SequencedDeadLetterQueue, EnqueuePolicy, DeadLetter } from "./dead-letter-queue.js"
|
|
9
|
+
import type { SequencingPolicy } from "./sequencing-policy.js"
|
|
10
|
+
import { createDeadLetteringDelivery } from "./dead-lettering-handler.js"
|
|
11
|
+
import { type DeadLetterListener, noOpDeadLetterListener } from "./dead-letter-listener.js"
|
|
12
|
+
import {
|
|
13
|
+
type DeadLetterReprocessor,
|
|
14
|
+
createDeadLetterReprocessor,
|
|
15
|
+
} from "./dead-letter-reprocessor.js"
|
|
8
16
|
import type { TrackingToken } from "./tracking-token.js"
|
|
9
17
|
import {
|
|
10
18
|
globalSequenceToken,
|
|
@@ -47,6 +55,12 @@ export interface TrackingEventProcessor {
|
|
|
47
55
|
* The processor must be stopped before calling this.
|
|
48
56
|
*/
|
|
49
57
|
resetTokens(startPosition?: bigint, resetContext?: unknown): Promise<void>
|
|
58
|
+
/**
|
|
59
|
+
* Replay parked dead letters back through the handlers (the oldest matching
|
|
60
|
+
* sequence). No-op returning false when no DLQ is configured. Safe to call
|
|
61
|
+
* whether or not the processor is running.
|
|
62
|
+
*/
|
|
63
|
+
reprocessDeadLetters(filter?: (sequenceId: string) => boolean): Promise<boolean>
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
export interface TrackingEventProcessorOptions {
|
|
@@ -63,6 +77,24 @@ export interface TrackingEventProcessorOptions {
|
|
|
63
77
|
onEventDelivery?: () => void
|
|
64
78
|
unitOfWorkRunner?: UoWRunner
|
|
65
79
|
tokenStore?: TokenStore
|
|
80
|
+
/**
|
|
81
|
+
* Dead letter queue for poison-pill handling. When set, a handler failure
|
|
82
|
+
* parks the event in the DLQ (per {@link enqueuePolicy}) and the batch
|
|
83
|
+
* commits so the token advances past it — instead of redelivering the batch
|
|
84
|
+
* forever. The enqueue runs inside the batch UnitOfWork, so it commits in the
|
|
85
|
+
* same transaction as the token update.
|
|
86
|
+
*/
|
|
87
|
+
deadLetterQueue?: SequencedDeadLetterQueue
|
|
88
|
+
/** Decides whether a failed event is enqueued. Default: always enqueue. */
|
|
89
|
+
enqueuePolicy?: EnqueuePolicy
|
|
90
|
+
/** Decides each event's ordered sequence for the DLQ. Default: first tag value. */
|
|
91
|
+
sequencingPolicy?: SequencingPolicy
|
|
92
|
+
/** Observability hook for dead-letter lifecycle events. Default: no-op. */
|
|
93
|
+
deadLetterListener?: DeadLetterListener
|
|
94
|
+
/** When true, resetTokens() also clears this processor's DLQ (Axon allowReset). Default: false. */
|
|
95
|
+
resetClearsDeadLetters?: boolean
|
|
96
|
+
/** When set, automatically drains the DLQ on this interval (ms). Off by default. */
|
|
97
|
+
dlqRetryIntervalMs?: number
|
|
66
98
|
/** Polling interval when no events are available (ms). Default: 500. */
|
|
67
99
|
pollingIntervalMs?: number
|
|
68
100
|
batchSize?: number
|
|
@@ -118,8 +150,14 @@ export function createTrackingEventProcessor(
|
|
|
118
150
|
onEventDelivery,
|
|
119
151
|
unitOfWorkRunner = runInNewUoW,
|
|
120
152
|
tokenStore,
|
|
153
|
+
deadLetterQueue,
|
|
154
|
+
enqueuePolicy,
|
|
155
|
+
sequencingPolicy,
|
|
156
|
+
deadLetterListener = noOpDeadLetterListener(),
|
|
157
|
+
resetClearsDeadLetters = false,
|
|
158
|
+
dlqRetryIntervalMs,
|
|
121
159
|
pollingIntervalMs = 500,
|
|
122
|
-
batchSize =
|
|
160
|
+
batchSize = 1,
|
|
123
161
|
errorHandler = loggingErrorHandler(name),
|
|
124
162
|
handlerEnhancer,
|
|
125
163
|
onReset,
|
|
@@ -127,6 +165,31 @@ export function createTrackingEventProcessor(
|
|
|
127
165
|
|
|
128
166
|
const segment = 0
|
|
129
167
|
|
|
168
|
+
// Option A: when a DLQ is configured, handler failures are caught and parked
|
|
169
|
+
// (not propagated), so the batch commits and the token advances past the
|
|
170
|
+
// poison pill. Built once; invoked inside the batch UnitOfWork by deliverEvent.
|
|
171
|
+
const deadLetterDelivery = deadLetterQueue
|
|
172
|
+
? createDeadLetteringDelivery({
|
|
173
|
+
queue: deadLetterQueue,
|
|
174
|
+
policy: enqueuePolicy,
|
|
175
|
+
sequencingPolicy,
|
|
176
|
+
listener: deadLetterListener,
|
|
177
|
+
})
|
|
178
|
+
: undefined
|
|
179
|
+
|
|
180
|
+
// Reprocessor: replays a parked letter through the same handlers, with the
|
|
181
|
+
// same ALS resources as live delivery, so dependencies resolve identically.
|
|
182
|
+
const reprocessor: DeadLetterReprocessor | undefined = deadLetterQueue
|
|
183
|
+
? createDeadLetterReprocessor({
|
|
184
|
+
queue: deadLetterQueue,
|
|
185
|
+
policy: enqueuePolicy,
|
|
186
|
+
unitOfWorkRunner,
|
|
187
|
+
listener: deadLetterListener,
|
|
188
|
+
replay: replayDeadLetter,
|
|
189
|
+
})
|
|
190
|
+
: undefined
|
|
191
|
+
let dlqRetryTimer: ReturnType<typeof setInterval> | null = null
|
|
192
|
+
|
|
130
193
|
const handlerMap = new Map<string, Array<EventHandlerRegistration<any>>>()
|
|
131
194
|
for (const reg of eventHandlers) {
|
|
132
195
|
const eventName = qualifiedNameToString(reg.descriptor.name)
|
|
@@ -276,6 +339,14 @@ export function createTrackingEventProcessor(
|
|
|
276
339
|
// Optional per-event callback (e.g. monitoring hooks registered inside the UoW).
|
|
277
340
|
if (onEventDelivery) onEventDelivery()
|
|
278
341
|
|
|
342
|
+
// DLQ path: park failures, keep the batch committable (Option A). The DLQ
|
|
343
|
+
// delivery enforces per-sequence ordering and never propagates, so the
|
|
344
|
+
// errorHandler / batch-redelivery path is bypassed while a DLQ is active.
|
|
345
|
+
if (deadLetterDelivery) {
|
|
346
|
+
await deadLetterDelivery.deliver(sequencedEvent, handlers)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
279
350
|
for (const reg of handlers) {
|
|
280
351
|
try {
|
|
281
352
|
await reg.handler({ ...event, sequence: sequencedEvent.sequence })
|
|
@@ -285,6 +356,27 @@ export function createTrackingEventProcessor(
|
|
|
285
356
|
}
|
|
286
357
|
}
|
|
287
358
|
|
|
359
|
+
// Replay a parked dead letter through the handlers, re-establishing the same
|
|
360
|
+
// ALS resources as live delivery. Throws on the first handler failure so the
|
|
361
|
+
// reprocessor can requeue the letter (delivery is at-least-once → handlers
|
|
362
|
+
// must be idempotent).
|
|
363
|
+
async function replayDeadLetter(letter: DeadLetter): Promise<void> {
|
|
364
|
+
const event = letter.message
|
|
365
|
+
const eventName = qualifiedNameToString(event.name)
|
|
366
|
+
const handlers = handlerMap.get(eventName)
|
|
367
|
+
if (!handlers || handlers.length === 0) return
|
|
368
|
+
|
|
369
|
+
if (stateManager !== undefined) setResource(STATE_MANAGER_KEY, stateManager as any)
|
|
370
|
+
if (commandBus !== undefined) setResource(COMMAND_BUS_KEY, commandBus)
|
|
371
|
+
if (queryBus !== undefined) setResource(QUERY_BUS_KEY, queryBus)
|
|
372
|
+
|
|
373
|
+
const position =
|
|
374
|
+
typeof letter.diagnostics.position === "number" ? BigInt(letter.diagnostics.position) : 0n
|
|
375
|
+
for (const reg of handlers) {
|
|
376
|
+
await reg.handler({ ...event, sequence: position })
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
288
380
|
function scheduleImmediate() {
|
|
289
381
|
if (pollTimer !== null) {
|
|
290
382
|
clearTimeout(pollTimer)
|
|
@@ -303,6 +395,15 @@ export function createTrackingEventProcessor(
|
|
|
303
395
|
await initialize()
|
|
304
396
|
isRunning = true
|
|
305
397
|
poll()
|
|
398
|
+
// Optional scheduled DLQ drain — operators can also trigger reprocessing
|
|
399
|
+
// manually via reprocessDeadLetters().
|
|
400
|
+
if (reprocessor && dlqRetryIntervalMs && dlqRetryIntervalMs > 0) {
|
|
401
|
+
dlqRetryTimer = setInterval(() => {
|
|
402
|
+
void reprocessor.reprocessAll().catch((err) => {
|
|
403
|
+
console.error(`Event processor "${name}": scheduled DLQ drain failed:`, err)
|
|
404
|
+
})
|
|
405
|
+
}, dlqRetryIntervalMs)
|
|
406
|
+
}
|
|
306
407
|
},
|
|
307
408
|
|
|
308
409
|
stop() {
|
|
@@ -311,12 +412,21 @@ export function createTrackingEventProcessor(
|
|
|
311
412
|
clearTimeout(pollTimer)
|
|
312
413
|
pollTimer = null
|
|
313
414
|
}
|
|
415
|
+
if (dlqRetryTimer !== null) {
|
|
416
|
+
clearInterval(dlqRetryTimer)
|
|
417
|
+
dlqRetryTimer = null
|
|
418
|
+
}
|
|
314
419
|
if (stream) {
|
|
315
420
|
stream.close()
|
|
316
421
|
stream = null
|
|
317
422
|
}
|
|
318
423
|
},
|
|
319
424
|
|
|
425
|
+
async reprocessDeadLetters(filter?: (sequenceId: string) => boolean) {
|
|
426
|
+
if (!reprocessor) return false
|
|
427
|
+
return reprocessor.reprocess(filter)
|
|
428
|
+
},
|
|
429
|
+
|
|
320
430
|
async resetTokens(startPosition: bigint = 0n, resetContext?: unknown) {
|
|
321
431
|
if (isRunning) {
|
|
322
432
|
throw new Error(`Processor "${name}" must be stopped before resetting tokens`)
|
|
@@ -338,6 +448,13 @@ export function createTrackingEventProcessor(
|
|
|
338
448
|
await tokenStore.store(name, segment, token)
|
|
339
449
|
}
|
|
340
450
|
|
|
451
|
+
// Axon allowReset: a replay re-derives view state from scratch, so stale
|
|
452
|
+
// dead letters from the prior run are meaningless. Only clear when opted
|
|
453
|
+
// in — otherwise parked letters survive a reset for manual handling.
|
|
454
|
+
if (resetClearsDeadLetters && deadLetterQueue) {
|
|
455
|
+
await deadLetterQueue.clear()
|
|
456
|
+
}
|
|
457
|
+
|
|
341
458
|
if (onReset) {
|
|
342
459
|
await onReset()
|
|
343
460
|
}
|