@kronos-ts/messaging 0.2.0 → 0.3.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 (70) hide show
  1. package/dist/command-handler.d.ts +15 -15
  2. package/dist/command-handler.d.ts.map +1 -1
  3. package/dist/command-handler.js.map +1 -1
  4. package/dist/command-handling-module.js +2 -2
  5. package/dist/command-handling-module.js.map +1 -1
  6. package/dist/dead-lettering-handler.js +1 -1
  7. package/dist/dead-lettering-handler.js.map +1 -1
  8. package/dist/event-handler.d.ts +5 -5
  9. package/dist/event-handler.d.ts.map +1 -1
  10. package/dist/event-handler.js +2 -2
  11. package/dist/event-handler.js.map +1 -1
  12. package/dist/event-processor-builder.d.ts +3 -3
  13. package/dist/event-processor-builder.js +3 -3
  14. package/dist/gateway.d.ts +7 -5
  15. package/dist/gateway.d.ts.map +1 -1
  16. package/dist/gateway.js +9 -12
  17. package/dist/gateway.js.map +1 -1
  18. package/dist/handler.d.ts +13 -13
  19. package/dist/handler.d.ts.map +1 -1
  20. package/dist/handler.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/intercepting-command-bus.d.ts +1 -1
  25. package/dist/intercepting-command-bus.d.ts.map +1 -1
  26. package/dist/intercepting-command-bus.js +3 -4
  27. package/dist/intercepting-command-bus.js.map +1 -1
  28. package/dist/intercepting-query-bus.d.ts +1 -1
  29. package/dist/intercepting-query-bus.d.ts.map +1 -1
  30. package/dist/intercepting-query-bus.js +3 -4
  31. package/dist/intercepting-query-bus.js.map +1 -1
  32. package/dist/interceptor.d.ts +18 -3
  33. package/dist/interceptor.d.ts.map +1 -1
  34. package/dist/message.d.ts +4 -0
  35. package/dist/message.d.ts.map +1 -1
  36. package/dist/query-handler.d.ts +5 -5
  37. package/dist/query-handler.d.ts.map +1 -1
  38. package/dist/query-handler.js +2 -2
  39. package/dist/query-handler.js.map +1 -1
  40. package/dist/query-handling-module.js +1 -1
  41. package/dist/query-handling-module.js.map +1 -1
  42. package/dist/simple-command-bus.d.ts +11 -4
  43. package/dist/simple-command-bus.d.ts.map +1 -1
  44. package/dist/simple-command-bus.js +16 -10
  45. package/dist/simple-command-bus.js.map +1 -1
  46. package/dist/streaming-event-processor.js +1 -1
  47. package/dist/streaming-event-processor.js.map +1 -1
  48. package/dist/subscribing-event-processor.js +1 -1
  49. package/dist/subscribing-event-processor.js.map +1 -1
  50. package/dist/tracking-event-processor.js +1 -1
  51. package/dist/tracking-event-processor.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/command-handler.ts +15 -28
  54. package/src/command-handling-module.ts +2 -2
  55. package/src/dead-lettering-handler.ts +1 -1
  56. package/src/event-handler.ts +5 -8
  57. package/src/event-processor-builder.ts +3 -3
  58. package/src/gateway.ts +14 -22
  59. package/src/handler.ts +13 -22
  60. package/src/index.ts +1 -1
  61. package/src/intercepting-command-bus.ts +7 -6
  62. package/src/intercepting-query-bus.ts +7 -6
  63. package/src/interceptor.ts +21 -4
  64. package/src/message.ts +5 -0
  65. package/src/query-handler.ts +5 -5
  66. package/src/query-handling-module.ts +1 -1
  67. package/src/simple-command-bus.ts +17 -11
  68. package/src/streaming-event-processor.ts +1 -1
  69. package/src/subscribing-event-processor.ts +1 -1
  70. package/src/tracking-event-processor.ts +1 -1
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, event: z.infer<P>, id: unknown) => S | Promise<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 (event, ctx) => { ... })`
69
- * - Query handlers: `on(GetCourse, async (query, ctx) => { return { ... } })`
70
- * - Evolvers: `on(CourseCreated, (state, event) => ({ ...state, name: event.name }))`
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. Evolvers receive `(state, event)` or `(state, event, id)`,
74
- * while event handlers receive `(event, context)`.
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, event: z.infer<P>, id: unknown) => S | Promise<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: (event: z.infer<P>, metadata: Metadata) => Promise<void> | void,
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, event: z.infer<P>, id: unknown) => S | Promise<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(message)
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 = () => interceptor(message, next)
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(message)
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 = () => interceptor(message, next)
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)
@@ -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<R = unknown> {
47
- (message: Message, next: () => Promise<R>): Promise<R>
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
  */
@@ -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: (query: z.infer<Q>, metadata: Metadata) => Promise<R> | R
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 (q, metadata) => {
34
- * const view = courseViews.get(q.courseId)
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: (query: z.infer<Q>, metadata: Metadata) => Promise<R> | R,
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.payload, message.metadata)
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
- * Transactional wiring composes at the runner level via
20
- * `transactionalUnitOfWorkFactory(runInNewUoW, txManager)` and is consumed
21
- * by extensions / processors directly, not by the bus.
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, even when
38
- // dispatched from inside another handler. Dispatch interceptors have
39
- // already run in the caller's context (the intercepting bus wraps
40
- // this one), so correlation data is carried on `message.metadata`
41
- // before we cross into the new UoW.
42
- return runInNewUoW(message.metadata, () => handler(message))
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(
@@ -249,7 +249,7 @@ export function createStreamingEventProcessor(
249
249
 
250
250
  for (const reg of handlers) {
251
251
  try {
252
- await reg.handler(event.payload, event.metadata)
252
+ await reg.handler({ ...event, sequence: sequencedEvent.sequence })
253
253
  } catch (err) {
254
254
  await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
255
255
  }
@@ -135,7 +135,7 @@ export function createSubscribingEventProcessor(
135
135
 
136
136
  for (const reg of handlers) {
137
137
  try {
138
- await reg.handler(event.payload, event.metadata)
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
@@ -268,7 +268,7 @@ export function createTrackingEventProcessor(
268
268
 
269
269
  for (const reg of handlers) {
270
270
  try {
271
- await reg.handler(event.payload, event.metadata)
271
+ await reg.handler({ ...event, sequence: sequencedEvent.sequence })
272
272
  } catch (err) {
273
273
  await errorHandler.handleError(err, eventName, sequencedEvent.sequence)
274
274
  }