node-firebird 2.7.0 → 2.8.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.
package/README.md CHANGED
@@ -217,6 +217,44 @@ pool.get(function (err, db) {
217
217
  pool.destroy();
218
218
  ```
219
219
 
220
+ #### Pool events and metrics
221
+
222
+ The pool is an `EventEmitter` and exposes live counters, following the
223
+ `pg.Pool` conventions:
224
+
225
+ ```js
226
+ const pool = Firebird.pool(10, {
227
+ ...options,
228
+ idleTimeoutMillis: 30000, // close connections idle for 30s…
229
+ min: 2, // …but always keep 2 alive
230
+ connectTimeout: 5000,
231
+ });
232
+
233
+ pool.on('connect', (db) => console.log('new server connection'));
234
+ pool.on('acquire', (db) => console.log('connection handed to a caller'));
235
+ pool.on('release', (db) => console.log('connection returned to the pool'));
236
+ pool.on('remove', (db) => console.log('connection closed & removed'));
237
+ pool.on('error', (err, db) => console.error('background pool error', err));
238
+
239
+ // live metrics — e.g. for a /health endpoint or periodic monitoring
240
+ console.log({
241
+ total: pool.totalCount, // physical connections (idle + in use)
242
+ idle: pool.idleCount, // available in the pool
243
+ active: pool.activeCount, // handed out to callers
244
+ waiting: pool.waitingCount, // get() calls queued for a free slot
245
+ });
246
+ ```
247
+
248
+ - `idleTimeoutMillis` closes connections that sat idle in the pool for that
249
+ long, never shrinking below `min` — long-lived pools no longer hold every
250
+ connection they ever created (issue [#329](https://github.com/hgourvest/node-firebird/issues/329)).
251
+ The sweep also evicts idle connections whose socket has died, so callers
252
+ don't receive them after a server restart (issue [#343](https://github.com/hgourvest/node-firebird/issues/343)).
253
+ - `error` is a background-error channel (idle eviction failures and the
254
+ like); unlike a plain `EventEmitter`, it is only emitted when a listener
255
+ is attached, so existing applications keep working unchanged.
256
+ - Metrics are plain getters — reading them has no side effects.
257
+
220
258
  #### Advanced Pooling Features
221
259
 
222
260
  The pool implementation includes several safeguards for reliability:
@@ -224,6 +262,7 @@ The pool implementation includes several safeguards for reliability:
224
262
  1. **Connection Timeout**: Use `options.connectTimeout` to prevent the pool from hanging if a server accepts the TCP connection but fails to respond to the Firebird wire protocol (e.g., during high load or authentication stalls).
225
263
  2. **Pool Destruction**: Calling `pool.destroy()` now immediately drains the `pending` queue, notifying all waiting callers with an error. It also prevents any further `pool.get()` calls.
226
264
  3. **Slot Recovery**: If a connection attempt times out, the pool slot is correctly freed so subsequent requests can be served. Late-arriving connections are automatically discarded to prevent resource leaks.
265
+ 4. **Idle Reaping & Health**: `idleTimeoutMillis`/`min` shrink the pool when traffic drops and evict dead idle connections (see [Pool events and metrics](#pool-events-and-metrics)).
227
266
 
228
267
  #### Pool Lifecycle State Diagram
229
268
 
package/lib/pool.d.ts CHANGED
@@ -3,19 +3,61 @@
3
3
  * Simple Pooling
4
4
  *
5
5
  ***************************************/
6
+ import Events from 'events';
6
7
  import type { Callback } from './callback';
7
8
  type AttachFn = (options: any, callback: Callback) => void;
8
- declare class Pool {
9
+ /**
10
+ * Connection pool with pg.Pool-style observability.
11
+ *
12
+ * Events (all optional to listen to):
13
+ * 'connect' (db) — a new physical connection was established
14
+ * 'acquire' (db) — a connection was handed to a caller
15
+ * 'release' (db) — a connection was returned to the idle pool
16
+ * 'remove' (db) — a physical connection left the pool for good
17
+ * 'error' (err, db) — a background error (idle eviction, reaper detach);
18
+ * only emitted when a listener is attached, so
19
+ * existing applications never crash on it
20
+ *
21
+ * Metrics: totalCount, idleCount, activeCount, waitingCount.
22
+ *
23
+ * Options: max (factory argument), options.min (floor the reaper never
24
+ * shrinks below), options.idleTimeoutMillis (close idle connections after
25
+ * this many ms; 0/absent = never), options.connectTimeout.
26
+ */
27
+ declare class Pool extends Events.EventEmitter {
9
28
  attach: AttachFn;
10
29
  internaldb: any[];
11
30
  pooldb: any[];
12
31
  dbinuse: number;
13
32
  _creating: number;
14
33
  max: number;
34
+ min: number;
35
+ idleTimeoutMillis: number;
15
36
  pending: Callback[];
16
37
  options: any;
17
38
  _destroyed: boolean;
39
+ _reaper: NodeJS.Timeout | null;
18
40
  constructor(attach: AttachFn, max: number, options: any);
41
+ /** Physical connections owned by the pool (idle + in use). */
42
+ get totalCount(): number;
43
+ /** Connections sitting idle in the pool. */
44
+ get idleCount(): number;
45
+ /** Connections currently handed out to callers. */
46
+ get activeCount(): number;
47
+ /** get() requests queued for a free slot. */
48
+ get waitingCount(): number;
49
+ /** True when the connection can no longer be used. */
50
+ _isDead(db: any): boolean;
51
+ /** Drop a physical connection from the pool's books and emit 'remove'. */
52
+ _forget(db: any): void;
53
+ /** Emit 'error' only when someone listens — never crash the app. */
54
+ _emitError(err: any, db?: any): void;
55
+ /**
56
+ * Idle sweep: evict dead idle connections immediately and close healthy
57
+ * ones that have been idle longer than idleTimeoutMillis, keeping at
58
+ * least `min` physical connections. (issue #329)
59
+ */
60
+ _reap(): void;
19
61
  get(callback: Callback): this;
20
62
  check(): this;
21
63
  destroy(callback?: (err?: any) => void): void;
package/lib/pool.js CHANGED
@@ -4,18 +4,124 @@
4
4
  * Simple Pooling
5
5
  *
6
6
  ***************************************/
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ const events_1 = __importDefault(require("events"));
7
11
  const callback_1 = require("./callback");
8
- class Pool {
12
+ /**
13
+ * Connection pool with pg.Pool-style observability.
14
+ *
15
+ * Events (all optional to listen to):
16
+ * 'connect' (db) — a new physical connection was established
17
+ * 'acquire' (db) — a connection was handed to a caller
18
+ * 'release' (db) — a connection was returned to the idle pool
19
+ * 'remove' (db) — a physical connection left the pool for good
20
+ * 'error' (err, db) — a background error (idle eviction, reaper detach);
21
+ * only emitted when a listener is attached, so
22
+ * existing applications never crash on it
23
+ *
24
+ * Metrics: totalCount, idleCount, activeCount, waitingCount.
25
+ *
26
+ * Options: max (factory argument), options.min (floor the reaper never
27
+ * shrinks below), options.idleTimeoutMillis (close idle connections after
28
+ * this many ms; 0/absent = never), options.connectTimeout.
29
+ */
30
+ class Pool extends events_1.default.EventEmitter {
9
31
  constructor(attach, max, options) {
32
+ super();
10
33
  this.attach = attach;
11
34
  this.internaldb = []; // connections created by the pool (for destroy)
12
35
  this.pooldb = []; // connections available in the pool (idle)
13
36
  this.dbinuse = 0; // connections currently handed out to callers
14
37
  this._creating = 0; // connections currently being created (attach in flight)
15
38
  this.max = max || 4;
39
+ this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
40
+ this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
16
41
  this.pending = []; // callbacks waiting for a free slot
17
42
  this.options = options;
18
43
  this._destroyed = false; // true after destroy() — prevents further use
44
+ this._reaper = null;
45
+ if (this.idleTimeoutMillis) {
46
+ var self = this;
47
+ // Sweep at half the idle timeout (bounded to 100ms..30s) so a
48
+ // connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
49
+ // the timer from holding the process open.
50
+ var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
51
+ this._reaper = setInterval(function () { self._reap(); }, interval);
52
+ if (this._reaper.unref)
53
+ this._reaper.unref();
54
+ }
55
+ }
56
+ /** Physical connections owned by the pool (idle + in use). */
57
+ get totalCount() {
58
+ return this.internaldb.length;
59
+ }
60
+ /** Connections sitting idle in the pool. */
61
+ get idleCount() {
62
+ return this.pooldb.length;
63
+ }
64
+ /** Connections currently handed out to callers. */
65
+ get activeCount() {
66
+ return this.dbinuse;
67
+ }
68
+ /** get() requests queued for a free slot. */
69
+ get waitingCount() {
70
+ return this.pending.length;
71
+ }
72
+ /** True when the connection can no longer be used. */
73
+ _isDead(db) {
74
+ return !db.connection || db.connection._isClosed || db.connection._isDetach ||
75
+ !db.connection._socket || db.connection._socket.destroyed;
76
+ }
77
+ /** Drop a physical connection from the pool's books and emit 'remove'. */
78
+ _forget(db) {
79
+ var idx = this.internaldb.indexOf(db);
80
+ if (idx !== -1)
81
+ this.internaldb.splice(idx, 1);
82
+ idx = this.pooldb.indexOf(db);
83
+ if (idx !== -1)
84
+ this.pooldb.splice(idx, 1);
85
+ this.emit('remove', db);
86
+ }
87
+ /** Emit 'error' only when someone listens — never crash the app. */
88
+ _emitError(err, db) {
89
+ if (this.listenerCount('error') > 0)
90
+ this.emit('error', err, db);
91
+ }
92
+ /**
93
+ * Idle sweep: evict dead idle connections immediately and close healthy
94
+ * ones that have been idle longer than idleTimeoutMillis, keeping at
95
+ * least `min` physical connections. (issue #329)
96
+ */
97
+ _reap() {
98
+ if (this._destroyed)
99
+ return;
100
+ var self = this;
101
+ var now = Date.now();
102
+ // iterate over a copy — we splice from pooldb while walking
103
+ this.pooldb.slice().forEach(function (db) {
104
+ if (self._isDead(db)) {
105
+ self._forget(db);
106
+ return;
107
+ }
108
+ if (self.internaldb.length <= self.min)
109
+ return;
110
+ var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
111
+ if (now - idleSince < self.idleTimeoutMillis)
112
+ return;
113
+ self._forget(db);
114
+ db.connection._pooled = false;
115
+ try {
116
+ db.detach(function (err) {
117
+ if (err)
118
+ self._emitError(err, db);
119
+ });
120
+ }
121
+ catch (e) {
122
+ self._emitError(e, db);
123
+ }
124
+ });
19
125
  }
20
126
  get(callback) {
21
127
  // [Fix 2] Reject immediately if the pool has already been destroyed.
@@ -41,16 +147,15 @@ class Pool {
41
147
  if (self.pooldb.length) {
42
148
  var db = self.pooldb.shift();
43
149
  // Discard connections that have been closed or destroyed while idle
44
- if (db.connection && (db.connection._isClosed || db.connection._isDetach || !db.connection._socket || db.connection._socket.destroyed)) {
45
- var idx = self.internaldb.indexOf(db);
46
- if (idx !== -1)
47
- self.internaldb.splice(idx, 1);
150
+ if (self._isDead(db)) {
151
+ self._forget(db);
48
152
  self.pending.unshift(cb);
49
153
  setImmediate(function () { self.check(); });
50
154
  return self;
51
155
  }
52
156
  // Idle connection available — hand it out immediately.
53
157
  self.dbinuse++;
158
+ self.emit('acquire', db);
54
159
  cb(null, db);
55
160
  }
56
161
  else {
@@ -113,13 +218,20 @@ class Pool {
113
218
  if (self.pooldb.indexOf(db) !== -1 || self.internaldb.indexOf(db) === -1)
114
219
  return;
115
220
  // if not usable don't put it back in the pool
116
- if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false)
221
+ if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
117
222
  self.internaldb.splice(self.internaldb.indexOf(db), 1);
118
- else
223
+ self.emit('remove', db);
224
+ }
225
+ else {
226
+ db.__poolIdleSince = Date.now();
119
227
  self.pooldb.push(db);
228
+ self.emit('release', db);
229
+ }
120
230
  self.dbinuse--;
121
231
  self.check();
122
232
  });
233
+ self.emit('connect', db);
234
+ self.emit('acquire', db);
123
235
  }
124
236
  cb(err, db);
125
237
  });
@@ -132,6 +244,10 @@ class Pool {
132
244
  destroy(callback) {
133
245
  var self = this;
134
246
  self._destroyed = true;
247
+ if (self._reaper) {
248
+ clearInterval(self._reaper);
249
+ self._reaper = null;
250
+ }
135
251
  // [Fix 4] Drain pending callbacks so callers are not left hanging.
136
252
  // This is critical when destroy() is called as a recovery measure after
137
253
  // a timeout: without draining, every concurrent pool.get() that had not
@@ -170,6 +286,7 @@ class Pool {
170
286
  self.pooldb.splice(_db_in_pool, 1);
171
287
  db.connection._pooled = false;
172
288
  db.detach(detachCallback);
289
+ self.emit('remove', db);
173
290
  }
174
291
  else {
175
292
  // [Fix 5] Connection is currently in use (dbinuse > 0).
package/lib/types.d.ts CHANGED
@@ -224,6 +224,19 @@ export interface Options {
224
224
  * expected Firebird server response time under load.
225
225
  */
226
226
  connectTimeout?: number;
227
+ /**
228
+ * Pool only: minimum number of physical connections the idle reaper
229
+ * keeps alive. Only meaningful together with `idleTimeoutMillis`.
230
+ * Default 0 (the pool may shrink to no connections).
231
+ */
232
+ min?: number;
233
+ /**
234
+ * Pool only: close connections that have been idle in the pool for this
235
+ * many milliseconds, never shrinking below `min`. Dead idle connections
236
+ * (server restarts, dropped sockets) are evicted on the same sweep.
237
+ * Default 0 (idle connections are kept forever).
238
+ */
239
+ idleTimeoutMillis?: number;
227
240
  /**
228
241
  * **Firebird 6.0+ only (Protocol 20+)**
229
242
  *
@@ -265,9 +278,24 @@ export interface Options {
265
278
  export interface SvcMgrOptions extends Options {
266
279
  manager: true;
267
280
  }
281
+ export type PoolEvent = 'connect' | 'acquire' | 'release' | 'remove' | 'error';
268
282
  export interface ConnectionPool {
269
283
  get(callback: DatabaseCallback): void;
270
284
  destroy(callback?: SimpleCallback): void;
285
+ /** Physical connections owned by the pool (idle + in use). */
286
+ readonly totalCount: number;
287
+ /** Connections sitting idle in the pool. */
288
+ readonly idleCount: number;
289
+ /** Connections currently handed out to callers. */
290
+ readonly activeCount: number;
291
+ /** get() requests queued for a free slot. */
292
+ readonly waitingCount: number;
293
+ on(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
294
+ on(event: 'error', listener: (err: Error, db?: Database) => void): this;
295
+ once(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
296
+ once(event: 'error', listener: (err: Error, db?: Database) => void): this;
297
+ off(event: PoolEvent, listener: (...args: any[]) => void): this;
298
+ removeListener(event: PoolEvent, listener: (...args: any[]) => void): this;
271
299
  getAsync(): Promise<Database>;
272
300
  destroyAsync(): Promise<void>;
273
301
  /** Acquire a connection, run `work`, always return the connection to the pool. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/pool.ts CHANGED
@@ -4,32 +4,140 @@
4
4
  *
5
5
  ***************************************/
6
6
 
7
+ import Events from 'events';
7
8
  import { fromCallback } from './callback';
8
9
  import type { Callback } from './callback';
9
10
 
10
11
  type AttachFn = (options: any, callback: Callback) => void;
11
12
 
12
- class Pool {
13
+ /**
14
+ * Connection pool with pg.Pool-style observability.
15
+ *
16
+ * Events (all optional to listen to):
17
+ * 'connect' (db) — a new physical connection was established
18
+ * 'acquire' (db) — a connection was handed to a caller
19
+ * 'release' (db) — a connection was returned to the idle pool
20
+ * 'remove' (db) — a physical connection left the pool for good
21
+ * 'error' (err, db) — a background error (idle eviction, reaper detach);
22
+ * only emitted when a listener is attached, so
23
+ * existing applications never crash on it
24
+ *
25
+ * Metrics: totalCount, idleCount, activeCount, waitingCount.
26
+ *
27
+ * Options: max (factory argument), options.min (floor the reaper never
28
+ * shrinks below), options.idleTimeoutMillis (close idle connections after
29
+ * this many ms; 0/absent = never), options.connectTimeout.
30
+ */
31
+ class Pool extends Events.EventEmitter {
13
32
  attach: AttachFn;
14
33
  internaldb: any[];
15
34
  pooldb: any[];
16
35
  dbinuse: number;
17
36
  _creating: number;
18
37
  max: number;
38
+ min: number;
39
+ idleTimeoutMillis: number;
19
40
  pending: Callback[];
20
41
  options: any;
21
42
  _destroyed: boolean;
43
+ _reaper: NodeJS.Timeout | null;
22
44
 
23
45
  constructor(attach: AttachFn, max: number, options: any) {
46
+ super();
24
47
  this.attach = attach;
25
48
  this.internaldb = []; // connections created by the pool (for destroy)
26
49
  this.pooldb = []; // connections available in the pool (idle)
27
50
  this.dbinuse = 0; // connections currently handed out to callers
28
51
  this._creating = 0; // connections currently being created (attach in flight)
29
52
  this.max = max || 4;
53
+ this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
54
+ this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
30
55
  this.pending = []; // callbacks waiting for a free slot
31
56
  this.options = options;
32
57
  this._destroyed = false; // true after destroy() — prevents further use
58
+ this._reaper = null;
59
+
60
+ if (this.idleTimeoutMillis) {
61
+ var self = this;
62
+ // Sweep at half the idle timeout (bounded to 100ms..30s) so a
63
+ // connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
64
+ // the timer from holding the process open.
65
+ var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
66
+ this._reaper = setInterval(function() { self._reap(); }, interval);
67
+ if (this._reaper.unref) this._reaper.unref();
68
+ }
69
+ }
70
+
71
+ /** Physical connections owned by the pool (idle + in use). */
72
+ get totalCount(): number {
73
+ return this.internaldb.length;
74
+ }
75
+
76
+ /** Connections sitting idle in the pool. */
77
+ get idleCount(): number {
78
+ return this.pooldb.length;
79
+ }
80
+
81
+ /** Connections currently handed out to callers. */
82
+ get activeCount(): number {
83
+ return this.dbinuse;
84
+ }
85
+
86
+ /** get() requests queued for a free slot. */
87
+ get waitingCount(): number {
88
+ return this.pending.length;
89
+ }
90
+
91
+ /** True when the connection can no longer be used. */
92
+ _isDead(db: any): boolean {
93
+ return !db.connection || db.connection._isClosed || db.connection._isDetach ||
94
+ !db.connection._socket || db.connection._socket.destroyed;
95
+ }
96
+
97
+ /** Drop a physical connection from the pool's books and emit 'remove'. */
98
+ _forget(db: any): void {
99
+ var idx = this.internaldb.indexOf(db);
100
+ if (idx !== -1) this.internaldb.splice(idx, 1);
101
+ idx = this.pooldb.indexOf(db);
102
+ if (idx !== -1) this.pooldb.splice(idx, 1);
103
+ this.emit('remove', db);
104
+ }
105
+
106
+ /** Emit 'error' only when someone listens — never crash the app. */
107
+ _emitError(err: any, db?: any): void {
108
+ if (this.listenerCount('error') > 0) this.emit('error', err, db);
109
+ }
110
+
111
+ /**
112
+ * Idle sweep: evict dead idle connections immediately and close healthy
113
+ * ones that have been idle longer than idleTimeoutMillis, keeping at
114
+ * least `min` physical connections. (issue #329)
115
+ */
116
+ _reap(): void {
117
+ if (this._destroyed) return;
118
+ var self = this;
119
+ var now = Date.now();
120
+
121
+ // iterate over a copy — we splice from pooldb while walking
122
+ this.pooldb.slice().forEach(function(db) {
123
+ if (self._isDead(db)) {
124
+ self._forget(db);
125
+ return;
126
+ }
127
+ if (self.internaldb.length <= self.min) return;
128
+ var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
129
+ if (now - idleSince < self.idleTimeoutMillis) return;
130
+
131
+ self._forget(db);
132
+ db.connection._pooled = false;
133
+ try {
134
+ db.detach(function(err?: any) {
135
+ if (err) self._emitError(err, db);
136
+ });
137
+ } catch (e) {
138
+ self._emitError(e, db);
139
+ }
140
+ });
33
141
  }
34
142
 
35
143
  get(callback: Callback): this {
@@ -59,15 +167,15 @@ class Pool {
59
167
  if (self.pooldb.length) {
60
168
  var db = self.pooldb.shift();
61
169
  // Discard connections that have been closed or destroyed while idle
62
- if (db.connection && (db.connection._isClosed || db.connection._isDetach || !db.connection._socket || db.connection._socket.destroyed)) {
63
- var idx = self.internaldb.indexOf(db);
64
- if (idx !== -1) self.internaldb.splice(idx, 1);
170
+ if (self._isDead(db)) {
171
+ self._forget(db);
65
172
  self.pending.unshift(cb);
66
173
  setImmediate(function () { self.check(); });
67
174
  return self;
68
175
  }
69
176
  // Idle connection available — hand it out immediately.
70
177
  self.dbinuse++;
178
+ self.emit('acquire', db);
71
179
  cb(null, db);
72
180
  } else {
73
181
  // No idle connection — create a new one via attach().
@@ -133,14 +241,20 @@ class Pool {
133
241
  if (self.pooldb.indexOf(db) !== -1 || self.internaldb.indexOf(db) === -1)
134
242
  return;
135
243
  // if not usable don't put it back in the pool
136
- if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false)
244
+ if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
137
245
  self.internaldb.splice(self.internaldb.indexOf(db), 1);
138
- else
246
+ self.emit('remove', db);
247
+ } else {
248
+ db.__poolIdleSince = Date.now();
139
249
  self.pooldb.push(db);
250
+ self.emit('release', db);
251
+ }
140
252
 
141
253
  self.dbinuse--;
142
254
  self.check();
143
255
  });
256
+ self.emit('connect', db);
257
+ self.emit('acquire', db);
144
258
  }
145
259
 
146
260
  cb(err, db);
@@ -157,6 +271,11 @@ class Pool {
157
271
  var self = this;
158
272
  self._destroyed = true;
159
273
 
274
+ if (self._reaper) {
275
+ clearInterval(self._reaper);
276
+ self._reaper = null;
277
+ }
278
+
160
279
  // [Fix 4] Drain pending callbacks so callers are not left hanging.
161
280
  // This is critical when destroy() is called as a recovery measure after
162
281
  // a timeout: without draining, every concurrent pool.get() that had not
@@ -197,6 +316,7 @@ class Pool {
197
316
  self.pooldb.splice(_db_in_pool, 1);
198
317
  db.connection._pooled = false;
199
318
  db.detach(detachCallback);
319
+ self.emit('remove', db);
200
320
  } else {
201
321
  // [Fix 5] Connection is currently in use (dbinuse > 0).
202
322
  // The caller is responsible for releasing it via detach().
package/src/types.ts CHANGED
@@ -281,6 +281,19 @@ export interface Options {
281
281
  * expected Firebird server response time under load.
282
282
  */
283
283
  connectTimeout?: number;
284
+ /**
285
+ * Pool only: minimum number of physical connections the idle reaper
286
+ * keeps alive. Only meaningful together with `idleTimeoutMillis`.
287
+ * Default 0 (the pool may shrink to no connections).
288
+ */
289
+ min?: number;
290
+ /**
291
+ * Pool only: close connections that have been idle in the pool for this
292
+ * many milliseconds, never shrinking below `min`. Dead idle connections
293
+ * (server restarts, dropped sockets) are evicted on the same sweep.
294
+ * Default 0 (idle connections are kept forever).
295
+ */
296
+ idleTimeoutMillis?: number;
284
297
  /**
285
298
  * **Firebird 6.0+ only (Protocol 20+)**
286
299
  *
@@ -324,10 +337,30 @@ export interface SvcMgrOptions extends Options {
324
337
  manager: true; // Attach to ServiceManager
325
338
  }
326
339
 
340
+ export type PoolEvent = 'connect' | 'acquire' | 'release' | 'remove' | 'error';
341
+
327
342
  export interface ConnectionPool {
328
343
  get(callback: DatabaseCallback): void;
329
344
  destroy(callback?: SimpleCallback): void;
330
345
 
346
+ // Metrics (live counters, pg.Pool-style)
347
+ /** Physical connections owned by the pool (idle + in use). */
348
+ readonly totalCount: number;
349
+ /** Connections sitting idle in the pool. */
350
+ readonly idleCount: number;
351
+ /** Connections currently handed out to callers. */
352
+ readonly activeCount: number;
353
+ /** get() requests queued for a free slot. */
354
+ readonly waitingCount: number;
355
+
356
+ // Events
357
+ on(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
358
+ on(event: 'error', listener: (err: Error, db?: Database) => void): this;
359
+ once(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
360
+ once(event: 'error', listener: (err: Error, db?: Database) => void): this;
361
+ off(event: PoolEvent, listener: (...args: any[]) => void): this;
362
+ removeListener(event: PoolEvent, listener: (...args: any[]) => void): this;
363
+
331
364
  // Promise / async-await API
332
365
  getAsync(): Promise<Database>;
333
366
  destroyAsync(): Promise<void>;