@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
|
@@ -22,7 +22,7 @@ import { connectToAxonServer } from "./connection.js"
|
|
|
22
22
|
* await manager.disconnectAll()
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
export
|
|
25
|
+
export type AxonServerConnectionManager = {
|
|
26
26
|
/**
|
|
27
27
|
* Get or create a connection for the given context.
|
|
28
28
|
* Connections are created lazily and cached.
|
package/src/connection.ts
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createChannel,
|
|
3
|
+
createClient,
|
|
4
|
+
type Channel,
|
|
5
|
+
type Client,
|
|
6
|
+
type ChannelCredentials,
|
|
7
|
+
} from "nice-grpc"
|
|
2
8
|
import { ChannelCredentials as GrpcChannelCredentials } from "@grpc/grpc-js"
|
|
3
|
-
import { Metadata } from "nice-grpc"
|
|
4
9
|
import { readFileSync } from "node:fs"
|
|
10
|
+
import type { Serializer } from "@kronos-ts/core"
|
|
11
|
+
import { withRetry, healthCheck, type ResilienceConfig } from "./resilience.js"
|
|
5
12
|
import { PlatformServiceDefinition } from "./generated/control.js"
|
|
6
13
|
import { CommandServiceDefinition } from "./generated/command.js"
|
|
7
14
|
import { QueryServiceDefinition } from "./generated/query.js"
|
|
8
15
|
import { DcbEventStoreDefinition, DcbSnapshotStoreDefinition } from "./generated/dcb.js"
|
|
16
|
+
import { shutdownLatch, type ShutdownLatch } from "./shutdown-latch.js"
|
|
17
|
+
import {
|
|
18
|
+
platformConnection,
|
|
19
|
+
type PlatformConnection,
|
|
20
|
+
type PlatformServiceOptions,
|
|
21
|
+
} from "./platform-service.js"
|
|
9
22
|
|
|
10
23
|
/**
|
|
11
24
|
* Configuration for connecting to Axon Server.
|
|
12
25
|
*/
|
|
13
|
-
export
|
|
26
|
+
export type AxonServerConnectionConfig = {
|
|
14
27
|
/**
|
|
15
28
|
* Host of the Axon Server. Defaults to "localhost".
|
|
16
29
|
* For single-server setups.
|
|
@@ -84,13 +97,18 @@ export interface AxonServerConnectionConfig {
|
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
|
|
87
|
-
export type ConnectionState =
|
|
100
|
+
export type ConnectionState =
|
|
101
|
+
| "disconnected"
|
|
102
|
+
| "connecting"
|
|
103
|
+
| "connected"
|
|
104
|
+
| "reconnecting"
|
|
105
|
+
| "closed"
|
|
88
106
|
|
|
89
107
|
/**
|
|
90
108
|
* An active connection to Axon Server, providing typed gRPC clients
|
|
91
109
|
* for all services. Supports reconnection on failure.
|
|
92
110
|
*/
|
|
93
|
-
export
|
|
111
|
+
export type AxonServerConnection = {
|
|
94
112
|
/** The underlying gRPC channel. */
|
|
95
113
|
readonly channel: Channel
|
|
96
114
|
/** Platform service — connection management, topology. */
|
|
@@ -159,7 +177,11 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
159
177
|
const rootCerts = sslConfig.certFile ? readFileSync(sslConfig.certFile) : null
|
|
160
178
|
const clientKey = sslConfig.clientKeyFile ? readFileSync(sslConfig.clientKeyFile) : null
|
|
161
179
|
const clientCert = sslConfig.clientCertFile ? readFileSync(sslConfig.clientCertFile) : null
|
|
162
|
-
credentials = GrpcChannelCredentials.createSsl(
|
|
180
|
+
credentials = GrpcChannelCredentials.createSsl(
|
|
181
|
+
rootCerts,
|
|
182
|
+
clientKey,
|
|
183
|
+
clientCert,
|
|
184
|
+
) as ChannelCredentials
|
|
163
185
|
}
|
|
164
186
|
|
|
165
187
|
// gRPC channel options — keepalive to maintain persistent connections
|
|
@@ -170,9 +192,10 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
170
192
|
}
|
|
171
193
|
|
|
172
194
|
// Build server address list for failover
|
|
173
|
-
const serverAddresses =
|
|
174
|
-
|
|
175
|
-
|
|
195
|
+
const serverAddresses =
|
|
196
|
+
config.servers && config.servers.length > 0
|
|
197
|
+
? config.servers
|
|
198
|
+
: [`${resolvedConfig.host}:${resolvedConfig.port}`]
|
|
176
199
|
|
|
177
200
|
let currentServerIndex = 0
|
|
178
201
|
|
|
@@ -202,15 +225,29 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
202
225
|
let clients = createClients()
|
|
203
226
|
|
|
204
227
|
const connection: AxonServerConnection = {
|
|
205
|
-
get channel() {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
get
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
get channel() {
|
|
229
|
+
return channel
|
|
230
|
+
},
|
|
231
|
+
get platform() {
|
|
232
|
+
return clients.platform
|
|
233
|
+
},
|
|
234
|
+
get commands() {
|
|
235
|
+
return clients.commands
|
|
236
|
+
},
|
|
237
|
+
get queries() {
|
|
238
|
+
return clients.queries
|
|
239
|
+
},
|
|
240
|
+
get eventStore() {
|
|
241
|
+
return clients.eventStore
|
|
242
|
+
},
|
|
243
|
+
get snapshotStore() {
|
|
244
|
+
return clients.snapshotStore
|
|
245
|
+
},
|
|
211
246
|
config: resolvedConfig,
|
|
212
247
|
|
|
213
|
-
get state() {
|
|
248
|
+
get state() {
|
|
249
|
+
return state
|
|
250
|
+
},
|
|
214
251
|
|
|
215
252
|
onReconnect(callback) {
|
|
216
253
|
reconnectCallbacks.push(callback)
|
|
@@ -246,15 +283,17 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
246
283
|
|
|
247
284
|
// Notify listeners that we're back
|
|
248
285
|
for (const cb of reconnectCallbacks) {
|
|
249
|
-
try {
|
|
286
|
+
try {
|
|
287
|
+
cb()
|
|
288
|
+
} catch {
|
|
289
|
+
/* ignore listener errors */
|
|
290
|
+
}
|
|
250
291
|
}
|
|
251
292
|
return
|
|
252
293
|
} catch (err) {
|
|
253
294
|
if (maxAttempts > 0 && attempt >= maxAttempts) {
|
|
254
295
|
state = "disconnected"
|
|
255
|
-
throw new Error(
|
|
256
|
-
`Failed to reconnect after ${attempt} attempts: ${err}`,
|
|
257
|
-
)
|
|
296
|
+
throw new Error(`Failed to reconnect after ${attempt} attempts: ${err}`)
|
|
258
297
|
}
|
|
259
298
|
|
|
260
299
|
// Exponential backoff: base interval * 2^attempt, capped at 30s
|
|
@@ -270,3 +309,218 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
270
309
|
|
|
271
310
|
return connection
|
|
272
311
|
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// The shared RESOURCE: one channel, the platform stream on it, start()/close()
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Everything the shared Axon Server resource needs: where to dial, and how to
|
|
319
|
+
* put a payload on the wire.
|
|
320
|
+
*
|
|
321
|
+
* THE SERIALIZER IS A PROPERTY OF THIS CLIENT'S WIRE, not of a context and not
|
|
322
|
+
* of a bus. Every event, snapshot, command payload and query result this
|
|
323
|
+
* process exchanges with Axon Server goes through the same codec, so it is
|
|
324
|
+
* named once, here — which is also what leaves
|
|
325
|
+
* `axonServerEventStore(conn, context)` and `axonServerCommandBus(local, conn)`
|
|
326
|
+
* their honest two-argument shapes.
|
|
327
|
+
*/
|
|
328
|
+
export type AxonServerConnectionOptions = AxonServerConnectionConfig & {
|
|
329
|
+
/** Payload codec for every message this client exchanges with Axon Server. */
|
|
330
|
+
serializer: Serializer
|
|
331
|
+
/** Retry / health-check policy for the initial connect and stream re-establishment. */
|
|
332
|
+
resilience?: Partial<ResilienceConfig>
|
|
333
|
+
/** Platform stream tuning — heartbeat and processor-status cadence. */
|
|
334
|
+
platformService?: PlatformServiceOptions
|
|
335
|
+
/**
|
|
336
|
+
* How long {@link AxonServerConnectionHandle.start} waits for Axon Server's
|
|
337
|
+
* routing tables to register the subscribe frames sent on the command/query
|
|
338
|
+
* streams. This is the entire data-path readiness barrier.
|
|
339
|
+
*
|
|
340
|
+
* It is a timed wait rather than an observed signal because nothing on the
|
|
341
|
+
* client can observe it: subscribes travel on the bus streams, and the
|
|
342
|
+
* platform stream — which is where an ack would arrive — is a different
|
|
343
|
+
* stream that Axon Server holds open silently after `register`. Default:
|
|
344
|
+
* 1000, matching the legacy enhancer.
|
|
345
|
+
*/
|
|
346
|
+
busSubscriptionAckDelayMs?: number
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* What a context-scoped STORE borrows from the connection: the gRPC clients and
|
|
351
|
+
* the codec. Narrower than the handle on purpose — a test can drive a store
|
|
352
|
+
* with a fake `connection` and nothing else.
|
|
353
|
+
*/
|
|
354
|
+
export type AxonServerStoreSource = {
|
|
355
|
+
readonly connection: AxonServerConnection
|
|
356
|
+
readonly serializer: Serializer
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** What a BUS borrows: a store's two, plus the drain latch and the retry policy. */
|
|
360
|
+
export type AxonServerBusSource = AxonServerStoreSource & {
|
|
361
|
+
/**
|
|
362
|
+
* The connection-wide drain latch. In-flight dispatches register on it and
|
|
363
|
+
* `close()` waits for them, so a bus never has the transport pulled out from
|
|
364
|
+
* under a call it is still awaiting.
|
|
365
|
+
*/
|
|
366
|
+
readonly shutdown: ShutdownLatch
|
|
367
|
+
readonly resilience?: Partial<ResilienceConfig>
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** What the CONTROL PLANE borrows: the platform stream, and only that. */
|
|
371
|
+
export type AxonServerPlatformSource = {
|
|
372
|
+
readonly platform: PlatformConnection
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* The RESOURCE an Axon Server deployment shares: one gRPC channel, the platform
|
|
377
|
+
* stream riding on it, and the lifecycle pair.
|
|
378
|
+
*
|
|
379
|
+
* The stores and buses are NOT on it — `axonServerEventStore(conn, context)`,
|
|
380
|
+
* `axonServerCommandBus(local, conn)` and friends are plain functions over
|
|
381
|
+
* this, and a caller who wants only commands builds only that one. Multiple
|
|
382
|
+
* contexts share this ONE channel: the per-call `AxonIQ-Context` header is what
|
|
383
|
+
* separates them.
|
|
384
|
+
*/
|
|
385
|
+
export type AxonServerConnectionHandle = AxonServerBusSource & AxonServerPlatformSource & {
|
|
386
|
+
/** Config after defaults — the resolved host, context, client id. */
|
|
387
|
+
readonly config: AxonServerConnection["config"]
|
|
388
|
+
/**
|
|
389
|
+
* DATA-PATH START. Two things, both data path:
|
|
390
|
+
*
|
|
391
|
+
* 1. arm heartbeat-driven reconnect detection on the platform stream, and
|
|
392
|
+
* 2. wait until Axon Server can route to the handlers subscribed on the bus
|
|
393
|
+
* streams.
|
|
394
|
+
*
|
|
395
|
+
* Call AFTER `kronos` — the subscribe frames must already be on the wire for
|
|
396
|
+
* the readiness wait to mean anything. Takes no arguments and arms no
|
|
397
|
+
* control-plane state.
|
|
398
|
+
*/
|
|
399
|
+
start(): Promise<void>
|
|
400
|
+
/** Drain in-flight bus work, stop the platform stream, close the channel. */
|
|
401
|
+
close(): Promise<void>
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Open the shared Axon Server connection.
|
|
406
|
+
*
|
|
407
|
+
* ```ts
|
|
408
|
+
* const axon = await axonServerConnection({
|
|
409
|
+
* componentName: "university-service",
|
|
410
|
+
* host, port,
|
|
411
|
+
* serializer: jsonSerializer(),
|
|
412
|
+
* })
|
|
413
|
+
* const eventStore = axonServerEventStore(axon, "default")
|
|
414
|
+
* const eventStore = axonServerSnapshottingEventStore(
|
|
415
|
+
* axonServerEventStore(axon, "default"), axon, "default")
|
|
416
|
+
* const commandBus = interceptingCommandBus(
|
|
417
|
+
* axonServerCommandBus(localCommandBus(unitOfWork), axon), correlation)
|
|
418
|
+
* const queryBus = interceptingQueryBus(
|
|
419
|
+
* axonServerQueryBus(localQueryBus(unitOfWork), axon), correlation)
|
|
420
|
+
*
|
|
421
|
+
* const app = kronos({ commandHandlers, queryHandlers })
|
|
422
|
+
* await axon.start() // readiness barrier: the server can route to us
|
|
423
|
+
* // opt in to remote administration
|
|
424
|
+
* const control = await axonServerControlPlane(axon, app.processors.values())
|
|
425
|
+
* // …
|
|
426
|
+
* await app.stop(); await control.close(); await axon.close()
|
|
427
|
+
* ```
|
|
428
|
+
*
|
|
429
|
+
* ASYNC ON PURPOSE. Connecting before anything is built is what removes the
|
|
430
|
+
* lazy proxies and subscribe-buffering the container version needed: by the
|
|
431
|
+
* time `kronos` subscribes a handler, the gRPC streams are already live.
|
|
432
|
+
*
|
|
433
|
+
* REMOTE ADMINISTRATION IS NOT IN HERE. Processor instructions and processor
|
|
434
|
+
* status reporting are the platform CONTROL PLANE — they are neither
|
|
435
|
+
* persistence nor transport, and lived on this object only because they share
|
|
436
|
+
* the gRPC connection. See `control-plane.ts`.
|
|
437
|
+
*
|
|
438
|
+
* Axon-specific protocol invariants are preserved byte-for-byte:
|
|
439
|
+
*
|
|
440
|
+
* - CLIENT_SUPPORTS_STREAMING capability advertised on every dispatched query;
|
|
441
|
+
* - AxonIQ-Context + AxonIQ-Access-Token gRPC metadata headers on every
|
|
442
|
+
* outbound stream/RPC (see `contextView`);
|
|
443
|
+
* - permits-AFTER-subscriptions stream ordering on the initial handshake AND
|
|
444
|
+
* on reconnect;
|
|
445
|
+
* - shutdown ordering: drain latch → platform.stop → connection.close.
|
|
446
|
+
*/
|
|
447
|
+
export async function axonServerConnection(
|
|
448
|
+
options: AxonServerConnectionOptions,
|
|
449
|
+
): Promise<AxonServerConnectionHandle> {
|
|
450
|
+
const {
|
|
451
|
+
serializer,
|
|
452
|
+
resilience,
|
|
453
|
+
platformService,
|
|
454
|
+
busSubscriptionAckDelayMs,
|
|
455
|
+
...connectionConfig
|
|
456
|
+
} = options
|
|
457
|
+
|
|
458
|
+
const connection = await withRetry(async () => connectToAxonServer(connectionConfig), {
|
|
459
|
+
event: "initial-connect",
|
|
460
|
+
...resilience,
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// Health-check ping with warn-then-continue (D-100). AxonServerConnection has
|
|
464
|
+
// no dedicated probe surface today; the gRPC channel itself is created
|
|
465
|
+
// eagerly in connectToAxonServer so the meaningful probe is a round-trip — we
|
|
466
|
+
// approximate via a soft no-op promise that satisfies the threshold contract.
|
|
467
|
+
// Real network failure is surfaced by the first bus call against the channel.
|
|
468
|
+
await healthCheck(async () => undefined, {
|
|
469
|
+
thresholdMs: resilience?.healthCheckThresholdMs,
|
|
470
|
+
log: resilience?.log,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
// ONE latch for the connection. Both buses ride the same channel, so "drain
|
|
474
|
+
// in-flight work before the transport goes away" is one question, not two.
|
|
475
|
+
const shutdown = shutdownLatch()
|
|
476
|
+
|
|
477
|
+
// Built here, started by the control plane (or by `start()` below for the data
|
|
478
|
+
// path's half). Constructing it eagerly is what lets the control plane be a
|
|
479
|
+
// separate object at all — and it keeps `platformService` tuning and `stop()`
|
|
480
|
+
// ownership in one place, so the documented shutdown order holds whether or
|
|
481
|
+
// not anyone opted in.
|
|
482
|
+
const platform = platformConnection(connection, platformService)
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
connection,
|
|
486
|
+
config: connection.config,
|
|
487
|
+
serializer,
|
|
488
|
+
resilience,
|
|
489
|
+
shutdown,
|
|
490
|
+
platform,
|
|
491
|
+
|
|
492
|
+
async start() {
|
|
493
|
+
// RECONNECT DETECTION IS DATA PATH. The heartbeat on the platform stream
|
|
494
|
+
// is what notices a dead channel and calls `connection.reconnect()`; both
|
|
495
|
+
// buses hook `connection.onReconnect(...)` to rebuild their own streams.
|
|
496
|
+
// Arming it used to be a side effect of `platform.start()`, which only
|
|
497
|
+
// `axonServerControlPlane(...)` calls — so a service that never opted into
|
|
498
|
+
// remote administration had NO reconnect detection at all and would sit on
|
|
499
|
+
// a dead channel forever. It is armed here, unconditionally.
|
|
500
|
+
//
|
|
501
|
+
// `armConnectionMonitoring()` opens the stream and starts heartbeats but
|
|
502
|
+
// arms NO processor status reporting; that stays the control plane's, and
|
|
503
|
+
// a later `platform.start()` adds it to this same live stream. Both calls
|
|
504
|
+
// are idempotent, so either order works.
|
|
505
|
+
await platform.armConnectionMonitoring()
|
|
506
|
+
|
|
507
|
+
// The only thing the data path has to wait for: Axon Server's
|
|
508
|
+
// command/query routing tables registering the subscribe frames sent on
|
|
509
|
+
// the BUS streams. It cannot be derived from the platform stream, because
|
|
510
|
+
// subscribes travel on a different stream entirely — and the platform
|
|
511
|
+
// stream's own `subscriptionsAcked()` latch says nothing about them (it
|
|
512
|
+
// latches unconditionally once `register` has been flushed). So this
|
|
513
|
+
// barrier is the settle wait, deliberately independent of whether the
|
|
514
|
+
// platform stream is up at all. The legacy enhancer carried the same wait.
|
|
515
|
+
await new Promise((r) => setTimeout(r, busSubscriptionAckDelayMs ?? 1000))
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
async close() {
|
|
519
|
+
await shutdown.initiateShutdown()
|
|
520
|
+
// Idempotent, and independent of `control.close()` — a connection that was
|
|
521
|
+
// never administered still stops a platform stream someone else started.
|
|
522
|
+
platform.stop()
|
|
523
|
+
connection.close()
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Metadata } from "nice-grpc"
|
|
2
|
+
import type { AxonServerStoreSource } from "./connection.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A context-scoped view of the ONE shared connection.
|
|
6
|
+
*
|
|
7
|
+
* Contexts are Axon Server's tenancy boundary, and they are a per-CALL header —
|
|
8
|
+
* not a per-channel property. `AxonIQ-Context` selects the context on every
|
|
9
|
+
* outbound stream and RPC, so `axonServerEventStore(conn, "tenant-a")` and
|
|
10
|
+
* `axonServerEventStore(conn, "tenant-b")` are two views over one gRPC channel,
|
|
11
|
+
* exactly as one connection served one context before. Nothing about the
|
|
12
|
+
* channel, the codec or the drain latch differs between them; only this header
|
|
13
|
+
* does, which is why the whole difference fits in one small record.
|
|
14
|
+
*/
|
|
15
|
+
export type AxonServerContextView = AxonServerStoreSource & {
|
|
16
|
+
/** The Axon Server context every call through this view addresses. */
|
|
17
|
+
readonly context: string
|
|
18
|
+
/**
|
|
19
|
+
* The gRPC metadata headers Axon Server requires. `AxonIQ-Context` is
|
|
20
|
+
* mandatory (it identifies the context); `AxonIQ-Access-Token` is optional
|
|
21
|
+
* auth. Both must be attached to every outbound stream/RPC — preserved
|
|
22
|
+
* verbatim from the legacy enhancer.
|
|
23
|
+
*
|
|
24
|
+
* A function rather than a value so a caller that opens one long-lived stream
|
|
25
|
+
* can hoist it, and one that makes many unary calls can hand each its own.
|
|
26
|
+
*/
|
|
27
|
+
metadata(): Metadata
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Scope a connection to one Axon Server context. */
|
|
31
|
+
export function contextView(source: AxonServerStoreSource, context: string): AxonServerContextView {
|
|
32
|
+
const { connection, serializer } = source
|
|
33
|
+
return {
|
|
34
|
+
connection,
|
|
35
|
+
serializer,
|
|
36
|
+
context,
|
|
37
|
+
metadata() {
|
|
38
|
+
const metadata = new Metadata()
|
|
39
|
+
metadata.set("AxonIQ-Context", context)
|
|
40
|
+
if (connection.config.token) {
|
|
41
|
+
metadata.set("AxonIQ-Access-Token", connection.config.token)
|
|
42
|
+
}
|
|
43
|
+
return metadata
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/control-plane.ts
CHANGED
|
@@ -11,19 +11,19 @@
|
|
|
11
11
|
* 2. outbound — the client periodically reports each processor's status so
|
|
12
12
|
* the Axon Dashboard can render it.
|
|
13
13
|
*
|
|
14
|
-
* It lived inside `
|
|
14
|
+
* It lived inside the backend's `start()` only because it shares the gRPC
|
|
15
15
|
* connection with the data path. It is now separate and OPT-IN: a service that
|
|
16
|
-
* nobody administers remotely simply never builds one, and
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* nobody administers remotely simply never builds one, and
|
|
17
|
+
* `axonServerConnection()` is left with an argument-less `start()` readiness
|
|
18
|
+
* barrier and nothing else.
|
|
19
19
|
*
|
|
20
20
|
* ```ts
|
|
21
|
-
* const axon = await
|
|
22
|
-
* const app = kronos({
|
|
21
|
+
* const axon = await axonServerConnection({ ... })
|
|
22
|
+
* const app = kronos({ commandHandlers, queryHandlers, eventHandlers })
|
|
23
23
|
* await axon.start() // data path only
|
|
24
24
|
*
|
|
25
25
|
* // opt in to remote administration
|
|
26
|
-
* const control = await axonServerControlPlane(axon
|
|
26
|
+
* const control = await axonServerControlPlane(axon, app.processors.values())
|
|
27
27
|
* // …
|
|
28
28
|
* await app.stop(); await control.close(); await axon.close()
|
|
29
29
|
* ```
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* start — all three inside one function, so there is no call order for a
|
|
39
39
|
* caller to get wrong.
|
|
40
40
|
*/
|
|
41
|
-
import type {
|
|
41
|
+
import type { AxonServerPlatformSource } from "./connection.js"
|
|
42
42
|
import type { ProcessorStatus, SegmentStatus } from "./event-processor-info.js"
|
|
43
43
|
|
|
44
44
|
/**
|
|
@@ -53,7 +53,7 @@ import type { ProcessorStatus, SegmentStatus } from "./event-processor-info.js"
|
|
|
53
53
|
* may not implement (a subscribing processor has no segments), and the
|
|
54
54
|
* instruction handler simply skips what is absent.
|
|
55
55
|
*/
|
|
56
|
-
export
|
|
56
|
+
export type ManagedEventProcessor = {
|
|
57
57
|
readonly name: string
|
|
58
58
|
readonly running?: boolean
|
|
59
59
|
readonly replaying?: boolean
|
|
@@ -79,7 +79,7 @@ export interface ManagedEventProcessor {
|
|
|
79
79
|
* A running control plane. The platform stream is live; instructions are being
|
|
80
80
|
* routed and status is being reported until `close()`.
|
|
81
81
|
*/
|
|
82
|
-
export
|
|
82
|
+
export type AxonServerControlPlane = {
|
|
83
83
|
/**
|
|
84
84
|
* The processors this control plane addresses, keyed by name — the snapshot
|
|
85
85
|
* taken at construction. Exposed for introspection and tests.
|
|
@@ -90,9 +90,7 @@ export interface AxonServerControlPlane {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/** Map the managed processors into the status shape the platform stream reports. */
|
|
93
|
-
function processorStatuses(
|
|
94
|
-
processors: Iterable<ManagedEventProcessor>,
|
|
95
|
-
): ProcessorStatus[] {
|
|
93
|
+
function processorStatuses(processors: Iterable<ManagedEventProcessor>): ProcessorStatus[] {
|
|
96
94
|
return Array.from(processors, (proc) => {
|
|
97
95
|
const isStreamingProcessor = proc.supportsReset?.() !== false
|
|
98
96
|
const perSegment = proc.processingStatus?.()
|
|
@@ -132,9 +130,10 @@ function processorStatuses(
|
|
|
132
130
|
/**
|
|
133
131
|
* Wire remote administration onto an Axon Server platform stream and start it.
|
|
134
132
|
*
|
|
135
|
-
* `
|
|
136
|
-
*
|
|
137
|
-
*
|
|
133
|
+
* `conn` is the shared connection from `axonServerConnection(...)`. It owns the
|
|
134
|
+
* gRPC channel and the `platformService` tuning, and it builds the platform
|
|
135
|
+
* stream — but it never starts the control-plane half, because starting that is
|
|
136
|
+
* exactly what this function is for.
|
|
138
137
|
*
|
|
139
138
|
* `processors` are the LIVE processor instances, which only exist after
|
|
140
139
|
* `kronos` has built them — that is why this cannot be folded back into the
|
|
@@ -143,7 +142,7 @@ function processorStatuses(
|
|
|
143
142
|
* Caveat on that call: `kronos` types `processors` as
|
|
144
143
|
* `ReadonlyMap<string, unknown>`, so `.values()` needs a cast today —
|
|
145
144
|
* `app.processors.values() as Iterable<ManagedEventProcessor>`. Narrowing that
|
|
146
|
-
* map in `@kronos-ts/
|
|
145
|
+
* map in `@kronos-ts/core` would remove the cast; this package cannot.
|
|
147
146
|
*
|
|
148
147
|
* The collection is SNAPSHOTTED into a name-keyed map here, once. That is
|
|
149
148
|
* deliberate: a one-shot iterator (`Map.values()` is one) would otherwise yield
|
|
@@ -152,9 +151,10 @@ function processorStatuses(
|
|
|
152
151
|
* only the membership of the set is fixed at construction.
|
|
153
152
|
*/
|
|
154
153
|
export async function axonServerControlPlane(
|
|
155
|
-
|
|
154
|
+
conn: AxonServerPlatformSource,
|
|
156
155
|
processors: Iterable<ManagedEventProcessor> = [],
|
|
157
156
|
): Promise<AxonServerControlPlane> {
|
|
157
|
+
const { platform } = conn
|
|
158
158
|
// Name-keyed view so server-initiated instructions route to the right one.
|
|
159
159
|
const byName = new Map<string, ManagedEventProcessor>()
|
|
160
160
|
for (const proc of processors) byName.set(proc.name, proc)
|
|
@@ -181,7 +181,7 @@ export async function axonServerControlPlane(
|
|
|
181
181
|
|
|
182
182
|
platform.registerProcessorStatusSupplier(() => processorStatuses(byName.values()))
|
|
183
183
|
|
|
184
|
-
// ORDERING: strictly AFTER both
|
|
184
|
+
// ORDERING: strictly AFTER both handlers — see the file-level JSDoc.
|
|
185
185
|
// An instruction arriving in the gap would be dropped; an early status
|
|
186
186
|
// request would find no supplier.
|
|
187
187
|
await platform.start()
|
|
@@ -4,7 +4,7 @@ import type { EventProcessorInfo, EventProcessorInfo_SegmentStatus } from "./gen
|
|
|
4
4
|
* Status of a single event processor, reported to Axon Server.
|
|
5
5
|
* Aligned with Java's EventProcessorInfo proto message.
|
|
6
6
|
*/
|
|
7
|
-
export
|
|
7
|
+
export type ProcessorStatus = {
|
|
8
8
|
readonly name: string
|
|
9
9
|
readonly running: boolean
|
|
10
10
|
readonly mode: "Tracking" | "Subscribing"
|
|
@@ -17,7 +17,7 @@ export interface ProcessorStatus {
|
|
|
17
17
|
readonly segments: SegmentStatus[]
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export type SegmentStatus = {
|
|
21
21
|
readonly segmentId: number
|
|
22
22
|
readonly caughtUp: boolean
|
|
23
23
|
readonly replaying: boolean
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Aligned with Java's `FlowControlledResponseSender`.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export type FlowControlledSender<T> = {
|
|
11
11
|
/** Send an update. Buffers if no permits available. */
|
|
12
12
|
offer(value: T): boolean
|
|
13
13
|
/** Grant additional permits to the sender. */
|
package/src/generated/command.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
export const protobufPackage = "io.axoniq.axonserver.grpc.command";
|
|
20
20
|
|
|
21
21
|
/** An instruction from the components that provides the Command Handler towards AxonServer. */
|
|
22
|
-
export
|
|
22
|
+
export type CommandProviderOutbound = {
|
|
23
23
|
/** Instruction to subscribe this component as handler of a specific type of command */
|
|
24
24
|
subscribe?:
|
|
25
25
|
| CommandSubscription
|
|
@@ -45,7 +45,7 @@ export interface CommandProviderOutbound {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** An instruction or confirmation from AxonServer towards the component that provides the Command Handler */
|
|
48
|
-
export
|
|
48
|
+
export type CommandProviderInbound = {
|
|
49
49
|
/** Acknowledgement of previously sent instruction via outbound stream */
|
|
50
50
|
ack?:
|
|
51
51
|
| InstructionAck
|
|
@@ -59,7 +59,7 @@ export interface CommandProviderInbound {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/** A message representing a Command that needs to be routed to a component capable of handling it */
|
|
62
|
-
export
|
|
62
|
+
export type Command = {
|
|
63
63
|
/** The unique identifier of the Command Message */
|
|
64
64
|
messageIdentifier: string;
|
|
65
65
|
/** The name of the command, used for routing it to a destination capable of handling it */
|
|
@@ -80,13 +80,13 @@ export interface Command {
|
|
|
80
80
|
componentName: string;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export
|
|
83
|
+
export type Command_MetaDataEntry = {
|
|
84
84
|
key: string;
|
|
85
85
|
value: MetaDataValue | undefined;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/** Message representing the result of Command Handler execution */
|
|
89
|
-
export
|
|
89
|
+
export type CommandResponse = {
|
|
90
90
|
/** The unique identifier of the response message */
|
|
91
91
|
messageIdentifier: string;
|
|
92
92
|
/** An error code describing the error, if any */
|
|
@@ -107,13 +107,13 @@ export interface CommandResponse {
|
|
|
107
107
|
requestIdentifier: string;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export
|
|
110
|
+
export type CommandResponse_MetaDataEntry = {
|
|
111
111
|
key: string;
|
|
112
112
|
value: MetaDataValue | undefined;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/** Message describing a component's capability of handling a command type */
|
|
116
|
-
export
|
|
116
|
+
export type CommandSubscription = {
|
|
117
117
|
/**
|
|
118
118
|
* A unique identifier for this subscription. This identifier is returned in Acknowledgements to allow
|
|
119
119
|
* pipelining of subscription messages
|
|
@@ -1183,7 +1183,7 @@ export const CommandServiceDefinition = {
|
|
|
1183
1183
|
},
|
|
1184
1184
|
} as const;
|
|
1185
1185
|
|
|
1186
|
-
export
|
|
1186
|
+
export type CommandServiceImplementation<CallContextExt = {}> = {
|
|
1187
1187
|
/** Opens a stream allowing clients to register command handlers and receive commands. */
|
|
1188
1188
|
openStream(
|
|
1189
1189
|
request: AsyncIterable<CommandProviderOutbound>,
|
|
@@ -1193,7 +1193,7 @@ export interface CommandServiceImplementation<CallContextExt = {}> {
|
|
|
1193
1193
|
dispatch(request: Command, context: CallContext & CallContextExt): Promise<DeepPartial<CommandResponse>>;
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
1196
|
-
export
|
|
1196
|
+
export type CommandServiceClient<CallOptionsExt = {}> = {
|
|
1197
1197
|
/** Opens a stream allowing clients to register command handlers and receive commands. */
|
|
1198
1198
|
openStream(
|
|
1199
1199
|
request: AsyncIterable<DeepPartial<CommandProviderOutbound>>,
|
|
@@ -1221,7 +1221,7 @@ function isSet(value: any): boolean {
|
|
|
1221
1221
|
|
|
1222
1222
|
export type ServerStreamingMethodResult<Response> = { [Symbol.asyncIterator](): AsyncIterator<Response, void> };
|
|
1223
1223
|
|
|
1224
|
-
export
|
|
1224
|
+
export type MessageFns<T> = {
|
|
1225
1225
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
|
1226
1226
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
|
1227
1227
|
fromJSON(object: any): T;
|