@kronos-ts/axon-server 0.2.11 → 0.3.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 +1 -1
- package/dist/axon-server-event-store.d.ts.map +1 -1
- package/dist/axon-server-event-store.js +3 -3
- package/dist/axon-server-event-store.js.map +1 -1
- package/dist/axon-server-snapshot-store.d.ts +1 -1
- package/dist/axon-server-snapshot-store.d.ts.map +1 -1
- package/dist/axon-server-snapshot-store.js +1 -1
- package/dist/axon-server-snapshot-store.js.map +1 -1
- package/dist/axon-server.d.ts +169 -43
- package/dist/axon-server.d.ts.map +1 -1
- package/dist/axon-server.js +191 -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-manager.js +1 -1
- package/dist/connection-manager.js.map +1 -1
- package/dist/control-plane.d.ts +108 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +96 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/flow-controlled-sender.d.ts +1 -1
- package/dist/flow-controlled-sender.d.ts.map +1 -1
- package/dist/flow-controlled-sender.js +1 -1
- package/dist/flow-controlled-sender.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/message-size.d.ts +1 -1
- package/dist/message-size.d.ts.map +1 -1
- package/dist/message-size.js +1 -1
- package/dist/message-size.js.map +1 -1
- package/dist/outbound-stream.d.ts +1 -1
- package/dist/outbound-stream.d.ts.map +1 -1
- package/dist/outbound-stream.js +1 -1
- package/dist/outbound-stream.js.map +1 -1
- package/dist/platform-service.d.ts +34 -3
- package/dist/platform-service.d.ts.map +1 -1
- package/dist/platform-service.js +113 -42
- package/dist/platform-service.js.map +1 -1
- package/dist/shutdown-latch.d.ts +1 -1
- package/dist/shutdown-latch.d.ts.map +1 -1
- package/dist/shutdown-latch.js +1 -1
- package/dist/shutdown-latch.js.map +1 -1
- package/package.json +5 -5
- package/src/axon-server-event-store.ts +3 -3
- package/src/axon-server-snapshot-store.ts +1 -1
- package/src/axon-server.ts +283 -367
- package/src/connection-manager.ts +2 -2
- package/src/control-plane.ts +195 -0
- package/src/flow-controlled-sender.ts +1 -1
- package/src/index.ts +19 -8
- package/src/message-size.ts +1 -1
- package/src/outbound-stream.ts +1 -1
- package/src/platform-service.ts +152 -48
- package/src/shutdown-latch.ts +1 -1
package/dist/axon-server.js
CHANGED
|
@@ -1,46 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Axon Server backend for kronos.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `axonServer(config)` is an async factory: it connects eagerly, hands back
|
|
5
|
+
* the four components it provides (eventStore, snapshotStore, commandBus,
|
|
6
|
+
* queryBus), and gives you a `start`/`close` pair. There is no lifecycle
|
|
7
|
+
* framework — the ordering that used to be encoded as `onStart("connect")` /
|
|
8
|
+
* `onStart("processors")` / `onStop("connect")` is now three lines you write
|
|
9
|
+
* in your composition root:
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* ```ts
|
|
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.
|
|
18
29
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const control = await axonServerControlPlane(axon.platform, app.processors.values())
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* `start()` therefore takes NO arguments and does exactly one thing: the
|
|
41
|
+
* data-path readiness barrier. See `control-plane.ts`.
|
|
42
|
+
*
|
|
43
|
+
* Axon-specific protocol invariants are preserved byte-for-byte:
|
|
23
44
|
*
|
|
24
45
|
* - CLIENT_SUPPORTS_STREAMING capability advertised on every dispatched
|
|
25
46
|
* query via `defaultQueryInstructions(...)`;
|
|
26
47
|
* - AxonIQ-Context + AxonIQ-Access-Token gRPC metadata headers built by
|
|
27
48
|
* `createAxonMetadata(...)` and attached to every outbound stream/RPC;
|
|
28
49
|
* - permits-AFTER-subscriptions stream ordering preserved on the initial
|
|
29
|
-
* handshake AND on reconnect (
|
|
30
|
-
*
|
|
31
|
-
*
|
|
50
|
+
* handshake AND on reconnect (see `ensureStreamStarted` /
|
|
51
|
+
* `reestablishStreamBody`);
|
|
52
|
+
* - shutdown ordering: busLatches → platform.stop → connection.close.
|
|
32
53
|
*/
|
|
33
54
|
import { qualifiedNameToString, qualifiedNameFromString, generateIdentifier, withRetry, healthCheck, } from "@kronos-ts/common";
|
|
34
|
-
import { applySubscriptionFilter,
|
|
55
|
+
import { applySubscriptionFilter, correlationDataDispatchInterceptor, interceptingCommandBus, interceptingQueryBus, updateHandler, runAfterCommitOrImmediately, } from "@kronos-ts/messaging";
|
|
35
56
|
import { Metadata } from "nice-grpc";
|
|
36
57
|
import { connectToAxonServer } from "./connection.js";
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
58
|
+
import { axonServerEventStore } from "./axon-server-event-store.js";
|
|
59
|
+
import { axonServerSnapshotStore } from "./axon-server-snapshot-store.js";
|
|
39
60
|
import { metadataToProto, metadataFromProto } from "./metadata-conversion.js";
|
|
40
|
-
import {
|
|
61
|
+
import { outboundStream } from "./outbound-stream.js";
|
|
41
62
|
import { mapErrorCode, AxonServerErrorCode } from "./errors.js";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
63
|
+
import { shutdownLatch } from "./shutdown-latch.js";
|
|
64
|
+
import { platformConnection, } from "./platform-service.js";
|
|
44
65
|
/** Default flow control settings — aligned with Java's 5000 permits. */
|
|
45
66
|
const DEFAULT_PERMITS = 5000n;
|
|
46
67
|
const DEFAULT_THRESHOLD = 2500n;
|
|
@@ -98,284 +119,83 @@ function createAxonMetadata(config) {
|
|
|
98
119
|
return metadata;
|
|
99
120
|
}
|
|
100
121
|
/**
|
|
101
|
-
*
|
|
102
|
-
* as `(app: App) => void` per D-95.
|
|
122
|
+
* Connect to Axon Server and build the components it backs.
|
|
103
123
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* ```
|
|
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`.
|
|
109
128
|
*/
|
|
110
|
-
export function axonServer(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
pendingSubs.length = 0;
|
|
175
|
-
});
|
|
176
|
-
const wrapper = {
|
|
177
|
-
async dispatch(message) {
|
|
178
|
-
await connected;
|
|
179
|
-
return inner.dispatch(message);
|
|
180
|
-
},
|
|
181
|
-
subscribe(name, handler) {
|
|
182
|
-
if (inner)
|
|
183
|
-
inner.subscribe(name, handler);
|
|
184
|
-
else
|
|
185
|
-
pendingSubs.push([name, handler]);
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
return wrapper;
|
|
189
|
-
});
|
|
190
|
-
app.set("queryBus", (resolved) => {
|
|
191
|
-
const latch = createShutdownLatch();
|
|
192
|
-
busLatches.push(latch);
|
|
193
|
-
let inner;
|
|
194
|
-
const pendingSubs = [];
|
|
195
|
-
connected.then(() => {
|
|
196
|
-
inner = createDistributedQueryBus(getConnection(), resolved.unitOfWorkFactory, latch, resolved.serializer, serverConfig.queryFlowControl, serverConfig.shortcutQueriesToLocalHandlers, serverConfig.queryTimeoutMs, serverConfig.resilience);
|
|
197
|
-
for (const [name, h] of pendingSubs)
|
|
198
|
-
inner.subscribe(name, h);
|
|
199
|
-
pendingSubs.length = 0;
|
|
200
|
-
});
|
|
201
|
-
const wrapper = {
|
|
202
|
-
async query(message) {
|
|
203
|
-
await connected;
|
|
204
|
-
return inner.query(message);
|
|
205
|
-
},
|
|
206
|
-
subscribe(name, handler) {
|
|
207
|
-
if (inner)
|
|
208
|
-
inner.subscribe(name, handler);
|
|
209
|
-
else
|
|
210
|
-
pendingSubs.push([name, handler]);
|
|
211
|
-
},
|
|
212
|
-
subscriptionQuery(message, bufferSize) {
|
|
213
|
-
if (!inner) {
|
|
214
|
-
throw new Error("[kronos:axon-server] subscriptionQuery called before connect hook completed");
|
|
215
|
-
}
|
|
216
|
-
return inner.subscriptionQuery(message, bufferSize);
|
|
217
|
-
},
|
|
218
|
-
subscribeToUpdates(message, bufferSize) {
|
|
219
|
-
if (!inner) {
|
|
220
|
-
throw new Error("[kronos:axon-server] subscribeToUpdates called before connect hook completed");
|
|
221
|
-
}
|
|
222
|
-
return inner.subscribeToUpdates(message, bufferSize);
|
|
223
|
-
},
|
|
224
|
-
async emitUpdate(name, filter, update) {
|
|
225
|
-
await connected;
|
|
226
|
-
return inner.emitUpdate(name, filter, update);
|
|
227
|
-
},
|
|
228
|
-
async completeSubscription(name, filter) {
|
|
229
|
-
await connected;
|
|
230
|
-
return inner.completeSubscription(name, filter);
|
|
231
|
-
},
|
|
232
|
-
async completeSubscriptionExceptionally(name, error, filter) {
|
|
233
|
-
await connected;
|
|
234
|
-
return inner.completeSubscriptionExceptionally(name, error, filter);
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
return wrapper;
|
|
238
|
-
});
|
|
239
|
-
// ---- Lifecycle: connect (D-101 normative split) ---------------------
|
|
240
|
-
// connect = initial connect + health-check + platform setup +
|
|
241
|
-
// instruction wiring + platform.start.
|
|
242
|
-
app.onStart("connect", async () => {
|
|
243
|
-
connection = await withRetry(async () => connectToAxonServer(serverConfig), { event: "initial-connect", ...serverConfig.resilience });
|
|
244
|
-
// Health-check ping with warn-then-continue (D-100). AxonServerConnection
|
|
245
|
-
// has no dedicated probe surface today; the gRPC channel itself is
|
|
246
|
-
// created eagerly in connectToAxonServer so the meaningful probe is a
|
|
247
|
-
// round-trip — we approximate via a soft no-op promise that satisfies
|
|
248
|
-
// the threshold contract. Real network failure is surfaced by the
|
|
249
|
-
// first bus call against the live channel.
|
|
250
|
-
await healthCheck(async () => undefined, {
|
|
251
|
-
thresholdMs: serverConfig.resilience?.healthCheckThresholdMs,
|
|
252
|
-
log: serverConfig.resilience?.log,
|
|
253
|
-
});
|
|
254
|
-
platform = createPlatformConnection(connection, serverConfig.platformService);
|
|
255
|
-
// Build a name-keyed view of the EventProcessorModule list so server-
|
|
256
|
-
// initiated instructions can route to the right module. We resolve via
|
|
257
|
-
// `app.processors()` — Plan 09-01's zero-arg read accessor (D-103).
|
|
258
|
-
const processors = app.processors();
|
|
259
|
-
const processorMap = new Map();
|
|
260
|
-
for (const proc of processors)
|
|
261
|
-
processorMap.set(proc.name, proc);
|
|
262
|
-
platform.onInstruction(async (instruction) => {
|
|
263
|
-
switch (instruction.kind) {
|
|
264
|
-
case "pause-processor": {
|
|
265
|
-
const proc = processorMap.get(instruction.processorName);
|
|
266
|
-
if (proc?.stop)
|
|
267
|
-
proc.stop();
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
case "start-processor": {
|
|
271
|
-
const proc = processorMap.get(instruction.processorName);
|
|
272
|
-
if (proc?.start)
|
|
273
|
-
await proc.start();
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
case "release-segment": {
|
|
277
|
-
const proc = processorMap.get(instruction.processorName);
|
|
278
|
-
if (proc?.releaseSegment)
|
|
279
|
-
await proc.releaseSegment(instruction.segmentId);
|
|
280
|
-
break;
|
|
281
|
-
}
|
|
282
|
-
case "split-segment": {
|
|
283
|
-
const proc = processorMap.get(instruction.processorName);
|
|
284
|
-
if (proc?.splitSegment)
|
|
285
|
-
await proc.splitSegment(instruction.segmentId);
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
case "merge-segment": {
|
|
289
|
-
const proc = processorMap.get(instruction.processorName);
|
|
290
|
-
if (proc?.mergeSegment)
|
|
291
|
-
await proc.mergeSegment(instruction.segmentId);
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
platform.registerProcessorStatusSupplier(() => {
|
|
297
|
-
return processors.map((proc) => ({
|
|
298
|
-
name: proc.name,
|
|
299
|
-
running: proc.running ?? false,
|
|
300
|
-
mode: proc.supportsReset?.() === false ? "Subscribing" : "Tracking",
|
|
301
|
-
isStreamingProcessor: proc.supportsReset?.() !== false,
|
|
302
|
-
activeThreads: proc.running ? 1 : 0,
|
|
303
|
-
availableThreads: 0,
|
|
304
|
-
error: false,
|
|
305
|
-
tokenStoreIdentifier: "",
|
|
306
|
-
segments: proc.processingStatus
|
|
307
|
-
? Array.from(proc.processingStatus().entries()).map(([segId, status]) => ({
|
|
308
|
-
segmentId: segId,
|
|
309
|
-
caughtUp: status.caughtUp ?? false,
|
|
310
|
-
replaying: status.replaying ?? false,
|
|
311
|
-
onePartOf: 1,
|
|
312
|
-
tokenPosition: status.position ?? 0n,
|
|
313
|
-
errorState: status.error?.message ?? "",
|
|
314
|
-
}))
|
|
315
|
-
: [
|
|
316
|
-
{
|
|
317
|
-
segmentId: 0,
|
|
318
|
-
caughtUp: true,
|
|
319
|
-
replaying: proc.replaying ?? false,
|
|
320
|
-
onePartOf: 1,
|
|
321
|
-
tokenPosition: proc.position ?? 0n,
|
|
322
|
-
errorState: "",
|
|
323
|
-
},
|
|
324
|
-
],
|
|
325
|
-
}));
|
|
326
|
-
});
|
|
327
|
-
await platform.start();
|
|
328
|
-
// Latch the connected promise so the deferred bus wrappers built in
|
|
329
|
-
// the slot factories above construct their inner instances and replay
|
|
330
|
-
// any subscriptions that were buffered while connect was running.
|
|
331
|
-
// This MUST happen synchronously before any subsequent stage hook so
|
|
332
|
-
// register/processors-stage code sees the fully-wired buses. The
|
|
333
|
-
// microtask queue drains the `.then(...)` callbacks attached in the
|
|
334
|
-
// slot factories before this hook resolves.
|
|
335
|
-
resolveConnected();
|
|
336
|
-
await Promise.resolve();
|
|
337
|
-
});
|
|
338
|
-
// ---- Lifecycle: processors (D-101 / D-102) --------------------------
|
|
339
|
-
// processors = subscription-ack wait. The two-step shape mirrors the
|
|
340
|
-
// kronosdb sibling (Plan 09-03 / D-102) but is adapted for Axon Server's
|
|
341
|
-
// protocol shape, which differs from kronosdb's in one observable way:
|
|
342
|
-
//
|
|
343
|
-
// - kronosdb's PlatformService proactively emits a frame in response
|
|
344
|
-
// to `register`, so its `subscriptionsAcked` latches on the first
|
|
345
|
-
// inbound platform-stream message.
|
|
346
|
-
//
|
|
347
|
-
// - Axon Server's PlatformService holds the stream open silently
|
|
348
|
-
// until either a topology change or a heartbeat round-trip occurs.
|
|
349
|
-
// The platform stream therefore latches `acked` synchronously once
|
|
350
|
-
// the `register` frame has been flushed (see platform-service.ts).
|
|
351
|
-
//
|
|
352
|
-
// The bus-side subscription frames (sent on the command/query streams,
|
|
353
|
-
// not the platform stream) need a small processing window on the
|
|
354
|
-
// server before commands dispatched here are routed back to our
|
|
355
|
-
// handler. Empirically Axon Server processes the subscribe within
|
|
356
|
-
// 1 second — same number the legacy enhancer used. Wrapped in the same
|
|
357
|
-
// `withRetry({event: "per-operation"})` shape as kronosdb so per-extension
|
|
358
|
-
// resilience overrides still apply uniformly.
|
|
359
|
-
app.onStart("processors", async () => {
|
|
360
|
-
await withRetry(async () => {
|
|
361
|
-
const ok = await platform.subscriptionsAcked();
|
|
362
|
-
if (!ok)
|
|
363
|
-
throw new Error("axon-server subscriptions not yet acked");
|
|
364
|
-
}, { event: "per-operation", ...serverConfig.resilience });
|
|
365
|
-
// Axon-specific: give the server's command/query routing tables a
|
|
366
|
-
// beat to register the subscribe frames we just sent on the bus
|
|
367
|
-
// streams. The legacy enhancer carried this same 1s wait at line 264;
|
|
368
|
-
// it cannot be derived from the platform stream because subscribes
|
|
369
|
-
// travel on a different stream entirely.
|
|
370
|
-
await new Promise((r) => setTimeout(r, serverConfig.busSubscriptionAckDelayMs ?? 1000));
|
|
371
|
-
});
|
|
372
|
-
// ---- Lifecycle: stop (D-101.b — preserves legacy ordering) ----------
|
|
373
|
-
// busLatches drained first → platform.stop → connection.close.
|
|
374
|
-
app.onStop("connect", async () => {
|
|
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() {
|
|
375
193
|
await Promise.all(busLatches.map((l) => l.initiateShutdown()));
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
},
|
|
379
199
|
};
|
|
380
200
|
}
|
|
381
201
|
// ---------------------------------------------------------------------------
|
|
@@ -419,7 +239,36 @@ function createPayloadHelpers(serializer) {
|
|
|
419
239
|
* routes an inbound command to this node, it's executed on the local segment
|
|
420
240
|
* within a UnitOfWork.
|
|
421
241
|
*/
|
|
422
|
-
|
|
242
|
+
/**
|
|
243
|
+
* A command bus backed by Axon Server.
|
|
244
|
+
*
|
|
245
|
+
* ## Correlation lineage and the interceptor layer
|
|
246
|
+
*
|
|
247
|
+
* The returned bus is wrapped in {@link interceptingCommandBus} carrying
|
|
248
|
+
* {@link correlationDataDispatchInterceptor}, so lineage is stamped onto the
|
|
249
|
+
* outgoing message BEFORE it is serialized onto the wire.
|
|
250
|
+
*
|
|
251
|
+
* This is precisely how the Java client does it. AF4's `AxonServerCommandBus`
|
|
252
|
+
* holds its own `DispatchInterceptors` and dispatches as
|
|
253
|
+
* `doDispatch(dispatchInterceptors.intercept(commandMessage), cb)` — one call
|
|
254
|
+
* site, at the top, ahead of any routing; and its `doDispatch` (like this one)
|
|
255
|
+
* always goes to the server, letting Axon Server decide where the command lands.
|
|
256
|
+
* AF5 keeps the property via decorator order:
|
|
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.
|
|
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.
|
|
270
|
+
*/
|
|
271
|
+
export function distributedCommandBus(connection, unitOfWorkRunner, shutdownLatch, serializer, flowControl, commandLoadFactor, resilience) {
|
|
423
272
|
const metadata = createAxonMetadata(connection.config);
|
|
424
273
|
const { serializePayload, deserializePayload } = createPayloadHelpers(serializer);
|
|
425
274
|
const PERMITS = BigInt(flowControl?.permits ?? Number(DEFAULT_PERMITS));
|
|
@@ -427,7 +276,7 @@ function createDistributedCommandBus(connection, unitOfWorkRunner, shutdownLatch
|
|
|
427
276
|
// Local segment — handlers that execute on this node
|
|
428
277
|
const localSegment = new Map();
|
|
429
278
|
// Bidirectional stream for handler registration + inbound command handling
|
|
430
|
-
let outbound =
|
|
279
|
+
let outbound = outboundStream();
|
|
431
280
|
let streamStarted = false;
|
|
432
281
|
let permits = 0n;
|
|
433
282
|
function ensureStreamStarted() {
|
|
@@ -455,7 +304,7 @@ function createDistributedCommandBus(connection, unitOfWorkRunner, shutdownLatch
|
|
|
455
304
|
*/
|
|
456
305
|
function reestablishStreamBody() {
|
|
457
306
|
outbound.close();
|
|
458
|
-
outbound =
|
|
307
|
+
outbound = outboundStream();
|
|
459
308
|
streamStarted = false;
|
|
460
309
|
permits = 0n;
|
|
461
310
|
ensureStreamStarted();
|
|
@@ -563,7 +412,7 @@ function createDistributedCommandBus(connection, unitOfWorkRunner, shutdownLatch
|
|
|
563
412
|
});
|
|
564
413
|
}
|
|
565
414
|
}
|
|
566
|
-
|
|
415
|
+
const routing = {
|
|
567
416
|
async dispatch(message) {
|
|
568
417
|
const activity = shutdownLatch.registerActivity();
|
|
569
418
|
try {
|
|
@@ -605,6 +454,10 @@ function createDistributedCommandBus(connection, unitOfWorkRunner, shutdownLatch
|
|
|
605
454
|
grantPermits();
|
|
606
455
|
},
|
|
607
456
|
};
|
|
457
|
+
// Interception OUTSIDE routing — see the note on this function.
|
|
458
|
+
const bus = interceptingCommandBus(routing);
|
|
459
|
+
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor());
|
|
460
|
+
return bus;
|
|
608
461
|
}
|
|
609
462
|
// ---------------------------------------------------------------------------
|
|
610
463
|
// Distributed Query Bus
|
|
@@ -617,8 +470,20 @@ function createDistributedCommandBus(connection, unitOfWorkRunner, shutdownLatch
|
|
|
617
470
|
* - **Local segment**: Handlers registered here are stored locally and
|
|
618
471
|
* registered with Axon Server for inbound routing. Inbound queries
|
|
619
472
|
* are executed within a UnitOfWork.
|
|
473
|
+
*
|
|
474
|
+
* Wrapped in {@link interceptingQueryBus} with
|
|
475
|
+
* {@link correlationDataDispatchInterceptor}, matching AF4's
|
|
476
|
+
* `AxonServerQueryBus`, which calls `dispatchInterceptors.intercept(...)` at the
|
|
477
|
+
* top of `query`, `streamingQuery`, `scatterGather` and `subscriptionQuery`.
|
|
478
|
+
* Because the wrap is outside, the `shortcutQueriesToLocalHandlers` branch in
|
|
479
|
+
* `query()` gets identical lineage to the remote branch.
|
|
480
|
+
*
|
|
481
|
+
* KNOWN GAP: `subscriptionQuery` / `subscribeToUpdates` build their proto
|
|
482
|
+
* straight from `message.metadata`, and `interceptingQueryBus` (in
|
|
483
|
+
* `@kronos-ts/messaging`) forwards those two calls to the delegate without
|
|
484
|
+
* running the dispatch chain. Closing that needs a messaging-package change.
|
|
620
485
|
*/
|
|
621
|
-
export function
|
|
486
|
+
export function distributedQueryBus(connection, unitOfWorkRunner, shutdownLatch, serializer, flowControl, shortcutQueriesToLocalHandlers, queryTimeoutMs, resilience) {
|
|
622
487
|
const metadata = createAxonMetadata(connection.config);
|
|
623
488
|
const PERMITS = BigInt(flowControl?.permits ?? Number(DEFAULT_PERMITS));
|
|
624
489
|
const THRESHOLD = BigInt(flowControl?.refillThreshold ?? Number(DEFAULT_THRESHOLD));
|
|
@@ -633,7 +498,7 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
633
498
|
// to decide which subscriber IDs to target; the server forwards each response to the
|
|
634
499
|
// exact subscriber.
|
|
635
500
|
const handlerSubscriptions = new Map();
|
|
636
|
-
let outbound =
|
|
501
|
+
let outbound = outboundStream();
|
|
637
502
|
let streamStarted = false;
|
|
638
503
|
let permits = 0n;
|
|
639
504
|
function ensureStreamStarted() {
|
|
@@ -659,7 +524,7 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
659
524
|
*/
|
|
660
525
|
function reestablishStreamBody() {
|
|
661
526
|
outbound.close();
|
|
662
|
-
outbound =
|
|
527
|
+
outbound = outboundStream();
|
|
663
528
|
streamStarted = false;
|
|
664
529
|
permits = 0n;
|
|
665
530
|
ensureStreamStarted();
|
|
@@ -840,7 +705,7 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
840
705
|
});
|
|
841
706
|
}
|
|
842
707
|
}
|
|
843
|
-
|
|
708
|
+
const routing = {
|
|
844
709
|
async query(message) {
|
|
845
710
|
const activity = shutdownLatch.registerActivity();
|
|
846
711
|
try {
|
|
@@ -897,11 +762,11 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
897
762
|
if (subscriptions.has(queryId)) {
|
|
898
763
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`);
|
|
899
764
|
}
|
|
900
|
-
const
|
|
901
|
-
subscriptions.set(queryId,
|
|
765
|
+
const handler = updateHandler(message, bufferSize);
|
|
766
|
+
subscriptions.set(queryId, handler);
|
|
902
767
|
const queryName = qualifiedNameToString(message.name);
|
|
903
768
|
const subscriptionId = generateIdentifier();
|
|
904
|
-
const outboundSub =
|
|
769
|
+
const outboundSub = outboundStream();
|
|
905
770
|
outboundSub.send({
|
|
906
771
|
subscribe: {
|
|
907
772
|
subscriptionIdentifier: subscriptionId,
|
|
@@ -959,14 +824,14 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
959
824
|
}
|
|
960
825
|
else if (response.update) {
|
|
961
826
|
const update = deserializePayload(response.update.payload?.data);
|
|
962
|
-
|
|
827
|
+
handler.offer(update);
|
|
963
828
|
}
|
|
964
829
|
else if (response.complete) {
|
|
965
|
-
|
|
830
|
+
handler.complete();
|
|
966
831
|
break;
|
|
967
832
|
}
|
|
968
833
|
else if (response.completeExceptionally) {
|
|
969
|
-
|
|
834
|
+
handler.completeExceptionally(new Error(response.completeExceptionally.errorMessage?.message ?? "Subscription query failed"));
|
|
970
835
|
break;
|
|
971
836
|
}
|
|
972
837
|
}
|
|
@@ -977,7 +842,7 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
977
842
|
rejectInitial(error);
|
|
978
843
|
initialSettled = true;
|
|
979
844
|
}
|
|
980
|
-
|
|
845
|
+
handler.completeExceptionally(error);
|
|
981
846
|
}
|
|
982
847
|
finally {
|
|
983
848
|
subscriptions.delete(queryId);
|
|
@@ -985,7 +850,7 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
985
850
|
})();
|
|
986
851
|
return {
|
|
987
852
|
initialResult,
|
|
988
|
-
updates:
|
|
853
|
+
updates: handler.iterable,
|
|
989
854
|
close: () => {
|
|
990
855
|
outboundSub.send({
|
|
991
856
|
unsubscribe: {
|
|
@@ -994,7 +859,7 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
994
859
|
});
|
|
995
860
|
outboundSub.close();
|
|
996
861
|
subscriptions.delete(queryId);
|
|
997
|
-
|
|
862
|
+
handler.complete();
|
|
998
863
|
},
|
|
999
864
|
};
|
|
1000
865
|
},
|
|
@@ -1003,13 +868,13 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
1003
868
|
if (subscriptions.has(queryId)) {
|
|
1004
869
|
throw new Error(`Subscription query already registered for identifier "${queryId}"`);
|
|
1005
870
|
}
|
|
1006
|
-
const
|
|
1007
|
-
subscriptions.set(queryId,
|
|
871
|
+
const handler = updateHandler(message, bufferSize);
|
|
872
|
+
subscriptions.set(queryId, handler);
|
|
1008
873
|
return {
|
|
1009
|
-
[Symbol.asyncIterator]: () =>
|
|
874
|
+
[Symbol.asyncIterator]: () => handler.iterable[Symbol.asyncIterator](),
|
|
1010
875
|
close: () => {
|
|
1011
876
|
subscriptions.delete(queryId);
|
|
1012
|
-
|
|
877
|
+
handler.complete();
|
|
1013
878
|
},
|
|
1014
879
|
};
|
|
1015
880
|
},
|
|
@@ -1092,5 +957,8 @@ export function createDistributedQueryBus(connection, unitOfWorkRunner, shutdown
|
|
|
1092
957
|
});
|
|
1093
958
|
},
|
|
1094
959
|
};
|
|
960
|
+
const bus = interceptingQueryBus(routing);
|
|
961
|
+
bus.registerDispatchInterceptor(correlationDataDispatchInterceptor());
|
|
962
|
+
return bus;
|
|
1095
963
|
}
|
|
1096
964
|
//# sourceMappingURL=axon-server.js.map
|