@push.rocks/smartproxy 19.5.4 → 19.5.6
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_ts/core/utils/async-utils.d.ts +81 -0
- package/dist_ts/core/utils/async-utils.js +216 -0
- package/dist_ts/core/utils/binary-heap.d.ts +73 -0
- package/dist_ts/core/utils/binary-heap.js +193 -0
- package/dist_ts/core/utils/enhanced-connection-pool.d.ts +110 -0
- package/dist_ts/core/utils/enhanced-connection-pool.js +320 -0
- package/dist_ts/core/utils/fs-utils.d.ts +144 -0
- package/dist_ts/core/utils/fs-utils.js +252 -0
- package/dist_ts/core/utils/index.d.ts +6 -2
- package/dist_ts/core/utils/index.js +7 -3
- package/dist_ts/core/utils/lifecycle-component.d.ts +59 -0
- package/dist_ts/core/utils/lifecycle-component.js +195 -0
- package/dist_ts/core/utils/socket-utils.d.ts +28 -0
- package/dist_ts/core/utils/socket-utils.js +77 -0
- package/dist_ts/forwarding/handlers/http-handler.js +7 -4
- package/dist_ts/forwarding/handlers/https-passthrough-handler.js +14 -55
- package/dist_ts/forwarding/handlers/https-terminate-to-http-handler.js +52 -40
- package/dist_ts/forwarding/handlers/https-terminate-to-https-handler.js +31 -43
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/proxies/http-proxy/certificate-manager.d.ts +15 -0
- package/dist_ts/proxies/http-proxy/certificate-manager.js +49 -2
- package/dist_ts/proxies/http-proxy/connection-pool.js +4 -19
- package/dist_ts/proxies/http-proxy/http-proxy.js +3 -7
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +10 -0
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +53 -43
- package/dist_ts/proxies/smart-proxy/cert-store.js +22 -20
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +35 -9
- package/dist_ts/proxies/smart-proxy/connection-manager.js +243 -189
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +13 -2
- package/dist_ts/proxies/smart-proxy/port-manager.js +3 -3
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +35 -4
- package/package.json +2 -2
- package/readme.hints.md +96 -1
- package/readme.plan.md +1135 -221
- package/readme.problems.md +167 -83
- package/ts/core/utils/async-utils.ts +275 -0
- package/ts/core/utils/binary-heap.ts +225 -0
- package/ts/core/utils/enhanced-connection-pool.ts +420 -0
- package/ts/core/utils/fs-utils.ts +270 -0
- package/ts/core/utils/index.ts +6 -2
- package/ts/core/utils/lifecycle-component.ts +231 -0
- package/ts/core/utils/socket-utils.ts +96 -0
- package/ts/forwarding/handlers/http-handler.ts +7 -3
- package/ts/forwarding/handlers/https-passthrough-handler.ts +13 -62
- package/ts/forwarding/handlers/https-terminate-to-http-handler.ts +58 -46
- package/ts/forwarding/handlers/https-terminate-to-https-handler.ts +38 -53
- package/ts/plugins.ts +2 -1
- package/ts/proxies/http-proxy/certificate-manager.ts +52 -1
- package/ts/proxies/http-proxy/connection-pool.ts +3 -16
- package/ts/proxies/http-proxy/http-proxy.ts +2 -5
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +64 -79
- package/ts/proxies/smart-proxy/cert-store.ts +26 -20
- package/ts/proxies/smart-proxy/connection-manager.ts +277 -197
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +15 -1
- package/ts/proxies/smart-proxy/port-manager.ts +2 -2
- package/ts/proxies/smart-proxy/route-connection-handler.ts +39 -4
- package/readme.plan2.md +0 -764
- package/ts/common/eventUtils.ts +0 -34
- package/ts/common/types.ts +0 -91
- package/ts/core/utils/event-system.ts +0 -376
- package/ts/core/utils/event-utils.ts +0 -25
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A binary heap implementation for efficient priority queue operations
|
|
3
|
+
* Supports O(log n) insert and extract operations
|
|
4
|
+
*/
|
|
5
|
+
export class BinaryHeap<T> {
|
|
6
|
+
private heap: T[] = [];
|
|
7
|
+
private keyMap?: Map<string, number>; // For efficient key-based lookups
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private compareFn: (a: T, b: T) => number,
|
|
11
|
+
private extractKey?: (item: T) => string
|
|
12
|
+
) {
|
|
13
|
+
if (extractKey) {
|
|
14
|
+
this.keyMap = new Map();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the current size of the heap
|
|
20
|
+
*/
|
|
21
|
+
public get size(): number {
|
|
22
|
+
return this.heap.length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if the heap is empty
|
|
27
|
+
*/
|
|
28
|
+
public isEmpty(): boolean {
|
|
29
|
+
return this.heap.length === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Peek at the top element without removing it
|
|
34
|
+
*/
|
|
35
|
+
public peek(): T | undefined {
|
|
36
|
+
return this.heap[0];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Insert a new item into the heap
|
|
41
|
+
* O(log n) time complexity
|
|
42
|
+
*/
|
|
43
|
+
public insert(item: T): void {
|
|
44
|
+
const index = this.heap.length;
|
|
45
|
+
this.heap.push(item);
|
|
46
|
+
|
|
47
|
+
if (this.keyMap && this.extractKey) {
|
|
48
|
+
const key = this.extractKey(item);
|
|
49
|
+
this.keyMap.set(key, index);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.bubbleUp(index);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract the top element from the heap
|
|
57
|
+
* O(log n) time complexity
|
|
58
|
+
*/
|
|
59
|
+
public extract(): T | undefined {
|
|
60
|
+
if (this.heap.length === 0) return undefined;
|
|
61
|
+
if (this.heap.length === 1) {
|
|
62
|
+
const item = this.heap.pop()!;
|
|
63
|
+
if (this.keyMap && this.extractKey) {
|
|
64
|
+
this.keyMap.delete(this.extractKey(item));
|
|
65
|
+
}
|
|
66
|
+
return item;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = this.heap[0];
|
|
70
|
+
const lastItem = this.heap.pop()!;
|
|
71
|
+
this.heap[0] = lastItem;
|
|
72
|
+
|
|
73
|
+
if (this.keyMap && this.extractKey) {
|
|
74
|
+
this.keyMap.delete(this.extractKey(result));
|
|
75
|
+
this.keyMap.set(this.extractKey(lastItem), 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.bubbleDown(0);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract an element that matches the predicate
|
|
84
|
+
* O(n) time complexity for search, O(log n) for extraction
|
|
85
|
+
*/
|
|
86
|
+
public extractIf(predicate: (item: T) => boolean): T | undefined {
|
|
87
|
+
const index = this.heap.findIndex(predicate);
|
|
88
|
+
if (index === -1) return undefined;
|
|
89
|
+
|
|
90
|
+
return this.extractAt(index);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract an element by its key (if extractKey was provided)
|
|
95
|
+
* O(log n) time complexity
|
|
96
|
+
*/
|
|
97
|
+
public extractByKey(key: string): T | undefined {
|
|
98
|
+
if (!this.keyMap || !this.extractKey) {
|
|
99
|
+
throw new Error('extractKey function must be provided to use key-based extraction');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const index = this.keyMap.get(key);
|
|
103
|
+
if (index === undefined) return undefined;
|
|
104
|
+
|
|
105
|
+
return this.extractAt(index);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a key exists in the heap
|
|
110
|
+
* O(1) time complexity
|
|
111
|
+
*/
|
|
112
|
+
public hasKey(key: string): boolean {
|
|
113
|
+
if (!this.keyMap) return false;
|
|
114
|
+
return this.keyMap.has(key);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all elements as an array (does not modify heap)
|
|
119
|
+
* O(n) time complexity
|
|
120
|
+
*/
|
|
121
|
+
public toArray(): T[] {
|
|
122
|
+
return [...this.heap];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear the heap
|
|
127
|
+
*/
|
|
128
|
+
public clear(): void {
|
|
129
|
+
this.heap = [];
|
|
130
|
+
if (this.keyMap) {
|
|
131
|
+
this.keyMap.clear();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract element at specific index
|
|
137
|
+
*/
|
|
138
|
+
private extractAt(index: number): T {
|
|
139
|
+
const item = this.heap[index];
|
|
140
|
+
|
|
141
|
+
if (this.keyMap && this.extractKey) {
|
|
142
|
+
this.keyMap.delete(this.extractKey(item));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (index === this.heap.length - 1) {
|
|
146
|
+
this.heap.pop();
|
|
147
|
+
return item;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const lastItem = this.heap.pop()!;
|
|
151
|
+
this.heap[index] = lastItem;
|
|
152
|
+
|
|
153
|
+
if (this.keyMap && this.extractKey) {
|
|
154
|
+
this.keyMap.set(this.extractKey(lastItem), index);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Try bubbling up first
|
|
158
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
159
|
+
if (parentIndex >= 0 && this.compareFn(this.heap[index], this.heap[parentIndex]) < 0) {
|
|
160
|
+
this.bubbleUp(index);
|
|
161
|
+
} else {
|
|
162
|
+
this.bubbleDown(index);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return item;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Bubble up element at given index to maintain heap property
|
|
170
|
+
*/
|
|
171
|
+
private bubbleUp(index: number): void {
|
|
172
|
+
while (index > 0) {
|
|
173
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
174
|
+
|
|
175
|
+
if (this.compareFn(this.heap[index], this.heap[parentIndex]) >= 0) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.swap(index, parentIndex);
|
|
180
|
+
index = parentIndex;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Bubble down element at given index to maintain heap property
|
|
186
|
+
*/
|
|
187
|
+
private bubbleDown(index: number): void {
|
|
188
|
+
const length = this.heap.length;
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const leftChild = 2 * index + 1;
|
|
192
|
+
const rightChild = 2 * index + 2;
|
|
193
|
+
let smallest = index;
|
|
194
|
+
|
|
195
|
+
if (leftChild < length &&
|
|
196
|
+
this.compareFn(this.heap[leftChild], this.heap[smallest]) < 0) {
|
|
197
|
+
smallest = leftChild;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (rightChild < length &&
|
|
201
|
+
this.compareFn(this.heap[rightChild], this.heap[smallest]) < 0) {
|
|
202
|
+
smallest = rightChild;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (smallest === index) break;
|
|
206
|
+
|
|
207
|
+
this.swap(index, smallest);
|
|
208
|
+
index = smallest;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Swap two elements in the heap
|
|
214
|
+
*/
|
|
215
|
+
private swap(i: number, j: number): void {
|
|
216
|
+
const temp = this.heap[i];
|
|
217
|
+
this.heap[i] = this.heap[j];
|
|
218
|
+
this.heap[j] = temp;
|
|
219
|
+
|
|
220
|
+
if (this.keyMap && this.extractKey) {
|
|
221
|
+
this.keyMap.set(this.extractKey(this.heap[i]), i);
|
|
222
|
+
this.keyMap.set(this.extractKey(this.heap[j]), j);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { LifecycleComponent } from './lifecycle-component.js';
|
|
2
|
+
import { BinaryHeap } from './binary-heap.js';
|
|
3
|
+
import { AsyncMutex } from './async-utils.js';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for pooled connection
|
|
8
|
+
*/
|
|
9
|
+
export interface IPooledConnection<T> {
|
|
10
|
+
id: string;
|
|
11
|
+
connection: T;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
lastUsedAt: number;
|
|
14
|
+
useCount: number;
|
|
15
|
+
inUse: boolean;
|
|
16
|
+
metadata?: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration options for the connection pool
|
|
21
|
+
*/
|
|
22
|
+
export interface IConnectionPoolOptions<T> {
|
|
23
|
+
minSize?: number;
|
|
24
|
+
maxSize?: number;
|
|
25
|
+
acquireTimeout?: number;
|
|
26
|
+
idleTimeout?: number;
|
|
27
|
+
maxUseCount?: number;
|
|
28
|
+
validateOnAcquire?: boolean;
|
|
29
|
+
validateOnReturn?: boolean;
|
|
30
|
+
queueTimeout?: number;
|
|
31
|
+
connectionFactory: () => Promise<T>;
|
|
32
|
+
connectionValidator?: (connection: T) => Promise<boolean>;
|
|
33
|
+
connectionDestroyer?: (connection: T) => Promise<void>;
|
|
34
|
+
onConnectionError?: (error: Error, connection?: T) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Interface for queued acquire request
|
|
39
|
+
*/
|
|
40
|
+
interface IAcquireRequest<T> {
|
|
41
|
+
id: string;
|
|
42
|
+
priority: number;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
resolve: (connection: IPooledConnection<T>) => void;
|
|
45
|
+
reject: (error: Error) => void;
|
|
46
|
+
timeoutHandle?: NodeJS.Timeout;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enhanced connection pool with priority queue, backpressure, and lifecycle management
|
|
51
|
+
*/
|
|
52
|
+
export class EnhancedConnectionPool<T> extends LifecycleComponent {
|
|
53
|
+
private readonly options: Required<Omit<IConnectionPoolOptions<T>, 'connectionValidator' | 'connectionDestroyer' | 'onConnectionError'>> & Pick<IConnectionPoolOptions<T>, 'connectionValidator' | 'connectionDestroyer' | 'onConnectionError'>;
|
|
54
|
+
private readonly availableConnections: IPooledConnection<T>[] = [];
|
|
55
|
+
private readonly activeConnections: Map<string, IPooledConnection<T>> = new Map();
|
|
56
|
+
private readonly waitQueue: BinaryHeap<IAcquireRequest<T>>;
|
|
57
|
+
private readonly mutex = new AsyncMutex();
|
|
58
|
+
private readonly eventEmitter = new EventEmitter();
|
|
59
|
+
|
|
60
|
+
private connectionIdCounter = 0;
|
|
61
|
+
private requestIdCounter = 0;
|
|
62
|
+
private isClosing = false;
|
|
63
|
+
|
|
64
|
+
// Metrics
|
|
65
|
+
private metrics = {
|
|
66
|
+
connectionsCreated: 0,
|
|
67
|
+
connectionsDestroyed: 0,
|
|
68
|
+
connectionsAcquired: 0,
|
|
69
|
+
connectionsReleased: 0,
|
|
70
|
+
acquireTimeouts: 0,
|
|
71
|
+
validationFailures: 0,
|
|
72
|
+
queueHighWaterMark: 0,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
constructor(options: IConnectionPoolOptions<T>) {
|
|
76
|
+
super();
|
|
77
|
+
|
|
78
|
+
this.options = {
|
|
79
|
+
minSize: 0,
|
|
80
|
+
maxSize: 10,
|
|
81
|
+
acquireTimeout: 30000,
|
|
82
|
+
idleTimeout: 300000, // 5 minutes
|
|
83
|
+
maxUseCount: Infinity,
|
|
84
|
+
validateOnAcquire: true,
|
|
85
|
+
validateOnReturn: false,
|
|
86
|
+
queueTimeout: 60000,
|
|
87
|
+
...options,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Initialize priority queue (higher priority = extracted first)
|
|
91
|
+
this.waitQueue = new BinaryHeap<IAcquireRequest<T>>(
|
|
92
|
+
(a, b) => b.priority - a.priority || a.timestamp - b.timestamp,
|
|
93
|
+
(item) => item.id
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Start maintenance cycle
|
|
97
|
+
this.startMaintenance();
|
|
98
|
+
|
|
99
|
+
// Initialize minimum connections
|
|
100
|
+
this.initializeMinConnections();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Initialize minimum number of connections
|
|
105
|
+
*/
|
|
106
|
+
private async initializeMinConnections(): Promise<void> {
|
|
107
|
+
const promises: Promise<void>[] = [];
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < this.options.minSize; i++) {
|
|
110
|
+
promises.push(
|
|
111
|
+
this.createConnection()
|
|
112
|
+
.then(conn => {
|
|
113
|
+
this.availableConnections.push(conn);
|
|
114
|
+
})
|
|
115
|
+
.catch(err => {
|
|
116
|
+
if (this.options.onConnectionError) {
|
|
117
|
+
this.options.onConnectionError(err);
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await Promise.all(promises);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start maintenance timer for idle connection cleanup
|
|
128
|
+
*/
|
|
129
|
+
private startMaintenance(): void {
|
|
130
|
+
this.setInterval(() => {
|
|
131
|
+
this.performMaintenance();
|
|
132
|
+
}, 30000); // Every 30 seconds
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Perform maintenance tasks
|
|
137
|
+
*/
|
|
138
|
+
private async performMaintenance(): Promise<void> {
|
|
139
|
+
await this.mutex.runExclusive(async () => {
|
|
140
|
+
const now = Date.now();
|
|
141
|
+
const toRemove: IPooledConnection<T>[] = [];
|
|
142
|
+
|
|
143
|
+
// Check for idle connections beyond minimum size
|
|
144
|
+
for (let i = this.availableConnections.length - 1; i >= 0; i--) {
|
|
145
|
+
const conn = this.availableConnections[i];
|
|
146
|
+
|
|
147
|
+
// Keep minimum connections
|
|
148
|
+
if (this.availableConnections.length <= this.options.minSize) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Remove idle connections
|
|
153
|
+
if (now - conn.lastUsedAt > this.options.idleTimeout) {
|
|
154
|
+
toRemove.push(conn);
|
|
155
|
+
this.availableConnections.splice(i, 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Destroy idle connections
|
|
160
|
+
for (const conn of toRemove) {
|
|
161
|
+
await this.destroyConnection(conn);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Acquire a connection from the pool
|
|
168
|
+
*/
|
|
169
|
+
public async acquire(priority: number = 0, timeout?: number): Promise<IPooledConnection<T>> {
|
|
170
|
+
if (this.isClosing) {
|
|
171
|
+
throw new Error('Connection pool is closing');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return this.mutex.runExclusive(async () => {
|
|
175
|
+
// Try to get an available connection
|
|
176
|
+
const connection = await this.tryAcquireConnection();
|
|
177
|
+
if (connection) {
|
|
178
|
+
return connection;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if we can create a new connection
|
|
182
|
+
const totalConnections = this.availableConnections.length + this.activeConnections.size;
|
|
183
|
+
if (totalConnections < this.options.maxSize) {
|
|
184
|
+
try {
|
|
185
|
+
const newConnection = await this.createConnection();
|
|
186
|
+
return this.checkoutConnection(newConnection);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// Fall through to queue if creation fails
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add to wait queue
|
|
193
|
+
return this.queueAcquireRequest(priority, timeout);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Try to acquire an available connection
|
|
199
|
+
*/
|
|
200
|
+
private async tryAcquireConnection(): Promise<IPooledConnection<T> | null> {
|
|
201
|
+
while (this.availableConnections.length > 0) {
|
|
202
|
+
const connection = this.availableConnections.shift()!;
|
|
203
|
+
|
|
204
|
+
// Check if connection exceeded max use count
|
|
205
|
+
if (connection.useCount >= this.options.maxUseCount) {
|
|
206
|
+
await this.destroyConnection(connection);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validate connection if required
|
|
211
|
+
if (this.options.validateOnAcquire && this.options.connectionValidator) {
|
|
212
|
+
try {
|
|
213
|
+
const isValid = await this.options.connectionValidator(connection.connection);
|
|
214
|
+
if (!isValid) {
|
|
215
|
+
this.metrics.validationFailures++;
|
|
216
|
+
await this.destroyConnection(connection);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
this.metrics.validationFailures++;
|
|
221
|
+
await this.destroyConnection(connection);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.checkoutConnection(connection);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Checkout a connection for use
|
|
234
|
+
*/
|
|
235
|
+
private checkoutConnection(connection: IPooledConnection<T>): IPooledConnection<T> {
|
|
236
|
+
connection.inUse = true;
|
|
237
|
+
connection.lastUsedAt = Date.now();
|
|
238
|
+
connection.useCount++;
|
|
239
|
+
|
|
240
|
+
this.activeConnections.set(connection.id, connection);
|
|
241
|
+
this.metrics.connectionsAcquired++;
|
|
242
|
+
|
|
243
|
+
this.eventEmitter.emit('acquire', connection);
|
|
244
|
+
return connection;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Queue an acquire request
|
|
249
|
+
*/
|
|
250
|
+
private queueAcquireRequest(priority: number, timeout?: number): Promise<IPooledConnection<T>> {
|
|
251
|
+
return new Promise<IPooledConnection<T>>((resolve, reject) => {
|
|
252
|
+
const request: IAcquireRequest<T> = {
|
|
253
|
+
id: `req-${this.requestIdCounter++}`,
|
|
254
|
+
priority,
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
resolve,
|
|
257
|
+
reject,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Set timeout
|
|
261
|
+
const timeoutMs = timeout || this.options.queueTimeout;
|
|
262
|
+
request.timeoutHandle = this.setTimeout(() => {
|
|
263
|
+
if (this.waitQueue.extractByKey(request.id)) {
|
|
264
|
+
this.metrics.acquireTimeouts++;
|
|
265
|
+
reject(new Error(`Connection acquire timeout after ${timeoutMs}ms`));
|
|
266
|
+
}
|
|
267
|
+
}, timeoutMs);
|
|
268
|
+
|
|
269
|
+
this.waitQueue.insert(request);
|
|
270
|
+
this.metrics.queueHighWaterMark = Math.max(
|
|
271
|
+
this.metrics.queueHighWaterMark,
|
|
272
|
+
this.waitQueue.size
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
this.eventEmitter.emit('enqueue', { queueSize: this.waitQueue.size });
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Release a connection back to the pool
|
|
281
|
+
*/
|
|
282
|
+
public async release(connection: IPooledConnection<T>): Promise<void> {
|
|
283
|
+
return this.mutex.runExclusive(async () => {
|
|
284
|
+
if (!connection.inUse || !this.activeConnections.has(connection.id)) {
|
|
285
|
+
throw new Error('Connection is not active');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.activeConnections.delete(connection.id);
|
|
289
|
+
connection.inUse = false;
|
|
290
|
+
connection.lastUsedAt = Date.now();
|
|
291
|
+
this.metrics.connectionsReleased++;
|
|
292
|
+
|
|
293
|
+
// Check if connection should be destroyed
|
|
294
|
+
if (connection.useCount >= this.options.maxUseCount) {
|
|
295
|
+
await this.destroyConnection(connection);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Validate on return if required
|
|
300
|
+
if (this.options.validateOnReturn && this.options.connectionValidator) {
|
|
301
|
+
try {
|
|
302
|
+
const isValid = await this.options.connectionValidator(connection.connection);
|
|
303
|
+
if (!isValid) {
|
|
304
|
+
await this.destroyConnection(connection);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
await this.destroyConnection(connection);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if there are waiting requests
|
|
314
|
+
const request = this.waitQueue.extract();
|
|
315
|
+
if (request) {
|
|
316
|
+
this.clearTimeout(request.timeoutHandle!);
|
|
317
|
+
request.resolve(this.checkoutConnection(connection));
|
|
318
|
+
this.eventEmitter.emit('dequeue', { queueSize: this.waitQueue.size });
|
|
319
|
+
} else {
|
|
320
|
+
// Return to available pool
|
|
321
|
+
this.availableConnections.push(connection);
|
|
322
|
+
this.eventEmitter.emit('release', connection);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create a new connection
|
|
329
|
+
*/
|
|
330
|
+
private async createConnection(): Promise<IPooledConnection<T>> {
|
|
331
|
+
const rawConnection = await this.options.connectionFactory();
|
|
332
|
+
|
|
333
|
+
const connection: IPooledConnection<T> = {
|
|
334
|
+
id: `conn-${this.connectionIdCounter++}`,
|
|
335
|
+
connection: rawConnection,
|
|
336
|
+
createdAt: Date.now(),
|
|
337
|
+
lastUsedAt: Date.now(),
|
|
338
|
+
useCount: 0,
|
|
339
|
+
inUse: false,
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
this.metrics.connectionsCreated++;
|
|
343
|
+
this.eventEmitter.emit('create', connection);
|
|
344
|
+
|
|
345
|
+
return connection;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Destroy a connection
|
|
350
|
+
*/
|
|
351
|
+
private async destroyConnection(connection: IPooledConnection<T>): Promise<void> {
|
|
352
|
+
try {
|
|
353
|
+
if (this.options.connectionDestroyer) {
|
|
354
|
+
await this.options.connectionDestroyer(connection.connection);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
this.metrics.connectionsDestroyed++;
|
|
358
|
+
this.eventEmitter.emit('destroy', connection);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
if (this.options.onConnectionError) {
|
|
361
|
+
this.options.onConnectionError(err as Error, connection.connection);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get current pool statistics
|
|
368
|
+
*/
|
|
369
|
+
public getStats() {
|
|
370
|
+
return {
|
|
371
|
+
available: this.availableConnections.length,
|
|
372
|
+
active: this.activeConnections.size,
|
|
373
|
+
waiting: this.waitQueue.size,
|
|
374
|
+
total: this.availableConnections.length + this.activeConnections.size,
|
|
375
|
+
...this.metrics,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Subscribe to pool events
|
|
381
|
+
*/
|
|
382
|
+
public on(event: string, listener: Function): void {
|
|
383
|
+
this.addEventListener(this.eventEmitter, event, listener);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Close the pool and cleanup resources
|
|
388
|
+
*/
|
|
389
|
+
protected async onCleanup(): Promise<void> {
|
|
390
|
+
this.isClosing = true;
|
|
391
|
+
|
|
392
|
+
// Clear the wait queue
|
|
393
|
+
while (!this.waitQueue.isEmpty()) {
|
|
394
|
+
const request = this.waitQueue.extract();
|
|
395
|
+
if (request) {
|
|
396
|
+
this.clearTimeout(request.timeoutHandle!);
|
|
397
|
+
request.reject(new Error('Connection pool is closing'));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Wait for active connections to be released (with timeout)
|
|
402
|
+
const timeout = 30000;
|
|
403
|
+
const startTime = Date.now();
|
|
404
|
+
|
|
405
|
+
while (this.activeConnections.size > 0 && Date.now() - startTime < timeout) {
|
|
406
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Destroy all connections
|
|
410
|
+
const allConnections = [
|
|
411
|
+
...this.availableConnections,
|
|
412
|
+
...this.activeConnections.values(),
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
await Promise.all(allConnections.map(conn => this.destroyConnection(conn)));
|
|
416
|
+
|
|
417
|
+
this.availableConnections.length = 0;
|
|
418
|
+
this.activeConnections.clear();
|
|
419
|
+
}
|
|
420
|
+
}
|