@kronos-ts/axon-server 0.4.0 → 0.5.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 (84) hide show
  1. package/dist/axon-server-event-store.d.ts +0 -16
  2. package/dist/axon-server-event-store.d.ts.map +1 -1
  3. package/dist/axon-server-event-store.js +47 -14
  4. package/dist/axon-server-event-store.js.map +1 -1
  5. package/dist/axon-server-snapshotting-event-store.d.ts +30 -0
  6. package/dist/axon-server-snapshotting-event-store.d.ts.map +1 -0
  7. package/dist/axon-server-snapshotting-event-store.js +153 -0
  8. package/dist/axon-server-snapshotting-event-store.js.map +1 -0
  9. package/dist/axon-server.d.ts +33 -60
  10. package/dist/axon-server.d.ts.map +1 -1
  11. package/dist/axon-server.js +58 -59
  12. package/dist/axon-server.js.map +1 -1
  13. package/dist/connection-manager.d.ts +2 -2
  14. package/dist/connection-manager.d.ts.map +1 -1
  15. package/dist/connection.d.ts +23 -21
  16. package/dist/connection.d.ts.map +1 -1
  17. package/dist/connection.js +6 -5
  18. package/dist/connection.js.map +1 -1
  19. package/dist/context-view.d.ts +2 -2
  20. package/dist/context-view.d.ts.map +1 -1
  21. package/dist/control-plane.d.ts +5 -5
  22. package/dist/control-plane.d.ts.map +1 -1
  23. package/dist/event-processor-info.d.ts +4 -4
  24. package/dist/event-processor-info.d.ts.map +1 -1
  25. package/dist/flow-controlled-sender.d.ts +2 -2
  26. package/dist/flow-controlled-sender.d.ts.map +1 -1
  27. package/dist/generated/command.d.ts +20 -20
  28. package/dist/generated/command.d.ts.map +1 -1
  29. package/dist/generated/common.d.ts +20 -20
  30. package/dist/generated/common.d.ts.map +1 -1
  31. package/dist/generated/control.d.ts +36 -40
  32. package/dist/generated/control.d.ts.map +1 -1
  33. package/dist/generated/dcb.d.ts +88 -94
  34. package/dist/generated/dcb.d.ts.map +1 -1
  35. package/dist/generated/event.d.ts +81 -84
  36. package/dist/generated/event.d.ts.map +1 -1
  37. package/dist/generated/google/protobuf/empty.d.ts +3 -4
  38. package/dist/generated/google/protobuf/empty.d.ts.map +1 -1
  39. package/dist/generated/query.d.ts +40 -40
  40. package/dist/generated/query.d.ts.map +1 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/message-size.d.ts +2 -2
  46. package/dist/message-size.d.ts.map +1 -1
  47. package/dist/outbound-stream.d.ts +2 -2
  48. package/dist/outbound-stream.d.ts.map +1 -1
  49. package/dist/platform-service.d.ts +4 -4
  50. package/dist/platform-service.d.ts.map +1 -1
  51. package/dist/resilience.d.ts +62 -0
  52. package/dist/resilience.d.ts.map +1 -0
  53. package/dist/resilience.js +103 -0
  54. package/dist/resilience.js.map +1 -0
  55. package/dist/shutdown-latch.d.ts +4 -4
  56. package/dist/shutdown-latch.d.ts.map +1 -1
  57. package/package.json +3 -3
  58. package/src/axon-server-event-store.ts +46 -14
  59. package/src/axon-server-snapshotting-event-store.ts +191 -0
  60. package/src/axon-server.ts +72 -77
  61. package/src/connection-manager.ts +1 -1
  62. package/src/connection.ts +16 -14
  63. package/src/context-view.ts +1 -1
  64. package/src/control-plane.ts +3 -3
  65. package/src/event-processor-info.ts +2 -2
  66. package/src/flow-controlled-sender.ts +1 -1
  67. package/src/generated/command.ts +10 -10
  68. package/src/generated/common.ts +10 -10
  69. package/src/generated/control.ts +20 -20
  70. package/src/generated/dcb.ts +47 -47
  71. package/src/generated/event.ts +42 -42
  72. package/src/generated/google/protobuf/empty.ts +2 -2
  73. package/src/generated/query.ts +20 -20
  74. package/src/index.ts +1 -1
  75. package/src/message-size.ts +1 -1
  76. package/src/outbound-stream.ts +1 -1
  77. package/src/platform-service.ts +2 -2
  78. package/src/resilience.ts +149 -0
  79. package/src/shutdown-latch.ts +2 -2
  80. package/dist/axon-server-snapshot-store.d.ts +0 -12
  81. package/dist/axon-server-snapshot-store.d.ts.map +0 -1
  82. package/dist/axon-server-snapshot-store.js +0 -82
  83. package/dist/axon-server-snapshot-store.js.map +0 -1
  84. package/src/axon-server-snapshot-store.ts +0 -110
