@kronos-ts/kronosdb 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/connection.d.ts +86 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +133 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +72 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +149 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-processor-info.d.ts +32 -0
- package/dist/event-processor-info.d.ts.map +1 -0
- package/dist/event-processor-info.js +24 -0
- package/dist/event-processor-info.js.map +1 -0
- package/dist/flow-controlled-sender.d.ts +12 -0
- package/dist/flow-controlled-sender.d.ts.map +1 -0
- package/dist/flow-controlled-sender.js +53 -0
- package/dist/flow-controlled-sender.js.map +1 -0
- package/dist/generated/command.d.ts +169 -0
- package/dist/generated/command.d.ts.map +1 -0
- package/dist/generated/command.js +964 -0
- package/dist/generated/command.js.map +1 -0
- package/dist/generated/common.d.ts +76 -0
- package/dist/generated/common.d.ts.map +1 -0
- package/dist/generated/common.js +648 -0
- package/dist/generated/common.js.map +1 -0
- package/dist/generated/eventstore.d.ts +337 -0
- package/dist/generated/eventstore.d.ts.map +1 -0
- package/dist/generated/eventstore.js +1757 -0
- package/dist/generated/eventstore.js.map +1 -0
- package/dist/generated/platform.d.ts +242 -0
- package/dist/generated/platform.d.ts.map +1 -0
- package/dist/generated/platform.js +1525 -0
- package/dist/generated/platform.js.map +1 -0
- package/dist/generated/query.d.ts +265 -0
- package/dist/generated/query.d.ts.map +1 -0
- package/dist/generated/query.js +2114 -0
- package/dist/generated/query.js.map +1 -0
- package/dist/generated/snapshot.d.ts +180 -0
- package/dist/generated/snapshot.d.ts.map +1 -0
- package/dist/generated/snapshot.js +861 -0
- package/dist/generated/snapshot.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/kronosdb-event-store.d.ts +17 -0
- package/dist/kronosdb-event-store.d.ts.map +1 -0
- package/dist/kronosdb-event-store.js +328 -0
- package/dist/kronosdb-event-store.js.map +1 -0
- package/dist/kronosdb-snapshot-store.d.ts +10 -0
- package/dist/kronosdb-snapshot-store.d.ts.map +1 -0
- package/dist/kronosdb-snapshot-store.js +79 -0
- package/dist/kronosdb-snapshot-store.js.map +1 -0
- package/dist/kronosdb.d.ts +53 -0
- package/dist/kronosdb.d.ts.map +1 -0
- package/dist/kronosdb.js +852 -0
- package/dist/kronosdb.js.map +1 -0
- package/dist/metadata-conversion.d.ts +37 -0
- package/dist/metadata-conversion.d.ts.map +1 -0
- package/dist/metadata-conversion.js +75 -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 +87 -0
- package/dist/platform-service.d.ts.map +1 -0
- package/dist/platform-service.js +218 -0
- package/dist/platform-service.js.map +1 -0
- package/dist/service-definitions.d.ts +187 -0
- package/dist/service-definitions.d.ts.map +1 -0
- package/dist/service-definitions.js +18 -0
- package/dist/service-definitions.js.map +1 -0
- package/dist/shutdown-latch.d.ts +18 -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/connection.ts +235 -0
- package/src/errors.ts +173 -0
- package/src/event-processor-info.ts +53 -0
- package/src/flow-controlled-sender.ts +73 -0
- package/src/generated/command.ts +1226 -0
- package/src/generated/common.ts +770 -0
- package/src/generated/eventstore.ts +2241 -0
- package/src/generated/platform.ts +1914 -0
- package/src/generated/query.ts +2571 -0
- package/src/generated/snapshot.ts +1110 -0
- package/src/index.ts +87 -0
- package/src/kronosdb-event-store.ts +401 -0
- package/src/kronosdb-snapshot-store.ts +104 -0
- package/src/kronosdb.ts +1000 -0
- package/src/metadata-conversion.ts +85 -0
- package/src/outbound-stream.ts +52 -0
- package/src/platform-service.ts +297 -0
- package/src/service-definitions.ts +25 -0
- package/src/shutdown-latch.ts +74 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Metadata } from "@kronos-ts/common"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KronosDB MetadataValue type — matches the proto oneof.
|
|
5
|
+
*
|
|
6
|
+
* Since we don't have generated types yet, we define the shape
|
|
7
|
+
* inline. Once proto codegen runs, these can be replaced with imports.
|
|
8
|
+
*/
|
|
9
|
+
export interface MetadataValue {
|
|
10
|
+
textValue?: string
|
|
11
|
+
numberValue?: bigint
|
|
12
|
+
booleanValue?: boolean
|
|
13
|
+
doubleValue?: number
|
|
14
|
+
bytesValue?: { type: string; revision: string; data: Uint8Array }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert framework metadata to KronosDB proto metadata format.
|
|
19
|
+
* Used for command and query metadata (which use MetadataValue).
|
|
20
|
+
*/
|
|
21
|
+
export function metadataToProto(metadata: Metadata): Record<string, MetadataValue> {
|
|
22
|
+
const result: Record<string, MetadataValue> = {}
|
|
23
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
result[key] = { textValue: value }
|
|
26
|
+
} else if (typeof value === "number") {
|
|
27
|
+
if (Number.isInteger(value)) {
|
|
28
|
+
result[key] = { numberValue: BigInt(value) }
|
|
29
|
+
} else {
|
|
30
|
+
result[key] = { doubleValue: value }
|
|
31
|
+
}
|
|
32
|
+
} else if (typeof value === "boolean") {
|
|
33
|
+
result[key] = { booleanValue: value }
|
|
34
|
+
} else if (typeof value === "bigint") {
|
|
35
|
+
result[key] = { numberValue: value }
|
|
36
|
+
} else if (value !== undefined && value !== null) {
|
|
37
|
+
result[key] = { textValue: JSON.stringify(value) }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert KronosDB proto metadata format to framework metadata.
|
|
45
|
+
*/
|
|
46
|
+
export function metadataFromProto(protoMeta: Record<string, MetadataValue>): Metadata {
|
|
47
|
+
const result: Record<string, unknown> = {}
|
|
48
|
+
for (const [key, value] of Object.entries(protoMeta)) {
|
|
49
|
+
if (value.textValue !== undefined) {
|
|
50
|
+
result[key] = value.textValue
|
|
51
|
+
} else if (value.numberValue !== undefined) {
|
|
52
|
+
result[key] = Number(value.numberValue)
|
|
53
|
+
} else if (value.booleanValue !== undefined) {
|
|
54
|
+
result[key] = value.booleanValue
|
|
55
|
+
} else if (value.doubleValue !== undefined) {
|
|
56
|
+
result[key] = value.doubleValue
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert framework metadata to simple string map.
|
|
64
|
+
* Used for event and snapshot metadata (which use map<string, string>).
|
|
65
|
+
*/
|
|
66
|
+
export function metadataToStringMap(metadata: Metadata): Record<string, string> {
|
|
67
|
+
const result: Record<string, string> = {}
|
|
68
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
69
|
+
if (value !== undefined && value !== null) {
|
|
70
|
+
result[key] = String(value)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert simple string map to framework metadata.
|
|
78
|
+
*/
|
|
79
|
+
export function metadataFromStringMap(map: Record<string, string>): Metadata {
|
|
80
|
+
const result: Record<string, unknown> = {}
|
|
81
|
+
for (const [key, value] of Object.entries(map)) {
|
|
82
|
+
result[key] = value
|
|
83
|
+
}
|
|
84
|
+
return result
|
|
85
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A queue-backed async iterable for feeding outbound messages to a
|
|
3
|
+
* bidirectional gRPC stream. Buffers messages when the stream isn't
|
|
4
|
+
* consuming, and resolves promises when the stream is waiting.
|
|
5
|
+
*/
|
|
6
|
+
export interface OutboundStream<T> {
|
|
7
|
+
/** Send a message into the stream. */
|
|
8
|
+
send(message: T): void
|
|
9
|
+
/** The async iterable to pass to the gRPC client. */
|
|
10
|
+
readonly iterable: AsyncIterable<T>
|
|
11
|
+
/** Close the stream. */
|
|
12
|
+
close(): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createOutboundStream<T>(): OutboundStream<T> {
|
|
16
|
+
let resolve: ((value: IteratorResult<T>) => void) | null = null
|
|
17
|
+
const queue: T[] = []
|
|
18
|
+
let closed = false
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
send(message: T) {
|
|
22
|
+
if (resolve) {
|
|
23
|
+
const r = resolve
|
|
24
|
+
resolve = null
|
|
25
|
+
r({ value: message, done: false })
|
|
26
|
+
} else {
|
|
27
|
+
queue.push(message)
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
iterable: {
|
|
32
|
+
[Symbol.asyncIterator]() {
|
|
33
|
+
return {
|
|
34
|
+
next(): Promise<IteratorResult<T>> {
|
|
35
|
+
const queued = queue.shift()
|
|
36
|
+
if (queued) return Promise.resolve({ value: queued, done: false })
|
|
37
|
+
if (closed) return Promise.resolve({ value: undefined as any, done: true })
|
|
38
|
+
return new Promise((r) => { resolve = r })
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
close() {
|
|
45
|
+
closed = true
|
|
46
|
+
if (resolve) {
|
|
47
|
+
resolve({ value: undefined as any, done: true })
|
|
48
|
+
resolve = null
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { KronosDbConnection } from "./connection.js"
|
|
2
|
+
import { createKronosMetadata } from "./connection.js"
|
|
3
|
+
import { createOutboundStream } from "./outbound-stream.js"
|
|
4
|
+
import type { PlatformInbound } from "./generated/platform.js"
|
|
5
|
+
import type { ProcessorStatusSupplier } from "./event-processor-info.js"
|
|
6
|
+
import { toEventProcessorInfo } from "./event-processor-info.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Server-initiated instructions received via the platform stream.
|
|
10
|
+
*
|
|
11
|
+
* KronosDB uses direct fields on PlatformOutbound (not nested like Axon Server's
|
|
12
|
+
* eventProcessorControl/topologyChange wrappers).
|
|
13
|
+
*/
|
|
14
|
+
export type PlatformInstruction =
|
|
15
|
+
| { kind: "pause-processor"; processorName: string }
|
|
16
|
+
| { kind: "start-processor"; processorName: string }
|
|
17
|
+
| { kind: "release-segment"; processorName: string; segmentId: number }
|
|
18
|
+
| { kind: "split-segment"; processorName: string; segmentId: number }
|
|
19
|
+
| { kind: "merge-segment"; processorName: string; segmentId: number }
|
|
20
|
+
| { kind: "reconnect-request" }
|
|
21
|
+
|
|
22
|
+
export type InstructionHandler = (instruction: PlatformInstruction) => void | Promise<void>
|
|
23
|
+
|
|
24
|
+
export interface PlatformConnection {
|
|
25
|
+
start(): Promise<void>
|
|
26
|
+
stop(): void
|
|
27
|
+
onInstruction(handler: InstructionHandler): void
|
|
28
|
+
registerProcessorStatusSupplier(supplier: ProcessorStatusSupplier): void
|
|
29
|
+
readonly connected: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Resolves with `true` once KronosDB has acknowledged this client's
|
|
32
|
+
* registration on the platform stream (we use the first server-originated
|
|
33
|
+
* inbound message — typically a heartbeat reply — as the ack signal).
|
|
34
|
+
* Resolves with `false` if the platform is not yet started.
|
|
35
|
+
*
|
|
36
|
+
* This replaces the legacy 1-second sleep at
|
|
37
|
+
* kronosdb-configuration-enhancer.ts:216 (D-102): the kronosDb extension's
|
|
38
|
+
* `onStart('processors', ...)` hook polls this method via `withRetry` so the
|
|
39
|
+
* application waits exactly long enough for handler subscriptions to be
|
|
40
|
+
* routable, no longer or shorter.
|
|
41
|
+
*
|
|
42
|
+
* Implementation note: the platform stream sends a `register` frame on
|
|
43
|
+
* `start()` and KronosDB responds with a heartbeat tick at the configured
|
|
44
|
+
* heartbeat interval. We treat the first inbound frame as the ack signal,
|
|
45
|
+
* which is the earliest observable point at which the server has accepted
|
|
46
|
+
* the registration. (No explicit ack frame exists in the KronosDB protobuf
|
|
47
|
+
* surface today — Pitfall 5 / RESEARCH.md.)
|
|
48
|
+
*/
|
|
49
|
+
subscriptionsAcked(): Promise<boolean>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PlatformServiceOptions {
|
|
53
|
+
/** Heartbeat interval in ms. Default: 10000 */
|
|
54
|
+
heartbeatIntervalMs?: number
|
|
55
|
+
/** Heartbeat timeout in ms. Default: 7500 */
|
|
56
|
+
heartbeatTimeoutMs?: number
|
|
57
|
+
/** Processor status reporting interval in ms. Default: 500 */
|
|
58
|
+
processorsNotificationRateMs?: number
|
|
59
|
+
/** Delay before first processor status report in ms. Default: 5000 */
|
|
60
|
+
processorsNotificationInitialDelayMs?: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parses a raw PlatformOutbound message into a typed PlatformInstruction.
|
|
65
|
+
*
|
|
66
|
+
* Returns `null` for any arm that does not correspond to a known instruction
|
|
67
|
+
* (e.g. heartbeat, nodeNotification, topologyNotification). This is the
|
|
68
|
+
* correct catch-all for forward-compatibility: new proto fields added to
|
|
69
|
+
* PlatformOutbound will silently be ignored rather than causing errors.
|
|
70
|
+
*/
|
|
71
|
+
export function parseInstruction(message: any): PlatformInstruction | null {
|
|
72
|
+
if (message.requestReconnect) {
|
|
73
|
+
return { kind: "reconnect-request" }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// KronosDB sends processor instructions directly on PlatformOutbound
|
|
77
|
+
if (message.pauseEventProcessor) {
|
|
78
|
+
return {
|
|
79
|
+
kind: "pause-processor",
|
|
80
|
+
processorName: message.pauseEventProcessor.processorName ?? "",
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (message.startEventProcessor) {
|
|
84
|
+
return {
|
|
85
|
+
kind: "start-processor",
|
|
86
|
+
processorName: message.startEventProcessor.processorName ?? "",
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (message.releaseSegment) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "release-segment",
|
|
92
|
+
processorName: message.releaseSegment.processorName ?? "",
|
|
93
|
+
segmentId: message.releaseSegment.segmentIdentifier ?? 0,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (message.splitEventProcessorSegment) {
|
|
97
|
+
return {
|
|
98
|
+
kind: "split-segment",
|
|
99
|
+
processorName: message.splitEventProcessorSegment.processorName ?? "",
|
|
100
|
+
segmentId: message.splitEventProcessorSegment.segmentIdentifier ?? 0,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (message.mergeEventProcessorSegment) {
|
|
104
|
+
return {
|
|
105
|
+
kind: "merge-segment",
|
|
106
|
+
processorName: message.mergeEventProcessorSegment.processorName ?? "",
|
|
107
|
+
segmentId: message.mergeEventProcessorSegment.segmentIdentifier ?? 0,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Creates a PlatformService connection to KronosDB.
|
|
116
|
+
*
|
|
117
|
+
* The platform stream is the control plane:
|
|
118
|
+
* 1. Registers this client with KronosDB
|
|
119
|
+
* 2. Sends periodic heartbeats to verify connectivity
|
|
120
|
+
* 3. Receives instructions from KronosDB (split, merge, pause, resume)
|
|
121
|
+
* 4. Reports event processor status periodically
|
|
122
|
+
*/
|
|
123
|
+
export function createPlatformConnection(
|
|
124
|
+
connection: KronosDbConnection,
|
|
125
|
+
options?: PlatformServiceOptions,
|
|
126
|
+
): PlatformConnection {
|
|
127
|
+
const heartbeatIntervalMs = options?.heartbeatIntervalMs ?? 10000
|
|
128
|
+
const heartbeatTimeoutMs = options?.heartbeatTimeoutMs ?? 7500
|
|
129
|
+
const processorsNotificationRateMs = options?.processorsNotificationRateMs ?? 500
|
|
130
|
+
const processorsNotificationInitialDelayMs = options?.processorsNotificationInitialDelayMs ?? 5000
|
|
131
|
+
|
|
132
|
+
const instructionHandlers: InstructionHandler[] = []
|
|
133
|
+
const processorStatusSuppliers: ProcessorStatusSupplier[] = []
|
|
134
|
+
let isConnected = false
|
|
135
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
|
136
|
+
let processorStatusTimer: ReturnType<typeof setInterval> | null = null
|
|
137
|
+
let lastHeartbeatResponse = Date.now()
|
|
138
|
+
let outbound: ReturnType<typeof createOutboundStream<PlatformInbound>> | null = null
|
|
139
|
+
/**
|
|
140
|
+
* Latches once KronosDB sends its first inbound message after registration
|
|
141
|
+
* — the earliest observable signal that the platform stream is fully wired
|
|
142
|
+
* (D-102, replaces the legacy 1s sleep). Reset on every `start()` so a
|
|
143
|
+
* stop/start cycle re-arms the latch correctly.
|
|
144
|
+
*/
|
|
145
|
+
let acked = false
|
|
146
|
+
|
|
147
|
+
const grpcMetadata = createKronosMetadata(connection.config)
|
|
148
|
+
|
|
149
|
+
async function processInboundInstructions(inbound: AsyncIterable<any>) {
|
|
150
|
+
try {
|
|
151
|
+
for await (const message of inbound) {
|
|
152
|
+
// First inbound message after start() = the platform has accepted our
|
|
153
|
+
// registration and is talking back. Latch the ack flag (D-102).
|
|
154
|
+
acked = true
|
|
155
|
+
const instruction = parseInstruction(message)
|
|
156
|
+
if (instruction) {
|
|
157
|
+
for (const handler of instructionHandlers) {
|
|
158
|
+
try {
|
|
159
|
+
await handler(instruction)
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error("Platform instruction handler error:", err)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (message.heartbeat) {
|
|
167
|
+
lastHeartbeatResponse = Date.now()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (isConnected) {
|
|
172
|
+
console.error("Platform stream error:", err)
|
|
173
|
+
isConnected = false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function startHeartbeat() {
|
|
179
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer)
|
|
180
|
+
lastHeartbeatResponse = Date.now()
|
|
181
|
+
|
|
182
|
+
heartbeatTimer = setInterval(() => {
|
|
183
|
+
if (!isConnected || !outbound) return
|
|
184
|
+
|
|
185
|
+
const timeSinceLastResponse = Date.now() - lastHeartbeatResponse
|
|
186
|
+
if (timeSinceLastResponse > heartbeatTimeoutMs) {
|
|
187
|
+
console.warn(
|
|
188
|
+
`Platform heartbeat timeout: no response in ${timeSinceLastResponse}ms ` +
|
|
189
|
+
`(threshold: ${heartbeatTimeoutMs}ms). Marking connection as lost.`,
|
|
190
|
+
)
|
|
191
|
+
isConnected = false
|
|
192
|
+
connection.reconnect().catch((err) => {
|
|
193
|
+
console.error("Failed to reconnect after heartbeat timeout:", err)
|
|
194
|
+
})
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Send heartbeat — KronosDB PlatformInbound uses oneof, heartbeat field
|
|
199
|
+
outbound.send({
|
|
200
|
+
heartbeat: {},
|
|
201
|
+
})
|
|
202
|
+
}, heartbeatIntervalMs)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function startProcessorStatusReporting() {
|
|
206
|
+
if (processorStatusTimer) clearInterval(processorStatusTimer)
|
|
207
|
+
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
if (!isConnected) return
|
|
210
|
+
reportProcessorStatus()
|
|
211
|
+
|
|
212
|
+
processorStatusTimer = setInterval(() => {
|
|
213
|
+
if (!isConnected || !outbound) return
|
|
214
|
+
reportProcessorStatus()
|
|
215
|
+
}, processorsNotificationRateMs)
|
|
216
|
+
}, processorsNotificationInitialDelayMs)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function reportProcessorStatus() {
|
|
220
|
+
if (!outbound || processorStatusSuppliers.length === 0) return
|
|
221
|
+
|
|
222
|
+
for (const supplier of processorStatusSuppliers) {
|
|
223
|
+
try {
|
|
224
|
+
const statuses = supplier()
|
|
225
|
+
for (const status of statuses) {
|
|
226
|
+
outbound.send({
|
|
227
|
+
eventProcessorInfo: toEventProcessorInfo(status),
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.warn("Failed to report processor status:", err)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
async start() {
|
|
238
|
+
if (isConnected) return
|
|
239
|
+
|
|
240
|
+
// Re-arm the ack latch so a stop/start cycle correctly re-waits.
|
|
241
|
+
acked = false
|
|
242
|
+
outbound = createOutboundStream<PlatformInbound>()
|
|
243
|
+
|
|
244
|
+
// Register with KronosDB — first message must be ClientIdentification
|
|
245
|
+
outbound.send({
|
|
246
|
+
register: {
|
|
247
|
+
clientId: connection.config.clientId,
|
|
248
|
+
componentName: connection.config.componentName,
|
|
249
|
+
version: "1.0.0",
|
|
250
|
+
tags: {},
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Open bidirectional platform stream
|
|
255
|
+
const inbound = connection.platform.openStream(outbound.iterable, {
|
|
256
|
+
metadata: grpcMetadata,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
isConnected = true
|
|
260
|
+
startHeartbeat()
|
|
261
|
+
startProcessorStatusReporting()
|
|
262
|
+
processInboundInstructions(inbound)
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
stop() {
|
|
266
|
+
isConnected = false
|
|
267
|
+
if (heartbeatTimer) {
|
|
268
|
+
clearInterval(heartbeatTimer)
|
|
269
|
+
heartbeatTimer = null
|
|
270
|
+
}
|
|
271
|
+
if (processorStatusTimer) {
|
|
272
|
+
clearInterval(processorStatusTimer)
|
|
273
|
+
processorStatusTimer = null
|
|
274
|
+
}
|
|
275
|
+
if (outbound) {
|
|
276
|
+
outbound.close()
|
|
277
|
+
outbound = null
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
onInstruction(handler) {
|
|
282
|
+
instructionHandlers.push(handler)
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
registerProcessorStatusSupplier(supplier) {
|
|
286
|
+
processorStatusSuppliers.push(supplier)
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
get connected() {
|
|
290
|
+
return isConnected
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
async subscriptionsAcked(): Promise<boolean> {
|
|
294
|
+
return isConnected && acked
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports the generated gRPC service definitions for KronosDB.
|
|
3
|
+
* Used internally by the connector and can be passed to connectToKronosDb().
|
|
4
|
+
*/
|
|
5
|
+
import { PlatformServiceDefinition } from "./generated/platform.js"
|
|
6
|
+
import { CommandServiceDefinition } from "./generated/command.js"
|
|
7
|
+
import { QueryServiceDefinition } from "./generated/query.js"
|
|
8
|
+
import { EventStoreDefinition } from "./generated/eventstore.js"
|
|
9
|
+
import { SnapshotStoreDefinition } from "./generated/snapshot.js"
|
|
10
|
+
|
|
11
|
+
export const kronosDbServiceDefinitions = {
|
|
12
|
+
platform: PlatformServiceDefinition,
|
|
13
|
+
commands: CommandServiceDefinition,
|
|
14
|
+
queries: QueryServiceDefinition,
|
|
15
|
+
eventStore: EventStoreDefinition,
|
|
16
|
+
snapshotStore: SnapshotStoreDefinition,
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
PlatformServiceDefinition,
|
|
21
|
+
CommandServiceDefinition,
|
|
22
|
+
QueryServiceDefinition,
|
|
23
|
+
EventStoreDefinition,
|
|
24
|
+
SnapshotStoreDefinition,
|
|
25
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shutdown latch that tracks in-flight operations and enables
|
|
3
|
+
* graceful shutdown by draining pending work.
|
|
4
|
+
*/
|
|
5
|
+
export interface ShutdownLatch {
|
|
6
|
+
registerActivity(): ActivityHandle
|
|
7
|
+
initiateShutdown(): Promise<void>
|
|
8
|
+
readonly shuttingDown: boolean
|
|
9
|
+
readonly activeCount: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ActivityHandle {
|
|
13
|
+
end(): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ShutdownInProgressError extends Error {
|
|
17
|
+
constructor(message: string = "Shutdown in progress") {
|
|
18
|
+
super(message)
|
|
19
|
+
this.name = "ShutdownInProgressError"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createShutdownLatch(): ShutdownLatch {
|
|
24
|
+
let activeCount = 0
|
|
25
|
+
let shuttingDown = false
|
|
26
|
+
let drainResolve: (() => void) | null = null
|
|
27
|
+
|
|
28
|
+
function checkDrained() {
|
|
29
|
+
if (shuttingDown && activeCount === 0 && drainResolve) {
|
|
30
|
+
drainResolve()
|
|
31
|
+
drainResolve = null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
registerActivity(): ActivityHandle {
|
|
37
|
+
if (shuttingDown) {
|
|
38
|
+
throw new ShutdownInProgressError()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
activeCount++
|
|
42
|
+
let ended = false
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
end() {
|
|
46
|
+
if (ended) return
|
|
47
|
+
ended = true
|
|
48
|
+
activeCount--
|
|
49
|
+
checkDrained()
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
initiateShutdown(): Promise<void> {
|
|
55
|
+
shuttingDown = true
|
|
56
|
+
|
|
57
|
+
if (activeCount === 0) {
|
|
58
|
+
return Promise.resolve()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
drainResolve = resolve
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
get shuttingDown() {
|
|
67
|
+
return shuttingDown
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
get activeCount() {
|
|
71
|
+
return activeCount
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|