@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,80 @@
1
+ import type { CommandBus } from "./command-bus.js"
2
+ import type { CommandMessage } from "./message.js"
3
+
4
+ /**
5
+ * Determines whether a failed command dispatch should be retried.
6
+ */
7
+ export interface RetryPolicy {
8
+ /**
9
+ * Returns the delay in ms before the next retry, or undefined to stop retrying.
10
+ * @param error The error from the previous attempt
11
+ * @param attempt The attempt number (0-based, so 0 = first failure)
12
+ */
13
+ shouldRetry(error: unknown, attempt: number): number | undefined
14
+ }
15
+
16
+ /**
17
+ * Retries on transient errors (like AppendConditionError) with exponential backoff.
18
+ * Non-transient errors are rethrown immediately.
19
+ */
20
+ export function exponentialBackoffRetryPolicy(options?: {
21
+ maxRetries?: number
22
+ initialDelayMs?: number
23
+ isTransient?: (error: unknown) => boolean
24
+ }): RetryPolicy {
25
+ const maxRetries = options?.maxRetries ?? 5
26
+ const initialDelayMs = options?.initialDelayMs ?? 10
27
+ const isTransient = options?.isTransient ?? defaultIsTransient
28
+
29
+ return {
30
+ shouldRetry(error: unknown, attempt: number): number | undefined {
31
+ if (attempt >= maxRetries) return undefined
32
+ if (!isTransient(error)) return undefined
33
+ return initialDelayMs * Math.pow(2, attempt)
34
+ },
35
+ }
36
+ }
37
+
38
+ function defaultIsTransient(error: unknown): boolean {
39
+ if (error instanceof Error) {
40
+ return error.name === "AppendConditionError"
41
+ }
42
+ return false
43
+ }
44
+
45
+ /**
46
+ * A command bus decorator that retries failed dispatches based on a retry policy.
47
+ *
48
+ * When a command fails with a transient error (e.g., AppendConditionError from
49
+ * an optimistic concurrency conflict), the entire dispatch is retried. This means
50
+ * the handler re-sources state and re-makes its decision based on fresh data.
51
+ *
52
+ * Non-transient errors propagate immediately to the caller.
53
+ */
54
+ export function createRetryingCommandBus(
55
+ delegate: CommandBus,
56
+ policy: RetryPolicy,
57
+ ): CommandBus {
58
+ return {
59
+ async dispatch(message: CommandMessage): Promise<unknown> {
60
+ let attempt = 0
61
+ while (true) {
62
+ try {
63
+ return await delegate.dispatch(message)
64
+ } catch (error) {
65
+ const delay = policy.shouldRetry(error, attempt)
66
+ if (delay === undefined) throw error
67
+
68
+ attempt++
69
+ if (delay > 0) {
70
+ await new Promise((resolve) => setTimeout(resolve, delay))
71
+ }
72
+ }
73
+ }
74
+ },
75
+
76
+ subscribe(commandName: string, handler: (message: CommandMessage) => Promise<unknown>) {
77
+ delegate.subscribe(commandName, handler)
78
+ },
79
+ }
80
+ }
@@ -0,0 +1,59 @@
1
+ import type { CommandMessage } from "./message.js"
2
+
3
+ /**
4
+ * Determines the routing key for a command message.
5
+ *
6
+ * Used by the distributed command bus to route commands to the correct
7
+ * handler instance via consistent hashing. Commands with the same routing
8
+ * key are routed to the same handler.
9
+ */
10
+ export interface RoutingStrategy {
11
+ /**
12
+ * Get the routing key for the given command message.
13
+ * Returns a string that identifies the target for this command
14
+ * (typically an aggregate identifier).
15
+ */
16
+ getRoutingKey(message: CommandMessage): string
17
+ }
18
+
19
+ /**
20
+ * Extracts the routing key from a command message's metadata.
21
+ *
22
+ * @param metadataKey The metadata key to extract the routing key from.
23
+ */
24
+ export function metadataRoutingStrategy(metadataKey: string): RoutingStrategy {
25
+ return {
26
+ getRoutingKey(message: CommandMessage): string {
27
+ const value = message.metadata[metadataKey]
28
+ if (value == null) {
29
+ throw new Error(
30
+ `No routing key found in metadata key "${metadataKey}" ` +
31
+ `for command "${String(message.name)}"`,
32
+ )
33
+ }
34
+ return String(value)
35
+ },
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Extracts the routing key from a field of the command payload.
41
+ * The field name is specified explicitly on the descriptor/configuration.
42
+ *
43
+ * @param field The payload field to extract the routing key from.
44
+ */
45
+ export function payloadFieldRoutingStrategy(field: string): RoutingStrategy {
46
+ return {
47
+ getRoutingKey(message: CommandMessage): string {
48
+ const payload = message.payload as Record<string, unknown>
49
+ const value = payload?.[field]
50
+ if (value == null) {
51
+ throw new Error(
52
+ `No routing key found in payload field "${field}" ` +
53
+ `for command "${String(message.name)}"`,
54
+ )
55
+ }
56
+ return String(value)
57
+ },
58
+ }
59
+ }
package/src/segment.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Represents a fraction of the event stream assigned to a processor instance.
3
+ *
4
+ * Segments use bitmask-based routing to deterministically assign events
5
+ * to processors. An event matches a segment when `(hash(event) & mask) === segmentId`.
6
+ *
7
+ * Segments can be split (doubling parallelism) and merged (halving parallelism).
8
+ * This is the foundation for horizontal scaling — multiple instances of the
9
+ * same processor each claim different segments.
10
+ *
11
+ * The ROOT_SEGMENT (segmentId=0, mask=0) matches ALL events and is the
12
+ * starting point before any splitting.
13
+ */
14
+ export interface Segment {
15
+ /** The unique identifier of this segment. */
16
+ readonly segmentId: number
17
+ /**
18
+ * The bitmask used to match events to this segment.
19
+ * An event matches when `(hash & mask) === segmentId`.
20
+ */
21
+ readonly mask: number
22
+ }
23
+
24
+ /** The root segment — matches all events. Starting point before splitting. */
25
+ export const ROOT_SEGMENT: Segment = { segmentId: 0, mask: 0 }
26
+
27
+ /**
28
+ * Create a segment with the given id and mask.
29
+ */
30
+ export function segment(segmentId: number, mask: number): Segment {
31
+ return { segmentId, mask }
32
+ }
33
+
34
+ /**
35
+ * Check if a hash value matches this segment.
36
+ * Used to route events to the correct processor instance.
37
+ *
38
+ * @param seg The segment to check against
39
+ * @param hash The hash of the event's sequence identifier (e.g., aggregate ID hash)
40
+ */
41
+ export function segmentMatches(seg: Segment, hash: number): boolean {
42
+ return (hash & seg.mask) === seg.segmentId
43
+ }
44
+
45
+ /**
46
+ * Split a segment into two child segments.
47
+ * Doubles the processing parallelism for this segment's portion of the stream.
48
+ *
49
+ * Returns a tuple of [segment keeping the original ID, new sibling segment].
50
+ */
51
+ export function splitSegment(seg: Segment): [Segment, Segment] {
52
+ const newMask = (seg.mask << 1) | 1
53
+ const newSegmentId = seg.segmentId | (seg.mask + 1)
54
+
55
+ return [
56
+ { segmentId: seg.segmentId, mask: newMask },
57
+ { segmentId: newSegmentId, mask: newMask },
58
+ ]
59
+ }
60
+
61
+ /**
62
+ * Check if two segments can be merged (they are siblings from the same split).
63
+ */
64
+ export function isMergeable(a: Segment, b: Segment): boolean {
65
+ if (a.mask !== b.mask) return false
66
+ if (a.mask === 0) return false
67
+ // Siblings differ only in the highest bit of the mask
68
+ return (a.segmentId ^ b.segmentId) === (a.mask >>> 0) - (a.mask >>> 1)
69
+ }
70
+
71
+ /**
72
+ * Merge two sibling segments back into their parent.
73
+ * Halves the processing parallelism.
74
+ *
75
+ * @throws Error if segments are not mergeable
76
+ */
77
+ export function mergeSegments(a: Segment, b: Segment): Segment {
78
+ if (!isMergeable(a, b)) {
79
+ throw new Error(
80
+ `Segments ${a.segmentId}/${a.mask} and ${b.segmentId}/${b.mask} are not mergeable`,
81
+ )
82
+ }
83
+
84
+ return {
85
+ segmentId: Math.min(a.segmentId, b.segmentId),
86
+ mask: a.mask >>> 1,
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Compute the total number of segments at the current split level.
92
+ * For a segment with mask M, the total count is M + 1.
93
+ */
94
+ export function segmentCount(seg: Segment): number {
95
+ return seg.mask + 1
96
+ }
97
+
98
+ /**
99
+ * Compute a hash from a string value (for event routing).
100
+ * Uses a simple but well-distributed hash function.
101
+ */
102
+ export function hashOf(value: string): number {
103
+ let hash = 0
104
+ for (let i = 0; i < value.length; i++) {
105
+ const char = value.charCodeAt(i)
106
+ hash = ((hash << 5) - hash + char) | 0
107
+ }
108
+ return hash >>> 0 // Ensure unsigned
109
+ }
110
+
111
+ /**
112
+ * Create N balanced segments by splitting the root segment.
113
+ *
114
+ * @param count Number of segments (must be a power of 2)
115
+ * @returns Array of segments that together cover the entire event stream
116
+ */
117
+ export function createSegments(count: number): Segment[] {
118
+ if (count <= 0) throw new Error("Segment count must be positive")
119
+ if (count === 1) return [ROOT_SEGMENT]
120
+
121
+ // Find the nearest power of 2
122
+ const power = Math.ceil(Math.log2(count))
123
+ const actualCount = Math.pow(2, power)
124
+
125
+ let segments: Segment[] = [ROOT_SEGMENT]
126
+ while (segments.length < actualCount) {
127
+ const next: Segment[] = []
128
+ for (const seg of segments) {
129
+ const [a, b] = splitSegment(seg)
130
+ next.push(a, b)
131
+ }
132
+ segments = next
133
+ }
134
+
135
+ return segments
136
+ }
package/src/send.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { resourceKey, generateIdentifier, type ResourceKey } from "@kronos-ts/common"
2
+ import { requireInvocationPhase } from "./processing-state.js"
3
+ import type { CommandBus } from "./command-bus.js"
4
+ import type { CommandDescriptor } from "./descriptor.js"
5
+ import type { z } from "zod"
6
+
7
+ type CommandDispatchFunction = <P extends z.ZodType, R extends z.ZodType | undefined = undefined>(
8
+ descriptor: CommandDescriptor<P, R>,
9
+ payload: z.infer<P>,
10
+ ) => Promise<unknown>
11
+
12
+ /**
13
+ * Resource key for the command bus component.
14
+ * Written by handling modules + processors at handler-invocation entry (D-44).
15
+ */
16
+ export const COMMAND_BUS_KEY: ResourceKey<CommandBus> = resourceKey("commandBus")
17
+
18
+ /**
19
+ * Send a command from inside a handler.
20
+ *
21
+ * AF5-aligned semantics: every command is handled in its own fresh
22
+ * UnitOfWork (`commandBus.dispatch` always starts a new one — see
23
+ * `createSimpleCommandBus`). The command handler is therefore its own
24
+ * atomic boundary: it loads state, decides, appends events, and commits
25
+ * once — independent of the caller's UnitOfWork.
26
+ *
27
+ * The caller's `metadata` IS carried onto the outgoing command, so
28
+ * correlation/causation lineage propagates the AF5 way — through message
29
+ * metadata, applied by the correlation-data dispatch interceptor — across
30
+ * any transport, local or distributed. No processing-context object is
31
+ * threaded through the command API or over the wire.
32
+ */
33
+ export const send: CommandDispatchFunction = async (descriptor, payload) => {
34
+ const state = requireInvocationPhase() // D-43 mutator guard
35
+ const bus = state.resources.get(COMMAND_BUS_KEY.symbol) as CommandBus | undefined
36
+ if (!bus) throw new Error("No command bus configured")
37
+ return bus.dispatch({
38
+ identifier: generateIdentifier(),
39
+ name: descriptor.name,
40
+ payload,
41
+ metadata: state.metadata,
42
+ timestamp: Date.now(),
43
+ })
44
+ }
@@ -0,0 +1,122 @@
1
+ import type { Serializer, SerializedObject } from "@kronos-ts/common"
2
+ import type { z } from "zod"
3
+
4
+ const encoder = new TextEncoder()
5
+ const decoder = new TextDecoder()
6
+
7
+ /**
8
+ * JSON serializer — the default serializer for the TypeScript framework.
9
+ *
10
+ * Serializes values as JSON-encoded Uint8Array. Handles `undefined`,
11
+ * `null`, and all JSON-compatible values.
12
+ */
13
+ export function jsonSerializer(): Serializer {
14
+ return {
15
+ serialize(value: unknown, type: string, revision: string = ""): SerializedObject {
16
+ return {
17
+ type,
18
+ revision,
19
+ data: encoder.encode(JSON.stringify(value)),
20
+ }
21
+ },
22
+
23
+ deserialize<T>(data: SerializedObject): T {
24
+ if (data.data.length === 0) return undefined as T
25
+ return JSON.parse(decoder.decode(data.data)) as T
26
+ },
27
+
28
+ canConvert(): boolean {
29
+ return true
30
+ },
31
+ }
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Schema registries — per message type
36
+ // ---------------------------------------------------------------------------
37
+
38
+ /**
39
+ * A registry of Zod schemas indexed by type name + revision.
40
+ * Used by the validating serializer decorator to validate
41
+ * deserialized payloads against their expected schema.
42
+ */
43
+ export interface SchemaRegistry {
44
+ register(typeName: string, revision: string, schema: z.ZodType): void
45
+ get(typeName: string, revision: string): z.ZodType | undefined
46
+ }
47
+
48
+ /** Schema registry for event payloads. */
49
+ export function createEventSchemaRegistry(): SchemaRegistry {
50
+ return createSchemaRegistry()
51
+ }
52
+
53
+ /** Schema registry for command payloads. */
54
+ export function createCommandSchemaRegistry(): SchemaRegistry {
55
+ return createSchemaRegistry()
56
+ }
57
+
58
+ /** Schema registry for query payloads. */
59
+ export function createQuerySchemaRegistry(): SchemaRegistry {
60
+ return createSchemaRegistry()
61
+ }
62
+
63
+ function createSchemaRegistry(): SchemaRegistry {
64
+ const schemas = new Map<string, z.ZodType>()
65
+
66
+ function key(typeName: string, revision: string): string {
67
+ return `${typeName}@${revision}`
68
+ }
69
+
70
+ return {
71
+ register(typeName, revision, schema) {
72
+ schemas.set(key(typeName, revision), schema)
73
+ },
74
+
75
+ get(typeName, revision) {
76
+ // Try exact match first, then fallback to no revision
77
+ return schemas.get(key(typeName, revision)) ?? schemas.get(key(typeName, ""))
78
+ },
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Zod-validating serializer decorator
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Wraps a delegate serializer with Zod validation on deserialization.
88
+ *
89
+ * When deserializing, looks up the schema in the registry by type name
90
+ * and revision. If found, validates the deserialized value against it.
91
+ * If not found, passes through without validation.
92
+ *
93
+ * ```typescript
94
+ * const serializer = zodValidatingSerializer(
95
+ * jsonSerializer(),
96
+ * mySchemaRegistry,
97
+ * )
98
+ * ```
99
+ */
100
+ export function zodValidatingSerializer(
101
+ delegate: Serializer,
102
+ schemaRegistry: SchemaRegistry,
103
+ ): Serializer {
104
+ return {
105
+ serialize(value, type, revision) {
106
+ return delegate.serialize(value, type, revision)
107
+ },
108
+
109
+ deserialize<T>(data: SerializedObject): T {
110
+ const raw = delegate.deserialize<unknown>(data)
111
+ const schema = schemaRegistry.get(data.type, data.revision)
112
+ if (schema) {
113
+ return schema.parse(raw) as T
114
+ }
115
+ return raw as T
116
+ },
117
+
118
+ canConvert(type) {
119
+ return delegate.canConvert(type)
120
+ },
121
+ }
122
+ }
@@ -0,0 +1,59 @@
1
+ import type { CommandBus } from "./command-bus.js"
2
+ import type { CommandMessage } from "./message.js"
3
+ import { runInNewUoW } from "./unit-of-work.js"
4
+ import { qualifiedNameToString } from "@kronos-ts/common"
5
+
6
+ /**
7
+ * Simple in-process command bus.
8
+ *
9
+ * Maintains a local handler map and dispatches commands directly,
10
+ * wrapping each dispatch in a fresh UnitOfWork via `runInNewUoW`.
11
+ *
12
+ * AF5 parity: like `SimpleCommandBus`, every command — primary OR nested
13
+ * (dispatched from inside another handler via `send()`) — is handled in
14
+ * its own independent UnitOfWork with its own commit boundary. A command
15
+ * handler is the atomic unit; commands compose by independent commit, not
16
+ * by sharing a transaction. DCB read-set / append-condition merging
17
+ * happens only WITHIN a single handler's UnitOfWork.
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.
22
+ *
23
+ * Interceptor support is provided by wrapping with
24
+ * {@link createInterceptingCommandBus}.
25
+ */
26
+ export function createSimpleCommandBus(): CommandBus {
27
+ const handlers = new Map<string, (message: CommandMessage) => Promise<unknown>>()
28
+
29
+ return {
30
+ async dispatch(message: CommandMessage): Promise<unknown> {
31
+ const key = qualifiedNameToString(message.name)
32
+ const handler = handlers.get(key)
33
+ if (!handler) {
34
+ throw new Error(`No handler registered for command "${key}"`)
35
+ }
36
+
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
+ },
44
+
45
+ subscribe(
46
+ commandName: string,
47
+ handler: (message: CommandMessage) => Promise<unknown>,
48
+ ) {
49
+ const existing = handlers.get(commandName)
50
+ if (existing && existing !== handler) {
51
+ throw new Error(
52
+ `A different handler is already registered for command "${commandName}". ` +
53
+ `Duplicate command handler subscriptions are not allowed.`,
54
+ )
55
+ }
56
+ handlers.set(commandName, handler)
57
+ },
58
+ }
59
+ }
@@ -0,0 +1,158 @@
1
+ import type { QueryBus } from "./query-bus.js"
2
+ import type { QueryMessage } from "./message.js"
3
+ import type { SubscriptionQueryResult, UpdateHandler } from "./subscription-query.js"
4
+ import { createUpdateHandler, runAfterCommitOrImmediately } from "./subscription-query.js"
5
+ import { runInUoW } from "./unit-of-work.js"
6
+ import { qualifiedNameToString } from "@kronos-ts/common"
7
+
8
+ /**
9
+ * Simple in-process query bus with subscription query support.
10
+ *
11
+ * Direct queries are dispatched within a UnitOfWork via `runInUoW`.
12
+ * Subscription queries receive an initial result plus a stream of
13
+ * incremental updates emitted via `emitUpdate()`.
14
+ *
15
+ * Plan 03-04 (CTX-04 / D-34): the explicit `unitOfWorkFactory`
16
+ * parameter and branch are gone. `runInUoW` is the only codepath.
17
+ *
18
+ * Interceptor support is provided by wrapping with
19
+ * {@link createInterceptingQueryBus}.
20
+ */
21
+ export function createSimpleQueryBus(): QueryBus {
22
+ const handlers = new Map<string, (message: QueryMessage) => Promise<unknown>>()
23
+
24
+ // Active subscription query handlers, keyed by query identifier
25
+ const subscriptions = new Map<string, UpdateHandler>()
26
+
27
+ const bus: QueryBus = {
28
+ async query(message: QueryMessage): Promise<unknown> {
29
+ const key = qualifiedNameToString(message.name)
30
+ const handler = handlers.get(key)
31
+ if (!handler) {
32
+ throw new Error(`No handler registered for query "${key}"`)
33
+ }
34
+
35
+ // Plan 03-01 (D-32) / Plan 03-04 (CTX-04): mirrors
36
+ // simple-command-bus.dispatch. ALS-aware nesting; primary dispatch
37
+ // creates a new UoW.
38
+ return runInUoW(message.metadata, () => handler(message))
39
+ },
40
+
41
+ subscribe(
42
+ queryName: string,
43
+ handler: (message: QueryMessage) => Promise<unknown>,
44
+ ) {
45
+ const existing = handlers.get(queryName)
46
+ if (existing && existing !== handler) {
47
+ throw new Error(
48
+ `A different handler is already registered for query "${queryName}". ` +
49
+ `Duplicate query handler subscriptions are not allowed.`,
50
+ )
51
+ }
52
+ handlers.set(queryName, handler)
53
+ },
54
+
55
+ subscriptionQuery(message: QueryMessage, bufferSize?: number): SubscriptionQueryResult {
56
+ const queryId = message.identifier
57
+
58
+ if (subscriptions.has(queryId)) {
59
+ throw new Error(`Subscription query already registered for identifier "${queryId}"`)
60
+ }
61
+
62
+ const updateHandler = createUpdateHandler(message, bufferSize)
63
+ subscriptions.set(queryId, updateHandler)
64
+
65
+ const initialResult = bus.query(message)
66
+
67
+ return {
68
+ initialResult,
69
+ updates: updateHandler.iterable,
70
+ close: () => {
71
+ subscriptions.delete(queryId)
72
+ updateHandler.complete()
73
+ },
74
+ }
75
+ },
76
+
77
+ subscribeToUpdates(message: QueryMessage, bufferSize?: number): AsyncIterable<unknown> & { close(): void } {
78
+ const queryId = message.identifier
79
+
80
+ if (subscriptions.has(queryId)) {
81
+ throw new Error(`Subscription query already registered for identifier "${queryId}"`)
82
+ }
83
+
84
+ const updateHandler = createUpdateHandler(message, bufferSize)
85
+ subscriptions.set(queryId, updateHandler)
86
+
87
+ return {
88
+ [Symbol.asyncIterator]: () => updateHandler.iterable[Symbol.asyncIterator](),
89
+ close: () => {
90
+ subscriptions.delete(queryId)
91
+ updateHandler.complete()
92
+ },
93
+ }
94
+ },
95
+
96
+ async emitUpdate(
97
+ queryName: string,
98
+ filter: (queryPayload: unknown) => boolean,
99
+ update: unknown,
100
+ ): Promise<void> {
101
+ runAfterCommitOrImmediately(() => {
102
+ for (const [id, handler] of subscriptions) {
103
+ if (!handler.active) {
104
+ subscriptions.delete(id)
105
+ continue
106
+ }
107
+
108
+ const handlerQueryName = qualifiedNameToString(handler.query.name)
109
+ if (handlerQueryName !== queryName) continue
110
+ if (!filter(handler.query.payload)) continue
111
+
112
+ const accepted = handler.offer(update)
113
+ if (!accepted) {
114
+ handler.completeExceptionally(
115
+ new Error("Subscription query update buffer overflow"),
116
+ )
117
+ subscriptions.delete(id)
118
+ }
119
+ }
120
+ })
121
+ },
122
+
123
+ async completeSubscription(
124
+ queryName: string,
125
+ filter?: (queryPayload: unknown) => boolean,
126
+ ): Promise<void> {
127
+ runAfterCommitOrImmediately(() => {
128
+ for (const [id, handler] of subscriptions) {
129
+ const handlerQueryName = qualifiedNameToString(handler.query.name)
130
+ if (handlerQueryName !== queryName) continue
131
+ if (filter && !filter(handler.query.payload)) continue
132
+
133
+ handler.complete()
134
+ subscriptions.delete(id)
135
+ }
136
+ })
137
+ },
138
+
139
+ async completeSubscriptionExceptionally(
140
+ queryName: string,
141
+ error: Error,
142
+ filter?: (queryPayload: unknown) => boolean,
143
+ ): Promise<void> {
144
+ runAfterCommitOrImmediately(() => {
145
+ for (const [id, handler] of subscriptions) {
146
+ const handlerQueryName = qualifiedNameToString(handler.query.name)
147
+ if (handlerQueryName !== queryName) continue
148
+ if (filter && !filter(handler.query.payload)) continue
149
+
150
+ handler.completeExceptionally(error)
151
+ subscriptions.delete(id)
152
+ }
153
+ })
154
+ },
155
+ }
156
+
157
+ return bus
158
+ }