@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/src/connection.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
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 { withRetry, healthCheck, type ResilienceConfig, type Serializer } from "@kronos-ts/core"
|
|
5
11
|
import { PlatformServiceDefinition } from "./generated/control.js"
|
|
6
12
|
import { CommandServiceDefinition } from "./generated/command.js"
|
|
7
13
|
import { QueryServiceDefinition } from "./generated/query.js"
|
|
8
14
|
import { DcbEventStoreDefinition, DcbSnapshotStoreDefinition } from "./generated/dcb.js"
|
|
15
|
+
import { shutdownLatch, type ShutdownLatch } from "./shutdown-latch.js"
|
|
16
|
+
import {
|
|
17
|
+
platformConnection,
|
|
18
|
+
type PlatformConnection,
|
|
19
|
+
type PlatformServiceOptions,
|
|
20
|
+
} from "./platform-service.js"
|
|
9
21
|
|
|
10
22
|
/**
|
|
11
23
|
* Configuration for connecting to Axon Server.
|
|
@@ -84,7 +96,12 @@ export interface AxonServerConnectionConfig {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
export type ConnectionState =
|
|
99
|
+
export type ConnectionState =
|
|
100
|
+
| "disconnected"
|
|
101
|
+
| "connecting"
|
|
102
|
+
| "connected"
|
|
103
|
+
| "reconnecting"
|
|
104
|
+
| "closed"
|
|
88
105
|
|
|
89
106
|
/**
|
|
90
107
|
* An active connection to Axon Server, providing typed gRPC clients
|
|
@@ -159,7 +176,11 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
159
176
|
const rootCerts = sslConfig.certFile ? readFileSync(sslConfig.certFile) : null
|
|
160
177
|
const clientKey = sslConfig.clientKeyFile ? readFileSync(sslConfig.clientKeyFile) : null
|
|
161
178
|
const clientCert = sslConfig.clientCertFile ? readFileSync(sslConfig.clientCertFile) : null
|
|
162
|
-
credentials = GrpcChannelCredentials.createSsl(
|
|
179
|
+
credentials = GrpcChannelCredentials.createSsl(
|
|
180
|
+
rootCerts,
|
|
181
|
+
clientKey,
|
|
182
|
+
clientCert,
|
|
183
|
+
) as ChannelCredentials
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
// gRPC channel options — keepalive to maintain persistent connections
|
|
@@ -170,9 +191,10 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
// Build server address list for failover
|
|
173
|
-
const serverAddresses =
|
|
174
|
-
|
|
175
|
-
|
|
194
|
+
const serverAddresses =
|
|
195
|
+
config.servers && config.servers.length > 0
|
|
196
|
+
? config.servers
|
|
197
|
+
: [`${resolvedConfig.host}:${resolvedConfig.port}`]
|
|
176
198
|
|
|
177
199
|
let currentServerIndex = 0
|
|
178
200
|
|
|
@@ -202,15 +224,29 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
202
224
|
let clients = createClients()
|
|
203
225
|
|
|
204
226
|
const connection: AxonServerConnection = {
|
|
205
|
-
get channel() {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
get
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
get channel() {
|
|
228
|
+
return channel
|
|
229
|
+
},
|
|
230
|
+
get platform() {
|
|
231
|
+
return clients.platform
|
|
232
|
+
},
|
|
233
|
+
get commands() {
|
|
234
|
+
return clients.commands
|
|
235
|
+
},
|
|
236
|
+
get queries() {
|
|
237
|
+
return clients.queries
|
|
238
|
+
},
|
|
239
|
+
get eventStore() {
|
|
240
|
+
return clients.eventStore
|
|
241
|
+
},
|
|
242
|
+
get snapshotStore() {
|
|
243
|
+
return clients.snapshotStore
|
|
244
|
+
},
|
|
211
245
|
config: resolvedConfig,
|
|
212
246
|
|
|
213
|
-
get state() {
|
|
247
|
+
get state() {
|
|
248
|
+
return state
|
|
249
|
+
},
|
|
214
250
|
|
|
215
251
|
onReconnect(callback) {
|
|
216
252
|
reconnectCallbacks.push(callback)
|
|
@@ -246,15 +282,17 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
246
282
|
|
|
247
283
|
// Notify listeners that we're back
|
|
248
284
|
for (const cb of reconnectCallbacks) {
|
|
249
|
-
try {
|
|
285
|
+
try {
|
|
286
|
+
cb()
|
|
287
|
+
} catch {
|
|
288
|
+
/* ignore listener errors */
|
|
289
|
+
}
|
|
250
290
|
}
|
|
251
291
|
return
|
|
252
292
|
} catch (err) {
|
|
253
293
|
if (maxAttempts > 0 && attempt >= maxAttempts) {
|
|
254
294
|
state = "disconnected"
|
|
255
|
-
throw new Error(
|
|
256
|
-
`Failed to reconnect after ${attempt} attempts: ${err}`,
|
|
257
|
-
)
|
|
295
|
+
throw new Error(`Failed to reconnect after ${attempt} attempts: ${err}`)
|
|
258
296
|
}
|
|
259
297
|
|
|
260
298
|
// Exponential backoff: base interval * 2^attempt, capped at 30s
|
|
@@ -270,3 +308,217 @@ export function connectToAxonServer(config: AxonServerConnectionConfig): AxonSer
|
|
|
270
308
|
|
|
271
309
|
return connection
|
|
272
310
|
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// The shared RESOURCE: one channel, the platform stream on it, start()/close()
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Everything the shared Axon Server resource needs: where to dial, and how to
|
|
318
|
+
* put a payload on the wire.
|
|
319
|
+
*
|
|
320
|
+
* THE SERIALIZER IS A PROPERTY OF THIS CLIENT'S WIRE, not of a context and not
|
|
321
|
+
* of a bus. Every event, snapshot, command payload and query result this
|
|
322
|
+
* process exchanges with Axon Server goes through the same codec, so it is
|
|
323
|
+
* named once, here — which is also what leaves
|
|
324
|
+
* `axonServerEventStore(conn, context)` and `axonServerCommandBus(conn, local)`
|
|
325
|
+
* their honest two-argument shapes.
|
|
326
|
+
*/
|
|
327
|
+
export interface AxonServerConnectionOptions extends AxonServerConnectionConfig {
|
|
328
|
+
/** Payload codec for every message this client exchanges with Axon Server. */
|
|
329
|
+
serializer: Serializer
|
|
330
|
+
/** Retry / health-check policy for the initial connect and stream re-establishment. */
|
|
331
|
+
resilience?: Partial<ResilienceConfig>
|
|
332
|
+
/** Platform stream tuning — heartbeat and processor-status cadence. */
|
|
333
|
+
platformService?: PlatformServiceOptions
|
|
334
|
+
/**
|
|
335
|
+
* How long {@link AxonServerConnectionHandle.start} waits for Axon Server's
|
|
336
|
+
* routing tables to register the subscribe frames sent on the command/query
|
|
337
|
+
* streams. This is the entire data-path readiness barrier.
|
|
338
|
+
*
|
|
339
|
+
* It is a timed wait rather than an observed signal because nothing on the
|
|
340
|
+
* client can observe it: subscribes travel on the bus streams, and the
|
|
341
|
+
* platform stream — which is where an ack would arrive — is a different
|
|
342
|
+
* stream that Axon Server holds open silently after `register`. Default:
|
|
343
|
+
* 1000, matching the legacy enhancer.
|
|
344
|
+
*/
|
|
345
|
+
busSubscriptionAckDelayMs?: number
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* What a context-scoped STORE borrows from the connection: the gRPC clients and
|
|
350
|
+
* the codec. Narrower than the handle on purpose — a test can drive a store
|
|
351
|
+
* with a fake `connection` and nothing else.
|
|
352
|
+
*/
|
|
353
|
+
export interface AxonServerStoreSource {
|
|
354
|
+
readonly connection: AxonServerConnection
|
|
355
|
+
readonly serializer: Serializer
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** What a BUS borrows: a store's two, plus the drain latch and the retry policy. */
|
|
359
|
+
export interface AxonServerBusSource extends AxonServerStoreSource {
|
|
360
|
+
/**
|
|
361
|
+
* The connection-wide drain latch. In-flight dispatches register on it and
|
|
362
|
+
* `close()` waits for them, so a bus never has the transport pulled out from
|
|
363
|
+
* under a call it is still awaiting.
|
|
364
|
+
*/
|
|
365
|
+
readonly shutdown: ShutdownLatch
|
|
366
|
+
readonly resilience?: Partial<ResilienceConfig>
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** What the CONTROL PLANE borrows: the platform stream, and only that. */
|
|
370
|
+
export interface AxonServerPlatformSource {
|
|
371
|
+
readonly platform: PlatformConnection
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The RESOURCE an Axon Server deployment shares: one gRPC channel, the platform
|
|
376
|
+
* stream riding on it, and the lifecycle pair.
|
|
377
|
+
*
|
|
378
|
+
* The stores and buses are NOT on it — `axonServerEventStore(conn, context)`,
|
|
379
|
+
* `axonServerCommandBus(conn, local)` and friends are plain functions over
|
|
380
|
+
* this, and a caller who wants only commands builds only that one. Multiple
|
|
381
|
+
* contexts share this ONE channel: the per-call `AxonIQ-Context` header is what
|
|
382
|
+
* separates them.
|
|
383
|
+
*/
|
|
384
|
+
export interface AxonServerConnectionHandle extends AxonServerBusSource, AxonServerPlatformSource {
|
|
385
|
+
/** Config after defaults — the resolved host, context, client id. */
|
|
386
|
+
readonly config: AxonServerConnection["config"]
|
|
387
|
+
/**
|
|
388
|
+
* DATA-PATH START. Two things, both data path:
|
|
389
|
+
*
|
|
390
|
+
* 1. arm heartbeat-driven reconnect detection on the platform stream, and
|
|
391
|
+
* 2. wait until Axon Server can route to the handlers subscribed on the bus
|
|
392
|
+
* streams.
|
|
393
|
+
*
|
|
394
|
+
* Call AFTER `kronos` — the subscribe frames must already be on the wire for
|
|
395
|
+
* the readiness wait to mean anything. Takes no arguments and arms no
|
|
396
|
+
* control-plane state.
|
|
397
|
+
*/
|
|
398
|
+
start(): Promise<void>
|
|
399
|
+
/** Drain in-flight bus work, stop the platform stream, close the channel. */
|
|
400
|
+
close(): Promise<void>
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Open the shared Axon Server connection.
|
|
405
|
+
*
|
|
406
|
+
* ```ts
|
|
407
|
+
* const axon = await axonServerConnection({
|
|
408
|
+
* componentName: "university-service",
|
|
409
|
+
* host, port,
|
|
410
|
+
* serializer: jsonSerializer(),
|
|
411
|
+
* })
|
|
412
|
+
* const eventStore = axonServerEventStore(axon, "default")
|
|
413
|
+
* const snapshotStore = axonServerSnapshotStore(axon, "default")
|
|
414
|
+
* const commandBus = interceptingCommandBus(
|
|
415
|
+
* axonServerCommandBus(axon, simpleCommandBus(unitOfWork)), lineage)
|
|
416
|
+
* const queryBus = interceptingQueryBus(
|
|
417
|
+
* axonServerQueryBus(axon, simpleQueryBus(unitOfWork)), lineage)
|
|
418
|
+
*
|
|
419
|
+
* const app = kronos({ states, commandHandlers, queryHandlers })
|
|
420
|
+
* await axon.start() // readiness barrier: the server can route to us
|
|
421
|
+
* // opt in to remote administration
|
|
422
|
+
* const control = await axonServerControlPlane(axon, app.processors.values())
|
|
423
|
+
* // …
|
|
424
|
+
* await app.stop(); await control.close(); await axon.close()
|
|
425
|
+
* ```
|
|
426
|
+
*
|
|
427
|
+
* ASYNC ON PURPOSE. Connecting before anything is built is what removes the
|
|
428
|
+
* lazy proxies and subscribe-buffering the container version needed: by the
|
|
429
|
+
* time `kronos` subscribes a handler, the gRPC streams are already live.
|
|
430
|
+
*
|
|
431
|
+
* REMOTE ADMINISTRATION IS NOT IN HERE. Processor instructions and processor
|
|
432
|
+
* status reporting are the platform CONTROL PLANE — they are neither
|
|
433
|
+
* persistence nor transport, and lived on this object only because they share
|
|
434
|
+
* the gRPC connection. See `control-plane.ts`.
|
|
435
|
+
*
|
|
436
|
+
* Axon-specific protocol invariants are preserved byte-for-byte:
|
|
437
|
+
*
|
|
438
|
+
* - CLIENT_SUPPORTS_STREAMING capability advertised on every dispatched query;
|
|
439
|
+
* - AxonIQ-Context + AxonIQ-Access-Token gRPC metadata headers on every
|
|
440
|
+
* outbound stream/RPC (see `contextView`);
|
|
441
|
+
* - permits-AFTER-subscriptions stream ordering on the initial handshake AND
|
|
442
|
+
* on reconnect;
|
|
443
|
+
* - shutdown ordering: drain latch → platform.stop → connection.close.
|
|
444
|
+
*/
|
|
445
|
+
export async function axonServerConnection(
|
|
446
|
+
options: AxonServerConnectionOptions,
|
|
447
|
+
): Promise<AxonServerConnectionHandle> {
|
|
448
|
+
const {
|
|
449
|
+
serializer,
|
|
450
|
+
resilience,
|
|
451
|
+
platformService,
|
|
452
|
+
busSubscriptionAckDelayMs,
|
|
453
|
+
...connectionConfig
|
|
454
|
+
} = options
|
|
455
|
+
|
|
456
|
+
const connection = await withRetry(async () => connectToAxonServer(connectionConfig), {
|
|
457
|
+
event: "initial-connect",
|
|
458
|
+
...resilience,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// Health-check ping with warn-then-continue (D-100). AxonServerConnection has
|
|
462
|
+
// no dedicated probe surface today; the gRPC channel itself is created
|
|
463
|
+
// eagerly in connectToAxonServer so the meaningful probe is a round-trip — we
|
|
464
|
+
// approximate via a soft no-op promise that satisfies the threshold contract.
|
|
465
|
+
// Real network failure is surfaced by the first bus call against the channel.
|
|
466
|
+
await healthCheck(async () => undefined, {
|
|
467
|
+
thresholdMs: resilience?.healthCheckThresholdMs,
|
|
468
|
+
log: resilience?.log,
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// ONE latch for the connection. Both buses ride the same channel, so "drain
|
|
472
|
+
// in-flight work before the transport goes away" is one question, not two.
|
|
473
|
+
const shutdown = shutdownLatch()
|
|
474
|
+
|
|
475
|
+
// Built here, started by the control plane (or by `start()` below for the data
|
|
476
|
+
// path's half). Constructing it eagerly is what lets the control plane be a
|
|
477
|
+
// separate object at all — and it keeps `platformService` tuning and `stop()`
|
|
478
|
+
// ownership in one place, so the documented shutdown order holds whether or
|
|
479
|
+
// not anyone opted in.
|
|
480
|
+
const platform = platformConnection(connection, platformService)
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
connection,
|
|
484
|
+
config: connection.config,
|
|
485
|
+
serializer,
|
|
486
|
+
resilience,
|
|
487
|
+
shutdown,
|
|
488
|
+
platform,
|
|
489
|
+
|
|
490
|
+
async start() {
|
|
491
|
+
// RECONNECT DETECTION IS DATA PATH. The heartbeat on the platform stream
|
|
492
|
+
// is what notices a dead channel and calls `connection.reconnect()`; both
|
|
493
|
+
// buses hook `connection.onReconnect(...)` to rebuild their own streams.
|
|
494
|
+
// Arming it used to be a side effect of `platform.start()`, which only
|
|
495
|
+
// `axonServerControlPlane(...)` calls — so a service that never opted into
|
|
496
|
+
// remote administration had NO reconnect detection at all and would sit on
|
|
497
|
+
// a dead channel forever. It is armed here, unconditionally.
|
|
498
|
+
//
|
|
499
|
+
// `armConnectionMonitoring()` opens the stream and starts heartbeats but
|
|
500
|
+
// arms NO processor status reporting; that stays the control plane's, and
|
|
501
|
+
// a later `platform.start()` adds it to this same live stream. Both calls
|
|
502
|
+
// are idempotent, so either order works.
|
|
503
|
+
await platform.armConnectionMonitoring()
|
|
504
|
+
|
|
505
|
+
// The only thing the data path has to wait for: Axon Server's
|
|
506
|
+
// command/query routing tables registering the subscribe frames sent on
|
|
507
|
+
// the BUS streams. It cannot be derived from the platform stream, because
|
|
508
|
+
// subscribes travel on a different stream entirely — and the platform
|
|
509
|
+
// stream's own `subscriptionsAcked()` latch says nothing about them (it
|
|
510
|
+
// latches unconditionally once `register` has been flushed). So this
|
|
511
|
+
// barrier is the settle wait, deliberately independent of whether the
|
|
512
|
+
// platform stream is up at all. The legacy enhancer carried the same wait.
|
|
513
|
+
await new Promise((r) => setTimeout(r, busSubscriptionAckDelayMs ?? 1000))
|
|
514
|
+
},
|
|
515
|
+
|
|
516
|
+
async close() {
|
|
517
|
+
await shutdown.initiateShutdown()
|
|
518
|
+
// Idempotent, and independent of `control.close()` — a connection that was
|
|
519
|
+
// never administered still stops a platform stream someone else started.
|
|
520
|
+
platform.stop()
|
|
521
|
+
connection.close()
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
}
|
|
@@ -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 interface AxonServerContextView extends 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({ states, 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
|
/**
|
|
@@ -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()
|
package/src/index.ts
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
export {
|
|
2
2
|
type AxonServerConnectionConfig,
|
|
3
3
|
type AxonServerConnection,
|
|
4
|
+
type AxonServerConnectionOptions,
|
|
5
|
+
type AxonServerConnectionHandle,
|
|
6
|
+
type AxonServerStoreSource,
|
|
7
|
+
type AxonServerBusSource,
|
|
8
|
+
type AxonServerPlatformSource,
|
|
4
9
|
connectToAxonServer,
|
|
10
|
+
axonServerConnection,
|
|
5
11
|
} from "./connection.js"
|
|
6
12
|
|
|
7
|
-
export {
|
|
8
|
-
type AxonServerConnectionManager,
|
|
9
|
-
connectionManager,
|
|
10
|
-
} from "./connection-manager.js"
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
axonServerEventStore,
|
|
14
|
-
} from "./axon-server-event-store.js"
|
|
13
|
+
export { axonServerEventStore } from "./axon-server-event-store.js"
|
|
15
14
|
|
|
16
|
-
export {
|
|
17
|
-
axonServerSnapshotStore,
|
|
18
|
-
} from "./axon-server-snapshot-store.js"
|
|
15
|
+
export { axonServerSnapshotStore } from "./axon-server-snapshot-store.js"
|
|
19
16
|
|
|
20
17
|
export {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type
|
|
25
|
-
type AxonServerBackend,
|
|
26
|
-
type AxonServerComponents,
|
|
18
|
+
axonServerCommandBus,
|
|
19
|
+
axonServerQueryBus,
|
|
20
|
+
type AxonServerCommandBusOptions,
|
|
21
|
+
type AxonServerQueryBusOptions,
|
|
27
22
|
type FlowControlConfig,
|
|
28
23
|
type ProcessingInstructions,
|
|
29
24
|
} from "./axon-server.js"
|
|
@@ -83,4 +78,3 @@ export {
|
|
|
83
78
|
mapErrorCode,
|
|
84
79
|
isTransientError,
|
|
85
80
|
} from "./errors.js"
|
|
86
|
-
export type { AxonServerOptions } from "./axon-server.js"
|