refactor(api): tidy the api and shared packages (#2733)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 17:37:48 +02:00
1 parent 580401d2dc
commit 5afbf67a70
333 files changed
+10180 -7910

No files matched your search

@@ -2,30 +2,56 @@
import type {INatsConnectionManager} from '@pkgs/nats/src/INatsConnectionManager';
import type {NatsConnectionOptions} from '@pkgs/nats/src/NatsConnectionOptions';
import {connect, type NatsConnection} from 'nats';
import {connect, ErrorCode, type NatsConnection, NatsError} from 'nats';
const DEFAULT_MAX_RECONNECT_ATTEMPTS = -1;
const DEFAULT_RECONNECT_TIME_WAIT_MS = 500;
const DEFAULT_CONNECT_TIMEOUT_MS = 5000;
const DEFAULT_DRAIN_TIMEOUT_MS = 5000;
const MAX_TIMER_DELAY_MS = 2_147_483_647;
interface NatsDrainDeadline {
expired: boolean;
promise: Promise<never>;
}
export class NatsConnectionManager implements INatsConnectionManager {
private connection: NatsConnection | null = null;
private connectPromise: Promise<NatsConnection> | null = null;
private connectPromise: Promise<void> | null = null;
private drainPromise: Promise<void> | null = null;
private drainGeneration = 0;
private readonly options: NatsConnectionOptions;
private readonly drainTimeoutMs: number;
constructor(options: NatsConnectionOptions) {
this.options = options;
this.drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
if (
!Number.isSafeInteger(this.drainTimeoutMs) ||
this.drainTimeoutMs < 1 ||
this.drainTimeoutMs > MAX_TIMER_DELAY_MS
) {
throw new RangeError(`NATS drainTimeoutMs must be an integer between 1 and ${MAX_TIMER_DELAY_MS}`);
}
}
async connect(): Promise<void> {
this.assertNotDraining();
if (this.connection !== null && !this.connection.isClosed()) {
return;
}
if (this.connectPromise !== null) {
this.connection = await this.connectPromise;
return;
const generation = this.drainGeneration;
this.connectPromise ??= this.openConnection().finally(() => {
this.connectPromise = null;
});
await this.connectPromise;
if (generation !== this.drainGeneration) {
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
}
const connectPromise = connect({
}
private async openConnection(): Promise<void> {
this.connection = await connect({
servers: this.options.url,
token: this.options.token || undefined,
name: this.options.name,
@@ -33,39 +59,88 @@ export class NatsConnectionManager implements INatsConnectionManager {
reconnectTimeWait: this.options.reconnectTimeWaitMs ?? DEFAULT_RECONNECT_TIME_WAIT_MS,
timeout: this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
});
this.connectPromise = connectPromise;
try {
this.connection = await connectPromise;
} finally {
if (this.connectPromise === connectPromise) {
this.connectPromise = null;
}
}
}
getConnection(): NatsConnection {
this.assertNotDraining();
if (this.connection === null || this.connection.isClosed()) {
throw new Error('NATS connection is not established. Call connect() first.');
}
return this.connection;
}
async drain(): Promise<void> {
if (this.connectPromise !== null) {
drain(): Promise<void> {
if (this.drainPromise !== null) {
return this.drainPromise;
}
if (this.connection === null && this.connectPromise === null) {
return Promise.resolve();
}
this.drainGeneration++;
const connectPromise = this.connectPromise;
const timeout = Promise.withResolvers<never>();
const deadline: NatsDrainDeadline = {expired: false, promise: timeout.promise};
const timeoutError = new Error(`NATS connection drain exceeded ${this.drainTimeoutMs}ms`);
const timer = setTimeout(() => {
deadline.expired = true;
timeout.reject(timeoutError);
}, this.drainTimeoutMs);
const cleanup = Promise.resolve()
.then(() => this.drainConnection(connectPromise, deadline))
.catch((error: unknown) => {
if (deadline.expired && error !== timeoutError) {
process.emitWarning(new Error('NATS connection cleanup failed after its drain deadline', {cause: error}));
}
throw error;
})
.finally(() => {
clearTimeout(timer);
this.drainPromise = null;
});
this.drainPromise = Promise.race([cleanup, deadline.promise]);
return this.drainPromise;
}
private async drainConnection(connectPromise: Promise<void> | null, deadline: NatsDrainDeadline): Promise<void> {
if (connectPromise !== null) {
try {
this.connection = await this.connectPromise;
await connectPromise;
} catch {
this.connectPromise = null;
return;
}
}
if (this.connection === null) {
const connection = this.connection;
if (connection === null) {
return;
}
if (!this.connection.isClosed()) {
await this.connection.drain();
const errors: Array<unknown> = [];
try {
if (!deadline.expired && !connection.isClosed()) {
await Promise.race([connection.drain(), deadline.promise]);
if (!connection.isClosed()) {
throw new Error('NATS connection remained open after draining.');
}
}
} catch (error) {
errors.push(error);
}
try {
await connection.close();
} catch (error) {
errors.push(error);
} finally {
if (this.connection === connection) {
this.connection = null;
}
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw new AggregateError(errors, 'NATS connection drain and close failed');
}
private assertNotDraining(): void {
if (this.drainPromise !== null) {
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
}
this.connection = null;
}
isClosed(): boolean {
@@ -7,4 +7,5 @@ export interface NatsConnectionOptions {
maxReconnectAttempts?: number;
reconnectTimeWaitMs?: number;
connectTimeoutMs?: number;
drainTimeoutMs?: number;
}