@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.
- package/AGENTS.md +124 -0
- package/README.md +297 -0
- package/dist/client.d.ts +224 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +670 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +115 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/patch.d.ts +65 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +111 -0
- package/dist/patch.js.map +1 -0
- package/dist/postgres.d.ts +62 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +63 -0
- package/dist/postgres.js.map +1 -0
- package/dist/reconnect.d.ts +31 -0
- package/dist/reconnect.d.ts.map +1 -0
- package/dist/reconnect.js +60 -0
- package/dist/reconnect.js.map +1 -0
- package/dist/registry.d.ts +71 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +175 -0
- package/dist/registry.js.map +1 -0
- package/dist/transaction.d.ts +147 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +287 -0
- package/dist/transaction.js.map +1 -0
- package/dist/types.d.ts +213 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
- package/src/client.ts +776 -0
- package/src/errors.ts +154 -0
- package/src/index.ts +71 -0
- package/src/patch.ts +123 -0
- package/src/postgres.ts +65 -0
- package/src/reconnect.ts +74 -0
- package/src/registry.ts +194 -0
- package/src/transaction.ts +312 -0
- 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 };
|
package/src/postgres.ts
ADDED
|
@@ -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;
|
package/src/reconnect.ts
ADDED
|
@@ -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
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -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();
|