@kronos-ts/axon-server 0.1.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 +16 -0
- package/dist/axon-server-event-store.d.ts.map +1 -0
- package/dist/axon-server-event-store.js +282 -0
- package/dist/axon-server-event-store.js.map +1 -0
- package/dist/axon-server-snapshot-store.d.ts +12 -0
- package/dist/axon-server-snapshot-store.d.ts.map +1 -0
- package/dist/axon-server-snapshot-store.js +88 -0
- package/dist/axon-server-snapshot-store.js.map +1 -0
- package/dist/axon-server.d.ts +115 -0
- package/dist/axon-server.d.ts.map +1 -0
- package/dist/axon-server.js +986 -0
- package/dist/axon-server.js.map +1 -0
- package/dist/connection-manager.d.ts +49 -0
- package/dist/connection-manager.d.ts.map +1 -0
- package/dist/connection-manager.js +37 -0
- package/dist/connection-manager.js.map +1 -0
- package/dist/connection.d.ts +129 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +130 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +96 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +189 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-processor-info.d.ts +35 -0
- package/dist/event-processor-info.d.ts.map +1 -0
- package/dist/event-processor-info.js +28 -0
- package/dist/event-processor-info.js.map +1 -0
- package/dist/flow-controlled-sender.d.ts +30 -0
- package/dist/flow-controlled-sender.d.ts.map +1 -0
- package/dist/flow-controlled-sender.js +60 -0
- package/dist/flow-controlled-sender.js.map +1 -0
- package/dist/generated/command.d.ts +158 -0
- package/dist/generated/command.d.ts.map +1 -0
- package/dist/generated/command.js +970 -0
- package/dist/generated/command.js.map +1 -0
- package/dist/generated/common.d.ts +130 -0
- package/dist/generated/common.d.ts.map +1 -0
- package/dist/generated/common.js +908 -0
- package/dist/generated/common.js.map +1 -0
- package/dist/generated/control.d.ts +293 -0
- package/dist/generated/control.d.ts.map +1 -0
- package/dist/generated/control.js +1938 -0
- package/dist/generated/control.js.map +1 -0
- package/dist/generated/dcb.d.ts +650 -0
- package/dist/generated/dcb.d.ts.map +1 -0
- package/dist/generated/dcb.js +2943 -0
- package/dist/generated/dcb.js.map +1 -0
- package/dist/generated/event.d.ts +667 -0
- package/dist/generated/event.d.ts.map +1 -0
- package/dist/generated/event.js +3185 -0
- package/dist/generated/event.js.map +1 -0
- package/dist/generated/google/protobuf/empty.d.ts +30 -0
- package/dist/generated/google/protobuf/empty.d.ts.map +1 -0
- package/dist/generated/google/protobuf/empty.js +46 -0
- package/dist/generated/google/protobuf/empty.js.map +1 -0
- package/dist/generated/query.d.ts +300 -0
- package/dist/generated/query.d.ts.map +1 -0
- package/dist/generated/query.js +2183 -0
- package/dist/generated/query.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/message-size.d.ts +38 -0
- package/dist/message-size.d.ts.map +1 -0
- package/dist/message-size.js +57 -0
- package/dist/message-size.js.map +1 -0
- package/dist/metadata-conversion.d.ts +11 -0
- package/dist/metadata-conversion.d.ts.map +1 -0
- package/dist/metadata-conversion.js +51 -0
- package/dist/metadata-conversion.js.map +1 -0
- package/dist/outbound-stream.d.ts +15 -0
- package/dist/outbound-stream.d.ts.map +1 -0
- package/dist/outbound-stream.js +39 -0
- package/dist/outbound-stream.js.map +1 -0
- package/dist/platform-service.d.ts +119 -0
- package/dist/platform-service.d.ts.map +1 -0
- package/dist/platform-service.js +250 -0
- package/dist/platform-service.js.map +1 -0
- package/dist/shutdown-latch.d.ts +38 -0
- package/dist/shutdown-latch.d.ts.map +1 -0
- package/dist/shutdown-latch.js +51 -0
- package/dist/shutdown-latch.js.map +1 -0
- package/package.json +69 -0
- package/src/axon-server-event-store.ts +358 -0
- package/src/axon-server-snapshot-store.ts +118 -0
- package/src/axon-server.ts +1202 -0
- package/src/connection-manager.ts +88 -0
- package/src/connection.ts +272 -0
- package/src/errors.ts +223 -0
- package/src/event-processor-info.ts +62 -0
- package/src/flow-controlled-sender.ts +91 -0
- package/src/generated/command.ts +1231 -0
- package/src/generated/common.ts +1097 -0
- package/src/generated/control.ts +2419 -0
- package/src/generated/dcb.ts +3826 -0
- package/src/generated/event.ts +4076 -0
- package/src/generated/google/protobuf/empty.ts +84 -0
- package/src/generated/query.ts +2723 -0
- package/src/index.ts +75 -0
- package/src/message-size.ts +75 -0
- package/src/metadata-conversion.ts +46 -0
- package/src/outbound-stream.ts +52 -0
- package/src/platform-service.ts +361 -0
- package/src/shutdown-latch.ts +97 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AxonServerConnectionConfig, AxonServerConnection } from "./connection.js"
|
|
2
|
+
import { connectToAxonServer } from "./connection.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages Axon Server connections across multiple contexts.
|
|
6
|
+
*
|
|
7
|
+
* Creates and caches one connection per context. Used by the multi-tenancy
|
|
8
|
+
* extension to maintain separate channels for each tenant's context.
|
|
9
|
+
*
|
|
10
|
+
* Aligned with Java's `AxonServerConnectionManager`.
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const manager = createConnectionManager({
|
|
14
|
+
* componentName: "my-app",
|
|
15
|
+
* host: "axon-server",
|
|
16
|
+
* port: 8124,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* const defaultConn = manager.getConnection("default")
|
|
20
|
+
* const tenantConn = manager.getConnection("tenant-a")
|
|
21
|
+
*
|
|
22
|
+
* await manager.disconnectAll()
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export interface AxonServerConnectionManager {
|
|
26
|
+
/**
|
|
27
|
+
* Get or create a connection for the given context.
|
|
28
|
+
* Connections are created lazily and cached.
|
|
29
|
+
*/
|
|
30
|
+
getConnection(context: string): AxonServerConnection
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Disconnect a specific context's connection.
|
|
34
|
+
*/
|
|
35
|
+
disconnect(context: string): void
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Disconnect all cached connections.
|
|
39
|
+
*/
|
|
40
|
+
disconnectAll(): void
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List all active context names.
|
|
44
|
+
*/
|
|
45
|
+
activeContexts(): string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Creates a connection manager that lazily creates per-context connections.
|
|
50
|
+
*
|
|
51
|
+
* The base config (host, port, SSL, etc.) is shared across all contexts.
|
|
52
|
+
* Only the `context` field varies per connection.
|
|
53
|
+
*/
|
|
54
|
+
export function createConnectionManager(
|
|
55
|
+
baseConfig: Omit<AxonServerConnectionConfig, "context">,
|
|
56
|
+
): AxonServerConnectionManager {
|
|
57
|
+
const connections = new Map<string, AxonServerConnection>()
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
getConnection(context: string): AxonServerConnection {
|
|
61
|
+
let connection = connections.get(context)
|
|
62
|
+
if (!connection) {
|
|
63
|
+
connection = connectToAxonServer({ ...baseConfig, context })
|
|
64
|
+
connections.set(context, connection)
|
|
65
|
+
}
|
|
66
|
+
return connection
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
disconnect(context: string): void {
|
|
70
|
+
const connection = connections.get(context)
|
|
71
|
+
if (connection) {
|
|
72
|
+
connection.close()
|
|
73
|
+
connections.delete(context)
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
disconnectAll(): void {
|
|
78
|
+
for (const connection of connections.values()) {
|
|
79
|
+
connection.close()
|
|
80
|
+
}
|
|
81
|
+
connections.clear()
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
activeContexts(): string[] {
|
|
85
|
+
return [...connections.keys()]
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { createChannel, createClient, type Channel, type Client, type ChannelCredentials } from "nice-grpc"
|
|
2
|
+
import { ChannelCredentials as GrpcChannelCredentials } from "@grpc/grpc-js"
|
|
3
|
+
import { Metadata } from "nice-grpc"
|
|
4
|
+
import { readFileSync } from "node:fs"
|
|
5
|
+
import { PlatformServiceDefinition } from "./generated/control.js"
|
|
6
|
+
import { CommandServiceDefinition } from "./generated/command.js"
|
|
7
|
+
import { QueryServiceDefinition } from "./generated/query.js"
|
|
8
|
+
import { DcbEventStoreDefinition, DcbSnapshotStoreDefinition } from "./generated/dcb.js"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for connecting to Axon Server.
|
|
12
|
+
*/
|
|
13
|
+
export interface AxonServerConnectionConfig {
|
|
14
|
+
/**
|
|
15
|
+
* Host of the Axon Server. Defaults to "localhost".
|
|
16
|
+
* For single-server setups.
|
|
17
|
+
*/
|
|
18
|
+
host?: string
|
|
19
|
+
/** gRPC port of the Axon Server. Defaults to 8124. */
|
|
20
|
+
port?: number
|
|
21
|
+
/**
|
|
22
|
+
* Multiple server addresses for cluster deployments.
|
|
23
|
+
* Each entry is `"host:port"`. When provided, overrides `host` and `port`.
|
|
24
|
+
* The connector tries servers in order until one connects, and fails over
|
|
25
|
+
* to the next on connection failure.
|
|
26
|
+
*
|
|
27
|
+
* Aligned with Java's `axon.axonserver.servers` property.
|
|
28
|
+
*/
|
|
29
|
+
servers?: string[]
|
|
30
|
+
/** The context to connect to. Defaults to "default". */
|
|
31
|
+
context?: string
|
|
32
|
+
/** Name identifying this component to Axon Server. */
|
|
33
|
+
componentName: string
|
|
34
|
+
/** Unique client identifier. Defaults to a generated UUID. */
|
|
35
|
+
clientId?: string
|
|
36
|
+
/** Access token for authentication. Optional. */
|
|
37
|
+
token?: string
|
|
38
|
+
/** Reconnection interval in ms. Defaults to 2000. */
|
|
39
|
+
reconnectIntervalMs?: number
|
|
40
|
+
/** Maximum reconnection attempts before giving up. 0 = unlimited. Defaults to 0. */
|
|
41
|
+
maxReconnectAttempts?: number
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* gRPC keepalive ping interval in ms.
|
|
45
|
+
* Sends a ping after this period of inactivity to keep the connection alive.
|
|
46
|
+
* Default: 30000. Java's default is 1000ms but @grpc/grpc-js requires
|
|
47
|
+
* a minimum of 10000ms to avoid server-side RST_STREAM rejections.
|
|
48
|
+
*/
|
|
49
|
+
keepAliveTimeMs?: number
|
|
50
|
+
/**
|
|
51
|
+
* gRPC keepalive timeout in ms.
|
|
52
|
+
* Connection is considered dead if no response within this window.
|
|
53
|
+
* Default: 10000.
|
|
54
|
+
*/
|
|
55
|
+
keepAliveTimeoutMs?: number
|
|
56
|
+
/**
|
|
57
|
+
* Allow keepalive pings even when there are no active RPCs.
|
|
58
|
+
* Default: true.
|
|
59
|
+
*/
|
|
60
|
+
keepAlivePermitWithoutCalls?: boolean
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* TLS/SSL configuration. When enabled, the connection uses a secure gRPC channel.
|
|
64
|
+
* Aligned with Java's `axon.axonserver.ssl-enabled` and `axon.axonserver.cert-file`.
|
|
65
|
+
*/
|
|
66
|
+
ssl?: {
|
|
67
|
+
/** Enable TLS. When true, a secure channel is created. */
|
|
68
|
+
enabled: boolean
|
|
69
|
+
/**
|
|
70
|
+
* Path to the CA certificate file (PEM format) for server verification.
|
|
71
|
+
* If omitted, the system's default trust store is used.
|
|
72
|
+
*/
|
|
73
|
+
certFile?: string
|
|
74
|
+
/**
|
|
75
|
+
* Path to the client certificate file (PEM) for mutual TLS.
|
|
76
|
+
* Only needed if Axon Server requires client certificates.
|
|
77
|
+
*/
|
|
78
|
+
clientCertFile?: string
|
|
79
|
+
/**
|
|
80
|
+
* Path to the client private key file (PEM) for mutual TLS.
|
|
81
|
+
* Only needed if Axon Server requires client certificates.
|
|
82
|
+
*/
|
|
83
|
+
clientKeyFile?: string
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type ConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting" | "closed"
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* An active connection to Axon Server, providing typed gRPC clients
|
|
91
|
+
* for all services. Supports reconnection on failure.
|
|
92
|
+
*/
|
|
93
|
+
export interface AxonServerConnection {
|
|
94
|
+
/** The underlying gRPC channel. */
|
|
95
|
+
readonly channel: Channel
|
|
96
|
+
/** Platform service — connection management, topology. */
|
|
97
|
+
readonly platform: Client<typeof PlatformServiceDefinition>
|
|
98
|
+
/** Command service — dispatch and handle commands. */
|
|
99
|
+
readonly commands: Client<typeof CommandServiceDefinition>
|
|
100
|
+
/** Query service — dispatch and handle queries. */
|
|
101
|
+
readonly queries: Client<typeof QueryServiceDefinition>
|
|
102
|
+
/** Event store — event sourcing with Dynamic Consistency Boundaries. */
|
|
103
|
+
readonly eventStore: Client<typeof DcbEventStoreDefinition>
|
|
104
|
+
/** Snapshot store — state snapshots. */
|
|
105
|
+
readonly snapshotStore: Client<typeof DcbSnapshotStoreDefinition>
|
|
106
|
+
/** The resolved configuration. `servers` and `ssl` stay optional — they have no defaults. */
|
|
107
|
+
readonly config: Omit<Required<AxonServerConnectionConfig>, "servers" | "ssl"> & {
|
|
108
|
+
servers?: AxonServerConnectionConfig["servers"]
|
|
109
|
+
ssl?: AxonServerConnectionConfig["ssl"]
|
|
110
|
+
}
|
|
111
|
+
/** Current connection state. */
|
|
112
|
+
readonly state: ConnectionState
|
|
113
|
+
/**
|
|
114
|
+
* Register a callback invoked when the connection is (re)established.
|
|
115
|
+
* Used by buses to re-subscribe handlers after reconnection.
|
|
116
|
+
*/
|
|
117
|
+
onReconnect(callback: () => void): void
|
|
118
|
+
/**
|
|
119
|
+
* Register a callback invoked when the connection is lost.
|
|
120
|
+
*/
|
|
121
|
+
onDisconnect(callback: (error?: Error) => void): void
|
|
122
|
+
/** Close the connection permanently. No reconnection after this. */
|
|
123
|
+
close(): void
|
|
124
|
+
/**
|
|
125
|
+
* Attempt to reconnect if disconnected.
|
|
126
|
+
* Returns a promise that resolves when reconnected or rejects on failure.
|
|
127
|
+
*/
|
|
128
|
+
reconnect(): Promise<void>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Connects to Axon Server and returns typed gRPC clients for all services.
|
|
133
|
+
*
|
|
134
|
+
* Configures gRPC channel-level keepalive to maintain persistent connections,
|
|
135
|
+
* aligned with Java's ManagedChannel keepalive settings.
|
|
136
|
+
*/
|
|
137
|
+
export function connectToAxonServer(config: AxonServerConnectionConfig): AxonServerConnection {
|
|
138
|
+
const resolvedConfig = {
|
|
139
|
+
host: config.host ?? "localhost",
|
|
140
|
+
port: config.port ?? 8124,
|
|
141
|
+
context: config.context ?? "default",
|
|
142
|
+
componentName: config.componentName,
|
|
143
|
+
clientId: config.clientId ?? crypto.randomUUID(),
|
|
144
|
+
token: config.token ?? "",
|
|
145
|
+
reconnectIntervalMs: config.reconnectIntervalMs ?? 2000,
|
|
146
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? 0,
|
|
147
|
+
keepAliveTimeMs: config.keepAliveTimeMs ?? 30000,
|
|
148
|
+
keepAliveTimeoutMs: config.keepAliveTimeoutMs ?? 10000,
|
|
149
|
+
keepAlivePermitWithoutCalls: config.keepAlivePermitWithoutCalls ?? true,
|
|
150
|
+
servers: config.servers,
|
|
151
|
+
ssl: config.ssl,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build gRPC channel credentials
|
|
155
|
+
const sslConfig = config.ssl
|
|
156
|
+
let credentials: ChannelCredentials | undefined
|
|
157
|
+
|
|
158
|
+
if (sslConfig?.enabled) {
|
|
159
|
+
const rootCerts = sslConfig.certFile ? readFileSync(sslConfig.certFile) : null
|
|
160
|
+
const clientKey = sslConfig.clientKeyFile ? readFileSync(sslConfig.clientKeyFile) : null
|
|
161
|
+
const clientCert = sslConfig.clientCertFile ? readFileSync(sslConfig.clientCertFile) : null
|
|
162
|
+
credentials = GrpcChannelCredentials.createSsl(rootCerts, clientKey, clientCert) as ChannelCredentials
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// gRPC channel options — keepalive to maintain persistent connections
|
|
166
|
+
const channelOptions = {
|
|
167
|
+
"grpc.keepalive_time_ms": config.keepAliveTimeMs ?? 30000,
|
|
168
|
+
"grpc.keepalive_timeout_ms": config.keepAliveTimeoutMs ?? 10000,
|
|
169
|
+
"grpc.keepalive_permit_without_calls": (config.keepAlivePermitWithoutCalls ?? true) ? 1 : 0,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Build server address list for failover
|
|
173
|
+
const serverAddresses = config.servers && config.servers.length > 0
|
|
174
|
+
? config.servers
|
|
175
|
+
: [`${resolvedConfig.host}:${resolvedConfig.port}`]
|
|
176
|
+
|
|
177
|
+
let currentServerIndex = 0
|
|
178
|
+
|
|
179
|
+
function createGrpcChannel(): Channel {
|
|
180
|
+
const address = serverAddresses[currentServerIndex % serverAddresses.length]!
|
|
181
|
+
return credentials
|
|
182
|
+
? createChannel(address, credentials, channelOptions)
|
|
183
|
+
: createChannel(address, undefined, channelOptions)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let channel = createGrpcChannel()
|
|
187
|
+
let state: ConnectionState = "connected"
|
|
188
|
+
|
|
189
|
+
const reconnectCallbacks: Array<() => void> = []
|
|
190
|
+
const disconnectCallbacks: Array<(error?: Error) => void> = []
|
|
191
|
+
|
|
192
|
+
function createClients() {
|
|
193
|
+
return {
|
|
194
|
+
platform: createClient(PlatformServiceDefinition, channel),
|
|
195
|
+
commands: createClient(CommandServiceDefinition, channel),
|
|
196
|
+
queries: createClient(QueryServiceDefinition, channel),
|
|
197
|
+
eventStore: createClient(DcbEventStoreDefinition, channel),
|
|
198
|
+
snapshotStore: createClient(DcbSnapshotStoreDefinition, channel),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let clients = createClients()
|
|
203
|
+
|
|
204
|
+
const connection: AxonServerConnection = {
|
|
205
|
+
get channel() { return channel },
|
|
206
|
+
get platform() { return clients.platform },
|
|
207
|
+
get commands() { return clients.commands },
|
|
208
|
+
get queries() { return clients.queries },
|
|
209
|
+
get eventStore() { return clients.eventStore },
|
|
210
|
+
get snapshotStore() { return clients.snapshotStore },
|
|
211
|
+
config: resolvedConfig,
|
|
212
|
+
|
|
213
|
+
get state() { return state },
|
|
214
|
+
|
|
215
|
+
onReconnect(callback) {
|
|
216
|
+
reconnectCallbacks.push(callback)
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
onDisconnect(callback) {
|
|
220
|
+
disconnectCallbacks.push(callback)
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
close() {
|
|
224
|
+
state = "closed"
|
|
225
|
+
channel.close()
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async reconnect() {
|
|
229
|
+
if (state === "closed") {
|
|
230
|
+
throw new Error("Connection is permanently closed")
|
|
231
|
+
}
|
|
232
|
+
if (state === "connected" || state === "connecting") return
|
|
233
|
+
|
|
234
|
+
state = "reconnecting"
|
|
235
|
+
const maxAttempts = resolvedConfig.maxReconnectAttempts
|
|
236
|
+
let attempt = 0
|
|
237
|
+
|
|
238
|
+
while (state === "reconnecting") {
|
|
239
|
+
attempt++
|
|
240
|
+
try {
|
|
241
|
+
// Try the next server in the list on each reconnect attempt
|
|
242
|
+
currentServerIndex++
|
|
243
|
+
channel = createGrpcChannel()
|
|
244
|
+
clients = createClients()
|
|
245
|
+
state = "connected"
|
|
246
|
+
|
|
247
|
+
// Notify listeners that we're back
|
|
248
|
+
for (const cb of reconnectCallbacks) {
|
|
249
|
+
try { cb() } catch { /* ignore listener errors */ }
|
|
250
|
+
}
|
|
251
|
+
return
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (maxAttempts > 0 && attempt >= maxAttempts) {
|
|
254
|
+
state = "disconnected"
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Failed to reconnect after ${attempt} attempts: ${err}`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Exponential backoff: base interval * 2^attempt, capped at 30s
|
|
261
|
+
const delay = Math.min(
|
|
262
|
+
resolvedConfig.reconnectIntervalMs * Math.pow(2, attempt - 1),
|
|
263
|
+
30000,
|
|
264
|
+
)
|
|
265
|
+
await new Promise((r) => setTimeout(r, delay))
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return connection
|
|
272
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Axon Server error codes — mapped from the Java framework's ErrorCode enum.
|
|
3
|
+
*
|
|
4
|
+
* Error codes follow the AXONIQ-XXXX pattern where the first digit
|
|
5
|
+
* indicates the category:
|
|
6
|
+
* - 1xxx: Authentication/instruction errors
|
|
7
|
+
* - 2xxx: Event publishing errors
|
|
8
|
+
* - 3xxx: Communication errors
|
|
9
|
+
* - 4xxx: Command errors
|
|
10
|
+
* - 5xxx: Query errors
|
|
11
|
+
* - 9xxx: Internal/storage errors
|
|
12
|
+
*/
|
|
13
|
+
export const AxonServerErrorCode = {
|
|
14
|
+
// Authentication & instructions
|
|
15
|
+
AUTHENTICATION_TOKEN_MISSING: "AXONIQ-1000",
|
|
16
|
+
AUTHENTICATION_INVALID_TOKEN: "AXONIQ-1001",
|
|
17
|
+
UNSUPPORTED_INSTRUCTION: "AXONIQ-1002",
|
|
18
|
+
INSTRUCTION_ACK_ERROR: "AXONIQ-1003",
|
|
19
|
+
INSTRUCTION_EXECUTION_ERROR: "AXONIQ-1004",
|
|
20
|
+
|
|
21
|
+
// Event publishing
|
|
22
|
+
EVENT_PAYLOAD_TOO_LARGE: "AXONIQ-2001",
|
|
23
|
+
NO_EVENT_STORE_MASTER_AVAILABLE: "AXONIQ-2100",
|
|
24
|
+
CONCURRENCY_EXCEPTION: "AXONIQ-2000",
|
|
25
|
+
|
|
26
|
+
// Communication
|
|
27
|
+
CONNECTION_FAILED: "AXONIQ-3001",
|
|
28
|
+
GRPC_MESSAGE_TOO_LARGE: "AXONIQ-3002",
|
|
29
|
+
|
|
30
|
+
// Commands
|
|
31
|
+
NO_HANDLER_FOR_COMMAND: "AXONIQ-4000",
|
|
32
|
+
COMMAND_EXECUTION_ERROR: "AXONIQ-4002",
|
|
33
|
+
COMMAND_DISPATCH_ERROR: "AXONIQ-4003",
|
|
34
|
+
COMMAND_CONCURRENCY_ERROR: "AXONIQ-4004",
|
|
35
|
+
COMMAND_EXECUTION_NON_TRANSIENT_ERROR: "AXONIQ-4005",
|
|
36
|
+
|
|
37
|
+
// Queries
|
|
38
|
+
NO_HANDLER_FOR_QUERY: "AXONIQ-5000",
|
|
39
|
+
QUERY_EXECUTION_ERROR: "AXONIQ-5001",
|
|
40
|
+
QUERY_DISPATCH_ERROR: "AXONIQ-5002",
|
|
41
|
+
QUERY_EXECUTION_NON_TRANSIENT_ERROR: "AXONIQ-5003",
|
|
42
|
+
|
|
43
|
+
// Internal/storage
|
|
44
|
+
DATAFILE_READ_ERROR: "AXONIQ-9000",
|
|
45
|
+
INDEX_READ_ERROR: "AXONIQ-9001",
|
|
46
|
+
DATAFILE_WRITE_ERROR: "AXONIQ-9100",
|
|
47
|
+
INDEX_WRITE_ERROR: "AXONIQ-9101",
|
|
48
|
+
DIRECTORY_CREATION_FAILED: "AXONIQ-9102",
|
|
49
|
+
VALIDATION_FAILED: "AXONIQ-9200",
|
|
50
|
+
TRANSACTION_ROLLED_BACK: "AXONIQ-9900",
|
|
51
|
+
|
|
52
|
+
// Default
|
|
53
|
+
OTHER: "AXONIQ-0001",
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
export type AxonServerErrorCodeValue = typeof AxonServerErrorCode[keyof typeof AxonServerErrorCode]
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Exception hierarchy
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Base error for all Axon Server errors. Carries the error code
|
|
64
|
+
* and whether the error is transient (retryable).
|
|
65
|
+
*/
|
|
66
|
+
export class AxonServerError extends Error {
|
|
67
|
+
readonly errorCode: string
|
|
68
|
+
readonly transient: boolean
|
|
69
|
+
|
|
70
|
+
constructor(message: string, errorCode: string, transient: boolean) {
|
|
71
|
+
super(message)
|
|
72
|
+
this.name = "AxonServerError"
|
|
73
|
+
this.errorCode = errorCode
|
|
74
|
+
this.transient = transient
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** No handler registered for the dispatched command. */
|
|
79
|
+
export class NoHandlerForCommandError extends AxonServerError {
|
|
80
|
+
constructor(message: string) {
|
|
81
|
+
super(message, AxonServerErrorCode.NO_HANDLER_FOR_COMMAND, false)
|
|
82
|
+
this.name = "NoHandlerForCommandError"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** No handler registered for the dispatched query. */
|
|
87
|
+
export class NoHandlerForQueryError extends AxonServerError {
|
|
88
|
+
constructor(message: string) {
|
|
89
|
+
super(message, AxonServerErrorCode.NO_HANDLER_FOR_QUERY, false)
|
|
90
|
+
this.name = "NoHandlerForQueryError"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Command handler execution failed (transient — may succeed on retry). */
|
|
95
|
+
export class CommandExecutionError extends AxonServerError {
|
|
96
|
+
constructor(message: string, transient: boolean = true) {
|
|
97
|
+
super(
|
|
98
|
+
message,
|
|
99
|
+
transient
|
|
100
|
+
? AxonServerErrorCode.COMMAND_EXECUTION_ERROR
|
|
101
|
+
: AxonServerErrorCode.COMMAND_EXECUTION_NON_TRANSIENT_ERROR,
|
|
102
|
+
transient,
|
|
103
|
+
)
|
|
104
|
+
this.name = "CommandExecutionError"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Query handler execution failed (transient — may succeed on retry). */
|
|
109
|
+
export class QueryExecutionError extends AxonServerError {
|
|
110
|
+
constructor(message: string, transient: boolean = true) {
|
|
111
|
+
super(
|
|
112
|
+
message,
|
|
113
|
+
transient
|
|
114
|
+
? AxonServerErrorCode.QUERY_EXECUTION_ERROR
|
|
115
|
+
: AxonServerErrorCode.QUERY_EXECUTION_NON_TRANSIENT_ERROR,
|
|
116
|
+
transient,
|
|
117
|
+
)
|
|
118
|
+
this.name = "QueryExecutionError"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Command dispatch failed (infrastructure error). */
|
|
123
|
+
export class CommandDispatchError extends AxonServerError {
|
|
124
|
+
constructor(message: string) {
|
|
125
|
+
super(message, AxonServerErrorCode.COMMAND_DISPATCH_ERROR, true)
|
|
126
|
+
this.name = "CommandDispatchError"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Query dispatch failed (infrastructure error). */
|
|
131
|
+
export class QueryDispatchError extends AxonServerError {
|
|
132
|
+
constructor(message: string) {
|
|
133
|
+
super(message, AxonServerErrorCode.QUERY_DISPATCH_ERROR, true)
|
|
134
|
+
this.name = "QueryDispatchError"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Optimistic concurrency violation (transient — retry with fresh state). */
|
|
139
|
+
export class ConcurrencyError extends AxonServerError {
|
|
140
|
+
constructor(message: string) {
|
|
141
|
+
super(message, AxonServerErrorCode.CONCURRENCY_EXCEPTION, true)
|
|
142
|
+
this.name = "ConcurrencyError"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Connection to Axon Server failed. */
|
|
147
|
+
export class ConnectionFailedError extends AxonServerError {
|
|
148
|
+
constructor(message: string) {
|
|
149
|
+
super(message, AxonServerErrorCode.CONNECTION_FAILED, true)
|
|
150
|
+
this.name = "ConnectionFailedError"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Authentication/authorization failed. */
|
|
155
|
+
export class AuthenticationError extends AxonServerError {
|
|
156
|
+
constructor(message: string, code: string) {
|
|
157
|
+
super(message, code, false)
|
|
158
|
+
this.name = "AuthenticationError"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Error code → exception mapping
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
const TRANSIENT_CODES = new Set<string>([
|
|
167
|
+
AxonServerErrorCode.COMMAND_EXECUTION_ERROR,
|
|
168
|
+
AxonServerErrorCode.QUERY_EXECUTION_ERROR,
|
|
169
|
+
AxonServerErrorCode.COMMAND_DISPATCH_ERROR,
|
|
170
|
+
AxonServerErrorCode.QUERY_DISPATCH_ERROR,
|
|
171
|
+
AxonServerErrorCode.CONNECTION_FAILED,
|
|
172
|
+
AxonServerErrorCode.GRPC_MESSAGE_TOO_LARGE,
|
|
173
|
+
AxonServerErrorCode.CONCURRENCY_EXCEPTION,
|
|
174
|
+
AxonServerErrorCode.COMMAND_CONCURRENCY_ERROR,
|
|
175
|
+
AxonServerErrorCode.NO_EVENT_STORE_MASTER_AVAILABLE,
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert an Axon Server error code + message into a typed exception.
|
|
180
|
+
*/
|
|
181
|
+
export function mapErrorCode(errorCode: string, message: string): AxonServerError {
|
|
182
|
+
switch (errorCode) {
|
|
183
|
+
case AxonServerErrorCode.NO_HANDLER_FOR_COMMAND:
|
|
184
|
+
return new NoHandlerForCommandError(message)
|
|
185
|
+
case AxonServerErrorCode.NO_HANDLER_FOR_QUERY:
|
|
186
|
+
return new NoHandlerForQueryError(message)
|
|
187
|
+
case AxonServerErrorCode.COMMAND_EXECUTION_ERROR:
|
|
188
|
+
return new CommandExecutionError(message, true)
|
|
189
|
+
case AxonServerErrorCode.COMMAND_EXECUTION_NON_TRANSIENT_ERROR:
|
|
190
|
+
return new CommandExecutionError(message, false)
|
|
191
|
+
case AxonServerErrorCode.QUERY_EXECUTION_ERROR:
|
|
192
|
+
return new QueryExecutionError(message, true)
|
|
193
|
+
case AxonServerErrorCode.QUERY_EXECUTION_NON_TRANSIENT_ERROR:
|
|
194
|
+
return new QueryExecutionError(message, false)
|
|
195
|
+
case AxonServerErrorCode.COMMAND_DISPATCH_ERROR:
|
|
196
|
+
return new CommandDispatchError(message)
|
|
197
|
+
case AxonServerErrorCode.QUERY_DISPATCH_ERROR:
|
|
198
|
+
return new QueryDispatchError(message)
|
|
199
|
+
case AxonServerErrorCode.CONCURRENCY_EXCEPTION:
|
|
200
|
+
case AxonServerErrorCode.COMMAND_CONCURRENCY_ERROR:
|
|
201
|
+
return new ConcurrencyError(message)
|
|
202
|
+
case AxonServerErrorCode.CONNECTION_FAILED:
|
|
203
|
+
case AxonServerErrorCode.GRPC_MESSAGE_TOO_LARGE:
|
|
204
|
+
return new ConnectionFailedError(message)
|
|
205
|
+
case AxonServerErrorCode.AUTHENTICATION_TOKEN_MISSING:
|
|
206
|
+
case AxonServerErrorCode.AUTHENTICATION_INVALID_TOKEN:
|
|
207
|
+
return new AuthenticationError(message, errorCode)
|
|
208
|
+
default:
|
|
209
|
+
return new AxonServerError(
|
|
210
|
+
message,
|
|
211
|
+
errorCode,
|
|
212
|
+
TRANSIENT_CODES.has(errorCode),
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if an error is transient (retryable).
|
|
219
|
+
*/
|
|
220
|
+
export function isTransientError(error: unknown): boolean {
|
|
221
|
+
if (error instanceof AxonServerError) return error.transient
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EventProcessorInfo, EventProcessorInfo_SegmentStatus } from "./generated/control.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Status of a single event processor, reported to Axon Server.
|
|
5
|
+
* Aligned with Java's EventProcessorInfo proto message.
|
|
6
|
+
*/
|
|
7
|
+
export interface ProcessorStatus {
|
|
8
|
+
readonly name: string
|
|
9
|
+
readonly running: boolean
|
|
10
|
+
readonly mode: "Tracking" | "Subscribing"
|
|
11
|
+
readonly isStreamingProcessor: boolean
|
|
12
|
+
readonly activeThreads: number
|
|
13
|
+
readonly availableThreads: number
|
|
14
|
+
readonly error: boolean
|
|
15
|
+
readonly errorMessage?: string
|
|
16
|
+
readonly tokenStoreIdentifier: string
|
|
17
|
+
readonly segments: SegmentStatus[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SegmentStatus {
|
|
21
|
+
readonly segmentId: number
|
|
22
|
+
readonly caughtUp: boolean
|
|
23
|
+
readonly replaying: boolean
|
|
24
|
+
readonly onePartOf: number
|
|
25
|
+
readonly tokenPosition: bigint
|
|
26
|
+
readonly errorState: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Converts a ProcessorStatus to the proto EventProcessorInfo format.
|
|
31
|
+
*/
|
|
32
|
+
export function toEventProcessorInfo(status: ProcessorStatus): EventProcessorInfo {
|
|
33
|
+
return {
|
|
34
|
+
processorName: status.name,
|
|
35
|
+
mode: status.mode,
|
|
36
|
+
activeThreads: status.activeThreads,
|
|
37
|
+
running: status.running,
|
|
38
|
+
error: status.error,
|
|
39
|
+
segmentStatus: status.segments.map(toSegmentStatus),
|
|
40
|
+
availableThreads: status.availableThreads,
|
|
41
|
+
tokenStoreIdentifier: status.tokenStoreIdentifier,
|
|
42
|
+
isStreamingProcessor: status.isStreamingProcessor,
|
|
43
|
+
loadBalancingStrategyName: "",
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function toSegmentStatus(seg: SegmentStatus): EventProcessorInfo_SegmentStatus {
|
|
48
|
+
return {
|
|
49
|
+
segmentId: seg.segmentId,
|
|
50
|
+
caughtUp: seg.caughtUp,
|
|
51
|
+
replaying: seg.replaying,
|
|
52
|
+
onePartOf: seg.onePartOf,
|
|
53
|
+
tokenPosition: seg.tokenPosition,
|
|
54
|
+
errorState: seg.errorState,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Supplier function that returns the current status of all event processors.
|
|
60
|
+
* Registered with the platform connection for periodic reporting.
|
|
61
|
+
*/
|
|
62
|
+
export type ProcessorStatusSupplier = () => ProcessorStatus[]
|