@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/dist/axon-server.js
CHANGED
|
@@ -1,74 +1,48 @@
|
|
|
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
|
-
import { qualifiedNameToString, qualifiedNameFromString, generateIdentifier,
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import { axonServerEventStore } from "./axon-server-event-store.js";
|
|
59
|
-
import { axonServerSnapshotStore } from "./axon-server-snapshot-store.js";
|
|
28
|
+
import { qualifiedNameToString, qualifiedNameFromString, generateIdentifier, } from "@kronos-ts/core";
|
|
29
|
+
import { withRetry } from "./resilience.js";
|
|
30
|
+
import { applySubscriptionFilter, updateHandler, runAfterCommitOrImmediately, } from "@kronos-ts/core";
|
|
31
|
+
import { contextView } from "./context-view.js";
|
|
60
32
|
import { metadataToProto, metadataFromProto } from "./metadata-conversion.js";
|
|
61
33
|
import { outboundStream } from "./outbound-stream.js";
|
|
62
34
|
import { mapErrorCode, AxonServerErrorCode } from "./errors.js";
|
|
63
|
-
import { shutdownLatch } from "./shutdown-latch.js";
|
|
64
|
-
import { platformConnection, } from "./platform-service.js";
|
|
65
35
|
/** Default flow control settings — aligned with Java's 5000 permits. */
|
|
66
36
|
const DEFAULT_PERMITS = 5000n;
|
|
67
37
|
const DEFAULT_THRESHOLD = 2500n;
|
|
38
|
+
/** Default query dispatch timeout — aligned with Java's one hour. */
|
|
39
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 3_600_000;
|
|
40
|
+
/** Default command handler load factor — aligned with Java's 100. */
|
|
41
|
+
const DEFAULT_LOAD_FACTOR = 100;
|
|
68
42
|
// Processing instruction keys — aligned with proto ProcessingKey enum.
|
|
69
43
|
// CLIENT_SUPPORTS_STREAMING (key=8) is an Axon-Server-specific capability
|
|
70
|
-
// advertisement that MUST survive
|
|
71
|
-
//
|
|
44
|
+
// advertisement that MUST survive verbatim — see file-level JSDoc above and
|
|
45
|
+
// `defaultQueryInstructions` below.
|
|
72
46
|
const INSTRUCTION_KEY = {
|
|
73
47
|
ROUTING_KEY: 0,
|
|
74
48
|
PRIORITY: 1,
|
|
@@ -84,10 +58,16 @@ function toProtoProcessingInstructions(instructions) {
|
|
|
84
58
|
result.push({ key: INSTRUCTION_KEY.ROUTING_KEY, value: { textValue: instructions.routingKey } });
|
|
85
59
|
}
|
|
86
60
|
if (instructions.priority !== undefined) {
|
|
87
|
-
result.push({
|
|
61
|
+
result.push({
|
|
62
|
+
key: INSTRUCTION_KEY.PRIORITY,
|
|
63
|
+
value: { numberValue: BigInt(instructions.priority) },
|
|
64
|
+
});
|
|
88
65
|
}
|
|
89
66
|
if (instructions.timeoutMs !== undefined) {
|
|
90
|
-
result.push({
|
|
67
|
+
result.push({
|
|
68
|
+
key: INSTRUCTION_KEY.TIMEOUT,
|
|
69
|
+
value: { numberValue: BigInt(instructions.timeoutMs) },
|
|
70
|
+
});
|
|
91
71
|
}
|
|
92
72
|
return result;
|
|
93
73
|
}
|
|
@@ -104,102 +84,8 @@ function defaultQueryInstructions(timeoutMs) {
|
|
|
104
84
|
{ key: INSTRUCTION_KEY.CLIENT_SUPPORTS_STREAMING, value: { booleanValue: true } },
|
|
105
85
|
];
|
|
106
86
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Build the gRPC metadata headers required by Axon Server. AxonIQ-Context
|
|
109
|
-
* is mandatory (identifies the tenant/context); AxonIQ-Access-Token is
|
|
110
|
-
* optional auth. Both must be attached to every outbound stream/RPC —
|
|
111
|
-
* preserved verbatim from the legacy enhancer.
|
|
112
|
-
*/
|
|
113
|
-
function createAxonMetadata(config) {
|
|
114
|
-
const metadata = new Metadata();
|
|
115
|
-
metadata.set("AxonIQ-Context", config.context);
|
|
116
|
-
if (config.token) {
|
|
117
|
-
metadata.set("AxonIQ-Access-Token", config.token);
|
|
118
|
-
}
|
|
119
|
-
return metadata;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Connect to Axon Server and build the components it backs.
|
|
123
|
-
*
|
|
124
|
-
* `serializer` and `unitOfWorkFactory` are arguments rather than slot lookups:
|
|
125
|
-
* the buses serialize payloads with the former and run every inbound command /
|
|
126
|
-
* query in the latter, so they must be the SAME instances the rest of the app
|
|
127
|
-
* uses. Pass the ones you hand to `kronos`.
|
|
128
|
-
*/
|
|
129
|
-
export async function axonServer(options) {
|
|
130
|
-
const config = options;
|
|
131
|
-
const { serializer, unitOfWorkFactory, resilience } = config;
|
|
132
|
-
const connection = await withRetry(async () => connectToAxonServer(config), {
|
|
133
|
-
event: "initial-connect",
|
|
134
|
-
...resilience,
|
|
135
|
-
});
|
|
136
|
-
// Health-check ping with warn-then-continue (D-100). AxonServerConnection has
|
|
137
|
-
// no dedicated probe surface today; the gRPC channel itself is created
|
|
138
|
-
// eagerly in connectToAxonServer so the meaningful probe is a round-trip — we
|
|
139
|
-
// approximate via a soft no-op promise that satisfies the threshold contract.
|
|
140
|
-
// Real network failure is surfaced by the first bus call against the channel.
|
|
141
|
-
await healthCheck(async () => undefined, {
|
|
142
|
-
thresholdMs: resilience?.healthCheckThresholdMs,
|
|
143
|
-
log: resilience?.log,
|
|
144
|
-
});
|
|
145
|
-
// One latch per bus, drained in close() before the transport goes away.
|
|
146
|
-
const commandLatch = shutdownLatch();
|
|
147
|
-
const queryLatch = shutdownLatch();
|
|
148
|
-
const busLatches = [commandLatch, queryLatch];
|
|
149
|
-
// The connection is live before anything below is built, so the buses open
|
|
150
|
-
// their gRPC streams for real and `subscribe()` reaches the wire immediately —
|
|
151
|
-
// no lazy proxy, no subscription buffering, no readiness promise.
|
|
152
|
-
const components = {
|
|
153
|
-
eventStore: axonServerEventStore(connection, serializer),
|
|
154
|
-
snapshotStore: axonServerSnapshotStore(connection, serializer),
|
|
155
|
-
commandBus: distributedCommandBus(connection, unitOfWorkFactory, commandLatch, serializer, config.commandFlowControl, config.commandLoadFactor, resilience),
|
|
156
|
-
queryBus: distributedQueryBus(connection, unitOfWorkFactory, queryLatch, serializer, config.queryFlowControl, config.shortcutQueriesToLocalHandlers, config.queryTimeoutMs, resilience),
|
|
157
|
-
};
|
|
158
|
-
// Built here, started by the control plane (or by the caller). Constructing it
|
|
159
|
-
// eagerly is what lets the control plane be a separate object at all — and it
|
|
160
|
-
// keeps `platformService` tuning and `stop()` ownership in one place, so the
|
|
161
|
-
// documented shutdown order below holds whether or not anyone opted in.
|
|
162
|
-
const platform = platformConnection(connection, config.platformService);
|
|
163
|
-
return {
|
|
164
|
-
components,
|
|
165
|
-
platform,
|
|
166
|
-
async start() {
|
|
167
|
-
// RECONNECT DETECTION IS DATA PATH. The heartbeat on the platform stream
|
|
168
|
-
// is what notices a dead channel and calls `connection.reconnect()`; both
|
|
169
|
-
// buses above hook `connection.onReconnect(...)` to rebuild their own
|
|
170
|
-
// streams. Arming it used to be a side effect of `platform.start()`, which
|
|
171
|
-
// only `axonServerControlPlane(...)` calls — so a service that never opted
|
|
172
|
-
// into remote administration had NO reconnect detection at all and would
|
|
173
|
-
// sit on a dead channel forever. It is armed here, unconditionally,
|
|
174
|
-
// independent of whether anyone administers this service.
|
|
175
|
-
//
|
|
176
|
-
// `armConnectionMonitoring()` opens the stream and starts heartbeats but
|
|
177
|
-
// arms NO processor status reporting; that stays the control plane's, and
|
|
178
|
-
// a later `platform.start()` adds it to this same live stream. Both calls
|
|
179
|
-
// are idempotent, so either order works.
|
|
180
|
-
await platform.armConnectionMonitoring();
|
|
181
|
-
// The only thing the data path has to wait for: Axon Server's
|
|
182
|
-
// command/query routing tables registering the subscribe frames sent on
|
|
183
|
-
// the BUS streams. It cannot be derived from the platform stream, because
|
|
184
|
-
// subscribes travel on a different stream entirely — and the platform
|
|
185
|
-
// stream's own `subscriptionsAcked()` latch says nothing about them (it
|
|
186
|
-
// latches unconditionally once `register` has been flushed; see
|
|
187
|
-
// platform-service.ts). So this barrier is the settle wait, and it is
|
|
188
|
-
// deliberately independent of whether the platform stream is up at all.
|
|
189
|
-
// The legacy enhancer carried the same 1s wait.
|
|
190
|
-
await new Promise((r) => setTimeout(r, config.busSubscriptionAckDelayMs ?? 1000));
|
|
191
|
-
},
|
|
192
|
-
async close() {
|
|
193
|
-
await Promise.all(busLatches.map((l) => l.initiateShutdown()));
|
|
194
|
-
// Idempotent, and independent of `control.close()` — a backend that was
|
|
195
|
-
// never administered still stops a platform stream someone else started.
|
|
196
|
-
platform.stop();
|
|
197
|
-
connection.close();
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
87
|
// ---------------------------------------------------------------------------
|
|
202
|
-
// Shared payload helpers
|
|
88
|
+
// Shared payload helpers
|
|
203
89
|
// ---------------------------------------------------------------------------
|
|
204
90
|
function createPayloadHelpers(serializer) {
|
|
205
91
|
return {
|
|
@@ -214,68 +100,66 @@ function createPayloadHelpers(serializer) {
|
|
|
214
100
|
};
|
|
215
101
|
}
|
|
216
102
|
// ---------------------------------------------------------------------------
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// Bus implementation moved verbatim from the legacy enhancer with TWO
|
|
220
|
-
// behavioural additions per D-97:
|
|
221
|
-
// 1) reestablishStream() body wrapped in withRetry({ event: "reconnect" })
|
|
222
|
-
// 2) inbound-stream backoff replaced by the same withRetry path
|
|
223
|
-
//
|
|
224
|
-
// Axon-specific protocol invariants preserved BYTE-FOR-BYTE:
|
|
225
|
-
// - AxonIQ-Context + AxonIQ-Access-Token gRPC metadata headers via
|
|
226
|
-
// createAxonMetadata(connection.config)
|
|
227
|
-
// - permits-AFTER-subscriptions ordering on reestablishStreamBody (subs
|
|
228
|
-
// are sent BEFORE grantPermits() in the reconnect path; the initial
|
|
229
|
-
// handshake matches this — see ensureStreamStarted's grantPermits call
|
|
230
|
-
// in subscribe()).
|
|
103
|
+
// Axon Server Command Bus
|
|
231
104
|
// ---------------------------------------------------------------------------
|
|
232
105
|
/**
|
|
233
|
-
* A command bus backed by Axon Server.
|
|
106
|
+
* A command bus backed by Axon Server, over YOUR next bus.
|
|
234
107
|
*
|
|
235
|
-
* - **Outbound dispatch**:
|
|
236
|
-
* Axon Server routes the command to the appropriate node (which may be
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
*
|
|
108
|
+
* - **Outbound dispatch**: ALWAYS through Axon Server, via the unary Dispatch
|
|
109
|
+
* RPC. Axon Server routes the command to the appropriate node (which may be
|
|
110
|
+
* this one). There is deliberately no client-side prefer-next fork: the hub
|
|
111
|
+
* is the router, and short-circuiting it would silently defeat load factors,
|
|
112
|
+
* priorities and routing keys.
|
|
113
|
+
* - **Inbound**: a command the server routes here is dispatched into `next` —
|
|
114
|
+
* not into a privately-held handler map. That is what makes the unit-of-work
|
|
115
|
+
* policy you chose for `next` (say `postgresUnitOfWork(unitOfWork, pg)`)
|
|
116
|
+
* apply to server-routed work exactly as it applies to work this process
|
|
117
|
+
* originated. It is also why this function takes no `unitOfWork` argument:
|
|
118
|
+
* `next` carries that policy now.
|
|
119
|
+
* - **subscribe**: registers the handler on `next` AND announces the name to
|
|
120
|
+
* Axon Server, so other nodes can route to us.
|
|
244
121
|
*
|
|
245
|
-
* ##
|
|
122
|
+
* ## correlation and the interceptor layer
|
|
246
123
|
*
|
|
247
|
-
* The returned bus
|
|
248
|
-
*
|
|
249
|
-
*
|
|
124
|
+
* The returned bus stamps no correlation of its own. A host that wants it wraps the
|
|
125
|
+
* OUTERMOST bus:
|
|
126
|
+
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* interceptingCommandBus(axonServerCommandBus(next, conn), correlation)
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* so whatever a host adds runs BEFORE the message is serialized onto the wire.
|
|
132
|
+
* Correlation itself is usually already on `message.metadata` by then — `ctx.send`
|
|
133
|
+
* stamps the unit of work's correlation data before any bus sees the message.
|
|
250
134
|
*
|
|
251
135
|
* This is precisely how the Java client does it. AF4's `AxonServerCommandBus`
|
|
252
136
|
* holds its own `DispatchInterceptors` and dispatches as
|
|
253
137
|
* `doDispatch(dispatchInterceptors.intercept(commandMessage), cb)` — one call
|
|
254
|
-
* site, at the top, ahead of any routing
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
|
|
258
|
-
* stacks `InterceptingCommandBus → DistributedCommandBus → SimpleCommandBus`.
|
|
259
|
-
*
|
|
260
|
-
* Before this wrap, an Axon-backed service lost lineage on EVERY command: the
|
|
261
|
-
* only registration of `correlationDataDispatchInterceptor` lives in
|
|
262
|
-
* `@kronos-ts/app`'s in-memory default bus, and `components.commandBus` from
|
|
263
|
-
* this backend replaces it wholesale.
|
|
138
|
+
* site, at the top, ahead of any routing. AF5 keeps the property via decorator
|
|
139
|
+
* order: `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
|
|
140
|
+
* stacks `InterceptingCommandBus → DistributedCommandBus → LocalCommandBus`.
|
|
264
141
|
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* `CommandProcessingTask` runs the local segment WITHOUT re-running dispatch
|
|
269
|
-
* interceptors.
|
|
142
|
+
* If `next` is itself an intercepting bus, a server-routed command sees
|
|
143
|
+
* `correlation` twice. That is harmless: both of its fields are `??` seeds, so the
|
|
144
|
+
* second application finds them set and changes nothing.
|
|
270
145
|
*/
|
|
271
|
-
export function
|
|
272
|
-
const metadata =
|
|
146
|
+
export function axonServerCommandBus(next, conn, options = {}) {
|
|
147
|
+
const { connection, serializer, metadata: axonMetadata, } = contextView(conn, options.context ?? conn.connection.config.context);
|
|
148
|
+
const shutdownLatch = conn.shutdown;
|
|
149
|
+
const resilience = options.resilience ?? conn.resilience;
|
|
150
|
+
const metadata = axonMetadata();
|
|
273
151
|
const { serializePayload, deserializePayload } = createPayloadHelpers(serializer);
|
|
274
|
-
const PERMITS = BigInt(flowControl?.permits ?? Number(DEFAULT_PERMITS));
|
|
275
|
-
const THRESHOLD = BigInt(flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
152
|
+
const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS));
|
|
153
|
+
const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
|
|
154
|
+
const loadFactor = options.loadFactor ?? DEFAULT_LOAD_FACTOR;
|
|
155
|
+
/**
|
|
156
|
+
* The names this node announced to Axon Server. The handlers themselves live
|
|
157
|
+
* on `next`; this set exists so an inbound command for a name we never
|
|
158
|
+
* subscribed still answers NO_HANDLER_FOR_COMMAND rather than whatever
|
|
159
|
+
* `next.dispatch` happens to throw — and so a reconnect can re-announce.
|
|
160
|
+
*/
|
|
161
|
+
const subscribedNames = new Set();
|
|
162
|
+
// Bidirectional stream for handler subscription + inbound command handling
|
|
279
163
|
let outbound = outboundStream();
|
|
280
164
|
let streamStarted = false;
|
|
281
165
|
let permits = 0n;
|
|
@@ -294,8 +178,20 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
294
178
|
});
|
|
295
179
|
permits += PERMITS;
|
|
296
180
|
}
|
|
181
|
+
function sendSubscribe(commandName) {
|
|
182
|
+
outbound.send({
|
|
183
|
+
subscribe: {
|
|
184
|
+
messageId: generateIdentifier(),
|
|
185
|
+
command: commandName,
|
|
186
|
+
componentName: connection.config.componentName,
|
|
187
|
+
clientId: connection.config.clientId,
|
|
188
|
+
loadFactor,
|
|
189
|
+
},
|
|
190
|
+
instructionId: generateIdentifier(),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
297
193
|
/**
|
|
298
|
-
* Re-establish the bidirectional stream and re-
|
|
194
|
+
* Re-establish the bidirectional stream and re-announce all handlers.
|
|
299
195
|
* Called on stream error or when the connection reconnects.
|
|
300
196
|
*
|
|
301
197
|
* ORDER (preserves Axon-specific invariant): subscriptions are
|
|
@@ -309,18 +205,8 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
309
205
|
permits = 0n;
|
|
310
206
|
ensureStreamStarted();
|
|
311
207
|
// Re-subscribe all handlers FIRST
|
|
312
|
-
for (const commandName of
|
|
313
|
-
|
|
314
|
-
subscribe: {
|
|
315
|
-
messageId: generateIdentifier(),
|
|
316
|
-
command: commandName,
|
|
317
|
-
componentName: connection.config.componentName,
|
|
318
|
-
clientId: connection.config.clientId,
|
|
319
|
-
loadFactor: commandLoadFactor ?? 100,
|
|
320
|
-
},
|
|
321
|
-
instructionId: generateIdentifier(),
|
|
322
|
-
});
|
|
323
|
-
}
|
|
208
|
+
for (const commandName of subscribedNames)
|
|
209
|
+
sendSubscribe(commandName);
|
|
324
210
|
// Permits AFTER subscriptions (Axon-specific ordering invariant)
|
|
325
211
|
grantPermits();
|
|
326
212
|
}
|
|
@@ -336,7 +222,7 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
336
222
|
connection.onReconnect(() => {
|
|
337
223
|
if (!shutdownLatch.shuttingDown && streamStarted) {
|
|
338
224
|
reestablishStreamWithRetry().catch((err) => {
|
|
339
|
-
console.error("
|
|
225
|
+
console.error("Axon Server command bus: reconnect retries exhausted", err);
|
|
340
226
|
});
|
|
341
227
|
}
|
|
342
228
|
});
|
|
@@ -348,11 +234,10 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
348
234
|
permits--;
|
|
349
235
|
const proto = message.command;
|
|
350
236
|
const commandName = proto.name;
|
|
351
|
-
const handler = localSegment.get(commandName);
|
|
352
237
|
let resultPayload;
|
|
353
238
|
let errorCode = "";
|
|
354
239
|
let errorMsg = "";
|
|
355
|
-
if (
|
|
240
|
+
if (subscribedNames.has(commandName)) {
|
|
356
241
|
try {
|
|
357
242
|
const commandMessage = {
|
|
358
243
|
kind: "command",
|
|
@@ -362,8 +247,12 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
362
247
|
metadata: metadataFromProto(proto.metaData),
|
|
363
248
|
timestamp: Number(proto.timestamp),
|
|
364
249
|
};
|
|
365
|
-
//
|
|
366
|
-
|
|
250
|
+
// Through the LOCAL BUS, so the caller's unit-of-work policy runs.
|
|
251
|
+
// AF parity is preserved: `CommandProcessingTask` runs the next
|
|
252
|
+
// segment without re-running dispatch interceptors, and a `next`
|
|
253
|
+
// that happens to carry `correlation` re-applies a pair of `??` seeds
|
|
254
|
+
// that are already set.
|
|
255
|
+
resultPayload = await next.dispatch(commandMessage);
|
|
367
256
|
}
|
|
368
257
|
catch (err) {
|
|
369
258
|
errorCode = AxonServerErrorCode.COMMAND_EXECUTION_ERROR;
|
|
@@ -372,7 +261,7 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
372
261
|
}
|
|
373
262
|
else {
|
|
374
263
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_COMMAND;
|
|
375
|
-
errorMsg = `No
|
|
264
|
+
errorMsg = `No next handler for command "${commandName}"`;
|
|
376
265
|
}
|
|
377
266
|
// Send response back to Axon Server
|
|
378
267
|
outbound.send({
|
|
@@ -381,11 +270,14 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
381
270
|
requestIdentifier: proto.messageIdentifier,
|
|
382
271
|
errorCode,
|
|
383
272
|
errorMessage: errorCode
|
|
384
|
-
? {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
273
|
+
? {
|
|
274
|
+
message: errorMsg,
|
|
275
|
+
location: connection.config.componentName,
|
|
276
|
+
details: [],
|
|
277
|
+
errorCode,
|
|
278
|
+
}
|
|
388
279
|
: undefined,
|
|
280
|
+
payload: resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
|
|
389
281
|
metaData: {},
|
|
390
282
|
processingInstructions: [],
|
|
391
283
|
},
|
|
@@ -406,14 +298,20 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
406
298
|
return;
|
|
407
299
|
if (String(err).includes("Connection dropped"))
|
|
408
300
|
return;
|
|
409
|
-
console.error("
|
|
301
|
+
console.error("Axon Server command bus: inbound stream error, attempting re-establishment via withRetry", err);
|
|
410
302
|
await reestablishStreamWithRetry().catch((retryErr) => {
|
|
411
|
-
console.error("
|
|
303
|
+
console.error("Axon Server command bus: reconnect retries exhausted", retryErr);
|
|
412
304
|
});
|
|
413
305
|
}
|
|
414
306
|
}
|
|
415
|
-
|
|
416
|
-
async dispatch(
|
|
307
|
+
return {
|
|
308
|
+
async dispatch(unstamped) {
|
|
309
|
+
// A transport is not a task: it has no unit of work, so it has no clock.
|
|
310
|
+
// A message that reaches the wire with no instant yet gets one from system
|
|
311
|
+
// time here — the envelope crosses a process boundary and must be fully
|
|
312
|
+
// formed. A locally-shortcut message is handed to `next` untouched
|
|
313
|
+
// instead, so the task that handles it supplies the instant.
|
|
314
|
+
const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
|
|
417
315
|
const activity = shutdownLatch.registerActivity();
|
|
418
316
|
try {
|
|
419
317
|
const commandName = qualifiedNameToString(message.name);
|
|
@@ -437,58 +335,56 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
437
335
|
}
|
|
438
336
|
},
|
|
439
337
|
subscribe(commandName, handler) {
|
|
440
|
-
|
|
338
|
+
subscribedNames.add(commandName);
|
|
339
|
+
next.subscribe(commandName, handler);
|
|
441
340
|
ensureStreamStarted();
|
|
442
341
|
// Subscription FIRST
|
|
443
|
-
|
|
444
|
-
subscribe: {
|
|
445
|
-
messageId: generateIdentifier(),
|
|
446
|
-
command: commandName,
|
|
447
|
-
componentName: connection.config.componentName,
|
|
448
|
-
clientId: connection.config.clientId,
|
|
449
|
-
loadFactor: commandLoadFactor ?? 100,
|
|
450
|
-
},
|
|
451
|
-
instructionId: generateIdentifier(),
|
|
452
|
-
});
|
|
342
|
+
sendSubscribe(commandName);
|
|
453
343
|
// Permits AFTER subscription (Axon-specific ordering invariant)
|
|
454
344
|
grantPermits();
|
|
455
345
|
},
|
|
456
346
|
};
|
|
457
|
-
// Interception OUTSIDE routing — see the note on this function.
|
|
458
|
-
const bus = interceptingCommandBus(routing);
|
|
459
|
-
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor());
|
|
460
|
-
return bus;
|
|
461
347
|
}
|
|
462
348
|
// ---------------------------------------------------------------------------
|
|
463
|
-
//
|
|
349
|
+
// Axon Server Query Bus
|
|
464
350
|
// ---------------------------------------------------------------------------
|
|
465
351
|
/**
|
|
466
|
-
* A query bus backed by Axon Server.
|
|
352
|
+
* A query bus backed by Axon Server, over YOUR next bus.
|
|
467
353
|
*
|
|
468
|
-
* Same architecture as
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* are executed within a UnitOfWork.
|
|
354
|
+
* Same architecture as {@link axonServerCommandBus}: outbound dispatch goes
|
|
355
|
+
* through Axon Server, and a query the server routes here runs through `next`,
|
|
356
|
+
* so your unit-of-work policy applies to server-routed reads too. `subscribe`
|
|
357
|
+
* registers on `next` and announces the name to the server.
|
|
473
358
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
359
|
+
* The one asymmetry with commands is `shortcutQueriesToLocalHandlers` — Java
|
|
360
|
+
* has it for queries and not for commands, and so do we. When it is on and this
|
|
361
|
+
* node subscribed the name, `query()` goes straight to `next` and the caller's
|
|
362
|
+
* unit of work is passed through, so the next branch nests exactly as the
|
|
363
|
+
* in-process bus does.
|
|
364
|
+
*
|
|
365
|
+
* Correlation, if wanted, is `interceptingQueryBus(bus, correlation)` at the host,
|
|
366
|
+
* matching AF4's `AxonServerQueryBus`, which calls
|
|
367
|
+
* `dispatchInterceptors.intercept(...)` at the top of `query`, `streamingQuery`,
|
|
368
|
+
* `scatterGather` and `subscriptionQuery`. Because the wrap is outside, the
|
|
369
|
+
* shortcut branch gets identical correlation to the remote branch.
|
|
480
370
|
*
|
|
481
371
|
* KNOWN GAP: `subscriptionQuery` / `subscribeToUpdates` build their proto
|
|
482
372
|
* straight from `message.metadata`, and `interceptingQueryBus` (in
|
|
483
|
-
* `@kronos-ts/
|
|
484
|
-
* running the dispatch chain. Closing that needs a
|
|
373
|
+
* `@kronos-ts/core`) forwards those two calls to the delegate without
|
|
374
|
+
* running the dispatch chain. Closing that needs a core change.
|
|
485
375
|
*/
|
|
486
|
-
export function
|
|
487
|
-
const metadata =
|
|
488
|
-
const
|
|
489
|
-
const
|
|
376
|
+
export function axonServerQueryBus(next, conn, options = {}) {
|
|
377
|
+
const { connection, serializer, metadata: axonMetadata, } = contextView(conn, options.context ?? conn.connection.config.context);
|
|
378
|
+
const shutdownLatch = conn.shutdown;
|
|
379
|
+
const resilience = options.resilience ?? conn.resilience;
|
|
380
|
+
const metadata = axonMetadata();
|
|
381
|
+
const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS));
|
|
382
|
+
const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
|
|
383
|
+
const shortcutQueriesToLocalHandlers = options.shortcutQueriesToLocalHandlers ?? false;
|
|
384
|
+
const queryTimeoutMs = options.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS;
|
|
490
385
|
const { serializePayload, deserializePayload } = createPayloadHelpers(serializer);
|
|
491
|
-
|
|
386
|
+
/** Query names announced to Axon Server; the handlers live on `next`. */
|
|
387
|
+
const subscribedNames = new Set();
|
|
492
388
|
// Local subscription store — subscription queries opened by THIS instance.
|
|
493
389
|
// Inbound updates from the server are offered into these via the Subscription RPC loop.
|
|
494
390
|
const subscriptions = new Map();
|
|
@@ -515,8 +411,20 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
515
411
|
});
|
|
516
412
|
permits += PERMITS;
|
|
517
413
|
}
|
|
414
|
+
function sendSubscribe(queryName) {
|
|
415
|
+
outbound.send({
|
|
416
|
+
subscribe: {
|
|
417
|
+
messageId: generateIdentifier(),
|
|
418
|
+
query: queryName,
|
|
419
|
+
resultName: "",
|
|
420
|
+
componentName: connection.config.componentName,
|
|
421
|
+
clientId: connection.config.clientId,
|
|
422
|
+
},
|
|
423
|
+
instructionId: generateIdentifier(),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
518
426
|
/**
|
|
519
|
-
* Re-establish the bidirectional stream and re-
|
|
427
|
+
* Re-establish the bidirectional stream and re-announce all handlers.
|
|
520
428
|
* Called on stream error or when the connection reconnects.
|
|
521
429
|
*
|
|
522
430
|
* ORDER (preserves Axon-specific invariant): subscriptions are
|
|
@@ -528,18 +436,8 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
528
436
|
streamStarted = false;
|
|
529
437
|
permits = 0n;
|
|
530
438
|
ensureStreamStarted();
|
|
531
|
-
for (const queryName of
|
|
532
|
-
|
|
533
|
-
subscribe: {
|
|
534
|
-
messageId: generateIdentifier(),
|
|
535
|
-
query: queryName,
|
|
536
|
-
resultName: "",
|
|
537
|
-
componentName: connection.config.componentName,
|
|
538
|
-
clientId: connection.config.clientId,
|
|
539
|
-
},
|
|
540
|
-
instructionId: generateIdentifier(),
|
|
541
|
-
});
|
|
542
|
-
}
|
|
439
|
+
for (const queryName of subscribedNames)
|
|
440
|
+
sendSubscribe(queryName);
|
|
543
441
|
grantQueryPermits();
|
|
544
442
|
}
|
|
545
443
|
async function reestablishStreamWithRetry() {
|
|
@@ -554,7 +452,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
554
452
|
connection.onReconnect(() => {
|
|
555
453
|
if (!shutdownLatch.shuttingDown && streamStarted) {
|
|
556
454
|
reestablishStreamWithRetry().catch((err) => {
|
|
557
|
-
console.error("
|
|
455
|
+
console.error("Axon Server query bus: reconnect retries exhausted", err);
|
|
558
456
|
});
|
|
559
457
|
}
|
|
560
458
|
});
|
|
@@ -568,11 +466,10 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
568
466
|
const queryName = proto.query;
|
|
569
467
|
const payload = deserializePayload(proto.payload?.data, proto.payload?.type, proto.payload?.revision);
|
|
570
468
|
handlerSubscriptions.set(subId, { queryName, payload });
|
|
571
|
-
const handler = localSegment.get(queryName);
|
|
572
469
|
let resultPayload;
|
|
573
470
|
let errorCode = "";
|
|
574
471
|
let errorMsg = "";
|
|
575
|
-
if (
|
|
472
|
+
if (subscribedNames.has(queryName)) {
|
|
576
473
|
try {
|
|
577
474
|
const queryMessage = {
|
|
578
475
|
kind: "query",
|
|
@@ -582,9 +479,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
582
479
|
metadata: metadataFromProto(proto.metaData ?? {}),
|
|
583
480
|
timestamp: Number(proto.timestamp),
|
|
584
481
|
};
|
|
585
|
-
resultPayload = await
|
|
586
|
-
return handler(queryMessage);
|
|
587
|
-
});
|
|
482
|
+
resultPayload = await next.query(queryMessage);
|
|
588
483
|
}
|
|
589
484
|
catch (err) {
|
|
590
485
|
errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
|
|
@@ -593,11 +488,9 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
593
488
|
}
|
|
594
489
|
else {
|
|
595
490
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY;
|
|
596
|
-
errorMsg = `No
|
|
491
|
+
errorMsg = `No next handler for query "${queryName}"`;
|
|
597
492
|
}
|
|
598
|
-
const responseSerialized = resultPayload !== undefined
|
|
599
|
-
? serializePayload("result", resultPayload)
|
|
600
|
-
: undefined;
|
|
493
|
+
const responseSerialized = resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined;
|
|
601
494
|
outbound.send({
|
|
602
495
|
subscriptionQueryResponse: {
|
|
603
496
|
messageIdentifier: generateIdentifier(),
|
|
@@ -607,7 +500,12 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
607
500
|
requestIdentifier: proto.messageIdentifier,
|
|
608
501
|
errorCode,
|
|
609
502
|
errorMessage: errorCode
|
|
610
|
-
? {
|
|
503
|
+
? {
|
|
504
|
+
message: errorMsg,
|
|
505
|
+
location: connection.config.componentName,
|
|
506
|
+
details: [],
|
|
507
|
+
errorCode,
|
|
508
|
+
}
|
|
611
509
|
: undefined,
|
|
612
510
|
payload: responseSerialized,
|
|
613
511
|
metaData: {},
|
|
@@ -635,11 +533,10 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
635
533
|
permits--;
|
|
636
534
|
const proto = message.query;
|
|
637
535
|
const queryName = proto.query;
|
|
638
|
-
const handler = localSegment.get(queryName);
|
|
639
536
|
let resultPayload;
|
|
640
537
|
let errorCode = "";
|
|
641
538
|
let errorMsg = "";
|
|
642
|
-
if (
|
|
539
|
+
if (subscribedNames.has(queryName)) {
|
|
643
540
|
try {
|
|
644
541
|
const queryMessage = {
|
|
645
542
|
kind: "query",
|
|
@@ -649,9 +546,9 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
649
546
|
metadata: metadataFromProto(proto.metaData),
|
|
650
547
|
timestamp: Number(proto.timestamp),
|
|
651
548
|
};
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
549
|
+
// Through the LOCAL BUS: no unit of work is handed in, so `next`
|
|
550
|
+
// opens one under whatever policy the caller gave it.
|
|
551
|
+
resultPayload = await next.query(queryMessage);
|
|
655
552
|
}
|
|
656
553
|
catch (err) {
|
|
657
554
|
errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
|
|
@@ -660,7 +557,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
660
557
|
}
|
|
661
558
|
else {
|
|
662
559
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY;
|
|
663
|
-
errorMsg = `No
|
|
560
|
+
errorMsg = `No next handler for query "${queryName}"`;
|
|
664
561
|
}
|
|
665
562
|
outbound.send({
|
|
666
563
|
queryResponse: {
|
|
@@ -668,11 +565,14 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
668
565
|
requestIdentifier: proto.messageIdentifier,
|
|
669
566
|
errorCode,
|
|
670
567
|
errorMessage: errorCode
|
|
671
|
-
? {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
568
|
+
? {
|
|
569
|
+
message: errorMsg,
|
|
570
|
+
location: connection.config.componentName,
|
|
571
|
+
details: [],
|
|
572
|
+
errorCode,
|
|
573
|
+
}
|
|
675
574
|
: undefined,
|
|
575
|
+
payload: resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
|
|
676
576
|
metaData: {},
|
|
677
577
|
processingInstructions: [],
|
|
678
578
|
},
|
|
@@ -699,33 +599,38 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
699
599
|
return;
|
|
700
600
|
if (String(err).includes("Connection dropped"))
|
|
701
601
|
return;
|
|
702
|
-
console.error("
|
|
602
|
+
console.error("Axon Server query bus: inbound stream error, attempting re-establishment via withRetry", err);
|
|
703
603
|
await reestablishStreamWithRetry().catch((retryErr) => {
|
|
704
|
-
console.error("
|
|
604
|
+
console.error("Axon Server query bus: reconnect retries exhausted", retryErr);
|
|
705
605
|
});
|
|
706
606
|
}
|
|
707
607
|
}
|
|
708
608
|
const routing = {
|
|
709
|
-
async query(
|
|
609
|
+
async query(unstamped, uow) {
|
|
710
610
|
const activity = shutdownLatch.registerActivity();
|
|
711
611
|
try {
|
|
712
|
-
const queryName = qualifiedNameToString(
|
|
713
|
-
// Local shortcut — handle locally if handler is co-located
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
612
|
+
const queryName = qualifiedNameToString(unstamped.name);
|
|
613
|
+
// Local shortcut — handle locally if a handler is co-located. The
|
|
614
|
+
// caller's unit of work is passed straight through, so `next` makes the
|
|
615
|
+
// nest-or-open decision on the HANDLE exactly as it does for an
|
|
616
|
+
// in-process read: a live unit of work handed in by `ctx.query` is
|
|
617
|
+
// reused so the consulting read shares the caller's transaction.
|
|
618
|
+
if (shortcutQueriesToLocalHandlers && subscribedNames.has(queryName)) {
|
|
619
|
+
return next.query(unstamped, uow);
|
|
721
620
|
}
|
|
621
|
+
// A transport is not a task: it has no unit of work, so it has no clock.
|
|
622
|
+
// A message that reaches the wire with no instant yet gets one from system
|
|
623
|
+
// time here — the envelope crosses a process boundary and must be fully
|
|
624
|
+
// formed. A locally-shortcut message is handed to `next` untouched
|
|
625
|
+
// instead, so the task that handles it supplies the instant.
|
|
626
|
+
const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
|
|
722
627
|
const responseStream = connection.queries.query({
|
|
723
628
|
messageIdentifier: message.identifier,
|
|
724
629
|
query: queryName,
|
|
725
630
|
timestamp: BigInt(message.timestamp),
|
|
726
631
|
payload: serializePayload(queryName, message.payload),
|
|
727
632
|
metaData: metadataToProto(message.metadata),
|
|
728
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
633
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
729
634
|
clientId: connection.config.clientId,
|
|
730
635
|
componentName: connection.config.componentName,
|
|
731
636
|
}, { metadata });
|
|
@@ -742,22 +647,15 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
742
647
|
}
|
|
743
648
|
},
|
|
744
649
|
subscribe(queryName, handler) {
|
|
745
|
-
|
|
650
|
+
subscribedNames.add(queryName);
|
|
651
|
+
next.subscribe(queryName, handler);
|
|
746
652
|
ensureStreamStarted();
|
|
747
|
-
|
|
748
|
-
subscribe: {
|
|
749
|
-
messageId: generateIdentifier(),
|
|
750
|
-
query: queryName,
|
|
751
|
-
resultName: "",
|
|
752
|
-
componentName: connection.config.componentName,
|
|
753
|
-
clientId: connection.config.clientId,
|
|
754
|
-
},
|
|
755
|
-
instructionId: generateIdentifier(),
|
|
756
|
-
});
|
|
653
|
+
sendSubscribe(queryName);
|
|
757
654
|
// Permits AFTER subscription (Axon-specific ordering invariant)
|
|
758
655
|
grantQueryPermits();
|
|
759
656
|
},
|
|
760
|
-
subscriptionQuery(
|
|
657
|
+
subscriptionQuery(unstamped, bufferSize) {
|
|
658
|
+
const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
|
|
761
659
|
const queryId = message.identifier;
|
|
762
660
|
if (subscriptions.has(queryId)) {
|
|
763
661
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`);
|
|
@@ -777,7 +675,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
777
675
|
timestamp: BigInt(message.timestamp),
|
|
778
676
|
payload: serializePayload(queryName, message.payload),
|
|
779
677
|
metaData: metadataToProto(message.metadata),
|
|
780
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
678
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
781
679
|
clientId: connection.config.clientId,
|
|
782
680
|
componentName: connection.config.componentName,
|
|
783
681
|
},
|
|
@@ -793,7 +691,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
793
691
|
timestamp: BigInt(message.timestamp),
|
|
794
692
|
payload: serializePayload(queryName, message.payload),
|
|
795
693
|
metaData: metadataToProto(message.metadata),
|
|
796
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
694
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
797
695
|
clientId: connection.config.clientId,
|
|
798
696
|
componentName: connection.config.componentName,
|
|
799
697
|
},
|
|
@@ -831,7 +729,8 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
831
729
|
break;
|
|
832
730
|
}
|
|
833
731
|
else if (response.completeExceptionally) {
|
|
834
|
-
handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ??
|
|
732
|
+
handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ??
|
|
733
|
+
"Subscription query failed"));
|
|
835
734
|
break;
|
|
836
735
|
}
|
|
837
736
|
}
|
|
@@ -863,7 +762,8 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
863
762
|
},
|
|
864
763
|
};
|
|
865
764
|
},
|
|
866
|
-
subscribeToUpdates(
|
|
765
|
+
subscribeToUpdates(unstamped, bufferSize) {
|
|
766
|
+
const message = { ...unstamped, timestamp: unstamped.timestamp ?? Date.now() };
|
|
867
767
|
const queryId = message.identifier;
|
|
868
768
|
if (subscriptions.has(queryId)) {
|
|
869
769
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`);
|
|
@@ -957,8 +857,6 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
957
857
|
});
|
|
958
858
|
},
|
|
959
859
|
};
|
|
960
|
-
|
|
961
|
-
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor());
|
|
962
|
-
return bus;
|
|
860
|
+
return routing;
|
|
963
861
|
}
|
|
964
862
|
//# sourceMappingURL=axon-server.js.map
|