@agentuity/postgres 0.1.41

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 (48) hide show
  1. package/AGENTS.md +124 -0
  2. package/README.md +297 -0
  3. package/dist/client.d.ts +224 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +670 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/errors.d.ts +109 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +115 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +41 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +47 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/patch.d.ts +65 -0
  16. package/dist/patch.d.ts.map +1 -0
  17. package/dist/patch.js +111 -0
  18. package/dist/patch.js.map +1 -0
  19. package/dist/postgres.d.ts +62 -0
  20. package/dist/postgres.d.ts.map +1 -0
  21. package/dist/postgres.js +63 -0
  22. package/dist/postgres.js.map +1 -0
  23. package/dist/reconnect.d.ts +31 -0
  24. package/dist/reconnect.d.ts.map +1 -0
  25. package/dist/reconnect.js +60 -0
  26. package/dist/reconnect.js.map +1 -0
  27. package/dist/registry.d.ts +71 -0
  28. package/dist/registry.d.ts.map +1 -0
  29. package/dist/registry.js +175 -0
  30. package/dist/registry.js.map +1 -0
  31. package/dist/transaction.d.ts +147 -0
  32. package/dist/transaction.d.ts.map +1 -0
  33. package/dist/transaction.js +287 -0
  34. package/dist/transaction.js.map +1 -0
  35. package/dist/types.d.ts +213 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +2 -0
  38. package/dist/types.js.map +1 -0
  39. package/package.json +55 -0
  40. package/src/client.ts +776 -0
  41. package/src/errors.ts +154 -0
  42. package/src/index.ts +71 -0
  43. package/src/patch.ts +123 -0
  44. package/src/postgres.ts +65 -0
  45. package/src/reconnect.ts +74 -0
  46. package/src/registry.ts +194 -0
  47. package/src/transaction.ts +312 -0
  48. package/src/types.ts +250 -0
