@kronos-ts/axon-server 0.3.2 → 0.4.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 +9 -5
- package/dist/axon-server-event-store.d.ts.map +1 -1
- package/dist/axon-server-event-store.js +38 -30
- package/dist/axon-server-event-store.js.map +1 -1
- package/dist/axon-server-snapshot-store.d.ts +8 -8
- package/dist/axon-server-snapshot-store.d.ts.map +1 -1
- package/dist/axon-server-snapshot-store.js +8 -14
- package/dist/axon-server-snapshot-store.js.map +1 -1
- package/dist/axon-server.d.ts +97 -186
- package/dist/axon-server.d.ts.map +1 -1
- package/dist/axon-server.js +217 -318
- package/dist/axon-server.js.map +1 -1
- package/dist/connection.d.ts +128 -0
- package/dist/connection.d.ts.map +1 -1
- package/dist/connection.js +134 -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 +14 -13
- 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/flow-controlled-sender.js.map +1 -1
- package/dist/generated/command.d.ts.map +1 -1
- package/dist/generated/command.js.map +1 -1
- package/dist/generated/common.js.map +1 -1
- package/dist/generated/control.d.ts.map +1 -1
- package/dist/generated/control.js.map +1 -1
- package/dist/generated/dcb.d.ts.map +1 -1
- package/dist/generated/dcb.js.map +1 -1
- package/dist/generated/event.d.ts.map +1 -1
- package/dist/generated/event.js.map +1 -1
- package/dist/generated/google/protobuf/empty.js.map +1 -1
- 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.map +1 -1
- package/dist/metadata-conversion.d.ts +1 -1
- package/dist/metadata-conversion.d.ts.map +1 -1
- package/dist/shutdown-latch.d.ts.map +1 -1
- package/dist/shutdown-latch.js.map +1 -1
- package/package.json +4 -8
- package/src/axon-server-event-store.ts +51 -43
- package/src/axon-server-snapshot-store.ts +17 -25
- package/src/axon-server.ts +358 -512
- package/src/connection.ts +270 -18
- package/src/context-view.ts +46 -0
- package/src/control-plane.ts +17 -17
- package/src/index.ts +12 -18
- package/src/metadata-conversion.ts +1 -1
package/dist/axon-server.js
CHANGED
|
@@ -1,74 +1,47 @@
|
|
|
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-local 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 local bus:
|
|
35
10
|
*
|
|
36
11
|
* ```ts
|
|
37
|
-
* const
|
|
12
|
+
* const commandBus = interceptingCommandBus(
|
|
13
|
+
* axonServerCommandBus(axon, simpleCommandBus(unitOfWork)), lineage)
|
|
14
|
+
* const queryBus = interceptingQueryBus(
|
|
15
|
+
* axonServerQueryBus(axon, simpleQueryBus(unitOfWork)), lineage)
|
|
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, withRetry,
|
|
55
|
-
import { applySubscriptionFilter,
|
|
56
|
-
import {
|
|
57
|
-
import { connectToAxonServer } from "./connection.js";
|
|
58
|
-
import { axonServerEventStore } from "./axon-server-event-store.js";
|
|
59
|
-
import { axonServerSnapshotStore } from "./axon-server-snapshot-store.js";
|
|
28
|
+
import { qualifiedNameToString, qualifiedNameFromString, generateIdentifier, withRetry, } from "@kronos-ts/core";
|
|
29
|
+
import { applySubscriptionFilter, stamped, updateHandler, runAfterCommitOrImmediately, } from "@kronos-ts/core";
|
|
30
|
+
import { contextView } from "./context-view.js";
|
|
60
31
|
import { metadataToProto, metadataFromProto } from "./metadata-conversion.js";
|
|
61
32
|
import { outboundStream } from "./outbound-stream.js";
|
|
62
33
|
import { mapErrorCode, AxonServerErrorCode } from "./errors.js";
|
|
63
|
-
import { shutdownLatch } from "./shutdown-latch.js";
|
|
64
|
-
import { platformConnection, } from "./platform-service.js";
|
|
65
34
|
/** Default flow control settings — aligned with Java's 5000 permits. */
|
|
66
35
|
const DEFAULT_PERMITS = 5000n;
|
|
67
36
|
const DEFAULT_THRESHOLD = 2500n;
|
|
37
|
+
/** Default query dispatch timeout — aligned with Java's one hour. */
|
|
38
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 3_600_000;
|
|
39
|
+
/** Default command handler load factor — aligned with Java's 100. */
|
|
40
|
+
const DEFAULT_LOAD_FACTOR = 100;
|
|
68
41
|
// Processing instruction keys — aligned with proto ProcessingKey enum.
|
|
69
42
|
// CLIENT_SUPPORTS_STREAMING (key=8) is an Axon-Server-specific capability
|
|
70
|
-
// advertisement that MUST survive
|
|
71
|
-
//
|
|
43
|
+
// advertisement that MUST survive verbatim — see file-level JSDoc above and
|
|
44
|
+
// `defaultQueryInstructions` below.
|
|
72
45
|
const INSTRUCTION_KEY = {
|
|
73
46
|
ROUTING_KEY: 0,
|
|
74
47
|
PRIORITY: 1,
|
|
@@ -84,10 +57,16 @@ function toProtoProcessingInstructions(instructions) {
|
|
|
84
57
|
result.push({ key: INSTRUCTION_KEY.ROUTING_KEY, value: { textValue: instructions.routingKey } });
|
|
85
58
|
}
|
|
86
59
|
if (instructions.priority !== undefined) {
|
|
87
|
-
result.push({
|
|
60
|
+
result.push({
|
|
61
|
+
key: INSTRUCTION_KEY.PRIORITY,
|
|
62
|
+
value: { numberValue: BigInt(instructions.priority) },
|
|
63
|
+
});
|
|
88
64
|
}
|
|
89
65
|
if (instructions.timeoutMs !== undefined) {
|
|
90
|
-
result.push({
|
|
66
|
+
result.push({
|
|
67
|
+
key: INSTRUCTION_KEY.TIMEOUT,
|
|
68
|
+
value: { numberValue: BigInt(instructions.timeoutMs) },
|
|
69
|
+
});
|
|
91
70
|
}
|
|
92
71
|
return result;
|
|
93
72
|
}
|
|
@@ -104,102 +83,8 @@ function defaultQueryInstructions(timeoutMs) {
|
|
|
104
83
|
{ key: INSTRUCTION_KEY.CLIENT_SUPPORTS_STREAMING, value: { booleanValue: true } },
|
|
105
84
|
];
|
|
106
85
|
}
|
|
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
86
|
// ---------------------------------------------------------------------------
|
|
202
|
-
// Shared payload helpers
|
|
87
|
+
// Shared payload helpers
|
|
203
88
|
// ---------------------------------------------------------------------------
|
|
204
89
|
function createPayloadHelpers(serializer) {
|
|
205
90
|
return {
|
|
@@ -214,68 +99,66 @@ function createPayloadHelpers(serializer) {
|
|
|
214
99
|
};
|
|
215
100
|
}
|
|
216
101
|
// ---------------------------------------------------------------------------
|
|
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()).
|
|
102
|
+
// Axon Server Command Bus
|
|
231
103
|
// ---------------------------------------------------------------------------
|
|
232
104
|
/**
|
|
233
|
-
* A command bus backed by Axon Server.
|
|
105
|
+
* A command bus backed by Axon Server, over YOUR local bus.
|
|
234
106
|
*
|
|
235
|
-
* - **Outbound dispatch**:
|
|
236
|
-
* Axon Server routes the command to the appropriate node (which may be
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
*
|
|
107
|
+
* - **Outbound dispatch**: ALWAYS through Axon Server, via the unary Dispatch
|
|
108
|
+
* RPC. Axon Server routes the command to the appropriate node (which may be
|
|
109
|
+
* this one). There is deliberately no client-side prefer-local fork: the hub
|
|
110
|
+
* is the router, and short-circuiting it would silently defeat load factors,
|
|
111
|
+
* priorities and routing keys.
|
|
112
|
+
* - **Inbound**: a command the server routes here is dispatched into `local` —
|
|
113
|
+
* not into a privately-held handler map. That is what makes the unit-of-work
|
|
114
|
+
* policy you chose for `local` (say `postgresUnitOfWork(pg, unitOfWork)`)
|
|
115
|
+
* apply to server-routed work exactly as it applies to work this process
|
|
116
|
+
* originated. It is also why this function takes no `unitOfWork` argument:
|
|
117
|
+
* `local` carries that policy now.
|
|
118
|
+
* - **subscribe**: registers the handler on `local` AND announces the name to
|
|
119
|
+
* Axon Server, so other nodes can route to us.
|
|
244
120
|
*
|
|
245
121
|
* ## Correlation lineage and the interceptor layer
|
|
246
122
|
*
|
|
247
|
-
* The returned bus
|
|
248
|
-
*
|
|
249
|
-
*
|
|
123
|
+
* The returned bus stamps no lineage of its own. A host that wants it wraps the
|
|
124
|
+
* OUTERMOST bus:
|
|
125
|
+
*
|
|
126
|
+
* ```ts
|
|
127
|
+
* interceptingCommandBus(axonServerCommandBus(conn, local), lineage)
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* so whatever a host adds runs BEFORE the message is serialized onto the wire.
|
|
131
|
+
* Lineage itself is usually already on `message.metadata` by then — `ctx.send`
|
|
132
|
+
* stamps the unit of work's correlation data before any bus sees the message.
|
|
250
133
|
*
|
|
251
134
|
* This is precisely how the Java client does it. AF4's `AxonServerCommandBus`
|
|
252
135
|
* holds its own `DispatchInterceptors` and dispatches as
|
|
253
136
|
* `doDispatch(dispatchInterceptors.intercept(commandMessage), cb)` — one call
|
|
254
|
-
* site, at the top, ahead of any routing
|
|
255
|
-
*
|
|
256
|
-
* AF5 keeps the property via decorator order:
|
|
257
|
-
* `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
|
|
137
|
+
* site, at the top, ahead of any routing. AF5 keeps the property via decorator
|
|
138
|
+
* order: `DISTRIBUTED_COMMAND_BUS_ORDER = InterceptingCommandBus.DECORATION_ORDER - 50`
|
|
258
139
|
* stacks `InterceptingCommandBus → DistributedCommandBus → SimpleCommandBus`.
|
|
259
140
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* this backend replaces it wholesale.
|
|
264
|
-
*
|
|
265
|
-
* No double-application risk: the local segment here is a plain handler map, not
|
|
266
|
-
* a `CommandBus`, so this is the only interceptor in the chain. Inbound commands
|
|
267
|
-
* from the server are invoked through that map directly, which matches AF —
|
|
268
|
-
* `CommandProcessingTask` runs the local segment WITHOUT re-running dispatch
|
|
269
|
-
* interceptors.
|
|
141
|
+
* If `local` is itself an intercepting bus, a server-routed command sees
|
|
142
|
+
* `lineage` twice. That is harmless: both of its fields are `??` seeds, so the
|
|
143
|
+
* second application finds them set and changes nothing.
|
|
270
144
|
*/
|
|
271
|
-
export function
|
|
272
|
-
const metadata =
|
|
145
|
+
export function axonServerCommandBus(conn, local, options = {}) {
|
|
146
|
+
const { connection, serializer, metadata: axonMetadata, } = contextView(conn, options.context ?? conn.connection.config.context);
|
|
147
|
+
const shutdownLatch = conn.shutdown;
|
|
148
|
+
const resilience = options.resilience ?? conn.resilience;
|
|
149
|
+
const metadata = axonMetadata();
|
|
273
150
|
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
|
-
|
|
151
|
+
const PERMITS = BigInt(options.flowControl?.permits ?? Number(DEFAULT_PERMITS));
|
|
152
|
+
const THRESHOLD = BigInt(options.flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
|
|
153
|
+
const loadFactor = options.loadFactor ?? DEFAULT_LOAD_FACTOR;
|
|
154
|
+
/**
|
|
155
|
+
* The names this node announced to Axon Server. The handlers themselves live
|
|
156
|
+
* on `local`; this set exists so an inbound command for a name we never
|
|
157
|
+
* subscribed still answers NO_HANDLER_FOR_COMMAND rather than whatever
|
|
158
|
+
* `local.dispatch` happens to throw — and so a reconnect can re-announce.
|
|
159
|
+
*/
|
|
160
|
+
const subscribedNames = new Set();
|
|
161
|
+
// Bidirectional stream for handler subscription + inbound command handling
|
|
279
162
|
let outbound = outboundStream();
|
|
280
163
|
let streamStarted = false;
|
|
281
164
|
let permits = 0n;
|
|
@@ -294,8 +177,20 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
294
177
|
});
|
|
295
178
|
permits += PERMITS;
|
|
296
179
|
}
|
|
180
|
+
function sendSubscribe(commandName) {
|
|
181
|
+
outbound.send({
|
|
182
|
+
subscribe: {
|
|
183
|
+
messageId: generateIdentifier(),
|
|
184
|
+
command: commandName,
|
|
185
|
+
componentName: connection.config.componentName,
|
|
186
|
+
clientId: connection.config.clientId,
|
|
187
|
+
loadFactor,
|
|
188
|
+
},
|
|
189
|
+
instructionId: generateIdentifier(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
297
192
|
/**
|
|
298
|
-
* Re-establish the bidirectional stream and re-
|
|
193
|
+
* Re-establish the bidirectional stream and re-announce all handlers.
|
|
299
194
|
* Called on stream error or when the connection reconnects.
|
|
300
195
|
*
|
|
301
196
|
* ORDER (preserves Axon-specific invariant): subscriptions are
|
|
@@ -309,18 +204,8 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
309
204
|
permits = 0n;
|
|
310
205
|
ensureStreamStarted();
|
|
311
206
|
// 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
|
-
}
|
|
207
|
+
for (const commandName of subscribedNames)
|
|
208
|
+
sendSubscribe(commandName);
|
|
324
209
|
// Permits AFTER subscriptions (Axon-specific ordering invariant)
|
|
325
210
|
grantPermits();
|
|
326
211
|
}
|
|
@@ -336,7 +221,7 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
336
221
|
connection.onReconnect(() => {
|
|
337
222
|
if (!shutdownLatch.shuttingDown && streamStarted) {
|
|
338
223
|
reestablishStreamWithRetry().catch((err) => {
|
|
339
|
-
console.error("
|
|
224
|
+
console.error("Axon Server command bus: reconnect retries exhausted", err);
|
|
340
225
|
});
|
|
341
226
|
}
|
|
342
227
|
});
|
|
@@ -348,11 +233,10 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
348
233
|
permits--;
|
|
349
234
|
const proto = message.command;
|
|
350
235
|
const commandName = proto.name;
|
|
351
|
-
const handler = localSegment.get(commandName);
|
|
352
236
|
let resultPayload;
|
|
353
237
|
let errorCode = "";
|
|
354
238
|
let errorMsg = "";
|
|
355
|
-
if (
|
|
239
|
+
if (subscribedNames.has(commandName)) {
|
|
356
240
|
try {
|
|
357
241
|
const commandMessage = {
|
|
358
242
|
kind: "command",
|
|
@@ -362,8 +246,12 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
362
246
|
metadata: metadataFromProto(proto.metaData),
|
|
363
247
|
timestamp: Number(proto.timestamp),
|
|
364
248
|
};
|
|
365
|
-
//
|
|
366
|
-
|
|
249
|
+
// Through the LOCAL BUS, so the caller's unit-of-work policy runs.
|
|
250
|
+
// AF parity is preserved: `CommandProcessingTask` runs the local
|
|
251
|
+
// segment without re-running dispatch interceptors, and a `local`
|
|
252
|
+
// that happens to carry `lineage` re-applies a pair of `??` seeds
|
|
253
|
+
// that are already set.
|
|
254
|
+
resultPayload = await local.dispatch(commandMessage);
|
|
367
255
|
}
|
|
368
256
|
catch (err) {
|
|
369
257
|
errorCode = AxonServerErrorCode.COMMAND_EXECUTION_ERROR;
|
|
@@ -381,11 +269,14 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
381
269
|
requestIdentifier: proto.messageIdentifier,
|
|
382
270
|
errorCode,
|
|
383
271
|
errorMessage: errorCode
|
|
384
|
-
? {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
272
|
+
? {
|
|
273
|
+
message: errorMsg,
|
|
274
|
+
location: connection.config.componentName,
|
|
275
|
+
details: [],
|
|
276
|
+
errorCode,
|
|
277
|
+
}
|
|
388
278
|
: undefined,
|
|
279
|
+
payload: resultPayload !== undefined ? serializePayload("result", resultPayload) : undefined,
|
|
389
280
|
metaData: {},
|
|
390
281
|
processingInstructions: [],
|
|
391
282
|
},
|
|
@@ -406,14 +297,21 @@ export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatc
|
|
|
406
297
|
return;
|
|
407
298
|
if (String(err).includes("Connection dropped"))
|
|
408
299
|
return;
|
|
409
|
-
console.error("
|
|
300
|
+
console.error("Axon Server command bus: inbound stream error, attempting re-establishment via withRetry", err);
|
|
410
301
|
await reestablishStreamWithRetry().catch((retryErr) => {
|
|
411
|
-
console.error("
|
|
302
|
+
console.error("Axon Server command bus: reconnect retries exhausted", retryErr);
|
|
412
303
|
});
|
|
413
304
|
}
|
|
414
305
|
}
|
|
415
|
-
|
|
416
|
-
async dispatch(
|
|
306
|
+
return {
|
|
307
|
+
async dispatch(unstamped) {
|
|
308
|
+
// A transport is not a task: it has no unit of work, so it has no clock.
|
|
309
|
+
// A message that reaches the wire still {@link Unstamped} is therefore
|
|
310
|
+
// stamped from system time here — the envelope crosses a process boundary
|
|
311
|
+
// and must be fully formed. A locally-shortcut message is handed to
|
|
312
|
+
// `local` unstamped instead, so the task that handles it supplies the
|
|
313
|
+
// instant.
|
|
314
|
+
const message = stamped(unstamped, 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
|
+
local.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 local 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 `local`,
|
|
356
|
+
* so your unit-of-work policy applies to server-routed reads too. `subscribe`
|
|
357
|
+
* registers on `local` 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 `local` and the caller's
|
|
362
|
+
* unit of work is passed through, so the local branch nests exactly as the
|
|
363
|
+
* in-process bus does.
|
|
364
|
+
*
|
|
365
|
+
* Lineage, if wanted, is `interceptingQueryBus(bus, lineage)` 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 lineage 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(conn, local, 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 `local`. */
|
|
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 local.query(queryMessage);
|
|
588
483
|
}
|
|
589
484
|
catch (err) {
|
|
590
485
|
errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
|
|
@@ -595,9 +490,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
595
490
|
errorCode = AxonServerErrorCode.NO_HANDLER_FOR_QUERY;
|
|
596
491
|
errorMsg = `No local 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 `local`
|
|
550
|
+
// opens one under whatever policy the caller gave it.
|
|
551
|
+
resultPayload = await local.query(queryMessage);
|
|
655
552
|
}
|
|
656
553
|
catch (err) {
|
|
657
554
|
errorCode = AxonServerErrorCode.QUERY_EXECUTION_ERROR;
|
|
@@ -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,39 @@ 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 `local` 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 local.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 still {@link Unstamped} is therefore
|
|
623
|
+
// stamped from system time here — the envelope crosses a process boundary
|
|
624
|
+
// and must be fully formed. A locally-shortcut message is handed to
|
|
625
|
+
// `local` unstamped instead, so the task that handles it supplies the
|
|
626
|
+
// instant.
|
|
627
|
+
const message = stamped(unstamped, Date.now);
|
|
722
628
|
const responseStream = connection.queries.query({
|
|
723
629
|
messageIdentifier: message.identifier,
|
|
724
630
|
query: queryName,
|
|
725
631
|
timestamp: BigInt(message.timestamp),
|
|
726
632
|
payload: serializePayload(queryName, message.payload),
|
|
727
633
|
metaData: metadataToProto(message.metadata),
|
|
728
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
634
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
729
635
|
clientId: connection.config.clientId,
|
|
730
636
|
componentName: connection.config.componentName,
|
|
731
637
|
}, { metadata });
|
|
@@ -742,22 +648,15 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
742
648
|
}
|
|
743
649
|
},
|
|
744
650
|
subscribe(queryName, handler) {
|
|
745
|
-
|
|
651
|
+
subscribedNames.add(queryName);
|
|
652
|
+
local.subscribe(queryName, handler);
|
|
746
653
|
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
|
-
});
|
|
654
|
+
sendSubscribe(queryName);
|
|
757
655
|
// Permits AFTER subscription (Axon-specific ordering invariant)
|
|
758
656
|
grantQueryPermits();
|
|
759
657
|
},
|
|
760
|
-
subscriptionQuery(
|
|
658
|
+
subscriptionQuery(unstamped, bufferSize) {
|
|
659
|
+
const message = stamped(unstamped, Date.now);
|
|
761
660
|
const queryId = message.identifier;
|
|
762
661
|
if (subscriptions.has(queryId)) {
|
|
763
662
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`);
|
|
@@ -777,7 +676,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
777
676
|
timestamp: BigInt(message.timestamp),
|
|
778
677
|
payload: serializePayload(queryName, message.payload),
|
|
779
678
|
metaData: metadataToProto(message.metadata),
|
|
780
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
679
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
781
680
|
clientId: connection.config.clientId,
|
|
782
681
|
componentName: connection.config.componentName,
|
|
783
682
|
},
|
|
@@ -793,7 +692,7 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
793
692
|
timestamp: BigInt(message.timestamp),
|
|
794
693
|
payload: serializePayload(queryName, message.payload),
|
|
795
694
|
metaData: metadataToProto(message.metadata),
|
|
796
|
-
processingInstructions: defaultQueryInstructions(queryTimeoutMs
|
|
695
|
+
processingInstructions: defaultQueryInstructions(queryTimeoutMs),
|
|
797
696
|
clientId: connection.config.clientId,
|
|
798
697
|
componentName: connection.config.componentName,
|
|
799
698
|
},
|
|
@@ -831,7 +730,8 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
831
730
|
break;
|
|
832
731
|
}
|
|
833
732
|
else if (response.completeExceptionally) {
|
|
834
|
-
handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ??
|
|
733
|
+
handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ??
|
|
734
|
+
"Subscription query failed"));
|
|
835
735
|
break;
|
|
836
736
|
}
|
|
837
737
|
}
|
|
@@ -863,7 +763,8 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
863
763
|
},
|
|
864
764
|
};
|
|
865
765
|
},
|
|
866
|
-
subscribeToUpdates(
|
|
766
|
+
subscribeToUpdates(unstamped, bufferSize) {
|
|
767
|
+
const message = stamped(unstamped, Date.now);
|
|
867
768
|
const queryId = message.identifier;
|
|
868
769
|
if (subscriptions.has(queryId)) {
|
|
869
770
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`);
|
|
@@ -957,8 +858,6 @@ export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch,
|
|
|
957
858
|
});
|
|
958
859
|
},
|
|
959
860
|
};
|
|
960
|
-
|
|
961
|
-
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor());
|
|
962
|
-
return bus;
|
|
861
|
+
return routing;
|
|
963
862
|
}
|
|
964
863
|
//# sourceMappingURL=axon-server.js.map
|