@kronos-ts/messaging 0.2.0 → 0.3.1
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/command-handler.d.ts +15 -15
- package/dist/command-handler.d.ts.map +1 -1
- package/dist/command-handler.js.map +1 -1
- package/dist/command-handling-module.js +2 -2
- package/dist/command-handling-module.js.map +1 -1
- package/dist/dead-lettering-handler.js +1 -1
- package/dist/dead-lettering-handler.js.map +1 -1
- package/dist/event-handler.d.ts +5 -5
- package/dist/event-handler.d.ts.map +1 -1
- package/dist/event-handler.js +2 -2
- package/dist/event-handler.js.map +1 -1
- package/dist/event-processor-builder.d.ts +3 -3
- package/dist/event-processor-builder.js +3 -3
- package/dist/gateway.d.ts +7 -5
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +9 -12
- package/dist/gateway.js.map +1 -1
- package/dist/handler.d.ts +13 -13
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/intercepting-command-bus.d.ts +1 -1
- package/dist/intercepting-command-bus.d.ts.map +1 -1
- package/dist/intercepting-command-bus.js +3 -4
- package/dist/intercepting-command-bus.js.map +1 -1
- package/dist/intercepting-query-bus.d.ts +1 -1
- package/dist/intercepting-query-bus.d.ts.map +1 -1
- package/dist/intercepting-query-bus.js +3 -4
- package/dist/intercepting-query-bus.js.map +1 -1
- package/dist/interceptor.d.ts +18 -3
- package/dist/interceptor.d.ts.map +1 -1
- package/dist/message.d.ts +4 -0
- package/dist/message.d.ts.map +1 -1
- package/dist/query-handler.d.ts +5 -5
- package/dist/query-handler.d.ts.map +1 -1
- package/dist/query-handler.js +2 -2
- package/dist/query-handler.js.map +1 -1
- package/dist/query-handling-module.js +1 -1
- package/dist/query-handling-module.js.map +1 -1
- package/dist/simple-command-bus.d.ts +11 -4
- package/dist/simple-command-bus.d.ts.map +1 -1
- package/dist/simple-command-bus.js +16 -10
- package/dist/simple-command-bus.js.map +1 -1
- package/dist/streaming-event-processor.d.ts +2 -0
- package/dist/streaming-event-processor.d.ts.map +1 -1
- package/dist/streaming-event-processor.js +22 -3
- package/dist/streaming-event-processor.js.map +1 -1
- package/dist/subscribing-event-processor.js +1 -1
- package/dist/subscribing-event-processor.js.map +1 -1
- package/dist/tracking-event-processor.d.ts.map +1 -1
- package/dist/tracking-event-processor.js +11 -1
- package/dist/tracking-event-processor.js.map +1 -1
- package/package.json +1 -1
- package/src/command-handler.ts +15 -28
- package/src/command-handling-module.ts +2 -2
- package/src/dead-lettering-handler.ts +1 -1
- package/src/event-handler.ts +5 -8
- package/src/event-processor-builder.ts +3 -3
- package/src/gateway.ts +14 -22
- package/src/handler.ts +13 -22
- package/src/index.ts +1 -1
- package/src/intercepting-command-bus.ts +7 -6
- package/src/intercepting-query-bus.ts +7 -6
- package/src/interceptor.ts +21 -4
- package/src/message.ts +5 -0
- package/src/query-handler.ts +5 -5
- package/src/query-handling-module.ts +1 -1
- package/src/simple-command-bus.ts +17 -11
- package/src/streaming-event-processor.ts +29 -8
- package/src/subscribing-event-processor.ts +1 -1
- package/src/tracking-event-processor.ts +11 -1
package/src/event-handler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { z } from "zod"
|
|
2
|
-
import type { Metadata } from "@kronos-ts/common"
|
|
3
2
|
import type { EventDescriptor } from "./descriptor.js"
|
|
3
|
+
import type { SequencedEventMessage } from "./message.js"
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Singular factory — mirrors commandHandler / queryHandler.
|
|
@@ -16,18 +16,15 @@ import type { EventDescriptor } from "./descriptor.js"
|
|
|
16
16
|
export interface EventHandlerDefinition<P extends z.ZodType = z.ZodType> {
|
|
17
17
|
readonly kind: "event-handler"
|
|
18
18
|
readonly descriptor: EventDescriptor<P>
|
|
19
|
-
readonly handler: (
|
|
20
|
-
event: z.infer<P>,
|
|
21
|
-
metadata: Metadata,
|
|
22
|
-
) => Promise<void> | void
|
|
19
|
+
readonly handler: (message: SequencedEventMessage<z.infer<P>>) => Promise<void> | void
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
/**
|
|
26
23
|
* Defines a singular event handler.
|
|
27
24
|
*
|
|
28
25
|
* ```
|
|
29
|
-
* const onCourseCreated = eventHandler(CourseCreated, async (
|
|
30
|
-
* await db.courses.insert({ id:
|
|
26
|
+
* const onCourseCreated = eventHandler(CourseCreated, async ({ payload, metadata, timestamp }) => {
|
|
27
|
+
* await db.courses.insert({ id: payload.courseId, name: payload.name, createdAt: timestamp })
|
|
31
28
|
* })
|
|
32
29
|
* ```
|
|
33
30
|
*
|
|
@@ -38,7 +35,7 @@ export interface EventHandlerDefinition<P extends z.ZodType = z.ZodType> {
|
|
|
38
35
|
*/
|
|
39
36
|
export function eventHandler<P extends z.ZodType>(
|
|
40
37
|
descriptor: EventDescriptor<P>,
|
|
41
|
-
handler: (
|
|
38
|
+
handler: (message: SequencedEventMessage<z.infer<P>>) => Promise<void> | void,
|
|
42
39
|
): EventHandlerDefinition<P> {
|
|
43
40
|
return { kind: "event-handler", descriptor, handler }
|
|
44
41
|
}
|
|
@@ -60,8 +60,8 @@ export type EventProcessorModule = TrackingProcessorModule | SubscribingProcesso
|
|
|
60
60
|
* position via a token store, and support replay/reset.
|
|
61
61
|
*
|
|
62
62
|
* ```typescript
|
|
63
|
-
* const onCreated = eventHandler(CourseCreated, async (e) => { ... })
|
|
64
|
-
* const onCapChanged = eventHandler(CourseCapacityChanged, async (e) => { ... })
|
|
63
|
+
* const onCreated = eventHandler(CourseCreated, async ({ payload: e }) => { ... })
|
|
64
|
+
* const onCapChanged = eventHandler(CourseCapacityChanged, async ({ payload: e }) => { ... })
|
|
65
65
|
*
|
|
66
66
|
* trackingProcessor("course-projection")
|
|
67
67
|
* .eventHandlers(onCreated, onCapChanged)
|
|
@@ -186,7 +186,7 @@ export class TrackingProcessorBuilder {
|
|
|
186
186
|
* as they are appended. No token store, no position tracking, no replay.
|
|
187
187
|
*
|
|
188
188
|
* ```typescript
|
|
189
|
-
* const onNotification = eventHandler(NotificationRaised, async (e) => { ... })
|
|
189
|
+
* const onNotification = eventHandler(NotificationRaised, async ({ payload: e }) => { ... })
|
|
190
190
|
*
|
|
191
191
|
* subscribingProcessor("notifications")
|
|
192
192
|
* .eventHandlers(onNotification)
|
package/src/gateway.ts
CHANGED
|
@@ -74,32 +74,24 @@ export interface QueryGateway {
|
|
|
74
74
|
/**
|
|
75
75
|
* Creates a command gateway backed by a command bus.
|
|
76
76
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* AF5-aligned (CLAUDE.md command model): the gateway is a thin message-builder
|
|
78
|
+
* and does NOT establish a UnitOfWork. The command bus owns the single
|
|
79
|
+
* per-command UoW (and, via the configured `unitOfWorkFactory`, its
|
|
80
|
+
* transaction) — see `createSimpleCommandBus`. Dispatch interceptors run on
|
|
81
|
+
* the message before it crosses into that UoW; the dispatch-side hook channel
|
|
82
|
+
* is ALS, not a threaded ProcessingContext.
|
|
81
83
|
*/
|
|
82
|
-
export function createCommandGateway(
|
|
83
|
-
bus: CommandBus,
|
|
84
|
-
unitOfWorkRunner: UoWRunner = runInNewUoW,
|
|
85
|
-
): CommandGateway {
|
|
84
|
+
export function createCommandGateway(bus: CommandBus): CommandGateway {
|
|
86
85
|
return {
|
|
87
86
|
async send(descriptor, payload, metadata) {
|
|
88
87
|
const resolvedMetadata = metadata ?? emptyMetadata()
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
identifier: generateIdentifier(),
|
|
97
|
-
name: descriptor.name,
|
|
98
|
-
payload,
|
|
99
|
-
metadata: resolvedMetadata,
|
|
100
|
-
timestamp: Date.now(),
|
|
101
|
-
}) as Promise<any>,
|
|
102
|
-
) as any
|
|
88
|
+
return bus.dispatch({
|
|
89
|
+
identifier: generateIdentifier(),
|
|
90
|
+
name: descriptor.name,
|
|
91
|
+
payload,
|
|
92
|
+
metadata: resolvedMetadata,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
}) as any
|
|
103
95
|
},
|
|
104
96
|
}
|
|
105
97
|
}
|
package/src/handler.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { z } from "zod"
|
|
2
|
-
import type { Metadata } from "@kronos-ts/common"
|
|
3
2
|
import type {
|
|
4
3
|
CommandDescriptor,
|
|
5
4
|
EventDescriptor,
|
|
6
5
|
QueryDescriptor,
|
|
7
6
|
} from "./descriptor.js"
|
|
7
|
+
import type { EventMessage, QueryMessage, SequencedEventMessage } from "./message.js"
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Handler context shapes — DELETED (Plan 04-02, D-41)
|
|
@@ -22,10 +22,7 @@ import type {
|
|
|
22
22
|
export interface EventHandlerRegistration<P extends z.ZodType = z.ZodType> {
|
|
23
23
|
readonly kind: "event-handler"
|
|
24
24
|
readonly descriptor: EventDescriptor<P>
|
|
25
|
-
readonly handler: (
|
|
26
|
-
event: z.infer<P>,
|
|
27
|
-
metadata: Metadata,
|
|
28
|
-
) => Promise<void> | void
|
|
25
|
+
readonly handler: (message: SequencedEventMessage<z.infer<P>>) => Promise<void> | void
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
/**
|
|
@@ -40,7 +37,7 @@ export interface EvolverRegistration<
|
|
|
40
37
|
> {
|
|
41
38
|
readonly kind: "evolver"
|
|
42
39
|
readonly descriptor: EventDescriptor<P>
|
|
43
|
-
readonly evolve: (state: S,
|
|
40
|
+
readonly evolve: (state: S, message: EventMessage<z.infer<P>>) => S | Promise<S>
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
/** A paired query descriptor + handler function, with result type on the handler. */
|
|
@@ -50,10 +47,7 @@ export interface QueryHandlerRegistration<
|
|
|
50
47
|
> {
|
|
51
48
|
readonly kind: "query-handler"
|
|
52
49
|
readonly descriptor: QueryDescriptor<Q>
|
|
53
|
-
readonly handler: (
|
|
54
|
-
query: z.infer<Q>,
|
|
55
|
-
metadata: Metadata,
|
|
56
|
-
) => Promise<R> | R
|
|
50
|
+
readonly handler: (message: QueryMessage<z.infer<Q>>) => Promise<R> | R
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
// ---------------------------------------------------------------------------
|
|
@@ -65,13 +59,13 @@ export interface QueryHandlerRegistration<
|
|
|
65
59
|
* Pairs a descriptor with its handler for use in handler/evolve arrays.
|
|
66
60
|
*
|
|
67
61
|
* Usage:
|
|
68
|
-
* - Event handlers: `on(CourseCreated, async (
|
|
69
|
-
* - Query handlers: `on(GetCourse, async (
|
|
70
|
-
* - Evolvers: `on(CourseCreated, (state,
|
|
62
|
+
* - Event handlers: `on(CourseCreated, async ({ payload, metadata }) => { ... })`
|
|
63
|
+
* - Query handlers: `on(GetCourse, async ({ payload, metadata }) => { return { ... } })`
|
|
64
|
+
* - Evolvers: `on(CourseCreated, (state, { payload }) => ({ ...state, name: payload.name }))`
|
|
71
65
|
*
|
|
72
66
|
* The overload is resolved by the descriptor kind and how many arguments
|
|
73
|
-
* the callback declares.
|
|
74
|
-
*
|
|
67
|
+
* the callback declares. Event and query handlers receive the full typed message.
|
|
68
|
+
* Evolvers receive the current state plus the full typed event message.
|
|
75
69
|
*
|
|
76
70
|
* In practice the distinction is enforced by the array type:
|
|
77
71
|
* - `evolve: [on(...)]` expects `EvolverRegistration`
|
|
@@ -81,22 +75,19 @@ export interface QueryHandlerRegistration<
|
|
|
81
75
|
// Overload: evolver (event descriptor + state evolve function)
|
|
82
76
|
export function on<S, P extends z.ZodType>(
|
|
83
77
|
descriptor: EventDescriptor<P>,
|
|
84
|
-
evolve: (state: S,
|
|
78
|
+
evolve: (state: S, message: EventMessage<z.infer<P>>) => S | Promise<S>,
|
|
85
79
|
): EvolverRegistration<S, P>
|
|
86
80
|
|
|
87
81
|
// Overload: event handler (event descriptor + handler function)
|
|
88
82
|
export function on<P extends z.ZodType>(
|
|
89
83
|
descriptor: EventDescriptor<P>,
|
|
90
|
-
handler: (
|
|
84
|
+
handler: (message: SequencedEventMessage<z.infer<P>>) => Promise<void> | void,
|
|
91
85
|
): EventHandlerRegistration<P>
|
|
92
86
|
|
|
93
87
|
// Overload: query handler
|
|
94
88
|
export function on<Q extends z.ZodType, R>(
|
|
95
89
|
descriptor: QueryDescriptor<Q>,
|
|
96
|
-
handler: (
|
|
97
|
-
query: z.infer<Q>,
|
|
98
|
-
metadata: Metadata,
|
|
99
|
-
) => Promise<R> | R,
|
|
90
|
+
handler: (message: QueryMessage<z.infer<Q>>) => Promise<R> | R,
|
|
100
91
|
): QueryHandlerRegistration<Q, R>
|
|
101
92
|
|
|
102
93
|
export function on(
|
|
@@ -127,7 +118,7 @@ export function on(
|
|
|
127
118
|
*/
|
|
128
119
|
export function onEvent<S, P extends z.ZodType>(
|
|
129
120
|
descriptor: EventDescriptor<P>,
|
|
130
|
-
evolve: (state: S,
|
|
121
|
+
evolve: (state: S, message: EventMessage<z.infer<P>>) => S | Promise<S>,
|
|
131
122
|
): EvolverRegistration<S, P> {
|
|
132
123
|
return { kind: "evolver", descriptor, evolve }
|
|
133
124
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export {
|
|
|
4
4
|
type CommandMessage,
|
|
5
5
|
type CommandResultMessage,
|
|
6
6
|
type EventMessage,
|
|
7
|
+
type SequencedEventMessage,
|
|
7
8
|
type QueryMessage,
|
|
8
9
|
} from "./message.js"
|
|
9
10
|
|
|
@@ -375,4 +376,3 @@ export {
|
|
|
375
376
|
|
|
376
377
|
// Namespace factory
|
|
377
378
|
export { withNamespace } from "./with-namespace.js"
|
|
378
|
-
|
|
@@ -15,10 +15,10 @@ export function createInterceptingCommandBus(
|
|
|
15
15
|
/** Register a dispatch interceptor. Returns an unsubscribe function. */
|
|
16
16
|
registerDispatchInterceptor(interceptor: DispatchInterceptor<CommandMessage>): () => void
|
|
17
17
|
/** Register a handler interceptor. Returns an unsubscribe function. */
|
|
18
|
-
registerHandlerInterceptor(interceptor: HandlerInterceptor): () => void
|
|
18
|
+
registerHandlerInterceptor(interceptor: HandlerInterceptor<CommandMessage>): () => void
|
|
19
19
|
} {
|
|
20
20
|
const dispatchInterceptors: Array<DispatchInterceptor<CommandMessage>> = []
|
|
21
|
-
const handlerInterceptors: Array<HandlerInterceptor
|
|
21
|
+
const handlerInterceptors: Array<HandlerInterceptor<CommandMessage>> = []
|
|
22
22
|
|
|
23
23
|
return {
|
|
24
24
|
async dispatch(message: CommandMessage): Promise<unknown> {
|
|
@@ -35,20 +35,21 @@ export function createInterceptingCommandBus(
|
|
|
35
35
|
commandName: string,
|
|
36
36
|
handler: (message: CommandMessage) => Promise<unknown>,
|
|
37
37
|
) {
|
|
38
|
-
// Wrap the handler with handler interceptors
|
|
39
38
|
const wrappedHandler = (message: CommandMessage) => {
|
|
40
39
|
if (handlerInterceptors.length === 0) {
|
|
41
40
|
return handler(message)
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
let chain = () => handler(
|
|
43
|
+
let chain = (currentMessage: CommandMessage) => handler(currentMessage)
|
|
45
44
|
for (let i = handlerInterceptors.length - 1; i >= 0; i--) {
|
|
46
45
|
const interceptor = handlerInterceptors[i]!
|
|
47
46
|
const next = chain
|
|
48
|
-
chain = () =>
|
|
47
|
+
chain = (currentMessage: CommandMessage) =>
|
|
48
|
+
interceptor(currentMessage, (replacementMessage) =>
|
|
49
|
+
next(replacementMessage ?? currentMessage))
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
return chain()
|
|
52
|
+
return chain(message)
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
delegate.subscribe(commandName, wrappedHandler)
|
|
@@ -17,10 +17,10 @@ export function createInterceptingQueryBus(
|
|
|
17
17
|
/** Register a dispatch interceptor. Returns an unsubscribe function. */
|
|
18
18
|
registerDispatchInterceptor(interceptor: DispatchInterceptor<QueryMessage>): () => void
|
|
19
19
|
/** Register a handler interceptor. Returns an unsubscribe function. */
|
|
20
|
-
registerHandlerInterceptor(interceptor: HandlerInterceptor): () => void
|
|
20
|
+
registerHandlerInterceptor(interceptor: HandlerInterceptor<QueryMessage>): () => void
|
|
21
21
|
} {
|
|
22
22
|
const dispatchInterceptors: Array<DispatchInterceptor<QueryMessage>> = []
|
|
23
|
-
const handlerInterceptors: Array<HandlerInterceptor
|
|
23
|
+
const handlerInterceptors: Array<HandlerInterceptor<QueryMessage>> = []
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
async query(message: QueryMessage): Promise<unknown> {
|
|
@@ -36,20 +36,21 @@ export function createInterceptingQueryBus(
|
|
|
36
36
|
queryName: string,
|
|
37
37
|
handler: (message: QueryMessage) => Promise<unknown>,
|
|
38
38
|
) {
|
|
39
|
-
// Wrap the handler with handler interceptors
|
|
40
39
|
const wrappedHandler = (message: QueryMessage) => {
|
|
41
40
|
if (handlerInterceptors.length === 0) {
|
|
42
41
|
return handler(message)
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
let chain = () => handler(
|
|
44
|
+
let chain = (currentMessage: QueryMessage) => handler(currentMessage)
|
|
46
45
|
for (let i = handlerInterceptors.length - 1; i >= 0; i--) {
|
|
47
46
|
const interceptor = handlerInterceptors[i]!
|
|
48
47
|
const next = chain
|
|
49
|
-
chain = () =>
|
|
48
|
+
chain = (currentMessage: QueryMessage) =>
|
|
49
|
+
interceptor(currentMessage, (replacementMessage) =>
|
|
50
|
+
next(replacementMessage ?? currentMessage))
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
return chain()
|
|
53
|
+
return chain(message)
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
delegate.subscribe(queryName, wrappedHandler)
|
package/src/interceptor.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Metadata } from "@kronos-ts/common"
|
|
2
1
|
import type { Message } from "./message.js"
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -38,11 +37,29 @@ export interface DispatchInterceptor<M extends Message = Message> {
|
|
|
38
37
|
* accessors (`getResource` / `setResource`) — no `ProcessingContext`
|
|
39
38
|
* parameter is threaded.
|
|
40
39
|
*
|
|
40
|
+
* The first argument is the full message object. Prefer keeping it as
|
|
41
|
+
* `message` when transforming or inspecting broad message details:
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* app.handlerInterceptor(async (message, next) => {
|
|
45
|
+
* const { payload, metadata, timestamp } = message
|
|
46
|
+
* return next({
|
|
47
|
+
* ...message,
|
|
48
|
+
* metadata: { ...metadata, tenantId: "tenant-1" },
|
|
49
|
+
* })
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
41
53
|
* The `next` function calls the next interceptor in the chain, or the
|
|
42
|
-
* actual handler if this is the last interceptor.
|
|
54
|
+
* actual handler if this is the last interceptor. Call `next()` to proceed
|
|
55
|
+
* with the current message, or `next(replacementMessage)` to proceed with a
|
|
56
|
+
* transformed message.
|
|
43
57
|
*
|
|
44
58
|
* To skip handling entirely, don't call `next()` and return a result directly.
|
|
45
59
|
*/
|
|
46
|
-
export interface HandlerInterceptor<
|
|
47
|
-
|
|
60
|
+
export interface HandlerInterceptor<
|
|
61
|
+
M extends Message = Message,
|
|
62
|
+
R = unknown,
|
|
63
|
+
> {
|
|
64
|
+
(message: M, next: (message?: M) => Promise<R>): Promise<R>
|
|
48
65
|
}
|
package/src/message.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface EventMessage<P = unknown> extends Message<P> {
|
|
|
35
35
|
readonly tags: ReadonlyArray<{ readonly key: string; readonly value: string }>
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface SequencedEventMessage<P = unknown> extends EventMessage<P> {
|
|
39
|
+
/** Stream position when the source has one; absent for push-only delivery. */
|
|
40
|
+
readonly sequence?: bigint
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
/**
|
|
39
44
|
* A query message — dispatched to handler(s) that can answer it.
|
|
40
45
|
*/
|
package/src/query-handler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { z } from "zod"
|
|
2
|
-
import type { Metadata } from "@kronos-ts/common"
|
|
3
2
|
import type { QueryDescriptor } from "./descriptor.js"
|
|
3
|
+
import type { QueryMessage } from "./message.js"
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Singular factory — mirrors commandHandler / eventHandler.
|
|
@@ -23,15 +23,15 @@ export interface QueryHandlerDefinition<
|
|
|
23
23
|
> {
|
|
24
24
|
readonly kind: "query-handler"
|
|
25
25
|
readonly descriptor: QueryDescriptor<Q, z.ZodType | undefined>
|
|
26
|
-
readonly handler: (
|
|
26
|
+
readonly handler: (message: QueryMessage<z.infer<Q>>) => Promise<R> | R
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Defines a singular query handler.
|
|
31
31
|
*
|
|
32
32
|
* ```
|
|
33
|
-
* const getCourseView = queryHandler(GetCourseView, async (
|
|
34
|
-
* const view = courseViews.get(
|
|
33
|
+
* const getCourseView = queryHandler(GetCourseView, async ({ payload, metadata }) => {
|
|
34
|
+
* const view = courseViews.get(payload.courseId)
|
|
35
35
|
* if (!view) throw new Error("not found")
|
|
36
36
|
* return view
|
|
37
37
|
* })
|
|
@@ -43,7 +43,7 @@ export interface QueryHandlerDefinition<
|
|
|
43
43
|
*/
|
|
44
44
|
export function queryHandler<Q extends z.ZodType, R>(
|
|
45
45
|
descriptor: QueryDescriptor<Q, z.ZodType | undefined>,
|
|
46
|
-
handler: (
|
|
46
|
+
handler: (message: QueryMessage<z.infer<Q>>) => Promise<R> | R,
|
|
47
47
|
): QueryHandlerDefinition<Q, R> {
|
|
48
48
|
return { kind: "query-handler", descriptor, handler }
|
|
49
49
|
}
|
|
@@ -31,7 +31,7 @@ export function registerQueryHandlersNatively(
|
|
|
31
31
|
for (const reg of handlers) {
|
|
32
32
|
const queryName = qualifiedNameToString(reg.descriptor.name)
|
|
33
33
|
let invocation = async (message: QueryMessage) =>
|
|
34
|
-
reg.handler(message
|
|
34
|
+
reg.handler(message)
|
|
35
35
|
if (deps.handlerEnhancer) {
|
|
36
36
|
invocation = deps.handlerEnhancer.wrapHandler(invocation, {
|
|
37
37
|
messageType: "query",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandBus } from "./command-bus.js"
|
|
2
2
|
import type { CommandMessage } from "./message.js"
|
|
3
|
-
import { runInNewUoW } from "./unit-of-work.js"
|
|
3
|
+
import { runInNewUoW, type UoWRunner } from "./unit-of-work.js"
|
|
4
4
|
import { qualifiedNameToString } from "@kronos-ts/common"
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -16,14 +16,20 @@ import { qualifiedNameToString } from "@kronos-ts/common"
|
|
|
16
16
|
* by sharing a transaction. DCB read-set / append-condition merging
|
|
17
17
|
* happens only WITHIN a single handler's UnitOfWork.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The handler runs through `unitOfWorkRunner` — by default `runInNewUoW`, but
|
|
20
|
+
* the configurer injects the resolved `unitOfWorkFactory` slot (e.g. a
|
|
21
|
+
* transactional runner from a storage extension) so the per-command UoW
|
|
22
|
+
* carries whatever transaction that backend provides. This mirrors the
|
|
23
|
+
* distributed command buses (kronosdb / axon-server), which already run
|
|
24
|
+
* handlers through the configured runner. The runner is always built on
|
|
25
|
+
* `runInNewUoW`, so a command — primary OR nested via `send()` — still gets
|
|
26
|
+
* its own fresh UoW (and its own independent transaction); composition does
|
|
27
|
+
* not change AF5 isolation, only whether that fresh UoW has a transaction.
|
|
22
28
|
*
|
|
23
29
|
* Interceptor support is provided by wrapping with
|
|
24
30
|
* {@link createInterceptingCommandBus}.
|
|
25
31
|
*/
|
|
26
|
-
export function createSimpleCommandBus(): CommandBus {
|
|
32
|
+
export function createSimpleCommandBus(unitOfWorkRunner: UoWRunner = runInNewUoW): CommandBus {
|
|
27
33
|
const handlers = new Map<string, (message: CommandMessage) => Promise<unknown>>()
|
|
28
34
|
|
|
29
35
|
return {
|
|
@@ -34,12 +40,12 @@ export function createSimpleCommandBus(): CommandBus {
|
|
|
34
40
|
throw new Error(`No handler registered for command "${key}"`)
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
// AF5 parity: every command gets its own fresh UnitOfWork
|
|
38
|
-
// dispatched from inside another
|
|
39
|
-
// already run in the caller's context
|
|
40
|
-
// this one), so correlation data is carried
|
|
41
|
-
// before we cross into the new UoW.
|
|
42
|
-
return
|
|
43
|
+
// AF5 parity: every command gets its own fresh UnitOfWork (the runner is
|
|
44
|
+
// built on runInNewUoW), even when dispatched from inside another
|
|
45
|
+
// handler. Dispatch interceptors have already run in the caller's context
|
|
46
|
+
// (the intercepting bus wraps this one), so correlation data is carried
|
|
47
|
+
// on `message.metadata` before we cross into the new UoW.
|
|
48
|
+
return unitOfWorkRunner(message.metadata, () => handler(message))
|
|
43
49
|
},
|
|
44
50
|
|
|
45
51
|
subscribe(
|
|
@@ -75,6 +75,8 @@ export interface StreamingEventProcessorOptions {
|
|
|
75
75
|
unitOfWorkRunner?: UoWRunner
|
|
76
76
|
tokenStore?: TokenStore
|
|
77
77
|
batchSize?: number
|
|
78
|
+
/** Delay before retrying after a batch failure, in ms. Backs off to avoid hot-looping a deterministic failure. */
|
|
79
|
+
errorBackoffMs?: number
|
|
78
80
|
errorHandler?: EventProcessingErrorHandler
|
|
79
81
|
/** Optional handler enhancer applied to all event handlers at setup time. */
|
|
80
82
|
handlerEnhancer?: HandlerEnhancerDefinition
|
|
@@ -96,6 +98,7 @@ export function createStreamingEventProcessor(
|
|
|
96
98
|
unitOfWorkRunner = runInNewUoW,
|
|
97
99
|
tokenStore,
|
|
98
100
|
batchSize = 100,
|
|
101
|
+
errorBackoffMs = 1000,
|
|
99
102
|
errorHandler = loggingErrorHandler(name),
|
|
100
103
|
handlerEnhancer,
|
|
101
104
|
onReset,
|
|
@@ -158,6 +161,22 @@ export function createStreamingEventProcessor(
|
|
|
158
161
|
} catch (err) {
|
|
159
162
|
lastError = err instanceof Error ? err : new Error(String(err))
|
|
160
163
|
console.error(`Event processor "${name}" error:`, err)
|
|
164
|
+
// Realign the live stream to the committed checkpoint. During batch
|
|
165
|
+
// accumulation the stream cursor (and any read-ahead buffer) advanced
|
|
166
|
+
// past this batch, but `token` was NOT advanced — the failing UnitOfWork
|
|
167
|
+
// never reached PREPARE_COMMIT. Closing discards the stream's buffer so
|
|
168
|
+
// the next cycle reopens at token.position() and re-reads — and thus
|
|
169
|
+
// redelivers — the failed batch. Without this the stream cursor outruns
|
|
170
|
+
// the checkpoint and the failed events are skipped until a restart.
|
|
171
|
+
// Mirrors Axon's close-and-reopen-from-token recovery. Back off before
|
|
172
|
+
// retrying so a deterministically failing handler can't hot-loop.
|
|
173
|
+
stream?.close()
|
|
174
|
+
stream = null
|
|
175
|
+
if (isRunning) {
|
|
176
|
+
if (processTimer !== null) clearTimeout(processTimer)
|
|
177
|
+
processTimer = setTimeout(processAvailable, errorBackoffMs)
|
|
178
|
+
}
|
|
179
|
+
return
|
|
161
180
|
} finally {
|
|
162
181
|
processing = false
|
|
163
182
|
}
|
|
@@ -170,24 +189,26 @@ export function createStreamingEventProcessor(
|
|
|
170
189
|
}
|
|
171
190
|
|
|
172
191
|
async function processFromStream() {
|
|
173
|
-
|
|
192
|
+
// Lazily (re)open the stream at the committed token. The error path nulls
|
|
193
|
+
// `stream` so processing resumes from the checkpoint, not a stale cursor.
|
|
194
|
+
if (!stream) openStream()
|
|
174
195
|
|
|
175
196
|
// Check for stream errors — reopen if needed
|
|
176
|
-
const streamError = stream
|
|
197
|
+
const streamError = stream!.error()
|
|
177
198
|
if (streamError) {
|
|
178
199
|
console.error(`Event processor "${name}": stream error, reopening:`, streamError)
|
|
179
|
-
stream
|
|
200
|
+
stream!.close()
|
|
180
201
|
stream = null
|
|
181
202
|
openStream()
|
|
182
203
|
return
|
|
183
204
|
}
|
|
184
205
|
|
|
185
206
|
const batch: SequencedEvent[] = []
|
|
186
|
-
let event = stream
|
|
207
|
+
let event = stream!.next()
|
|
187
208
|
while (event && batch.length < batchSize) {
|
|
188
209
|
batch.push(event)
|
|
189
|
-
if (batch.length < batchSize && stream
|
|
190
|
-
event = stream
|
|
210
|
+
if (batch.length < batchSize && stream!.hasNextAvailable()) {
|
|
211
|
+
event = stream!.next()
|
|
191
212
|
} else {
|
|
192
213
|
break
|
|
193
214
|
}
|
|
@@ -196,7 +217,7 @@ export function createStreamingEventProcessor(
|
|
|
196
217
|
if (batch.length > 0) {
|
|
197
218
|
caughtUp = false
|
|
198
219
|
await processBatch(batch)
|
|
199
|
-
if (stream
|
|
220
|
+
if (stream!.hasNextAvailable()) {
|
|
200
221
|
scheduleImmediate()
|
|
201
222
|
}
|
|
202
223
|
} else {
|
|
@@ -249,7 +270,7 @@ export function createStreamingEventProcessor(
|
|
|
249
270
|
|
|
250
271
|
for (const reg of handlers) {
|
|
251
272
|
try {
|
|
252
|
-
await reg.handler(event
|
|
273
|
+
await reg.handler({ ...event, sequence: sequencedEvent.sequence })
|
|
253
274
|
} catch (err) {
|
|
254
275
|
await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
|
|
255
276
|
}
|
|
@@ -135,7 +135,7 @@ export function createSubscribingEventProcessor(
|
|
|
135
135
|
|
|
136
136
|
for (const reg of handlers) {
|
|
137
137
|
try {
|
|
138
|
-
await reg.handler(event
|
|
138
|
+
await reg.handler(event)
|
|
139
139
|
} catch (err) {
|
|
140
140
|
// SubscribingEventProcessor doesn't have position tracking,
|
|
141
141
|
// so pass -1n as position indicator
|
|
@@ -224,6 +224,16 @@ export function createTrackingEventProcessor(
|
|
|
224
224
|
}
|
|
225
225
|
} catch (err) {
|
|
226
226
|
console.error(`Event processor "${name}" error during poll:`, err)
|
|
227
|
+
// Realign the live stream to the committed checkpoint. During batch
|
|
228
|
+
// accumulation the stream cursor (and any read-ahead buffer) advanced
|
|
229
|
+
// past this batch, but `token` was NOT advanced — the failing UnitOfWork
|
|
230
|
+
// never reached PREPARE_COMMIT. Closing discards the stream's buffer so
|
|
231
|
+
// the next poll reopens at token.position() and re-reads — and thus
|
|
232
|
+
// redelivers — the failed batch. Without this the stream cursor outruns
|
|
233
|
+
// the checkpoint and the failed events are skipped until a restart.
|
|
234
|
+
// Mirrors Axon's close-and-reopen-from-token recovery.
|
|
235
|
+
stream?.close()
|
|
236
|
+
stream = null
|
|
227
237
|
if (isRunning) pollTimer = setTimeout(poll, pollingIntervalMs * 2)
|
|
228
238
|
} finally {
|
|
229
239
|
processing = false
|
|
@@ -268,7 +278,7 @@ export function createTrackingEventProcessor(
|
|
|
268
278
|
|
|
269
279
|
for (const reg of handlers) {
|
|
270
280
|
try {
|
|
271
|
-
await reg.handler(event
|
|
281
|
+
await reg.handler({ ...event, sequence: sequencedEvent.sequence })
|
|
272
282
|
} catch (err) {
|
|
273
283
|
await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
|
|
274
284
|
}
|