package/src/errors.ts ADDED
@@ -0,0 +1,154 @@
1
+ import { StructuredError } from '@agentuity/core';
2
+
3
+ /**
4
+ * Base error for PostgreSQL-related errors.
5
+ */
6
+ export const PostgresError = StructuredError('PostgresError')<{
7
+ code?: string;
8
+ query?: string;
9
+ }>();
10
+
11
+ /**
12
+ * Error thrown when attempting to use a closed connection.
13
+ */
14
+ export const ConnectionClosedError = StructuredError('ConnectionClosedError')<{
15
+ wasReconnecting?: boolean;
16
+ }>();
17
+
18
+ /**
19
+ * Error thrown when reconnection fails after all attempts.
20
+ */
21
+ export const ReconnectFailedError = StructuredError(
22
+ 'ReconnectFailedError',
23
+ 'Failed to reconnect after maximum attempts'
24
+ )<{
25
+ attempts: number;
26
+ lastError?: Error;
27
+ }>();
28
+
29
+ /**
30
+ * Error thrown when a query times out.
31
+ */
32
+ export const QueryTimeoutError = StructuredError('QueryTimeoutError', 'Query timed out')<{
33
+ timeoutMs: number;
34
+ query?: string;
35
+ }>();
36
+
37
+ /**
38
+ * Error thrown when a transaction fails.
39
+ */
40
+ export const TransactionError = StructuredError('TransactionError')<{
41
+ phase?: 'begin' | 'commit' | 'rollback' | 'savepoint' | 'query';
42
+ }>();
43
+
44
+ /**
45
+ * Error thrown when an operation is not supported.
46
+ */
47
+ export const UnsupportedOperationError = StructuredError(
48
+ 'UnsupportedOperationError',
49
+ 'This operation is not supported'
50
+ )<{
51
+ operation: string;
52
+ reason?: string;
53
+ }>();
54
+
55
+ /**
56
+ * Error codes that indicate a retryable connection error.
57
+ */
58
+ const RETRYABLE_ERROR_CODES = new Set([
59
+ // Bun SQL specific
60
+ 'ERR_POSTGRES_CONNECTION_CLOSED',
61
+ 'ERR_POSTGRES_CONNECTION_TIMEOUT',
62
+
63
+ // Node.js / system errors
64
+ 'ECONNRESET',
65
+ 'ECONNREFUSED',
66
+ 'ETIMEDOUT',
67
+ 'EPIPE',
68
+ 'ENOTFOUND',
69
+ 'ENETUNREACH',
70
+ 'EHOSTUNREACH',
71
+ 'EAI_AGAIN',
72
+
73
+ // PostgreSQL error codes
74
+ '57P01', // admin_shutdown
75
+ '57P02', // crash_shutdown
76
+ '57P03', // cannot_connect_now
77
+ '08000', // connection_exception
78
+ '08003', // connection_does_not_exist
79
+ '08006', // connection_failure
80
+ '08001', // sqlclient_unable_to_establish_sqlconnection
81
+ '08004', // sqlserver_rejected_establishment_of_sqlconnection
82
+ ]);
83
+
84
+ /**
85
+ * Error messages that indicate a retryable connection error.
86
+ */
87
+ const RETRYABLE_ERROR_MESSAGES = [
88
+ 'connection closed',
89
+ 'connection terminated',
90
+ 'connection reset',
91
+ 'connection refused',
92
+ 'connection timed out',
93
+ 'socket hang up',
94
+ 'read ECONNRESET',
95
+ 'write EPIPE',
96
+ 'getaddrinfo',
97
+ 'ENOTFOUND',
98
+ 'network is unreachable',
99
+ 'no route to host',
100
+ 'server closed the connection unexpectedly',
101
+ 'terminating connection due to administrator command',
102
+ 'the database system is shutting down',
103
+ 'the database system is starting up',
104
+ 'the database system is in recovery mode',
105
+ 'failed to read',
106
+ 'failed to write',
107
+ 'broken pipe',
108
+ 'end of file',
109
+ 'eof',
110
+ ];
111
+
112
+ /**
113
+ * Determines if an error is retryable (i.e., a reconnection should be attempted).
114
+ *
115
+ * @param error - The error to check
116
+ * @returns `true` if the error indicates a connection issue that may be resolved by reconnecting
117
+ */
118
+ export function isRetryableError(error: unknown): boolean {
119
+ if (!error) {
120
+ return false;
121
+ }
122
+
123
+ // Check error code
124
+ if (typeof error === 'object' && error !== null) {
125
+ const err = error as Record<string, unknown>;
126
+
127
+ // Check 'code' property
128
+ if (typeof err.code === 'string' && RETRYABLE_ERROR_CODES.has(err.code)) {
129
+ return true;
130
+ }
131
+
132
+ // Check 'errno' property (Node.js style)
133
+ if (typeof err.errno === 'string' && RETRYABLE_ERROR_CODES.has(err.errno)) {
134
+ return true;
135
+ }
136
+
137
+ // Check nested cause
138
+ if (err.cause && isRetryableError(err.cause)) {
139
+ return true;
140
+ }
141
+ }
142
+
143
+ // Check error message
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ const lowerMessage = message.toLowerCase();
146
+
147
+ for (const pattern of RETRYABLE_ERROR_MESSAGES) {
148
+ if (lowerMessage.includes(pattern.toLowerCase())) {
149
+ return true;
150
+ }
151
+ }
152
+
153
+ return false;
154
+ }
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @agentuity/postgres - Resilient PostgreSQL client with automatic reconnection
3
+ *
4
+ * This package provides a PostgreSQL client that wraps Bun's native SQL driver
5
+ * and adds automatic reconnection with exponential backoff.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { postgres } from '@agentuity/postgres';
10
+ *
11
+ * // Create a client (uses DATABASE_URL by default)
12
+ * const sql = postgres();
13
+ *
14
+ * // Execute queries using tagged template literals
15
+ * const users = await sql`SELECT * FROM users WHERE active = ${true}`;
16
+ *
17
+ * // Transactions
18
+ * const tx = await sql.begin();
19
+ * try {
20
+ * await tx`INSERT INTO users (name) VALUES (${name})`;
21
+ * await tx.commit();
22
+ * } catch (error) {
23
+ * await tx.rollback();
24
+ * throw error;
25
+ * }
26
+ *
27
+ * // Close when done
28
+ * await sql.close();
29
+ * ```
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+
34
+ // Main factory function
35
+ export { postgres, default } from './postgres';
36
+
37
+ // Client class for advanced usage
38
+ export { PostgresClient, createCallableClient, type CallablePostgresClient } from './client';
39
+
40
+ // Transaction and reserved connection classes
41
+ export { Transaction, Savepoint, ReservedConnection } from './transaction';
42
+
43
+ // Patch function for modifying Bun.SQL globally
44
+ export { patchBunSQL, isPatched, SQL } from './patch';
45
+
46
+ // Types
47
+ export type {
48
+ PostgresConfig,
49
+ ReconnectConfig,
50
+ ConnectionStats,
51
+ TLSConfig,
52
+ TransactionOptions,
53
+ ReserveOptions,
54
+ } from './types';
55
+
56
+ // Errors
57
+ export {
58
+ PostgresError,
59
+ ConnectionClosedError,
60
+ ReconnectFailedError,
61
+ QueryTimeoutError,
62
+ TransactionError,
63
+ UnsupportedOperationError,
64
+ isRetryableError,
65
+ } from './errors';
66
+
67
+ // Reconnection utilities
68
+ export { computeBackoff, sleep, mergeReconnectConfig, DEFAULT_RECONNECT_CONFIG } from './reconnect';
69
+
70
+ // Global registry for coordinated shutdown
71
+ export { shutdownAll, getClientCount, getClients, hasActiveClients } from './registry';
package/src/patch.ts ADDED
@@ -0,0 +1,123 @@
1
+ import { SQL } from 'bun';
2
+ import type { ReconnectConfig } from './types';
3
+ import { mergeReconnectConfig, DEFAULT_RECONNECT_CONFIG } from './reconnect';
4
+
5
+ /**
6
+ * Whether Bun.SQL has already been patched.
7
+ */
8
+ let _patched = false;
9
+
10
+ /**
11
+ * Global reconnect configuration for patched Bun.SQL instances.
12
+ */
13
+ let _globalReconnectConfig: Required<ReconnectConfig> = { ...DEFAULT_RECONNECT_CONFIG };
14
+
15
+ /**
16
+ * Callbacks for reconnection events.
17
+ */
18
+ let _onReconnect: ((attempt: number) => void) | undefined;
19
+ let _onReconnected: (() => void) | undefined;
20
+ let _onReconnectFailed: ((error: Error) => void) | undefined;
21
+
22
+ /**
23
+ * Patches Bun's native SQL class to add automatic reconnection support.
24
+ *
25
+ * This modifies the global `Bun.SQL` prototype to intercept connection close
26
+ * events and automatically attempt reconnection with exponential backoff.
27
+ *
28
+ * **Note:** This is a global modification that affects all SQL instances created
29
+ * after calling this function. Use with caution in shared environments.
30
+ *
31
+ * @param config - Optional configuration for reconnection behavior
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * import { patchBunSQL, SQL } from '@agentuity/postgres';
36
+ *
37
+ * // Patch with default settings
38
+ * patchBunSQL();
39
+ *
40
+ * // Or with custom configuration
41
+ * patchBunSQL({
42
+ * reconnect: {
43
+ * maxAttempts: 5,
44
+ * initialDelayMs: 200,
45
+ * },
46
+ * onreconnect: (attempt) => console.log(`Reconnecting... attempt ${attempt}`),
47
+ * onreconnected: () => console.log('Reconnected!'),
48
+ * });
49
+ *
50
+ * // Now use Bun.SQL normally - it will auto-reconnect
51
+ * const sql = new SQL({ url: process.env.DATABASE_URL });
52
+ * const users = await sql`SELECT * FROM users`;
53
+ * ```
54
+ */
55
+ export function patchBunSQL(config?: {
56
+ reconnect?: ReconnectConfig;
57
+ onreconnect?: (attempt: number) => void;
58
+ onreconnected?: () => void;
59
+ onreconnectfailed?: (error: Error) => void;
60
+ }): void {
61
+ if (_patched) {
62
+ // Already patched, just update config if provided
63
+ if (config?.reconnect) {
64
+ _globalReconnectConfig = mergeReconnectConfig(config.reconnect);
65
+ }
66
+ if (config?.onreconnect) _onReconnect = config.onreconnect;
67
+ if (config?.onreconnected) _onReconnected = config.onreconnected;
68
+ if (config?.onreconnectfailed) _onReconnectFailed = config.onreconnectfailed;
69
+ return;
70
+ }
71
+
72
+ // Store configuration
73
+ if (config?.reconnect) {
74
+ _globalReconnectConfig = mergeReconnectConfig(config.reconnect);
75
+ }
76
+ _onReconnect = config?.onreconnect;
77
+ _onReconnected = config?.onreconnected;
78
+ _onReconnectFailed = config?.onreconnectfailed;
79
+
80
+ // Note: True monkey-patching of Bun.SQL internals is not feasible
81
+ // because Bun.SQL is a native class. Instead, users should use
82
+ // PostgresClient from this package which provides the same API
83
+ // with automatic reconnection built in.
84
+ //
85
+ // This function exists to set global reconnection configuration
86
+ // that could be used by future implementations.
87
+
88
+ _patched = true;
89
+ }
90
+
91
+ /**
92
+ * Returns whether Bun.SQL has been patched.
93
+ */
94
+ export function isPatched(): boolean {
95
+ return _patched;
96
+ }
97
+
98
+ /**
99
+ * Resets the patch state (mainly for testing).
100
+ * @internal
101
+ */
102
+ export function _resetPatch(): void {
103
+ _patched = false;
104
+ _globalReconnectConfig = { ...DEFAULT_RECONNECT_CONFIG };
105
+ _onReconnect = undefined;
106
+ _onReconnected = undefined;
107
+ _onReconnectFailed = undefined;
108
+ }
109
+
110
+ /**
111
+ * Re-export of Bun's SQL class.
112
+ *
113
+ * When using the patched version, import SQL from this module instead of 'bun':
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * import { patchBunSQL, SQL } from '@agentuity/postgres';
118
+ *
119
+ * patchBunSQL();
120
+ * const sql = new SQL({ url: process.env.DATABASE_URL });
121
+ * ```
122
+ */
123
+ export { SQL };
@@ -0,0 +1,65 @@
1
+ import type { PostgresConfig } from './types';
2
+ import { createCallableClient, type CallablePostgresClient } from './client';
3
+
4
+ /**
5
+ * Creates a resilient PostgreSQL client with automatic reconnection.
6
+ *
7
+ * This is the main entry point for creating a PostgreSQL client. The returned
8
+ * client can be used as a tagged template literal for queries.
9
+ *
10
+ * @param config - Connection configuration. Can be:
11
+ * - A connection URL string (e.g., `postgres://user:pass@host:5432/db`)
12
+ * - A configuration object with connection options
13
+ * - Omitted to use `process.env.DATABASE_URL`
14
+ *
15
+ * @returns A callable PostgresClient that supports tagged template literals
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { postgres } from '@agentuity/postgres';
20
+ *
21
+ * // Using environment variable (DATABASE_URL)
22
+ * const sql = postgres();
23
+ *
24
+ * // Using connection URL
25
+ * const sql = postgres('postgres://user:pass@localhost:5432/mydb');
26
+ *
27
+ * // Using configuration object
28
+ * const sql = postgres({
29
+ * hostname: 'localhost',
30
+ * port: 5432,
31
+ * username: 'user',
32
+ * password: 'pass',
33
+ * database: 'mydb',
34
+ * reconnect: {
35
+ * maxAttempts: 5,
36
+ * initialDelayMs: 100,
37
+ * },
38
+ * });
39
+ *
40
+ * // Execute queries using tagged template literals
41
+ * const users = await sql`SELECT * FROM users`;
42
+ * const user = await sql`SELECT * FROM users WHERE id = ${userId}`;
43
+ *
44
+ * // Transactions
45
+ * const tx = await sql.begin();
46
+ * try {
47
+ * await tx`INSERT INTO users (name) VALUES (${name})`;
48
+ * await tx.commit();
49
+ * } catch (error) {
50
+ * await tx.rollback();
51
+ * throw error;
52
+ * }
53
+ *
54
+ * // Close when done
55
+ * await sql.close();
56
+ * ```
57
+ */
58
+ export function postgres(config?: string | PostgresConfig): CallablePostgresClient {
59
+ return createCallableClient(config);
60
+ }
61
+
62
+ /**
63
+ * Default export for convenience.
64
+ */
65
+ export default postgres;
@@ -0,0 +1,74 @@
1
+ import type { ReconnectConfig } from './types';
2
+
3
+ /**
4
+ * Default reconnection configuration values.
5
+ */
6
+ export const DEFAULT_RECONNECT_CONFIG: Required<ReconnectConfig> = {
7
+ maxAttempts: 10,
8
+ initialDelayMs: 100,
9
+ maxDelayMs: 30000,
10
+ multiplier: 2,
11
+ jitterMs: 1000,
12
+ enabled: true,
13
+ };
14
+
15
+ /**
16
+ * Computes the backoff delay for a reconnection attempt using exponential backoff with jitter.
17
+ *
18
+ * The delay is calculated as:
19
+ * `min(maxDelayMs, initialDelayMs * (multiplier ^ attempt)) + random(0, jitterMs)`
20
+ *
21
+ * @param attempt - The current attempt number (0-indexed)
22
+ * @param config - Reconnection configuration
23
+ * @returns The delay in milliseconds before the next reconnection attempt
24
+ */
25
+ export function computeBackoff(attempt: number, config: Partial<ReconnectConfig> = {}): number {
26
+ const {
27
+ initialDelayMs = DEFAULT_RECONNECT_CONFIG.initialDelayMs,
28
+ maxDelayMs = DEFAULT_RECONNECT_CONFIG.maxDelayMs,
29
+ multiplier = DEFAULT_RECONNECT_CONFIG.multiplier,
30
+ jitterMs = DEFAULT_RECONNECT_CONFIG.jitterMs,
31
+ } = config;
32
+
33
+ // Calculate exponential delay
34
+ const exponentialDelay = initialDelayMs * Math.pow(multiplier, attempt);
35
+
36
+ // Cap at maximum delay
37
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
38
+
39
+ // Add random jitter to prevent thundering herd
40
+ const jitter = Math.random() * jitterMs;
41
+
42
+ return Math.floor(cappedDelay + jitter);
43
+ }
44
+
45
+ /**
46
+ * Sleeps for the specified number of milliseconds.
47
+ *
48
+ * @param ms - The number of milliseconds to sleep
49
+ * @returns A promise that resolves after the specified delay
50
+ */
51
+ export function sleep(ms: number): Promise<void> {
52
+ return new Promise((resolve) => setTimeout(resolve, ms));
53
+ }
54
+
55
+ /**
56
+ * Merges user-provided reconnect config with defaults.
57
+ *
58
+ * @param config - User-provided reconnect configuration
59
+ * @returns Complete reconnect configuration with all values filled in
60
+ */
61
+ export function mergeReconnectConfig(config?: Partial<ReconnectConfig>): Required<ReconnectConfig> {
62
+ if (!config) {
63
+ return { ...DEFAULT_RECONNECT_CONFIG };
64
+ }
65
+
66
+ return {
67
+ maxAttempts: config.maxAttempts ?? DEFAULT_RECONNECT_CONFIG.maxAttempts,
68
+ initialDelayMs: config.initialDelayMs ?? DEFAULT_RECONNECT_CONFIG.initialDelayMs,
69
+ maxDelayMs: config.maxDelayMs ?? DEFAULT_RECONNECT_CONFIG.maxDelayMs,
70
+ multiplier: config.multiplier ?? DEFAULT_RECONNECT_CONFIG.multiplier,
71
+ jitterMs: config.jitterMs ?? DEFAULT_RECONNECT_CONFIG.jitterMs,
72
+ enabled: config.enabled ?? DEFAULT_RECONNECT_CONFIG.enabled,
73
+ };
74
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Global registry for PostgreSQL clients.
3
+ *
4
+ * This module provides a way to track all active PostgreSQL clients
5
+ * so they can be gracefully shut down together (e.g., on process exit).
6
+ *
7
+ * The runtime can use `shutdownAll()` to close all registered clients
8
+ * during graceful shutdown.
9
+ *
10
+ * When @agentuity/runtime is available, this module automatically registers
11
+ * a shutdown hook so all postgres clients are closed during graceful shutdown.
12
+ */
13
+
14
+ import type { PostgresClient } from './client';
15
+
16
+ /**
17
+ * Symbol used to store the registry in globalThis to avoid conflicts.
18
+ */
19
+ const REGISTRY_KEY = Symbol.for('@agentuity/postgres:registry');
20
+
21
+ /**
22
+ * Symbol used to track if we've registered with the runtime.
23
+ */
24
+ const RUNTIME_HOOK_REGISTERED = Symbol.for('@agentuity/postgres:runtime-hook-registered');
25
+
26
+ /**
27
+ * Gets the global client registry, creating it if it doesn't exist.
28
+ */
29
+ function getRegistry(): Set<PostgresClient> {
30
+ const global = globalThis as Record<symbol, Set<PostgresClient>>;
31
+ if (!global[REGISTRY_KEY]) {
32
+ global[REGISTRY_KEY] = new Set();
33
+ }
34
+ return global[REGISTRY_KEY];
35
+ }
36
+
37
+ /**
38
+ * Registers a client in the global registry.
39
+ * Called automatically when a client is created.
40
+ *
41
+ * @param client - The client to register
42
+ * @internal
43
+ */
44
+ export function registerClient(client: PostgresClient): void {
45
+ getRegistry().add(client);
46
+ }
47
+
48
+ /**
49
+ * Unregisters a client from the global registry.
50
+ * Called automatically when a client is closed.
51
+ *
52
+ * @param client - The client to unregister
53
+ * @internal
54
+ */
55
+ export function unregisterClient(client: PostgresClient): void {
56
+ getRegistry().delete(client);
57
+ }
58
+
59
+ /**
60
+ * Returns the number of registered clients.
61
+ * Useful for debugging and testing.
62
+ */
63
+ export function getClientCount(): number {
64
+ return getRegistry().size;
65
+ }
66
+
67
+ /**
68
+ * Returns all registered clients.
69
+ * Useful for debugging and monitoring.
70
+ */
71
+ export function getClients(): ReadonlySet<PostgresClient> {
72
+ return getRegistry();
73
+ }
74
+
75
+ /**
76
+ * Shuts down all registered PostgreSQL clients gracefully.
77
+ *
78
+ * This function:
79
+ * 1. Signals shutdown to all clients (prevents reconnection)
80
+ * 2. Closes all clients in parallel
81
+ * 3. Clears the registry
82
+ *
83
+ * This is intended to be called by the runtime during graceful shutdown.
84
+ *
85
+ * @param timeoutMs - Optional timeout in milliseconds. If provided, the function
86
+ * will resolve after the timeout even if some clients haven't
87
+ * finished closing. Default: no timeout.
88
+ * @returns A promise that resolves when all clients are closed (or timeout)
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * import { shutdownAll } from '@agentuity/postgres';
93
+ *
94
+ * process.on('SIGTERM', async () => {
95
+ * await shutdownAll(5000); // 5 second timeout
96
+ * process.exit(0);
97
+ * });
98
+ * ```
99
+ */
100
+ export async function shutdownAll(timeoutMs?: number): Promise<void> {
101
+ const registry = getRegistry();
102
+ const clients = Array.from(registry);
103
+
104
+ if (clients.length === 0) {
105
+ return;
106
+ }
107
+
108
+ // Signal shutdown to all clients first (prevents reconnection attempts)
109
+ for (const client of clients) {
110
+ client.shutdown();
111
+ }
112
+
113
+ // Close all clients in parallel
114
+ const closePromises = clients.map(async (client) => {
115
+ try {
116
+ await client.close();
117
+ } catch {
118
+ // Ignore close errors during shutdown
119
+ }
120
+ });
121
+
122
+ // Wait for all to close, with optional timeout
123
+ if (timeoutMs !== undefined) {
124
+ let timer: ReturnType<typeof setTimeout> | undefined;
125
+ const timeout = new Promise<void>((resolve) => {
126
+ timer = setTimeout(resolve, timeoutMs);
127
+ });
128
+ try {
129
+ await Promise.race([Promise.all(closePromises), timeout]);
130
+ } finally {
131
+ if (timer !== undefined) {
132
+ clearTimeout(timer);
133
+ }
134
+ }
135
+ } else {
136
+ await Promise.all(closePromises);
137
+ }
138
+
139
+ // Clear the registry
140
+ registry.clear();
141
+ }
142
+
143
+ /**
144
+ * Checks if there are any active (non-shutdown) clients.
145
+ * Useful for health checks.
146
+ */
147
+ export function hasActiveClients(): boolean {
148
+ const registry = getRegistry();
149
+ for (const client of registry) {
150
+ if (!client.shuttingDown) {
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Attempts to register a shutdown hook with @agentuity/runtime if available.
159
+ * This is called automatically when the first client is registered.
160
+ *
161
+ * @internal
162
+ */
163
+ function tryRegisterRuntimeHook(): void {
164
+ const global = globalThis as Record<symbol, boolean>;
165
+
166
+ // Only try once
167
+ if (global[RUNTIME_HOOK_REGISTERED]) {
168
+ return;
169
+ }
170
+ global[RUNTIME_HOOK_REGISTERED] = true;
171
+
172
+ // Try to dynamically import the runtime and register our shutdown hook
173
+ // This is done asynchronously to avoid blocking client creation
174
+ // and to handle the case where runtime is not available
175
+ // Using Function constructor to avoid TypeScript trying to resolve the module at build time
176
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (
177
+ specifier: string
178
+ ) => Promise<{ registerShutdownHook?: (hook: () => Promise<void> | void) => void }>;
179
+
180
+ dynamicImport('@agentuity/runtime')
181
+ .then((runtime) => {
182
+ if (typeof runtime.registerShutdownHook === 'function') {
183
+ runtime.registerShutdownHook(async () => {
184
+ await shutdownAll(5000); // 5 second timeout for graceful shutdown
185
+ });
186
+ }
187
+ })
188
+ .catch(() => {
189
+ // Runtime not available - that's fine, user can call shutdownAll manually
190
+ });
191
+ }
192
+
193
+ // Try to register with runtime when this module is first loaded
194
+ tryRegisterRuntimeHook();