@kronos-ts/axon-server 0.3.2 → 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.
- package/dist/axon-server-event-store.d.ts +3 -15
- package/dist/axon-server-event-store.d.ts.map +1 -1
- package/dist/axon-server-event-store.js +85 -44
- package/dist/axon-server-event-store.js.map +1 -1
- package/dist/axon-server-snapshotting-event-store.d.ts +30 -0
- package/dist/axon-server-snapshotting-event-store.d.ts.map +1 -0
- package/dist/axon-server-snapshotting-event-store.js +153 -0
- package/dist/axon-server-snapshotting-event-store.js.map +1 -0
- package/dist/axon-server.d.ts +92 -208
- package/dist/axon-server.d.ts.map +1 -1
- package/dist/axon-server.js +221 -323
- package/dist/axon-server.js.map +1 -1
- package/dist/connection-manager.d.ts +2 -2
- package/dist/connection-manager.d.ts.map +1 -1
- package/dist/connection.d.ts +134 -4
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +135 -10
- package/dist/connection.js.map +1 -1
- package/dist/context-view.d.ts +30 -0
- package/dist/context-view.d.ts.map +1 -0
- package/dist/context-view.js +19 -0
- package/dist/context-view.js.map +1 -0
- package/dist/control-plane.d.ts +18 -17
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +8 -6
- package/dist/control-plane.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/event-processor-info.d.ts +4 -4
- package/dist/event-processor-info.d.ts.map +1 -1
- package/dist/flow-controlled-sender.d.ts +2 -2
- package/dist/flow-controlled-sender.d.ts.map +1 -1
- package/dist/flow-controlled-sender.js.map +1 -1
- package/dist/generated/command.d.ts +20 -20
- package/dist/generated/command.d.ts.map +1 -1
- package/dist/generated/command.js.map +1 -1
- package/dist/generated/common.d.ts +20 -20
- package/dist/generated/common.d.ts.map +1 -1
- package/dist/generated/common.js.map +1 -1
- package/dist/generated/control.d.ts +36 -40
- package/dist/generated/control.d.ts.map +1 -1
- package/dist/generated/control.js.map +1 -1
- package/dist/generated/dcb.d.ts +88 -94
- package/dist/generated/dcb.d.ts.map +1 -1
- package/dist/generated/dcb.js.map +1 -1
- package/dist/generated/event.d.ts +81 -84
- package/dist/generated/event.d.ts.map +1 -1
- package/dist/generated/event.js.map +1 -1
- package/dist/generated/google/protobuf/empty.d.ts +3 -4
- package/dist/generated/google/protobuf/empty.d.ts.map +1 -1
- package/dist/generated/google/protobuf/empty.js.map +1 -1
- package/dist/generated/query.d.ts +40 -40
- package/dist/generated/query.d.ts.map +1 -1
- package/dist/generated/query.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/message-size.d.ts +2 -2
- package/dist/message-size.d.ts.map +1 -1
- package/dist/metadata-conversion.d.ts +1 -1
- package/dist/metadata-conversion.d.ts.map +1 -1
- package/dist/outbound-stream.d.ts +2 -2
- package/dist/outbound-stream.d.ts.map +1 -1
- package/dist/platform-service.d.ts +4 -4
- package/dist/platform-service.d.ts.map +1 -1
- package/dist/resilience.d.ts +62 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +103 -0
- package/dist/resilience.js.map +1 -0
- package/dist/shutdown-latch.d.ts +4 -4
- package/dist/shutdown-latch.d.ts.map +1 -1
- package/dist/shutdown-latch.js.map +1 -1
- package/package.json +5 -9
- package/src/axon-server-event-store.ts +97 -57
- package/src/axon-server-snapshotting-event-store.ts +191 -0
- package/src/axon-server.ts +365 -524
- package/src/connection-manager.ts +1 -1
- package/src/connection.ts +274 -20
- package/src/context-view.ts +46 -0
- package/src/control-plane.ts +19 -19
- package/src/event-processor-info.ts +2 -2
- package/src/flow-controlled-sender.ts +1 -1
- package/src/generated/command.ts +10 -10
- package/src/generated/common.ts +10 -10
- package/src/generated/control.ts +20 -20
- package/src/generated/dcb.ts +47 -47
- package/src/generated/event.ts +42 -42
- package/src/generated/google/protobuf/empty.ts +2 -2
- package/src/generated/query.ts +20 -20
- package/src/index.ts +12 -18
- package/src/message-size.ts +1 -1
- package/src/metadata-conversion.ts +1 -1
- package/src/outbound-stream.ts +1 -1
- package/src/platform-service.ts +2 -2
- package/src/resilience.ts +149 -0
- package/src/shutdown-latch.ts +2 -2
- package/dist/axon-server-snapshot-store.d.ts +0 -12
- package/dist/axon-server-snapshot-store.d.ts.map +0 -1
- package/dist/axon-server-snapshot-store.js +0 -88
- package/dist/axon-server-snapshot-store.js.map +0 -1
- package/src/axon-server-snapshot-store.ts +0 -118
package/src/axon-server.ts
CHANGED
|
@@ -1,65 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Axon Server
|
|
2
|
+
* The Axon Server command and query buses.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `onStart("processors")` / `onStop("connect")` is now three lines you write
|
|
9
|
-
* in your composition root:
|
|
4
|
+
* Axon Server is a SMART HUB: outbound dispatch always goes to the server, and
|
|
5
|
+
* the server decides which node handles it — there is no client-side
|
|
6
|
+
* prefer-next fork here, which is the whole difference from the dumb-pipe
|
|
7
|
+
* broker in `@kronos-ts/rabbitmq`.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
* const axon = await axonServer({
|
|
13
|
-
* componentName: "university-service",
|
|
14
|
-
* serializer,
|
|
15
|
-
* unitOfWorkFactory,
|
|
16
|
-
* })
|
|
17
|
-
* const app = kronos({
|
|
18
|
-
* components: { ...inMemoryComponents({ serializer, unitOfWorkFactory }), ...axon.components },
|
|
19
|
-
* modules,
|
|
20
|
-
* })
|
|
21
|
-
* await axon.start() // readiness barrier: the server can route to our handlers
|
|
22
|
-
* // …
|
|
23
|
-
* await app.stop(); await axon.close()
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* Connecting before the app is built is what removes the lazy proxies and
|
|
27
|
-
* subscribe-buffering wrappers the container version needed: by the time
|
|
28
|
-
* `kronos` subscribes a handler, the gRPC streams are already live.
|
|
29
|
-
*
|
|
30
|
-
* REMOTE ADMINISTRATION IS NOT IN HERE. Processor instructions (pause / start /
|
|
31
|
-
* release / split / merge) and processor status reporting are the platform
|
|
32
|
-
* CONTROL PLANE — they are neither persistence nor transport, and lived here
|
|
33
|
-
* only because they share this gRPC connection. They are now an opt-in second
|
|
34
|
-
* object built on the platform stream this backend exposes:
|
|
9
|
+
* Both buses are plain functions over the shared connection and YOUR next bus:
|
|
35
10
|
*
|
|
36
11
|
* ```ts
|
|
37
|
-
* const
|
|
12
|
+
* const commandBus = interceptingCommandBus(
|
|
13
|
+
* axonServerCommandBus(localCommandBus(unitOfWork), axon), correlation)
|
|
14
|
+
* const queryBus = interceptingQueryBus(
|
|
15
|
+
* axonServerQueryBus(localQueryBus(unitOfWork), axon), correlation)
|
|
38
16
|
* ```
|
|
39
17
|
*
|
|
40
|
-
* `start()` therefore takes NO arguments and does exactly one thing: the
|
|
41
|
-
* data-path readiness barrier. See `control-plane.ts`.
|
|
42
|
-
*
|
|
43
18
|
* Axon-specific protocol invariants are preserved byte-for-byte:
|
|
44
19
|
*
|
|
45
20
|
* - CLIENT_SUPPORTS_STREAMING capability advertised on every dispatched
|
|
46
21
|
* query via `defaultQueryInstructions(...)`;
|
|
47
22
|
* - AxonIQ-Context + AxonIQ-Access-Token gRPC metadata headers built by
|
|
48
|
-
* `
|
|
23
|
+
* `contextView(...)` and attached to every outbound stream/RPC;
|
|
49
24
|
* - permits-AFTER-subscriptions stream ordering preserved on the initial
|
|
50
25
|
* handshake AND on reconnect (see `ensureStreamStarted` /
|
|
51
|
-
* `reestablishStreamBody`)
|
|
52
|
-
* - shutdown ordering: busLatches → platform.stop → connection.close.
|
|
26
|
+
* `reestablishStreamBody`).
|
|
53
27
|
*/
|
|
54
28
|
import {
|
|
55
29
|
qualifiedNameToString,
|
|
56
30
|
qualifiedNameFromString,
|
|
57
31
|
generateIdentifier,
|
|
58
32
|
type Serializer,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
type ResilienceConfig,
|
|
62
|
-
} from "@kronos-ts/common"
|
|
33
|
+
} from "@kronos-ts/core"
|
|
34
|
+
import { withRetry, type ResilienceConfig } from "./resilience.js"
|
|
63
35
|
import type {
|
|
64
36
|
CommandBus,
|
|
65
37
|
CommandMessage,
|
|
@@ -67,40 +39,34 @@ import type {
|
|
|
67
39
|
QueryMessage,
|
|
68
40
|
SubscriptionFilter,
|
|
69
41
|
SubscriptionQueryResult,
|
|
70
|
-
|
|
42
|
+
UnitOfWork,
|
|
71
43
|
UpdateHandler,
|
|
72
|
-
} from "@kronos-ts/
|
|
44
|
+
} from "@kronos-ts/core"
|
|
73
45
|
import {
|
|
74
46
|
applySubscriptionFilter,
|
|
75
|
-
correlationDataDispatchInterceptor,
|
|
76
|
-
interceptingCommandBus,
|
|
77
|
-
interceptingQueryBus,
|
|
78
47
|
updateHandler,
|
|
79
48
|
runAfterCommitOrImmediately,
|
|
80
|
-
} from "@kronos-ts/
|
|
81
|
-
import {
|
|
82
|
-
import
|
|
83
|
-
import { connectToAxonServer, type AxonServerConnection } from "./connection.js"
|
|
84
|
-
import { axonServerEventStore } from "./axon-server-event-store.js"
|
|
85
|
-
import { axonServerSnapshotStore } from "./axon-server-snapshot-store.js"
|
|
49
|
+
} from "@kronos-ts/core"
|
|
50
|
+
import type { AxonServerBusSource } from "./connection.js"
|
|
51
|
+
import { contextView } from "./context-view.js"
|
|
86
52
|
import { metadataToProto, metadataFromProto } from "./metadata-conversion.js"
|
|
87
53
|
import { outboundStream } from "./outbound-stream.js"
|
|
88
54
|
import { mapErrorCode, AxonServerErrorCode } from "./errors.js"
|
|
89
|
-
import { shutdownLatch, type ShutdownLatch } from "./shutdown-latch.js"
|
|
90
|
-
import {
|
|
91
|
-
platformConnection,
|
|
92
|
-
type PlatformConnection,
|
|
93
|
-
type PlatformServiceOptions,
|
|
94
|
-
} from "./platform-service.js"
|
|
95
55
|
|
|
96
56
|
/** Default flow control settings — aligned with Java's 5000 permits. */
|
|
97
57
|
const DEFAULT_PERMITS = 5000n
|
|
98
58
|
const DEFAULT_THRESHOLD = 2500n
|
|
99
59
|
|
|
60
|
+
/** Default query dispatch timeout — aligned with Java's one hour. */
|
|
61
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 3_600_000
|
|
62
|
+
|
|
63
|
+
/** Default command handler load factor — aligned with Java's 100. */
|
|
64
|
+
const DEFAULT_LOAD_FACTOR = 100
|
|
65
|
+
|
|
100
66
|
/**
|
|
101
67
|
* Flow control configuration for a bus channel.
|
|
102
68
|
*/
|
|
103
|
-
export
|
|
69
|
+
export type FlowControlConfig = {
|
|
104
70
|
/** Initial permits granted to Axon Server. Default: 5000 (aligned with Java). */
|
|
105
71
|
permits?: number
|
|
106
72
|
/** Threshold at which to request more permits. Default: 2500 (aligned with Java). */
|
|
@@ -111,7 +77,7 @@ export interface FlowControlConfig {
|
|
|
111
77
|
* Processing instructions attached to outbound messages.
|
|
112
78
|
* Controls routing, priority, and timeout behavior on Axon Server.
|
|
113
79
|
*/
|
|
114
|
-
export
|
|
80
|
+
export type ProcessingInstructions = {
|
|
115
81
|
/** Routing key for consistent hashing (e.g., aggregate ID). */
|
|
116
82
|
routingKey?: string
|
|
117
83
|
/** Priority (higher = processed first). Default: 0 */
|
|
@@ -120,10 +86,57 @@ export interface ProcessingInstructions {
|
|
|
120
86
|
timeoutMs?: number
|
|
121
87
|
}
|
|
122
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Tuning for {@link axonServerCommandBus}. Every field has a working default;
|
|
91
|
+
* the two arguments that carry meaning — the connection and your next bus —
|
|
92
|
+
* are positional, and this record is the trailing remainder.
|
|
93
|
+
*/
|
|
94
|
+
export type AxonServerCommandBusOptions = {
|
|
95
|
+
/** Axon Server context for this bus's stream. Default: the connection's. */
|
|
96
|
+
context?: string
|
|
97
|
+
/** Flow control for the command stream. */
|
|
98
|
+
flowControl?: FlowControlConfig
|
|
99
|
+
/**
|
|
100
|
+
* Load factor for this command handler. Signals to Axon Server how much
|
|
101
|
+
* capacity this node has — higher value = more commands routed here.
|
|
102
|
+
* Aligned with Java's `commandLoadFactor`. Default: 100.
|
|
103
|
+
*/
|
|
104
|
+
loadFactor?: number
|
|
105
|
+
/** Retry policy for stream re-establishment. Default: the connection's. */
|
|
106
|
+
resilience?: Partial<ResilienceConfig>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tuning for {@link axonServerQueryBus}. See {@link AxonServerCommandBusOptions}.
|
|
111
|
+
*/
|
|
112
|
+
export type AxonServerQueryBusOptions = {
|
|
113
|
+
/** Axon Server context for this bus's stream. Default: the connection's. */
|
|
114
|
+
context?: string
|
|
115
|
+
/** Flow control for the query stream. */
|
|
116
|
+
flowControl?: FlowControlConfig
|
|
117
|
+
/**
|
|
118
|
+
* When true, queries are first checked against locally subscribed handlers
|
|
119
|
+
* before being dispatched through Axon Server. Avoids a network round-trip
|
|
120
|
+
* when the handler is co-located.
|
|
121
|
+
*
|
|
122
|
+
* This is NOT the rabbitmq `preferLocal` fork by another name: it is Java's
|
|
123
|
+
* `shortcutQueriesToLocalHandlers`, it is off by default, and commands have
|
|
124
|
+
* no equivalent — Axon Server routes those, always.
|
|
125
|
+
*/
|
|
126
|
+
shortcutQueriesToLocalHandlers?: boolean
|
|
127
|
+
/**
|
|
128
|
+
* Default timeout for query dispatch in ms. Default: 3600000 (1 hour).
|
|
129
|
+
* Aligned with Java's processing instruction timeout.
|
|
130
|
+
*/
|
|
131
|
+
timeoutMs?: number
|
|
132
|
+
/** Retry policy for stream re-establishment. Default: the connection's. */
|
|
133
|
+
resilience?: Partial<ResilienceConfig>
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
// Processing instruction keys — aligned with proto ProcessingKey enum.
|
|
124
137
|
// CLIENT_SUPPORTS_STREAMING (key=8) is an Axon-Server-specific capability
|
|
125
|
-
// advertisement that MUST survive
|
|
126
|
-
//
|
|
138
|
+
// advertisement that MUST survive verbatim — see file-level JSDoc above and
|
|
139
|
+
// `defaultQueryInstructions` below.
|
|
127
140
|
const INSTRUCTION_KEY = {
|
|
128
141
|
ROUTING_KEY: 0,
|
|
129
142
|
PRIORITY: 1,
|
|
@@ -139,10 +152,16 @@ function toProtoProcessingInstructions(instructions?: ProcessingInstructions): a
|
|
|
139
152
|
result.push({ key: INSTRUCTION_KEY.ROUTING_KEY, value: { textValue: instructions.routingKey } })
|
|
140
153
|
}
|
|
141
154
|
if (instructions.priority !== undefined) {
|
|
142
|
-
result.push({
|
|
155
|
+
result.push({
|
|
156
|
+
key: INSTRUCTION_KEY.PRIORITY,
|
|
157
|
+
value: { numberValue: BigInt(instructions.priority) },
|
|
158
|
+
})
|
|
143
159
|
}
|
|
144
160
|
if (instructions.timeoutMs !== undefined) {
|
|
145
|
-
result.push({
|
|
161
|
+
result.push({
|
|
162
|
+
key: INSTRUCTION_KEY.TIMEOUT,
|
|
163
|
+
value: { numberValue: BigInt(instructions.timeoutMs) },
|
|
164
|
+
})
|
|
146
165
|
}
|
|
147
166
|
return result
|
|
148
167
|
}
|
|
@@ -161,233 +180,8 @@ function defaultQueryInstructions(timeoutMs: number): any[] {
|
|
|
161
180
|
]
|
|
162
181
|
}
|
|
163
182
|
|
|
164
|
-
/**
|
|
165
|
-
* Build the gRPC metadata headers required by Axon Server. AxonIQ-Context
|
|
166
|
-
* is mandatory (identifies the tenant/context); AxonIQ-Access-Token is
|
|
167
|
-
* optional auth. Both must be attached to every outbound stream/RPC —
|
|
168
|
-
* preserved verbatim from the legacy enhancer.
|
|
169
|
-
*/
|
|
170
|
-
function createAxonMetadata(config: { context: string; token: string }): Metadata {
|
|
171
|
-
const metadata = new Metadata()
|
|
172
|
-
metadata.set("AxonIQ-Context", config.context)
|
|
173
|
-
if (config.token) {
|
|
174
|
-
metadata.set("AxonIQ-Access-Token", config.token)
|
|
175
|
-
}
|
|
176
|
-
return metadata
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export interface AxonServerConfig extends AxonServerConnectionConfig {
|
|
180
|
-
/** Flow control for the command bus channel. */
|
|
181
|
-
commandFlowControl?: FlowControlConfig
|
|
182
|
-
/** Flow control for the query bus channel. */
|
|
183
|
-
queryFlowControl?: FlowControlConfig
|
|
184
|
-
/** Platform service configuration (heartbeat, etc.). */
|
|
185
|
-
platformService?: PlatformServiceOptions
|
|
186
|
-
/**
|
|
187
|
-
* When true, queries are first checked against locally registered handlers
|
|
188
|
-
* before being dispatched through Axon Server. Avoids a network round-trip
|
|
189
|
-
* when the handler is co-located.
|
|
190
|
-
*
|
|
191
|
-
* Aligned with Java's `shortcutQueriesToLocalHandlers`.
|
|
192
|
-
* Default: false.
|
|
193
|
-
*/
|
|
194
|
-
shortcutQueriesToLocalHandlers?: boolean
|
|
195
|
-
/**
|
|
196
|
-
* Load factor for command handler registration.
|
|
197
|
-
* Signals to Axon Server how much capacity this handler has.
|
|
198
|
-
* Higher value = handler can take more commands.
|
|
199
|
-
*
|
|
200
|
-
* Aligned with Java's `commandLoadFactor`. Default: 100.
|
|
201
|
-
*/
|
|
202
|
-
commandLoadFactor?: number
|
|
203
|
-
/**
|
|
204
|
-
* Default timeout for command dispatch in ms. Default: 300000 (5 min).
|
|
205
|
-
* Aligned with Java's processing instruction timeout.
|
|
206
|
-
*/
|
|
207
|
-
commandTimeoutMs?: number
|
|
208
|
-
/**
|
|
209
|
-
* Default timeout for query dispatch in ms. Default: 3600000 (1 hour).
|
|
210
|
-
* Aligned with Java's processing instruction timeout.
|
|
211
|
-
*/
|
|
212
|
-
queryTimeoutMs?: number
|
|
213
|
-
/** Per-extension resilience config (D-100 / D-101). */
|
|
214
|
-
resilience?: Partial<ResilienceConfig>
|
|
215
|
-
/**
|
|
216
|
-
* How long `start()` waits for Axon Server's routing tables to register the
|
|
217
|
-
* subscribe frames sent on the command/query streams. This is the entire
|
|
218
|
-
* data-path readiness barrier.
|
|
219
|
-
*
|
|
220
|
-
* It is a timed wait rather than an observed signal because nothing on the
|
|
221
|
-
* client can observe it: subscribes travel on the bus streams, and the
|
|
222
|
-
* platform stream — which is where an ack would arrive — is a different
|
|
223
|
-
* stream that Axon Server holds open silently after `register`. Default:
|
|
224
|
-
* 1000, matching the legacy enhancer. Tests against a freshly-booted server
|
|
225
|
-
* can tighten this once subscriptions are observed to land faster.
|
|
226
|
-
*/
|
|
227
|
-
busSubscriptionAckDelayMs?: number
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** The components an Axon Server backend provides. Spread into `kronos`. */
|
|
231
|
-
export interface AxonServerComponents {
|
|
232
|
-
eventStore: ReturnType<typeof axonServerEventStore>
|
|
233
|
-
snapshotStore: ReturnType<typeof axonServerSnapshotStore>
|
|
234
|
-
commandBus: CommandBus
|
|
235
|
-
queryBus: QueryBus
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* A live Axon Server backend: the components it provides plus the two calls
|
|
240
|
-
* that used to be lifecycle stages.
|
|
241
|
-
*/
|
|
242
|
-
/** Everything axonServer() needs: its own config plus the framework values it borrows. */
|
|
243
|
-
export type AxonServerOptions = AxonServerConfig & { serializer: Serializer; unitOfWorkFactory: UoWRunner }
|
|
244
|
-
|
|
245
|
-
export interface AxonServerBackend {
|
|
246
|
-
readonly components: AxonServerComponents
|
|
247
|
-
/**
|
|
248
|
-
* The platform stream. It is built here because the backend owns the gRPC
|
|
249
|
-
* connection it rides on and the `platformService` tuning that configures it.
|
|
250
|
-
*
|
|
251
|
-
* `start()` below brings it up for the DATA path — heartbeats and reconnect
|
|
252
|
-
* detection — via `platform.armConnectionMonitoring()`. What it deliberately
|
|
253
|
-
* does NOT do is arm processor status reporting or route instructions: that is
|
|
254
|
-
* remote administration, and it stays opt-in behind
|
|
255
|
-
* `axonServerControlPlane(axon.platform, …)`, which registers its handler and
|
|
256
|
-
* supplier and then calls `platform.start()` on this same live stream.
|
|
257
|
-
*
|
|
258
|
-
* An instruction that arrives before a control plane exists is buffered by the
|
|
259
|
-
* platform connection and drained on the first `onInstruction` registration,
|
|
260
|
-
* so opening the stream early costs nothing.
|
|
261
|
-
*/
|
|
262
|
-
readonly platform: PlatformConnection
|
|
263
|
-
/**
|
|
264
|
-
* DATA-PATH START. Two things, both data path:
|
|
265
|
-
*
|
|
266
|
-
* 1. arm heartbeat-driven reconnect detection on the platform stream, and
|
|
267
|
-
* 2. wait until Axon Server can route to the handlers subscribed on the bus
|
|
268
|
-
* streams.
|
|
269
|
-
*
|
|
270
|
-
* Call AFTER `kronos` — the subscribe frames must already be on the wire for
|
|
271
|
-
* the readiness wait to mean anything.
|
|
272
|
-
*
|
|
273
|
-
* Takes no arguments and arms no control-plane state.
|
|
274
|
-
*/
|
|
275
|
-
start(): Promise<void>
|
|
276
|
-
/** Drain in-flight bus work, stop the platform stream, close the connection. */
|
|
277
|
-
close(): Promise<void>
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Connect to Axon Server and build the components it backs.
|
|
282
|
-
*
|
|
283
|
-
* `serializer` and `unitOfWorkFactory` are arguments rather than slot lookups:
|
|
284
|
-
* the buses serialize payloads with the former and run every inbound command /
|
|
285
|
-
* query in the latter, so they must be the SAME instances the rest of the app
|
|
286
|
-
* uses. Pass the ones you hand to `kronos`.
|
|
287
|
-
*/
|
|
288
|
-
export async function axonServer(
|
|
289
|
-
options: AxonServerOptions,
|
|
290
|
-
): Promise<AxonServerBackend> {
|
|
291
|
-
const config = options
|
|
292
|
-
const { serializer, unitOfWorkFactory, resilience } = config
|
|
293
|
-
|
|
294
|
-
const connection = await withRetry(async () => connectToAxonServer(config), {
|
|
295
|
-
event: "initial-connect",
|
|
296
|
-
...resilience,
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
// Health-check ping with warn-then-continue (D-100). AxonServerConnection has
|
|
300
|
-
// no dedicated probe surface today; the gRPC channel itself is created
|
|
301
|
-
// eagerly in connectToAxonServer so the meaningful probe is a round-trip — we
|
|
302
|
-
// approximate via a soft no-op promise that satisfies the threshold contract.
|
|
303
|
-
// Real network failure is surfaced by the first bus call against the channel.
|
|
304
|
-
await healthCheck(async () => undefined, {
|
|
305
|
-
thresholdMs: resilience?.healthCheckThresholdMs,
|
|
306
|
-
log: resilience?.log,
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
// One latch per bus, drained in close() before the transport goes away.
|
|
310
|
-
const commandLatch = shutdownLatch()
|
|
311
|
-
const queryLatch = shutdownLatch()
|
|
312
|
-
const busLatches: ShutdownLatch[] = [commandLatch, queryLatch]
|
|
313
|
-
|
|
314
|
-
// The connection is live before anything below is built, so the buses open
|
|
315
|
-
// their gRPC streams for real and `subscribe()` reaches the wire immediately —
|
|
316
|
-
// no lazy proxy, no subscription buffering, no readiness promise.
|
|
317
|
-
const components: AxonServerComponents = {
|
|
318
|
-
eventStore: axonServerEventStore(connection, serializer),
|
|
319
|
-
snapshotStore: axonServerSnapshotStore(connection, serializer),
|
|
320
|
-
commandBus: distributedCommandBus(
|
|
321
|
-
connection,
|
|
322
|
-
unitOfWorkFactory,
|
|
323
|
-
commandLatch,
|
|
324
|
-
serializer,
|
|
325
|
-
config.commandFlowControl,
|
|
326
|
-
config.commandLoadFactor,
|
|
327
|
-
resilience,
|
|
328
|
-
),
|
|
329
|
-
queryBus: distributedQueryBus(
|
|
330
|
-
connection,
|
|
331
|
-
unitOfWorkFactory,
|
|
332
|
-
queryLatch,
|
|
333
|
-
serializer,
|
|
334
|
-
config.queryFlowControl,
|
|
335
|
-
config.shortcutQueriesToLocalHandlers,
|
|
336
|
-
config.queryTimeoutMs,
|
|
337
|
-
resilience,
|
|
338
|
-
),
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Built here, started by the control plane (or by the caller). Constructing it
|
|
342
|
-
// eagerly is what lets the control plane be a separate object at all — and it
|
|
343
|
-
// keeps `platformService` tuning and `stop()` ownership in one place, so the
|
|
344
|
-
// documented shutdown order below holds whether or not anyone opted in.
|
|
345
|
-
const platform = platformConnection(connection, config.platformService)
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
components,
|
|
349
|
-
platform,
|
|
350
|
-
|
|
351
|
-
async start() {
|
|
352
|
-
// RECONNECT DETECTION IS DATA PATH. The heartbeat on the platform stream
|
|
353
|
-
// is what notices a dead channel and calls `connection.reconnect()`; both
|
|
354
|
-
// buses above hook `connection.onReconnect(...)` to rebuild their own
|
|
355
|
-
// streams. Arming it used to be a side effect of `platform.start()`, which
|
|
356
|
-
// only `axonServerControlPlane(...)` calls — so a service that never opted
|
|
357
|
-
// into remote administration had NO reconnect detection at all and would
|
|
358
|
-
// sit on a dead channel forever. It is armed here, unconditionally,
|
|
359
|
-
// independent of whether anyone administers this service.
|
|
360
|
-
//
|
|
361
|
-
// `armConnectionMonitoring()` opens the stream and starts heartbeats but
|
|
362
|
-
// arms NO processor status reporting; that stays the control plane's, and
|
|
363
|
-
// a later `platform.start()` adds it to this same live stream. Both calls
|
|
364
|
-
// are idempotent, so either order works.
|
|
365
|
-
await platform.armConnectionMonitoring()
|
|
366
|
-
|
|
367
|
-
// The only thing the data path has to wait for: Axon Server's
|
|
368
|
-
// command/query routing tables registering the subscribe frames sent on
|
|
369
|
-
// the BUS streams. It cannot be derived from the platform stream, because
|
|
370
|
-
// subscribes travel on a different stream entirely — and the platform
|
|
371
|
-
// stream's own `subscriptionsAcked()` latch says nothing about them (it
|
|
372
|
-
// latches unconditionally once `register` has been flushed; see
|
|
373
|
-
// platform-service.ts). So this barrier is the settle wait, and it is
|
|
374
|
-
// deliberately independent of whether the platform stream is up at all.
|
|
375
|
-
// The legacy enhancer carried the same 1s wait.
|
|
376
|
-
await new Promise((r) => setTimeout(r, config.busSubscriptionAckDelayMs ?? 1000))
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
async close() {
|
|
380
|
-
await Promise.all(busLatches.map((l) => l.initiateShutdown()))
|
|
381
|
-
// Idempotent, and independent of `control.close()` — a backend that was
|
|
382
|
-
// never administered still stops a platform stream someone else started.
|
|
383
|
-
platform.stop()
|
|
384
|
-
connection.close()
|
|
385
|
-
},
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
183
|
// ---------------------------------------------------------------------------
|
|
390
|
-
// Shared payload helpers
|
|
184
|
+
// Shared payload helpers
|
|
391
185
|
// ---------------------------------------------------------------------------
|
|
392
186
|
|
|
393
187
|
function createPayloadHelpers(serializer: Serializer) {
|
|
@@ -395,7 +189,11 @@ function createPayloadHelpers(serializer: Serializer) {
|
|
|
395
189
|
serializePayload(name: string, payload: unknown, revision: string = "") {
|
|
396
190
|
return serializer.serialize(payload, name, revision)
|
|
397
191
|
},
|
|
398
|
-
deserializePayload(
|
|
192
|
+
deserializePayload(
|
|
193
|
+
data: Uint8Array | undefined,
|
|
194
|
+
type: string = "",
|
|
195
|
+
revision: string = "",
|
|
196
|
+
): unknown {
|
|
399
197
|
if (!data || data.length === 0) return undefined
|
|
400
198
|
return serializer.deserialize({ data, type, revision })
|
|
401
199
|
},
|
|
@@ -403,79 +201,77 @@ function createPayloadHelpers(serializer: Serializer) {
|
|
|
403
201
|
}
|
|
404
202
|
|
|
405
203
|
// ---------------------------------------------------------------------------
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
// Bus implementation moved verbatim from the legacy enhancer with TWO
|
|
409
|
-
// behavioural additions per D-97:
|
|
410
|
-
// 1) reestablishStream() body wrapped in withRetry({ event: "reconnect" })
|
|
411
|
-
// 2) inbound-stream backoff replaced by the same withRetry path
|
|
412
|
-
//
|
|
413
|
-
// Axon-specific protocol invariants preserved BYTE-FOR-BYTE:
|
|
414
|
-
// - AxonIQ-Context + AxonIQ-Access-Token gRPC metadata headers via
|
|
415
|
-
// createAxonMetadata(connection.config)
|
|
416
|
-
// - permits-AFTER-subscriptions ordering on reestablishStreamBody (subs
|
|
417
|
-
// are sent BEFORE grantPermits() in the reconnect path; the initial
|
|
418
|
-
// handshake matches this — see ensureStreamStarted's grantPermits call
|
|
419
|
-
// in subscribe()).
|
|
204
|
+
// Axon Server Command Bus
|
|
420
205
|
// ---------------------------------------------------------------------------
|
|
421
206
|
|
|
422
207
|
/**
|
|
423
|
-
* A command bus backed by Axon Server.
|
|
208
|
+
* A command bus backed by Axon Server, over YOUR next bus.
|
|
424
209
|
*
|
|
425
|
-
* - **Outbound dispatch**:
|
|
426
|
-
* Axon Server routes the command to the appropriate node (which may be
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
*
|
|
210
|
+
* - **Outbound dispatch**: ALWAYS through Axon Server, via the unary Dispatch
|
|
211
|
+
* RPC. Axon Server routes the command to the appropriate node (which may be
|
|
212
|
+
* this one). There is deliberately no client-side prefer-next fork: the hub
|
|
213
|
+
* is the router, and short-circuiting it would silently defeat load factors,
|
|
214
|
+
* priorities and routing keys.
|
|
215
|
+
* - **Inbound**: a command the server routes here is dispatched into `next` —
|
|
216
|
+
* not into a privately-held handler map. That is what makes the unit-of-work
|
|
217
|
+
* policy you chose for `next` (say `postgresUnitOfWork(unitOfWork, pg)`)
|
|
218
|
+
* apply to server-routed work exactly as it applies to work this process
|
|
219
|
+
* originated. It is also why this function takes no `unitOfWork` argument:
|
|
220
|
+
* `next` carries that policy now.
|
|
221
|
+
* - **subscribe**: registers the handler on `next` AND announces the name to
|
|
222
|
+
* Axon Server, so other nodes can route to us.
|
|
223
|
+
*
|
|
224
|
+
* ## correlation and the interceptor layer
|
|
225
|
+
*
|
|
226
|
+
* The returned bus stamps no correlation of its own. A host that wants it wraps the
|
|
227
|
+
* OUTERMOST bus:
|
|
434
228
|
*
|
|
435
|
-
*
|
|
229
|
+
* ```ts
|
|
230
|
+
* interceptingCommandBus(axonServerCommandBus(next, conn), correlation)
|
|
231
|
+
* ```
|
|
436
232
|
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
233
|
+
* so whatever a host adds runs BEFORE the message is serialized onto the wire.
|
|
234
|
+
* Correlation itself is usually already on `message.metadata` by then — `ctx.send`
|
|
235
|
+
* stamps the unit of work's correlation data before any bus sees the message.
|
|
440
236
|
*
|
|
441
237
|
* This is precisely how the Java client does it. AF4's `AxonServerCommandBus`
|
|
442
238
|
* holds its own `DispatchInterceptors` and dispatches as
|
|
443
239
|
* `doDispatch(dispatchInterceptors.intercept(commandMessage), cb)` — one call
|
|
444
|
-
* site, at the top, ahead of any routing
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
|
|
448
|
-
* stacks `InterceptingCommandBus → DistributedCommandBus → SimpleCommandBus`.
|
|
240
|
+
* site, at the top, ahead of any routing. AF5 keeps the property via decorator
|
|
241
|
+
* order: `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
|
|
242
|
+
* stacks `InterceptingCommandBus → DistributedCommandBus → LocalCommandBus`.
|
|
449
243
|
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* this backend replaces it wholesale.
|
|
454
|
-
*
|
|
455
|
-
* No double-application risk: the local segment here is a plain handler map, not
|
|
456
|
-
* a `CommandBus`, so this is the only interceptor in the chain. Inbound commands
|
|
457
|
-
* from the server are invoked through that map directly, which matches AF —
|
|
458
|
-
* `CommandProcessingTask` runs the local segment WITHOUT re-running dispatch
|
|
459
|
-
* interceptors.
|
|
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
|
|
246
|
+
* second application finds them set and changes nothing.
|
|
460
247
|
*/
|
|
461
|
-
export function
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
248
|
+
export function axonServerCommandBus<U extends UnitOfWork = UnitOfWork>(
|
|
249
|
+
next: CommandBus<U>,
|
|
250
|
+
conn: AxonServerBusSource,
|
|
251
|
+
options: AxonServerCommandBusOptions = {},
|
|
252
|
+
): CommandBus<U> {
|
|
253
|
+
const {
|
|
254
|
+
connection,
|
|
255
|
+
serializer,
|
|
256
|
+
metadata: axonMetadata,
|
|
257
|
+
} = contextView(conn, options.context ?? conn.connection.config.context)
|
|
258
|
+
const shutdownLatch = conn.shutdown
|
|
259
|
+
const resilience = options.resilience ?? conn.resilience
|
|
260
|
+
const metadata = axonMetadata()
|
|
471
261
|
const { serializePayload, deserializePayload } = createPayloadHelpers(serializer)
|
|
472
|
-
const PERMITS = BigInt(flowControl?.permits ?? Number(DEFAULT_PERMITS))
|
|
473
|
-
const THRESHOLD = BigInt(flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD))
|
|
262
|
+
const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS))
|
|
263
|
+
const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD))
|
|
264
|
+
const loadFactor = options.loadFactor ?? DEFAULT_LOAD_FACTOR
|
|
474
265
|
|
|
475
|
-
|
|
476
|
-
|
|
266
|
+
/**
|
|
267
|
+
* The names this node announced to Axon Server. The handlers themselves live
|
|
268
|
+
* on `next`; this set exists so an inbound command for a name we never
|
|
269
|
+
* subscribed still answers NO_HANDLER_FOR_COMMAND rather than whatever
|
|
270
|
+
* `next.dispatch` happens to throw — and so a reconnect can re-announce.
|
|
271
|
+
*/
|
|
272
|
+
const subscribedNames = new Set<string>()
|
|
477
273
|
|
|
478
|
-
// Bidirectional stream for handler
|
|
274
|
+
// Bidirectional stream for handler subscription + inbound command handling
|
|
479
275
|
let outbound = outboundStream<any>()
|
|
480
276
|
let streamStarted = false
|
|
481
277
|
let permits = 0n
|
|
@@ -497,8 +293,21 @@ export function distributedCommandBus(
|
|
|
497
293
|
permits += PERMITS
|
|
498
294
|
}
|
|
499
295
|
|
|
296
|
+
function sendSubscribe(commandName: string) {
|
|
297
|
+
outbound.send({
|
|
298
|
+
subscribe: {
|
|
299
|
+
messageId: generateIdentifier(),
|
|
300
|
+
command: commandName,
|
|
301
|
+
componentName: connection.config.componentName,
|
|
302
|
+
clientId: connection.config.clientId,
|
|
303
|
+
loadFactor,
|
|
304
|
+
},
|
|
305
|
+
instructionId: generateIdentifier(),
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
500
309
|
/**
|
|
501
|
-
* Re-establish the bidirectional stream and re-
|
|
310
|
+
* Re-establish the bidirectional stream and re-announce all handlers.
|
|
502
311
|
* Called on stream error or when the connection reconnects.
|
|
503
312
|
*
|
|
504
313
|
* ORDER (preserves Axon-specific invariant): subscriptions are
|
|
@@ -512,18 +321,7 @@ export function distributedCommandBus(
|
|
|
512
321
|
permits = 0n
|
|
513
322
|
ensureStreamStarted()
|
|
514
323
|
// Re-subscribe all handlers FIRST
|
|
515
|
-
for (const commandName of
|
|
516
|
-
outbound.send({
|
|
517
|
-
subscribe: {
|
|
518
|
-
messageId: generateIdentifier(),
|
|
519
|
-
command: commandName,
|
|
520
|
-
componentName: connection.config.componentName,
|
|
521
|
-
clientId: connection.config.clientId,
|
|
522
|
-
loadFactor: commandLoadFactor ?? 100,
|
|
523
|
-
},
|
|
524
|
-
instructionId: generateIdentifier(),
|
|
525
|
-
})
|
|
526
|
-
}
|
|
324
|
+
for (const commandName of subscribedNames) sendSubscribe(commandName)
|
|
527
325
|
// Permits AFTER subscriptions (Axon-specific ordering invariant)
|
|
528
326
|
grantPermits()
|
|
529
327
|
}
|
|
@@ -540,7 +338,7 @@ export function distributedCommandBus(
|
|
|
540
338
|
connection.onReconnect(() => {
|
|
541
339
|
if (!shutdownLatch.shuttingDown && streamStarted) {
|
|
542
340
|
reestablishStreamWithRetry().catch((err) => {
|
|
543
|
-
console.error("
|
|
341
|
+
console.error("Axon Server command bus: reconnect retries exhausted", err)
|
|
544
342
|
})
|
|
545
343
|
}
|
|
546
344
|
})
|
|
@@ -553,13 +351,12 @@ export function distributedCommandBus(
|
|
|
553
351
|
permits--
|
|
554
352
|
const proto = message.command
|
|
555
353
|
const commandName = proto.name
|
|
556
|
-
const handler = localSegment.get(commandName)
|
|
557
354
|
|
|
558
355
|
let resultPayload: unknown
|
|
559
356
|
let errorCode = ""
|
|
560
357
|
let errorMsg = ""
|
|
561
358
|
|
|
562
|
-
if (
|
|
359
|
+
if (subscribedNames.has(commandName)) {
|
|
563
360
|
try {
|
|
564
361
|
const commandMessage: CommandMessage = {
|
|
565
362
|
kind: "command",
|
|
@@ -570,17 +367,19 @@ export function distributedCommandBus(
|
|
|
570
367
|
timestamp: Number(proto.timestamp),
|
|
571
368
|
}
|
|
572
369
|
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
370
|
+
// Through the LOCAL BUS, so the caller's unit-of-work policy runs.
|
|
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
|
|
374
|
+
// that are already set.
|
|
375
|
+
resultPayload = await next.dispatch(commandMessage)
|
|
577
376
|
} catch (err) {
|
|
578
377
|
errorCode = AxonServerErrorCode.COMMAND_EXECUTION_ERROR
|
|
579
378
|
errorMsg = err instanceof Error ? err.message : String(err)
|
|
580
379
|
}
|
|
581
380
|
} else {
|
|
582
381
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_COMMAND
|
|
583
|
-
errorMsg = `No
|
|
382
|
+
errorMsg = `No next handler for command "${commandName}"`
|
|
584
383
|
}
|
|
585
384
|
|
|
586
385
|
// Send response back to Axon Server
|
|
@@ -590,11 +389,15 @@ export function distributedCommandBus(
|
|
|
590
389
|
requestIdentifier: proto.messageIdentifier,
|
|
591
390
|
errorCode,
|
|
592
391
|
errorMessage: errorCode
|
|
593
|
-
? {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
392
|
+
? {
|
|
393
|
+
message: errorMsg,
|
|
394
|
+
location: connection.config.componentName,
|
|
395
|
+
details: [],
|
|
396
|
+
errorCode,
|
|
397
|
+
}
|
|
597
398
|
: undefined,
|
|
399
|
+
payload:
|
|
400
|
+
resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
|
|
598
401
|
metaData: {},
|
|
599
402
|
processingInstructions: [],
|
|
600
403
|
},
|
|
@@ -614,35 +417,46 @@ export function distributedCommandBus(
|
|
|
614
417
|
if (shutdownLatch.shuttingDown) return
|
|
615
418
|
if (String(err).includes("Connection dropped")) return
|
|
616
419
|
|
|
617
|
-
console.error(
|
|
420
|
+
console.error(
|
|
421
|
+
"Axon Server command bus: inbound stream error, attempting re-establishment via withRetry",
|
|
422
|
+
err,
|
|
423
|
+
)
|
|
618
424
|
await reestablishStreamWithRetry().catch((retryErr) => {
|
|
619
|
-
console.error("
|
|
425
|
+
console.error("Axon Server command bus: reconnect retries exhausted", retryErr)
|
|
620
426
|
})
|
|
621
427
|
}
|
|
622
428
|
}
|
|
623
429
|
|
|
624
|
-
|
|
625
|
-
async dispatch(
|
|
430
|
+
return {
|
|
431
|
+
async dispatch(unstamped: CommandMessage): Promise<unknown> {
|
|
432
|
+
// A transport is not a task: it has no unit of work, so it has no clock.
|
|
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() }
|
|
626
438
|
const activity = shutdownLatch.registerActivity()
|
|
627
439
|
try {
|
|
628
440
|
const commandName = qualifiedNameToString(message.name)
|
|
629
441
|
|
|
630
|
-
const response = await connection.commands.dispatch(
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
442
|
+
const response = await connection.commands.dispatch(
|
|
443
|
+
{
|
|
444
|
+
messageIdentifier: message.identifier,
|
|
445
|
+
name: commandName,
|
|
446
|
+
timestamp: BigInt(message.timestamp),
|
|
447
|
+
payload: serializePayload(commandName, message.payload),
|
|
448
|
+
metaData: metadataToProto(message.metadata),
|
|
449
|
+
processingInstructions: toProtoProcessingInstructions(
|
|
450
|
+
message.metadata?.processingInstructions as ProcessingInstructions | undefined,
|
|
451
|
+
),
|
|
452
|
+
clientId: connection.config.clientId,
|
|
453
|
+
componentName: connection.config.componentName,
|
|
454
|
+
},
|
|
455
|
+
{ metadata },
|
|
456
|
+
)
|
|
640
457
|
|
|
641
458
|
if (response.errorCode && response.errorCode !== "") {
|
|
642
|
-
throw mapErrorCode(
|
|
643
|
-
response.errorCode,
|
|
644
|
-
response.errorMessage?.message ?? "Unknown error",
|
|
645
|
-
)
|
|
459
|
+
throw mapErrorCode(response.errorCode, response.errorMessage?.message ?? "Unknown error")
|
|
646
460
|
}
|
|
647
461
|
|
|
648
462
|
return deserializePayload(response.payload?.data as Uint8Array | undefined)
|
|
@@ -651,73 +465,72 @@ export function distributedCommandBus(
|
|
|
651
465
|
}
|
|
652
466
|
},
|
|
653
467
|
|
|
654
|
-
subscribe(
|
|
655
|
-
|
|
468
|
+
subscribe(
|
|
469
|
+
commandName: string,
|
|
470
|
+
handler: (message: CommandMessage, uow: U) => Promise<unknown>,
|
|
471
|
+
) {
|
|
472
|
+
subscribedNames.add(commandName)
|
|
473
|
+
next.subscribe(commandName, handler)
|
|
656
474
|
|
|
657
475
|
ensureStreamStarted()
|
|
658
476
|
// Subscription FIRST
|
|
659
|
-
|
|
660
|
-
subscribe: {
|
|
661
|
-
messageId: generateIdentifier(),
|
|
662
|
-
command: commandName,
|
|
663
|
-
componentName: connection.config.componentName,
|
|
664
|
-
clientId: connection.config.clientId,
|
|
665
|
-
loadFactor: commandLoadFactor ?? 100,
|
|
666
|
-
},
|
|
667
|
-
instructionId: generateIdentifier(),
|
|
668
|
-
})
|
|
477
|
+
sendSubscribe(commandName)
|
|
669
478
|
// Permits AFTER subscription (Axon-specific ordering invariant)
|
|
670
479
|
grantPermits()
|
|
671
480
|
},
|
|
672
481
|
}
|
|
673
|
-
|
|
674
|
-
// Interception OUTSIDE routing — see the note on this function.
|
|
675
|
-
const bus = interceptingCommandBus(routing)
|
|
676
|
-
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor())
|
|
677
|
-
return bus
|
|
678
482
|
}
|
|
679
483
|
|
|
680
484
|
// ---------------------------------------------------------------------------
|
|
681
|
-
//
|
|
485
|
+
// Axon Server Query Bus
|
|
682
486
|
// ---------------------------------------------------------------------------
|
|
683
487
|
|
|
684
488
|
/**
|
|
685
|
-
* A query bus backed by Axon Server.
|
|
489
|
+
* A query bus backed by Axon Server, over YOUR next bus.
|
|
490
|
+
*
|
|
491
|
+
* Same architecture as {@link axonServerCommandBus}: outbound dispatch goes
|
|
492
|
+
* through Axon Server, and a query the server routes here runs through `next`,
|
|
493
|
+
* so your unit-of-work policy applies to server-routed reads too. `subscribe`
|
|
494
|
+
* registers on `next` and announces the name to the server.
|
|
686
495
|
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
496
|
+
* The one asymmetry with commands is `shortcutQueriesToLocalHandlers` — Java
|
|
497
|
+
* has it for queries and not for commands, and so do we. When it is on and this
|
|
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
|
|
500
|
+
* in-process bus does.
|
|
692
501
|
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* `
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
* `query()` gets identical lineage to the remote branch.
|
|
502
|
+
* Correlation, if wanted, is `interceptingQueryBus(bus, correlation)` at the host,
|
|
503
|
+
* matching AF4's `AxonServerQueryBus`, which calls
|
|
504
|
+
* `dispatchInterceptors.intercept(...)` at the top of `query`, `streamingQuery`,
|
|
505
|
+
* `scatterGather` and `subscriptionQuery`. Because the wrap is outside, the
|
|
506
|
+
* shortcut branch gets identical correlation to the remote branch.
|
|
699
507
|
*
|
|
700
508
|
* KNOWN GAP: `subscriptionQuery` / `subscribeToUpdates` build their proto
|
|
701
509
|
* straight from `message.metadata`, and `interceptingQueryBus` (in
|
|
702
|
-
* `@kronos-ts/
|
|
703
|
-
* running the dispatch chain. Closing that needs a
|
|
510
|
+
* `@kronos-ts/core`) forwards those two calls to the delegate without
|
|
511
|
+
* running the dispatch chain. Closing that needs a core change.
|
|
704
512
|
*/
|
|
705
|
-
export function
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
const
|
|
717
|
-
const
|
|
513
|
+
export function axonServerQueryBus<U extends UnitOfWork = UnitOfWork>(
|
|
514
|
+
next: QueryBus<U>,
|
|
515
|
+
conn: AxonServerBusSource,
|
|
516
|
+
options: AxonServerQueryBusOptions = {},
|
|
517
|
+
): QueryBus<U> {
|
|
518
|
+
const {
|
|
519
|
+
connection,
|
|
520
|
+
serializer,
|
|
521
|
+
metadata: axonMetadata,
|
|
522
|
+
} = contextView(conn, options.context ?? conn.connection.config.context)
|
|
523
|
+
const shutdownLatch = conn.shutdown
|
|
524
|
+
const resilience = options.resilience ?? conn.resilience
|
|
525
|
+
const metadata = axonMetadata()
|
|
526
|
+
const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS))
|
|
527
|
+
const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD))
|
|
528
|
+
const shortcutQueriesToLocalHandlers = options.shortcutQueriesToLocalHandlers ?? false
|
|
529
|
+
const queryTimeoutMs = options.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
|
|
718
530
|
const { serializePayload, deserializePayload } = createPayloadHelpers(serializer)
|
|
719
531
|
|
|
720
|
-
|
|
532
|
+
/** Query names announced to Axon Server; the handlers live on `next`. */
|
|
533
|
+
const subscribedNames = new Set<string>()
|
|
721
534
|
|
|
722
535
|
// Local subscription store — subscription queries opened by THIS instance.
|
|
723
536
|
// Inbound updates from the server are offered into these via the Subscription RPC loop.
|
|
@@ -750,8 +563,21 @@ export function distributedQueryBus(
|
|
|
750
563
|
permits += PERMITS
|
|
751
564
|
}
|
|
752
565
|
|
|
566
|
+
function sendSubscribe(queryName: string) {
|
|
567
|
+
outbound.send({
|
|
568
|
+
subscribe: {
|
|
569
|
+
messageId: generateIdentifier(),
|
|
570
|
+
query: queryName,
|
|
571
|
+
resultName: "",
|
|
572
|
+
componentName: connection.config.componentName,
|
|
573
|
+
clientId: connection.config.clientId,
|
|
574
|
+
},
|
|
575
|
+
instructionId: generateIdentifier(),
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
|
|
753
579
|
/**
|
|
754
|
-
* Re-establish the bidirectional stream and re-
|
|
580
|
+
* Re-establish the bidirectional stream and re-announce all handlers.
|
|
755
581
|
* Called on stream error or when the connection reconnects.
|
|
756
582
|
*
|
|
757
583
|
* ORDER (preserves Axon-specific invariant): subscriptions are
|
|
@@ -763,18 +589,7 @@ export function distributedQueryBus(
|
|
|
763
589
|
streamStarted = false
|
|
764
590
|
permits = 0n
|
|
765
591
|
ensureStreamStarted()
|
|
766
|
-
for (const queryName of
|
|
767
|
-
outbound.send({
|
|
768
|
-
subscribe: {
|
|
769
|
-
messageId: generateIdentifier(),
|
|
770
|
-
query: queryName,
|
|
771
|
-
resultName: "",
|
|
772
|
-
componentName: connection.config.componentName,
|
|
773
|
-
clientId: connection.config.clientId,
|
|
774
|
-
},
|
|
775
|
-
instructionId: generateIdentifier(),
|
|
776
|
-
})
|
|
777
|
-
}
|
|
592
|
+
for (const queryName of subscribedNames) sendSubscribe(queryName)
|
|
778
593
|
grantQueryPermits()
|
|
779
594
|
}
|
|
780
595
|
|
|
@@ -790,7 +605,7 @@ export function distributedQueryBus(
|
|
|
790
605
|
connection.onReconnect(() => {
|
|
791
606
|
if (!shutdownLatch.shuttingDown && streamStarted) {
|
|
792
607
|
reestablishStreamWithRetry().catch((err) => {
|
|
793
|
-
console.error("
|
|
608
|
+
console.error("Axon Server query bus: reconnect retries exhausted", err)
|
|
794
609
|
})
|
|
795
610
|
}
|
|
796
611
|
})
|
|
@@ -810,12 +625,11 @@ export function distributedQueryBus(
|
|
|
810
625
|
)
|
|
811
626
|
handlerSubscriptions.set(subId, { queryName, payload })
|
|
812
627
|
|
|
813
|
-
const handler = localSegment.get(queryName)
|
|
814
628
|
let resultPayload: unknown
|
|
815
629
|
let errorCode = ""
|
|
816
630
|
let errorMsg = ""
|
|
817
631
|
|
|
818
|
-
if (
|
|
632
|
+
if (subscribedNames.has(queryName)) {
|
|
819
633
|
try {
|
|
820
634
|
const queryMessage: QueryMessage = {
|
|
821
635
|
kind: "query",
|
|
@@ -825,21 +639,18 @@ export function distributedQueryBus(
|
|
|
825
639
|
metadata: metadataFromProto(proto.metaData ?? {}),
|
|
826
640
|
timestamp: Number(proto.timestamp),
|
|
827
641
|
}
|
|
828
|
-
resultPayload = await
|
|
829
|
-
return handler(queryMessage)
|
|
830
|
-
})
|
|
642
|
+
resultPayload = await next.query(queryMessage)
|
|
831
643
|
} catch (err) {
|
|
832
644
|
errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR
|
|
833
645
|
errorMsg = err instanceof Error ? err.message : String(err)
|
|
834
646
|
}
|
|
835
647
|
} else {
|
|
836
648
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY
|
|
837
|
-
errorMsg = `No
|
|
649
|
+
errorMsg = `No next handler for query "${queryName}"`
|
|
838
650
|
}
|
|
839
651
|
|
|
840
|
-
const responseSerialized =
|
|
841
|
-
? serializePayload("result", resultPayload)
|
|
842
|
-
: undefined
|
|
652
|
+
const responseSerialized =
|
|
653
|
+
resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined
|
|
843
654
|
|
|
844
655
|
outbound.send({
|
|
845
656
|
subscriptionQueryResponse: {
|
|
@@ -850,7 +661,12 @@ export function distributedQueryBus(
|
|
|
850
661
|
requestIdentifier: proto.messageIdentifier,
|
|
851
662
|
errorCode,
|
|
852
663
|
errorMessage: errorCode
|
|
853
|
-
? {
|
|
664
|
+
? {
|
|
665
|
+
message: errorMsg,
|
|
666
|
+
location: connection.config.componentName,
|
|
667
|
+
details: [],
|
|
668
|
+
errorCode,
|
|
669
|
+
}
|
|
854
670
|
: undefined,
|
|
855
671
|
payload: responseSerialized,
|
|
856
672
|
metaData: {},
|
|
@@ -879,13 +695,12 @@ export function distributedQueryBus(
|
|
|
879
695
|
permits--
|
|
880
696
|
const proto = message.query
|
|
881
697
|
const queryName = proto.query
|
|
882
|
-
const handler = localSegment.get(queryName)
|
|
883
698
|
|
|
884
699
|
let resultPayload: unknown
|
|
885
700
|
let errorCode = ""
|
|
886
701
|
let errorMsg = ""
|
|
887
702
|
|
|
888
|
-
if (
|
|
703
|
+
if (subscribedNames.has(queryName)) {
|
|
889
704
|
try {
|
|
890
705
|
const queryMessage: QueryMessage = {
|
|
891
706
|
kind: "query",
|
|
@@ -896,16 +711,16 @@ export function distributedQueryBus(
|
|
|
896
711
|
timestamp: Number(proto.timestamp),
|
|
897
712
|
}
|
|
898
713
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
714
|
+
// Through the LOCAL BUS: no unit of work is handed in, so `next`
|
|
715
|
+
// opens one under whatever policy the caller gave it.
|
|
716
|
+
resultPayload = await next.query(queryMessage)
|
|
902
717
|
} catch (err) {
|
|
903
718
|
errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR
|
|
904
719
|
errorMsg = err instanceof Error ? err.message : String(err)
|
|
905
720
|
}
|
|
906
721
|
} else {
|
|
907
722
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY
|
|
908
|
-
errorMsg = `No
|
|
723
|
+
errorMsg = `No next handler for query "${queryName}"`
|
|
909
724
|
}
|
|
910
725
|
|
|
911
726
|
outbound.send({
|
|
@@ -914,11 +729,15 @@ export function distributedQueryBus(
|
|
|
914
729
|
requestIdentifier: proto.messageIdentifier,
|
|
915
730
|
errorCode,
|
|
916
731
|
errorMessage: errorCode
|
|
917
|
-
? {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
732
|
+
? {
|
|
733
|
+
message: errorMsg,
|
|
734
|
+
location: connection.config.componentName,
|
|
735
|
+
details: [],
|
|
736
|
+
errorCode,
|
|
737
|
+
}
|
|
921
738
|
: undefined,
|
|
739
|
+
payload:
|
|
740
|
+
resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
|
|
922
741
|
metaData: {},
|
|
923
742
|
processingInstructions: [],
|
|
924
743
|
},
|
|
@@ -945,39 +764,51 @@ export function distributedQueryBus(
|
|
|
945
764
|
if (shutdownLatch.shuttingDown) return
|
|
946
765
|
if (String(err).includes("Connection dropped")) return
|
|
947
766
|
|
|
948
|
-
console.error(
|
|
767
|
+
console.error(
|
|
768
|
+
"Axon Server query bus: inbound stream error, attempting re-establishment via withRetry",
|
|
769
|
+
err,
|
|
770
|
+
)
|
|
949
771
|
await reestablishStreamWithRetry().catch((retryErr) => {
|
|
950
|
-
console.error("
|
|
772
|
+
console.error("Axon Server query bus: reconnect retries exhausted", retryErr)
|
|
951
773
|
})
|
|
952
774
|
}
|
|
953
775
|
}
|
|
954
776
|
|
|
955
|
-
const routing: QueryBus = {
|
|
956
|
-
async query(
|
|
777
|
+
const routing: QueryBus<U> = {
|
|
778
|
+
async query(unstamped: QueryMessage, uow?: UnitOfWork): Promise<unknown> {
|
|
957
779
|
const activity = shutdownLatch.registerActivity()
|
|
958
780
|
try {
|
|
959
|
-
const queryName = qualifiedNameToString(
|
|
960
|
-
|
|
961
|
-
// Local shortcut — handle locally if handler is co-located
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
781
|
+
const queryName = qualifiedNameToString(unstamped.name)
|
|
782
|
+
|
|
783
|
+
// Local shortcut — handle locally if a handler is co-located. The
|
|
784
|
+
// caller's unit of work is passed straight through, so `next` makes the
|
|
785
|
+
// nest-or-open decision on the HANDLE exactly as it does for an
|
|
786
|
+
// in-process read: a live unit of work handed in by `ctx.query` is
|
|
787
|
+
// reused so the consulting read shares the caller's transaction.
|
|
788
|
+
if (shortcutQueriesToLocalHandlers && subscribedNames.has(queryName)) {
|
|
789
|
+
return next.query(unstamped, uow)
|
|
969
790
|
}
|
|
970
791
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
792
|
+
// A transport is not a task: it has no unit of work, so it has no clock.
|
|
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() }
|
|
798
|
+
|
|
799
|
+
const responseStream = connection.queries.query(
|
|
800
|
+
{
|
|
801
|
+
messageIdentifier: message.identifier,
|
|
802
|
+
query: queryName,
|
|
803
|
+
timestamp: BigInt(message.timestamp),
|
|
804
|
+
payload: serializePayload(queryName, message.payload),
|
|
805
|
+
metaData: metadataToProto(message.metadata),
|
|
806
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
807
|
+
clientId: connection.config.clientId,
|
|
808
|
+
componentName: connection.config.componentName,
|
|
809
|
+
},
|
|
810
|
+
{ metadata },
|
|
811
|
+
)
|
|
981
812
|
|
|
982
813
|
for await (const response of responseStream) {
|
|
983
814
|
if (response.errorCode && response.errorCode !== "") {
|
|
@@ -995,25 +826,24 @@ export function distributedQueryBus(
|
|
|
995
826
|
}
|
|
996
827
|
},
|
|
997
828
|
|
|
998
|
-
subscribe(
|
|
999
|
-
|
|
829
|
+
subscribe(
|
|
830
|
+
queryName: string,
|
|
831
|
+
handler: (message: QueryMessage, uow: U) => Promise<unknown>,
|
|
832
|
+
) {
|
|
833
|
+
subscribedNames.add(queryName)
|
|
834
|
+
next.subscribe(queryName, handler)
|
|
1000
835
|
|
|
1001
836
|
ensureStreamStarted()
|
|
1002
|
-
|
|
1003
|
-
subscribe: {
|
|
1004
|
-
messageId: generateIdentifier(),
|
|
1005
|
-
query: queryName,
|
|
1006
|
-
resultName: "",
|
|
1007
|
-
componentName: connection.config.componentName,
|
|
1008
|
-
clientId: connection.config.clientId,
|
|
1009
|
-
},
|
|
1010
|
-
instructionId: generateIdentifier(),
|
|
1011
|
-
})
|
|
837
|
+
sendSubscribe(queryName)
|
|
1012
838
|
// Permits AFTER subscription (Axon-specific ordering invariant)
|
|
1013
839
|
grantQueryPermits()
|
|
1014
840
|
},
|
|
1015
841
|
|
|
1016
|
-
subscriptionQuery(
|
|
842
|
+
subscriptionQuery(
|
|
843
|
+
unstamped: QueryMessage,
|
|
844
|
+
bufferSize?: number,
|
|
845
|
+
): SubscriptionQueryResult {
|
|
846
|
+
const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() }
|
|
1017
847
|
const queryId = message.identifier
|
|
1018
848
|
if (subscriptions.has(queryId)) {
|
|
1019
849
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`)
|
|
@@ -1037,7 +867,7 @@ export function distributedQueryBus(
|
|
|
1037
867
|
timestamp: BigInt(message.timestamp),
|
|
1038
868
|
payload: serializePayload(queryName, message.payload),
|
|
1039
869
|
metaData: metadataToProto(message.metadata),
|
|
1040
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
870
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
1041
871
|
clientId: connection.config.clientId,
|
|
1042
872
|
componentName: connection.config.componentName,
|
|
1043
873
|
},
|
|
@@ -1054,7 +884,7 @@ export function distributedQueryBus(
|
|
|
1054
884
|
timestamp: BigInt(message.timestamp),
|
|
1055
885
|
payload: serializePayload(queryName, message.payload),
|
|
1056
886
|
metaData: metadataToProto(message.metadata),
|
|
1057
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
887
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
1058
888
|
clientId: connection.config.clientId,
|
|
1059
889
|
componentName: connection.config.componentName,
|
|
1060
890
|
},
|
|
@@ -1078,21 +908,33 @@ export function distributedQueryBus(
|
|
|
1078
908
|
const initial = response.initialResult
|
|
1079
909
|
if (!initialSettled) {
|
|
1080
910
|
if (initial.errorCode && initial.errorCode !== "") {
|
|
1081
|
-
rejectInitial(
|
|
911
|
+
rejectInitial(
|
|
912
|
+
mapErrorCode(
|
|
913
|
+
initial.errorCode,
|
|
914
|
+
initial.errorMessage?.message ?? "Unknown error",
|
|
915
|
+
),
|
|
916
|
+
)
|
|
1082
917
|
} else {
|
|
1083
|
-
resolveInitial(
|
|
918
|
+
resolveInitial(
|
|
919
|
+
deserializePayload(initial.payload?.data as Uint8Array | undefined),
|
|
920
|
+
)
|
|
1084
921
|
}
|
|
1085
922
|
initialSettled = true
|
|
1086
923
|
}
|
|
1087
924
|
} else if (response.update) {
|
|
1088
|
-
const update = deserializePayload(
|
|
925
|
+
const update = deserializePayload(
|
|
926
|
+
response.update.payload?.data as Uint8Array | undefined,
|
|
927
|
+
)
|
|
1089
928
|
handler.offer(update)
|
|
1090
929
|
} else if (response.complete) {
|
|
1091
930
|
handler.complete()
|
|
1092
931
|
break
|
|
1093
932
|
} else if (response.completeExceptionally) {
|
|
1094
933
|
handler.completeExceptionally(
|
|
1095
|
-
new Error(
|
|
934
|
+
new Error(
|
|
935
|
+
response.completeExceptionally.errorMessage?.message ??
|
|
936
|
+
"Subscription query failed",
|
|
937
|
+
),
|
|
1096
938
|
)
|
|
1097
939
|
break
|
|
1098
940
|
}
|
|
@@ -1125,7 +967,11 @@ export function distributedQueryBus(
|
|
|
1125
967
|
}
|
|
1126
968
|
},
|
|
1127
969
|
|
|
1128
|
-
subscribeToUpdates(
|
|
970
|
+
subscribeToUpdates(
|
|
971
|
+
unstamped: QueryMessage,
|
|
972
|
+
bufferSize?: number,
|
|
973
|
+
): AsyncIterable<unknown> & { close(): void } {
|
|
974
|
+
const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() }
|
|
1129
975
|
const queryId = message.identifier
|
|
1130
976
|
if (subscriptions.has(queryId)) {
|
|
1131
977
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`)
|
|
@@ -1174,10 +1020,7 @@ export function distributedQueryBus(
|
|
|
1174
1020
|
})
|
|
1175
1021
|
},
|
|
1176
1022
|
|
|
1177
|
-
async completeSubscription(
|
|
1178
|
-
queryName: string,
|
|
1179
|
-
filter?: SubscriptionFilter,
|
|
1180
|
-
): Promise<void> {
|
|
1023
|
+
async completeSubscription(queryName: string, filter?: SubscriptionFilter): Promise<void> {
|
|
1181
1024
|
runAfterCommitOrImmediately(() => {
|
|
1182
1025
|
for (const [subId, sub] of handlerSubscriptions) {
|
|
1183
1026
|
if (sub.queryName !== queryName) continue
|
|
@@ -1233,7 +1076,5 @@ export function distributedQueryBus(
|
|
|
1233
1076
|
},
|
|
1234
1077
|
}
|
|
1235
1078
|
|
|
1236
|
-
|
|
1237
|
-
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor())
|
|
1238
|
-
return bus
|
|
1079
|
+
return routing
|
|
1239
1080
|
}
|