@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.
Files changed (96) hide show
  1. package/dist/connection.d.ts +86 -0
  2. package/dist/connection.d.ts.map +1 -0
  3. package/dist/connection.js +133 -0
  4. package/dist/connection.js.map +1 -0
  5. package/dist/errors.d.ts +72 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +149 -0
  8. package/dist/errors.js.map +1 -0
  9. package/dist/event-processor-info.d.ts +32 -0
  10. package/dist/event-processor-info.d.ts.map +1 -0
  11. package/dist/event-processor-info.js +24 -0
  12. package/dist/event-processor-info.js.map +1 -0
  13. package/dist/flow-controlled-sender.d.ts +12 -0
  14. package/dist/flow-controlled-sender.d.ts.map +1 -0
  15. package/dist/flow-controlled-sender.js +53 -0
  16. package/dist/flow-controlled-sender.js.map +1 -0
  17. package/dist/generated/command.d.ts +169 -0
  18. package/dist/generated/command.d.ts.map +1 -0
  19. package/dist/generated/command.js +964 -0
  20. package/dist/generated/command.js.map +1 -0
  21. package/dist/generated/common.d.ts +76 -0
  22. package/dist/generated/common.d.ts.map +1 -0
  23. package/dist/generated/common.js +648 -0
  24. package/dist/generated/common.js.map +1 -0
  25. package/dist/generated/eventstore.d.ts +337 -0
  26. package/dist/generated/eventstore.d.ts.map +1 -0
  27. package/dist/generated/eventstore.js +1757 -0
  28. package/dist/generated/eventstore.js.map +1 -0
  29. package/dist/generated/platform.d.ts +242 -0
  30. package/dist/generated/platform.d.ts.map +1 -0
  31. package/dist/generated/platform.js +1525 -0
  32. package/dist/generated/platform.js.map +1 -0
  33. package/dist/generated/query.d.ts +265 -0
  34. package/dist/generated/query.d.ts.map +1 -0
  35. package/dist/generated/query.js +2114 -0
  36. package/dist/generated/query.js.map +1 -0
  37. package/dist/generated/snapshot.d.ts +180 -0
  38. package/dist/generated/snapshot.d.ts.map +1 -0
  39. package/dist/generated/snapshot.js +861 -0
  40. package/dist/generated/snapshot.js.map +1 -0
  41. package/dist/index.d.ts +13 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +13 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/kronosdb-event-store.d.ts +17 -0
  46. package/dist/kronosdb-event-store.d.ts.map +1 -0
  47. package/dist/kronosdb-event-store.js +328 -0
  48. package/dist/kronosdb-event-store.js.map +1 -0
  49. package/dist/kronosdb-snapshot-store.d.ts +10 -0
  50. package/dist/kronosdb-snapshot-store.d.ts.map +1 -0
  51. package/dist/kronosdb-snapshot-store.js +79 -0
  52. package/dist/kronosdb-snapshot-store.js.map +1 -0
  53. package/dist/kronosdb.d.ts +53 -0
  54. package/dist/kronosdb.d.ts.map +1 -0
  55. package/dist/kronosdb.js +852 -0
  56. package/dist/kronosdb.js.map +1 -0
  57. package/dist/metadata-conversion.d.ts +37 -0
  58. package/dist/metadata-conversion.d.ts.map +1 -0
  59. package/dist/metadata-conversion.js +75 -0
  60. package/dist/metadata-conversion.js.map +1 -0
  61. package/dist/outbound-stream.d.ts +15 -0
  62. package/dist/outbound-stream.d.ts.map +1 -0
  63. package/dist/outbound-stream.js +39 -0
  64. package/dist/outbound-stream.js.map +1 -0
  65. package/dist/platform-service.d.ts +87 -0
  66. package/dist/platform-service.d.ts.map +1 -0
  67. package/dist/platform-service.js +218 -0
  68. package/dist/platform-service.js.map +1 -0
  69. package/dist/service-definitions.d.ts +187 -0
  70. package/dist/service-definitions.d.ts.map +1 -0
  71. package/dist/service-definitions.js +18 -0
  72. package/dist/service-definitions.js.map +1 -0
  73. package/dist/shutdown-latch.d.ts +18 -0
  74. package/dist/shutdown-latch.d.ts.map +1 -0
  75. package/dist/shutdown-latch.js +51 -0
  76. package/dist/shutdown-latch.js.map +1 -0
  77. package/package.json +69 -0
  78. package/src/connection.ts +235 -0
  79. package/src/errors.ts +173 -0
  80. package/src/event-processor-info.ts +53 -0
  81. package/src/flow-controlled-sender.ts +73 -0
  82. package/src/generated/command.ts +1226 -0
  83. package/src/generated/common.ts +770 -0
  84. package/src/generated/eventstore.ts +2241 -0
  85. package/src/generated/platform.ts +1914 -0
  86. package/src/generated/query.ts +2571 -0
  87. package/src/generated/snapshot.ts +1110 -0
  88. package/src/index.ts +87 -0
  89. package/src/kronosdb-event-store.ts +401 -0
  90. package/src/kronosdb-snapshot-store.ts +104 -0
  91. package/src/kronosdb.ts +1000 -0
  92. package/src/metadata-conversion.ts +85 -0
  93. package/src/outbound-stream.ts +52 -0
  94. package/src/platform-service.ts +297 -0
  95. package/src/service-definitions.ts +25 -0
  96. 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
+ }