@@ -161,6 +161,19 @@ function createEventConverters(serializer: Serializer) {
161
161
  * a single gRPC Stream RPC call that stays open indefinitely, aligned with
162
162
  * Java's infinite {@code ResultStream}.
163
163
  */
164
+ /**
165
+ * Axon Server's way of saying "there is nothing at or after that position".
166
+ *
167
+ * Matched on the gRPC status code first — `OUT_OF_RANGE` is 11 — with the
168
+ * server's own message as a second, narrower gate, so an unrelated future
169
+ * OUT_OF_RANGE is not swallowed along with it.
170
+ */
171
+ function isStartPastHead(err: unknown): boolean {
172
+ const code = (err as { code?: unknown }).code
173
+ const details = String((err as { details?: unknown }).details ?? "")
174
+ return code === 11 && details.includes("Start sequence cannot be larger than end sequence")
175
+ }
176
+
164
177
  export function axonServerEventStore(conn: AxonServerStoreSource, context: string): EventStore {
165
178
  const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
166
179
  const { eventToProto, eventFromProto } = createEventConverters(serializer)
@@ -182,30 +195,49 @@ export function axonServerEventStore(conn: AxonServerStoreSource, context: strin
182
195
  return {
183
196
  async source(condition: SourcingCondition): Promise<SourcingResult> {
184
197
  const criterions = criteriaToCriterions(compileQuery(condition.query))
198
+ const start = condition.start ?? 0n
185
199
 
186
200
  const request = {
187
- fromSequence: condition.start ?? 0n,
201
+ fromSequence: start,
188
202
  criterion: criterions,
189
203
  }
190
204
 
191
205
  const events: EventMessage[] = []
192
206
  let marker: ConsistencyMarker = noMarker()
193
207
 
194
- const stream = connection.eventStore.source(request, { metadata: createAxonMetadata() })
195
- for await (const response of stream) {
196
- if (response.event) {
197
- const taggedEvent = response.event
198
- const protoEvent = taggedEvent.event
199
- if (protoEvent) {
200
- // DCB source/stream responses carry no tags — the server indexes
201
- // them write-side but does not echo them back (SequencedEvent has
202
- // only sequence + event). Reconstructed events get empty tags.
203
- events.push(eventFromProto(protoEvent, []))
208
+ try {
209
+ const stream = connection.eventStore.source(request, { metadata: createAxonMetadata() })
210
+ for await (const response of stream) {
211
+ if (response.event) {
212
+ const taggedEvent = response.event
213
+ const protoEvent = taggedEvent.event
214
+ if (protoEvent) {
215
+ // DCB source/stream responses carry no tags the server indexes
216
+ // them write-side but does not echo them back (SequencedEvent has
217
+ // only sequence + event). Reconstructed events get empty tags.
218
+ events.push(eventFromProto(protoEvent, []))
219
+ }
220
+ }
221
+ if (response.consistencyMarker !== undefined) {
222
+ marker = markerAt(response.consistencyMarker)
204
223
  }
205
224
  }
206
- if (response.consistencyMarker !== undefined) {
207
- marker = markerAt(response.consistencyMarker)
208
- }
225
+ } catch (err) {
226
+ // READING PAST THE HEAD IS AN EMPTY ANSWER, NOT AN ERROR — and Axon
227
+ // Server disagrees, so this is where the two vocabularies are
228
+ // reconciled. `Source` fails OUT_OF_RANGE with "Start sequence cannot
229
+ // be larger than end sequence" whenever `fromSequence` is beyond the
230
+ // global head, which is the ORDINARY STEADY STATE of a snapshotted
231
+ // load: an entry written at the head means the very next read resumes
232
+ // at head + 1 and legitimately finds nothing.
233
+ //
234
+ // The marker is `start - 1`, and it is exact rather than conservative:
235
+ // the caller has already accounted for everything up to there (that is
236
+ // what asking to start later MEANS), and nothing can exist after it or
237
+ // the server would not have refused. So an append conditioned on this
238
+ // read is checked against precisely the range that was read.
239
+ if (!isStartPastHead(err)) throw err
240
+ return { events: [], marker: start > 0n ? markerAt(start - 1n) : noMarker() }
209
241
  }
210
242
 
211
243
  return { events, marker }
@@ -0,0 +1,191 @@
1
+ // ---------------------------------------------------------------------------
2
+ // AXON SERVER SNAPSHOTS — the capability tier, served by `DcbSnapshotStore`.
3
+ //
4
+ // THE PROBE, AND WHAT IT SETTLED. Axon Server's public API defines both a
5
+ // snapshot STORE service (`DcbSnapshotStore`: Add / Delete / List / GetLast)
6
+ // and, on newer API drops, a fused `SnapshottedDcbEventStore.Source` whose
7
+ // stream leads with the snapshot. Only the first of those is actually served:
8
+ //
9
+ // axoniq/axonserver:2025.2.5 DcbSnapshotStore/GetLast → answers
10
+ // SnapshottedDcbEventStore/Source → UNIMPLEMENTED
11
+ // axoniq/axonserver:2026.0.4 same, both ways
12
+ //
13
+ // So there is no server-side fusion to call here — there is nothing on the
14
+ // other end of the wire. The READ is therefore fused CLIENT-SIDE: `GetLast` for
15
+ // the entry, then a source after its position, assembled into the one
16
+ // `SourcingResult` a fold expects. Two round trips, the same answers, correct
17
+ // today.
18
+ //
19
+ // When a server version does serve `SnapshottedDcbEventStore.Source`, the fused
20
+ // call lands INSIDE this function — `source` stops making two calls and starts
21
+ // making one — and no host changes a line, because the capability was never a
22
+ // promise about round trips.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ import type {
26
+ EventStore,
27
+ Snapshot,
28
+ SnapshotCapability,
29
+ SourcingCondition,
30
+ SourcingResult,
31
+ } from "@kronos-ts/core"
32
+ import type { Serializer } from "@kronos-ts/core"
33
+ import { withoutSnapshotKey } from "@kronos-ts/core"
34
+ import type { AxonServerStoreSource } from "./connection.js"
35
+ import { contextView } from "./context-view.js"
36
+ import type { Snapshot as ProtoSnapshot } from "./generated/dcb.js"
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Conversion — framework Snapshot ↔ proto Snapshot
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const encoder = new TextEncoder()
43
+
44
+ /**
45
+ * The proto message carries `name`, `version`, `timestamp` and a metadata map
46
+ * the narrowed seam has no equivalent for. `name` carries the cache key,
47
+ * `timestamp` is filled at write time for operators, and `version` stays EMPTY
48
+ * — snapshots have no versions, and inventing one to fill a proto field would
49
+ * be exactly the bookkeeping this design removed. The metadata map stays empty
50
+ * too: a cache entry carries no cargo.
51
+ */
52
+ function createSnapshotConverters(serializer: Serializer) {
53
+ return {
54
+ snapshotToProto(name: string, snapshot: Snapshot): ProtoSnapshot {
55
+ const serialized = serializer.serialize(snapshot.state, name, "")
56
+ return {
57
+ name,
58
+ version: "",
59
+ payload: serialized.data,
60
+ timestamp: BigInt(Date.now()),
61
+ metadata: {},
62
+ }
63
+ },
64
+
65
+ snapshotFromProto(name: string, proto: ProtoSnapshot, position: bigint): Snapshot {
66
+ const state =
67
+ proto.payload.length > 0
68
+ ? serializer.deserialize({ data: proto.payload, type: name, revision: "" })
69
+ : {}
70
+
71
+ return { state, position }
72
+ },
73
+ }
74
+ }
75
+
76
+ function encodeKey(key: string): Uint8Array {
77
+ return encoder.encode(key)
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Axon Server snapshot store
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /**
85
+ * Add the snapshotting capability to an Axon Server-backed log.
86
+ *
87
+ * ```ts
88
+ * const eventStore = axonServerSnapshottingEventStore(
89
+ * axonServerEventStore(axon, "default"),
90
+ * axon,
91
+ * "default",
92
+ * )
93
+ * ```
94
+ *
95
+ * ADDITIVE, NOT COLLAPSING. It returns `E & SnapshotCapability` — the store you
96
+ * passed in, plus the write — so capabilities stack in either order and nothing
97
+ * the inner store carried is laundered on the way through.
98
+ *
99
+ * `context` is a per-call header, so this shares the one channel `conn` holds
100
+ * with every other context — see `contextView`. Payloads go through the
101
+ * connection's serializer.
102
+ *
103
+ * LATEST-ONLY, over a service that is not. `DcbSnapshotStore` keeps a sequence
104
+ * of snapshots per key; the capability keeps one. `add` therefore sets
105
+ * `prune: true`, so writing an entry retires the ones before it, and `getLast`
106
+ * is the only read. That is the narrowing doing its job: the framework programs
107
+ * against the cache it needs, not against everything the backend happens to
108
+ * offer.
109
+ */
110
+ export function axonServerSnapshottingEventStore<E extends EventStore>(
111
+ next: E,
112
+ conn: AxonServerStoreSource,
113
+ context: string,
114
+ ): E & SnapshotCapability {
115
+ const { connection, serializer, metadata: createAxonMetadata } = contextView(conn, context)
116
+ const { snapshotToProto, snapshotFromProto } = createSnapshotConverters(serializer)
117
+
118
+ /** The cached fold filed under `key`, or nothing — the first of the two calls. */
119
+ async function loadSnapshot(key: string): Promise<Snapshot | undefined> {
120
+ try {
121
+ const response = await connection.snapshotStore.getLast(
122
+ { key: encodeKey(key) },
123
+ { metadata: createAxonMetadata() },
124
+ )
125
+
126
+ if (!response.snapshot) {
127
+ return undefined
128
+ }
129
+
130
+ return snapshotFromProto(key, response.snapshot, response.sequence)
131
+ } catch (err) {
132
+ // Axon Server throws rather than answering empty when no snapshot
133
+ // exists — and a cache miss is not an error to anybody upstream.
134
+ if (
135
+ String(err).includes("No snapshot found") ||
136
+ String(err).includes("not found")
137
+ ) {
138
+ return undefined
139
+ }
140
+ throw err
141
+ }
142
+ }
143
+
144
+ return {
145
+ ...next,
146
+
147
+ async storeSnapshot(key: string, snapshot: Snapshot): Promise<void> {
148
+ await connection.snapshotStore.add(
149
+ {
150
+ key: encodeKey(key),
151
+ sequence: snapshot.position,
152
+ prune: true,
153
+ snapshot: snapshotToProto(key, snapshot),
154
+ },
155
+ { metadata: createAxonMetadata() },
156
+ )
157
+ },
158
+
159
+ /**
160
+ * THE CLIENT-SIDE FUSION. Two calls, assembled into the one result a fold
161
+ * expects — and the cache is NEVER LOAD-BEARING, so a miss or an outright
162
+ * throw from the snapshot service both fall through to a full read.
163
+ */
164
+ async source(condition: SourcingCondition): Promise<SourcingResult> {
165
+ const key = condition.snapshot
166
+ if (key === undefined) return next.source(condition)
167
+
168
+ // The strategy is CONSUMED here; the store below gets a plain condition.
169
+ const plain = withoutSnapshotKey(condition)
170
+
171
+ let snapshot: Snapshot | undefined
172
+ try {
173
+ snapshot = await loadSnapshot(key.key)
174
+ } catch {
175
+ // A cache you cannot reach is a cache miss. Loads stay correct; they
176
+ // just cost what they always cost.
177
+ return next.source(plain)
178
+ }
179
+ if (snapshot === undefined) return next.source(plain)
180
+
181
+ // Resume AFTER the position the snapshot already folded; a condition that
182
+ // independently asked to start later keeps its own floor.
183
+ const resumeFrom = snapshot.position + 1n
184
+ const start =
185
+ plain.start !== undefined && plain.start > resumeFrom ? plain.start : resumeFrom
186
+
187
+ const result = await next.source({ ...plain, start })
188
+ return { ...result, snapshot }
189
+ },
190
+ } as E & SnapshotCapability
191
+ }
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * Axon Server is a SMART HUB: outbound dispatch always goes to the server, and
5
5
  * the server decides which node handles it — there is no client-side
6
- * prefer-local fork here, which is the whole difference from the dumb-pipe
6
+ * prefer-next fork here, which is the whole difference from the dumb-pipe
7
7
  * broker in `@kronos-ts/rabbitmq`.
8
8
  *
9
- * Both buses are plain functions over the shared connection and YOUR local bus:
9
+ * Both buses are plain functions over the shared connection and YOUR next bus:
10
10
  *
11
11
  * ```ts
12
12
  * const commandBus = interceptingCommandBus(
13
- * axonServerCommandBus(axon, simpleCommandBus(unitOfWork)), lineage)
13
+ * axonServerCommandBus(localCommandBus(unitOfWork), axon), correlation)
14
14
  * const queryBus = interceptingQueryBus(
15
- * axonServerQueryBus(axon, simpleQueryBus(unitOfWork)), lineage)
15
+ * axonServerQueryBus(localQueryBus(unitOfWork), axon), correlation)
16
16
  * ```
17
17
  *
18
18
  * Axon-specific protocol invariants are preserved byte-for-byte:
@@ -30,9 +30,8 @@ import {
30
30
  qualifiedNameFromString,
31
31
  generateIdentifier,
32
32
  type Serializer,
33
- withRetry,
34
- type ResilienceConfig,
35
33
  } from "@kronos-ts/core"
34
+ import { withRetry, type ResilienceConfig } from "./resilience.js"
36
35
  import type {
37
36
  CommandBus,
38
37
  CommandMessage,
@@ -41,12 +40,10 @@ import type {
41
40
  SubscriptionFilter,
42
41
  SubscriptionQueryResult,
43
42
  UnitOfWork,
44
- Unstamped,
45
43
  UpdateHandler,
46
44
  } from "@kronos-ts/core"
47
45
  import {
48
46
  applySubscriptionFilter,
49
- stamped,
50
47
  updateHandler,
51
48
  runAfterCommitOrImmediately,
52
49
  } from "@kronos-ts/core"
@@ -69,7 +66,7 @@ const DEFAULT_LOAD_FACTOR = 100
69
66
  /**
70
67
  * Flow control configuration for a bus channel.
71
68
  */
72
- export interface FlowControlConfig {
69
+ export type FlowControlConfig = {
73
70
  /** Initial permits granted to Axon Server. Default: 5000 (aligned with Java). */
74
71
  permits?: number
75
72
  /** Threshold at which to request more permits. Default: 2500 (aligned with Java). */
@@ -80,7 +77,7 @@ export interface FlowControlConfig {
80
77
  * Processing instructions attached to outbound messages.
81
78
  * Controls routing, priority, and timeout behavior on Axon Server.
82
79
  */
83
- export interface ProcessingInstructions {
80
+ export type ProcessingInstructions = {
84
81
  /** Routing key for consistent hashing (e.g., aggregate ID). */
85
82
  routingKey?: string
86
83
  /** Priority (higher = processed first). Default: 0 */
@@ -91,10 +88,10 @@ export interface ProcessingInstructions {
91
88
 
92
89
  /**
93
90
  * Tuning for {@link axonServerCommandBus}. Every field has a working default;
94
- * the two arguments that carry meaning — the connection and your local bus —
91
+ * the two arguments that carry meaning — the connection and your next bus —
95
92
  * are positional, and this record is the trailing remainder.
96
93
  */
97
- export interface AxonServerCommandBusOptions {
94
+ export type AxonServerCommandBusOptions = {
98
95
  /** Axon Server context for this bus's stream. Default: the connection's. */
99
96
  context?: string
100
97
  /** Flow control for the command stream. */
@@ -112,7 +109,7 @@ export interface AxonServerCommandBusOptions {
112
109
  /**
113
110
  * Tuning for {@link axonServerQueryBus}. See {@link AxonServerCommandBusOptions}.
114
111
  */
115
- export interface AxonServerQueryBusOptions {
112
+ export type AxonServerQueryBusOptions = {
116
113
  /** Axon Server context for this bus's stream. Default: the connection's. */
117
114
  context?: string
118
115
  /** Flow control for the query stream. */
@@ -208,33 +205,33 @@ function createPayloadHelpers(serializer: Serializer) {
208
205
  // ---------------------------------------------------------------------------
209
206
 
210
207
  /**
211
- * A command bus backed by Axon Server, over YOUR local bus.
208
+ * A command bus backed by Axon Server, over YOUR next bus.
212
209
  *
213
210
  * - **Outbound dispatch**: ALWAYS through Axon Server, via the unary Dispatch
214
211
  * RPC. Axon Server routes the command to the appropriate node (which may be
215
- * this one). There is deliberately no client-side prefer-local fork: the hub
212
+ * this one). There is deliberately no client-side prefer-next fork: the hub
216
213
  * is the router, and short-circuiting it would silently defeat load factors,
217
214
  * priorities and routing keys.
218
- * - **Inbound**: a command the server routes here is dispatched into `local` —
215
+ * - **Inbound**: a command the server routes here is dispatched into `next` —
219
216
  * not into a privately-held handler map. That is what makes the unit-of-work
220
- * policy you chose for `local` (say `postgresUnitOfWork(pg, unitOfWork)`)
217
+ * policy you chose for `next` (say `postgresUnitOfWork(unitOfWork, pg)`)
221
218
  * apply to server-routed work exactly as it applies to work this process
222
219
  * originated. It is also why this function takes no `unitOfWork` argument:
223
- * `local` carries that policy now.
224
- * - **subscribe**: registers the handler on `local` AND announces the name to
220
+ * `next` carries that policy now.
221
+ * - **subscribe**: registers the handler on `next` AND announces the name to
225
222
  * Axon Server, so other nodes can route to us.
226
223
  *
227
- * ## Correlation lineage and the interceptor layer
224
+ * ## correlation and the interceptor layer
228
225
  *
229
- * The returned bus stamps no lineage of its own. A host that wants it wraps the
226
+ * The returned bus stamps no correlation of its own. A host that wants it wraps the
230
227
  * OUTERMOST bus:
231
228
  *
232
229
  * ```ts
233
- * interceptingCommandBus(axonServerCommandBus(conn, local), lineage)
230
+ * interceptingCommandBus(axonServerCommandBus(next, conn), correlation)
234
231
  * ```
235
232
  *
236
233
  * so whatever a host adds runs BEFORE the message is serialized onto the wire.
237
- * Lineage itself is usually already on `message.metadata` by then — `ctx.send`
234
+ * Correlation itself is usually already on `message.metadata` by then — `ctx.send`
238
235
  * stamps the unit of work's correlation data before any bus sees the message.
239
236
  *
240
237
  * This is precisely how the Java client does it. AF4's `AxonServerCommandBus`
@@ -242,17 +239,17 @@ function createPayloadHelpers(serializer: Serializer) {
242
239
  * `doDispatch(dispatchInterceptors.intercept(commandMessage), cb)` — one call
243
240
  * site, at the top, ahead of any routing. AF5 keeps the property via decorator
244
241
  * order: `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
245
- * stacks `InterceptingCommandBus → DistributedCommandBus → SimpleCommandBus`.
242
+ * stacks `InterceptingCommandBus → DistributedCommandBus → LocalCommandBus`.
246
243
  *
247
- * If `local` is itself an intercepting bus, a server-routed command sees
248
- * `lineage` twice. That is harmless: both of its fields are `??` seeds, so the
244
+ * If `next` is itself an intercepting bus, a server-routed command sees
245
+ * `correlation` twice. That is harmless: both of its fields are `??` seeds, so the
249
246
  * second application finds them set and changes nothing.
250
247
  */
251
- export function axonServerCommandBus(
248
+ export function axonServerCommandBus<U extends UnitOfWork = UnitOfWork>(
249
+ next: CommandBus<U>,
252
250
  conn: AxonServerBusSource,
253
- local: CommandBus,
254
251
  options: AxonServerCommandBusOptions = {},
255
- ): CommandBus {
252
+ ): CommandBus<U> {
256
253
  const {
257
254
  connection,
258
255
  serializer,
@@ -268,9 +265,9 @@ export function axonServerCommandBus(
268
265
 
269
266
  /**
270
267
  * The names this node announced to Axon Server. The handlers themselves live
271
- * on `local`; this set exists so an inbound command for a name we never
268
+ * on `next`; this set exists so an inbound command for a name we never
272
269
  * subscribed still answers NO_HANDLER_FOR_COMMAND rather than whatever
273
- * `local.dispatch` happens to throw — and so a reconnect can re-announce.
270
+ * `next.dispatch` happens to throw — and so a reconnect can re-announce.
274
271
  */
275
272
  const subscribedNames = new Set<string>()
276
273
 
@@ -371,18 +368,18 @@ export function axonServerCommandBus(
371
368
  }
372
369
 
373
370
  // Through the LOCAL BUS, so the caller's unit-of-work policy runs.
374
- // AF parity is preserved: `CommandProcessingTask` runs the local
375
- // segment without re-running dispatch interceptors, and a `local`
376
- // that happens to carry `lineage` re-applies a pair of `??` seeds
371
+ // AF parity is preserved: `CommandProcessingTask` runs the next
372
+ // segment without re-running dispatch interceptors, and a `next`
373
+ // that happens to carry `correlation` re-applies a pair of `??` seeds
377
374
  // that are already set.
378
- resultPayload = await local.dispatch(commandMessage)
375
+ resultPayload = await next.dispatch(commandMessage)
379
376
  } catch (err) {
380
377
  errorCode = AxonServerErrorCode.COMMAND_EXECUTION_ERROR
381
378
  errorMsg = err instanceof Error ? err.message : String(err)
382
379
  }
383
380
  } else {
384
381
  errorCode = AxonServerErrorCode.NO_HANDLER_FOR_COMMAND
385
- errorMsg = `No local handler for command "${commandName}"`
382
+ errorMsg = `No next handler for command "${commandName}"`
386
383
  }
387
384
 
388
385
  // Send response back to Axon Server
@@ -431,14 +428,13 @@ export function axonServerCommandBus(
431
428
  }
432
429
 
433
430
  return {
434
- async dispatch(unstamped: Unstamped<CommandMessage>): Promise<unknown> {
431
+ async dispatch(unstamped: CommandMessage): Promise<unknown> {
435
432
  // A transport is not a task: it has no unit of work, so it has no clock.
436
- // A message that reaches the wire still {@link Unstamped} is therefore
437
- // stamped from system time here — the envelope crosses a process boundary
438
- // and must be fully formed. A locally-shortcut message is handed to
439
- // `local` unstamped instead, so the task that handles it supplies the
440
- // instant.
441
- const message = stamped(unstamped, Date.now)
433
+ // A message that reaches the wire with no instant yet gets one from system
434
+ // time here — the envelope crosses a process boundary and must be fully
435
+ // formed. A locally-shortcut message is handed to `next` untouched
436
+ // instead, so the task that handles it supplies the instant.
437
+ const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() }
442
438
  const activity = shutdownLatch.registerActivity()
443
439
  try {
444
440
  const commandName = qualifiedNameToString(message.name)
@@ -471,10 +467,10 @@ export function axonServerCommandBus(
471
467
 
472
468
  subscribe(
473
469
  commandName: string,
474
- handler: (message: CommandMessage, uow: UnitOfWork) => Promise<unknown>,
470
+ handler: (message: CommandMessage, uow: U) => Promise<unknown>,
475
471
  ) {
476
472
  subscribedNames.add(commandName)
477
- local.subscribe(commandName, handler)
473
+ next.subscribe(commandName, handler)
478
474
 
479
475
  ensureStreamStarted()
480
476
  // Subscription FIRST
@@ -490,35 +486,35 @@ export function axonServerCommandBus(
490
486
  // ---------------------------------------------------------------------------
491
487
 
492
488
  /**
493
- * A query bus backed by Axon Server, over YOUR local bus.
489
+ * A query bus backed by Axon Server, over YOUR next bus.
494
490
  *
495
491
  * Same architecture as {@link axonServerCommandBus}: outbound dispatch goes
496
- * through Axon Server, and a query the server routes here runs through `local`,
492
+ * through Axon Server, and a query the server routes here runs through `next`,
497
493
  * so your unit-of-work policy applies to server-routed reads too. `subscribe`
498
- * registers on `local` and announces the name to the server.
494
+ * registers on `next` and announces the name to the server.
499
495
  *
500
496
  * The one asymmetry with commands is `shortcutQueriesToLocalHandlers` — Java
501
497
  * has it for queries and not for commands, and so do we. When it is on and this
502
- * node subscribed the name, `query()` goes straight to `local` and the caller's
503
- * unit of work is passed through, so the local branch nests exactly as the
498
+ * node subscribed the name, `query()` goes straight to `next` and the caller's
499
+ * unit of work is passed through, so the next branch nests exactly as the
504
500
  * in-process bus does.
505
501
  *
506
- * Lineage, if wanted, is `interceptingQueryBus(bus, lineage)` at the host,
502
+ * Correlation, if wanted, is `interceptingQueryBus(bus, correlation)` at the host,
507
503
  * matching AF4's `AxonServerQueryBus`, which calls
508
504
  * `dispatchInterceptors.intercept(...)` at the top of `query`, `streamingQuery`,
509
505
  * `scatterGather` and `subscriptionQuery`. Because the wrap is outside, the
510
- * shortcut branch gets identical lineage to the remote branch.
506
+ * shortcut branch gets identical correlation to the remote branch.
511
507
  *
512
508
  * KNOWN GAP: `subscriptionQuery` / `subscribeToUpdates` build their proto
513
509
  * straight from `message.metadata`, and `interceptingQueryBus` (in
514
510
  * `@kronos-ts/core`) forwards those two calls to the delegate without
515
511
  * running the dispatch chain. Closing that needs a core change.
516
512
  */
517
- export function axonServerQueryBus(
513
+ export function axonServerQueryBus<U extends UnitOfWork = UnitOfWork>(
514
+ next: QueryBus<U>,
518
515
  conn: AxonServerBusSource,
519
- local: QueryBus,
520
516
  options: AxonServerQueryBusOptions = {},
521
- ): QueryBus {
517
+ ): QueryBus<U> {
522
518
  const {
523
519
  connection,
524
520
  serializer,
@@ -533,7 +529,7 @@ export function axonServerQueryBus(
533
529
  const queryTimeoutMs = options.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
534
530
  const { serializePayload, deserializePayload } = createPayloadHelpers(serializer)
535
531
 
536
- /** Query names announced to Axon Server; the handlers live on `local`. */
532
+ /** Query names announced to Axon Server; the handlers live on `next`. */
537
533
  const subscribedNames = new Set<string>()
538
534
 
539
535
  // Local subscription store — subscription queries opened by THIS instance.
@@ -643,14 +639,14 @@ export function axonServerQueryBus(
643
639
  metadata: metadataFromProto(proto.metaData ?? {}),
644
640
  timestamp: Number(proto.timestamp),
645
641
  }
646
- resultPayload = await local.query(queryMessage)
642
+ resultPayload = await next.query(queryMessage)
647
643
  } catch (err) {
648
644
  errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR
649
645
  errorMsg = err instanceof Error ? err.message : String(err)
650
646
  }
651
647
  } else {
652
648
  errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY
653
- errorMsg = `No local handler for query "${queryName}"`
649
+ errorMsg = `No next handler for query "${queryName}"`
654
650
  }
655
651
 
656
652
  const responseSerialized =
@@ -715,16 +711,16 @@ export function axonServerQueryBus(
715
711
  timestamp: Number(proto.timestamp),
716
712
  }
717
713
 
718
- // Through the LOCAL BUS: no unit of work is handed in, so `local`
714
+ // Through the LOCAL BUS: no unit of work is handed in, so `next`
719
715
  // opens one under whatever policy the caller gave it.
720
- resultPayload = await local.query(queryMessage)
716
+ resultPayload = await next.query(queryMessage)
721
717
  } catch (err) {
722
718
  errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR
723
719
  errorMsg = err instanceof Error ? err.message : String(err)
724
720
  }
725
721
  } else {
726
722
  errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY
727
- errorMsg = `No local handler for query "${queryName}"`
723
+ errorMsg = `No next handler for query "${queryName}"`
728
724
  }
729
725
 
730
726
  outbound.send({
@@ -778,28 +774,27 @@ export function axonServerQueryBus(
778
774
  }
779
775
  }
780
776
 
781
- const routing: QueryBus = {
782
- async query(unstamped: Unstamped<QueryMessage>, uow?: UnitOfWork): Promise<unknown> {
777
+ const routing: QueryBus<U> = {
778
+ async query(unstamped: QueryMessage, uow?: UnitOfWork): Promise<unknown> {
783
779
  const activity = shutdownLatch.registerActivity()
784
780
  try {
785
781
  const queryName = qualifiedNameToString(unstamped.name)
786
782
 
787
783
  // Local shortcut — handle locally if a handler is co-located. The
788
- // caller's unit of work is passed straight through, so `local` makes the
784
+ // caller's unit of work is passed straight through, so `next` makes the
789
785
  // nest-or-open decision on the HANDLE exactly as it does for an
790
786
  // in-process read: a live unit of work handed in by `ctx.query` is
791
787
  // reused so the consulting read shares the caller's transaction.
792
788
  if (shortcutQueriesToLocalHandlers && subscribedNames.has(queryName)) {
793
- return local.query(unstamped, uow)
789
+ return next.query(unstamped, uow)
794
790
  }
795
791
 
796
792
  // A transport is not a task: it has no unit of work, so it has no clock.
797
- // A message that reaches the wire still {@link Unstamped} is therefore
798
- // stamped from system time here — the envelope crosses a process boundary
799
- // and must be fully formed. A locally-shortcut message is handed to
800
- // `local` unstamped instead, so the task that handles it supplies the
801
- // instant.
802
- const message = stamped(unstamped, Date.now)
793
+ // A message that reaches the wire with no instant yet gets one from system
794
+ // time here — the envelope crosses a process boundary and must be fully
795
+ // formed. A locally-shortcut message is handed to `next` untouched
796
+ // instead, so the task that handles it supplies the instant.
797
+ const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() }
803
798
 
804
799
  const responseStream = connection.queries.query(
805
800
  {
@@ -833,10 +828,10 @@ export function axonServerQueryBus(
833
828
 
834
829
  subscribe(
835
830
  queryName: string,
836
- handler: (message: QueryMessage, uow: UnitOfWork) => Promise<unknown>,
831
+ handler: (message: QueryMessage, uow: U) => Promise<unknown>,
837
832
  ) {
838
833
  subscribedNames.add(queryName)
839
- local.subscribe(queryName, handler)
834
+ next.subscribe(queryName, handler)
840
835
 
841
836
  ensureStreamStarted()
842
837
  sendSubscribe(queryName)
@@ -845,10 +840,10 @@ export function axonServerQueryBus(
845
840
  },
846
841
 
847
842
  subscriptionQuery(
848
- unstamped: Unstamped<QueryMessage>,
843
+ unstamped: QueryMessage,
849
844
  bufferSize?: number,
850
845
  ): SubscriptionQueryResult {
851
- const message = stamped(unstamped, Date.now)
846
+ const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() }
852
847
  const queryId = message.identifier
853
848
  if (subscriptions.has(queryId)) {
854
849
  throw new Error(`Subscription query already registered for identifier "${queryId}"`)
@@ -973,10 +968,10 @@ export function axonServerQueryBus(
973
968
  },
974
969
 
975
970
  subscribeToUpdates(
976
- unstamped: Unstamped<QueryMessage>,
971
+ unstamped: QueryMessage,
977
972
  bufferSize?: number,
978
973
  ): AsyncIterable<unknown> & { close(): void } {
979
- const message = stamped(unstamped, Date.now)
974
+ const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() }
980
975
  const queryId = message.identifier
981
976
  if (subscriptions.has(queryId)) {
982
977
  throw new Error(`Subscription query already registered for identifier "${queryId}"`)
@@ -22,7 +22,7 @@ import { connectToAxonServer } from "./connection.js"
22
22
  * await manager.disconnectAll()
23
23
  * ```
24
24
  */
25
- export interface AxonServerConnectionManager {
25
+ export type AxonServerConnectionManager = {
26
26
  /**
27
27
  * Get or create a connection for the given context.
28
28
  * Connections are created lazily and cached.