node-firebird 2.6.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.
@@ -226,10 +226,10 @@ class XdrWriter {
226
226
  }
227
227
  addInt64(value) {
228
228
  this.ensure(8);
229
- // Note: precision is limited to Number.MAX_SAFE_INTEGER (±2^53-1).
230
- // Values outside this range lose precision, which matches the previous
231
- // Long.fromNumber() behaviour.
232
- this.buffer.writeBigInt64BE(BigInt(Math.trunc(value)), this.pos);
229
+ // Note: for numbers, precision is limited to Number.MAX_SAFE_INTEGER
230
+ // (±2^53-1); values outside this range lose precision, which matches
231
+ // the previous Long.fromNumber() behaviour. BigInts keep full precision.
232
+ this.buffer.writeBigInt64BE(typeof value === 'bigint' ? value : BigInt(Math.trunc(value)), this.pos);
233
233
  this.pos += 8;
234
234
  }
235
235
  addInt128(value) {
@@ -306,6 +306,11 @@ class XdrWriter {
306
306
  this.buffer.writeDoubleBE(value, this.pos);
307
307
  this.pos += 8;
308
308
  }
309
+ addFloat(value) {
310
+ this.ensure(4);
311
+ this.buffer.writeFloatBE(value, this.pos);
312
+ this.pos += 4;
313
+ }
309
314
  addQuad(quad) {
310
315
  this.ensure(8);
311
316
  var b = this.buffer;
@@ -21,7 +21,13 @@ declare class Statement {
21
21
  fetch(transaction: any, count: number | string, callback: (err: any, result?: any) => void): void;
22
22
  fetchScroll(transaction: any, direction: string | number, offset?: any, count?: any, callback?: any): void;
23
23
  fetchAll(transaction: any, callback: (err: any, result?: any) => void): void;
24
+ /**
25
+ * Execute this statement once per row via the Firebird 4 batch API
26
+ * (protocol 16+). `rows` is an array of parameter arrays.
27
+ */
28
+ executeBatch(transaction: any, rows: any[][], callback?: any, options?: any): void;
24
29
  executeAsync(transaction: any, params?: any, options?: any): Promise<any>;
30
+ executeBatchAsync(transaction: any, rows: any[][], options?: any): Promise<any>;
25
31
  fetchAsync(transaction: any, count: number | string): Promise<any>;
26
32
  fetchScrollAsync(transaction: any, direction: string | number, offset?: any, count?: any): Promise<any>;
27
33
  fetchAllAsync(transaction: any): Promise<any>;
@@ -49,11 +49,22 @@ class Statement {
49
49
  fetchAll(transaction, callback) {
50
50
  this.connection.fetchAll(this, transaction, callback);
51
51
  }
52
+ /**
53
+ * Execute this statement once per row via the Firebird 4 batch API
54
+ * (protocol 16+). `rows` is an array of parameter arrays.
55
+ */
56
+ executeBatch(transaction, rows, callback, options) {
57
+ this.connection.executeBatch(transaction, this, rows, callback, options);
58
+ }
52
59
  /* Promise / async-await API — wrappers over the callback methods above. */
53
60
  executeAsync(transaction, params, options) {
54
61
  var self = this;
55
62
  return (0, callback_1.fromCallback)(function (cb) { self.execute(transaction, params, cb, options); });
56
63
  }
64
+ executeBatchAsync(transaction, rows, options) {
65
+ var self = this;
66
+ return (0, callback_1.fromCallback)(function (cb) { self.executeBatch(transaction, rows, cb, options); });
67
+ }
57
68
  fetchAsync(transaction, count) {
58
69
  var self = this;
59
70
  return (0, callback_1.fromCallback)(function (cb) { self.fetch(transaction, count, cb); });
@@ -8,6 +8,17 @@ declare class Transaction {
8
8
  execute(query: string, params?: any, callback?: any, options?: any): void;
9
9
  sequentially(query: string, params?: any, on?: any, callback?: any, options?: any): this;
10
10
  query(query: string, params?: any, callback?: any, options?: any): void;
11
+ /**
12
+ * Execute `query` once per row in `rows` using the Firebird 4 batch API
13
+ * (protocol 16+, single network flush). The callback receives a
14
+ * completion object: { recordCount, updateCounts, errors:
15
+ * [{recordNumber, error}], errorRecordNumbers, success }. Per-record
16
+ * failures do NOT roll anything back here — inspect the completion and
17
+ * commit or roll back yourself (or use db.executeBatch for
18
+ * all-or-nothing semantics).
19
+ */
20
+ executeBatch(query: string, rows: any[][], callback?: any, options?: any): void;
21
+ executeBatchAsync(query: string, rows: any[][], options?: any): Promise<any>;
11
22
  commit(callback?: (err?: any) => void): void;
12
23
  rollback(callback?: (err?: any) => void): void;
13
24
  commitRetaining(callback?: (err?: any) => void): void;
@@ -200,6 +200,33 @@ class Transaction {
200
200
  };
201
201
  this.execute(query, params, callback, options);
202
202
  }
203
+ /**
204
+ * Execute `query` once per row in `rows` using the Firebird 4 batch API
205
+ * (protocol 16+, single network flush). The callback receives a
206
+ * completion object: { recordCount, updateCounts, errors:
207
+ * [{recordNumber, error}], errorRecordNumbers, success }. Per-record
208
+ * failures do NOT roll anything back here — inspect the completion and
209
+ * commit or roll back yourself (or use db.executeBatch for
210
+ * all-or-nothing semantics).
211
+ */
212
+ executeBatch(query, rows, callback, options) {
213
+ var self = this;
214
+ this.newStatement(query, function (err, statement) {
215
+ if (err) {
216
+ (0, callback_1.doError)(err, callback);
217
+ return;
218
+ }
219
+ statement.executeBatch(self, rows, function (err, result) {
220
+ statement.release();
221
+ if (callback)
222
+ callback(err, result);
223
+ }, options);
224
+ });
225
+ }
226
+ executeBatchAsync(query, rows, options) {
227
+ var self = this;
228
+ return (0, callback_1.fromCallback)(function (cb) { self.executeBatch(query, rows, cb, options); });
229
+ }
203
230
  commit(callback) {
204
231
  this.connection.commit(this, callback);
205
232
  }
@@ -155,6 +155,15 @@ export declare class SQLParamBuffer {
155
155
  encode(data: XdrWriter): void;
156
156
  calcBlr(blr: BlrWriter): void;
157
157
  }
158
+ /**
159
+ * Split a JS Date into the Firebird wire representation used by
160
+ * TIMESTAMP/DATE/TIME columns: `date` is the modified-Julian day number and
161
+ * `time` the count of 100-microsecond units since midnight (local time).
162
+ */
163
+ export declare function encodeDateTimeParts(value: Date): {
164
+ date: number;
165
+ time: number;
166
+ };
158
167
  export declare class SQLParamQuad {
159
168
  value: any;
160
169
  constructor(value: any);
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SQLParamBool = exports.SQLParamDate = exports.SQLParamQuad = exports.SQLParamBuffer = exports.SQLParamString = exports.SQLParamDouble = exports.SQLParamDecFloat34 = exports.SQLParamDecFloat16 = exports.SQLParamInt128 = exports.SQLParamInt64 = exports.SQLParamInt = exports.SQLVarBoolean = exports.SQLVarTimeStampTzEx = exports.SQLVarTimeStampTz = exports.SQLVarTimeTzEx = exports.SQLVarTimeTz = exports.SQLVarTimeStamp = exports.SQLVarTime = exports.SQLVarDate = exports.SQLVarDouble = exports.SQLVarFloat = exports.SQLVarDecFloat34 = exports.SQLVarDecFloat16 = exports.SQLVarInt128 = exports.SQLVarInt64 = exports.SQLVarShort = exports.SQLVarInt = exports.SQLVarArray = exports.SQLVarBlob = exports.SQLVarQuad = exports.SQLVarString = exports.SQLVarNull = exports.SQLVarText = exports.SQLVarBase = void 0;
7
+ exports.encodeDateTimeParts = encodeDateTimeParts;
7
8
  const const_1 = __importDefault(require("./const"));
8
9
  /***************************************
9
10
  *
@@ -593,6 +594,24 @@ class SQLParamBuffer {
593
594
  }
594
595
  exports.SQLParamBuffer = SQLParamBuffer;
595
596
  //------------------------------------------------------
597
+ /**
598
+ * Split a JS Date into the Firebird wire representation used by
599
+ * TIMESTAMP/DATE/TIME columns: `date` is the modified-Julian day number and
600
+ * `time` the count of 100-microsecond units since midnight (local time).
601
+ */
602
+ function encodeDateTimeParts(value) {
603
+ var ms = value.getTime() - value.getTimezoneOffset() * MsPerMinute;
604
+ var time = ms % TimeCoeff;
605
+ var date = (ms - time) / TimeCoeff + DateOffset;
606
+ time *= 10;
607
+ // check overflow (dates before the epoch)
608
+ if (time < 0) {
609
+ date--;
610
+ time = TimeCoeff * 10 + time;
611
+ }
612
+ return { date: date, time: time };
613
+ }
614
+ //------------------------------------------------------
596
615
  class SQLParamQuad {
597
616
  constructor(value) {
598
617
  this.value = value;
@@ -621,17 +640,9 @@ class SQLParamDate {
621
640
  }
622
641
  encode(data) {
623
642
  if (this.value != null) {
624
- var value = this.value.getTime() - this.value.getTimezoneOffset() * MsPerMinute;
625
- var time = value % TimeCoeff;
626
- var date = (value - time) / TimeCoeff + DateOffset;
627
- time *= 10;
628
- // check overflow
629
- if (time < 0) {
630
- date--;
631
- time = TimeCoeff * 10 + time;
632
- }
633
- data.addInt(date);
634
- data.addUInt(time);
643
+ var parts = encodeDateTimeParts(this.value);
644
+ data.addInt(parts.date);
645
+ data.addUInt(parts.time);
635
646
  }
636
647
  else {
637
648
  data.addInt(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.6.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
@@ -75,6 +75,31 @@ export type TransactionOptions = {
75
75
  waitTimeout?: number;
76
76
  };
77
77
 
78
+ /** Result of an executeBatch call (Firebird 4 batch API, protocol 16+). */
79
+ export interface BatchResult {
80
+ /** Total number of records processed by the server. */
81
+ recordCount: number;
82
+ /** Per-record update counts (in record order). */
83
+ updateCounts: number[];
84
+ /** Per-record failures with full status vectors (capped by `detailedErrors`). */
85
+ errors: Array<{ recordNumber: number; error: Error }>;
86
+ /** Record numbers of ALL failed records (detailed + status-less). */
87
+ errorRecordNumbers: number[];
88
+ /** True when every record executed without error. */
89
+ success: boolean;
90
+ }
91
+
92
+ export type BatchOptions = {
93
+ /** Continue past per-record errors and report them all (default true). */
94
+ multiError?: boolean;
95
+ /** Server-side batch buffer limit in bytes (BATCH_TAG_BUFFER_BYTES_SIZE). */
96
+ bufferSize?: number;
97
+ /** Max number of detailed per-record status vectors returned (server default 64). */
98
+ detailedErrors?: number;
99
+ /** Rows per op_batch_msg packet (default 500). */
100
+ chunkSize?: number;
101
+ };
102
+
78
103
  export type QueryOptions = {
79
104
  timeout?: number;
80
105
  scrollable?: boolean;
@@ -95,6 +120,8 @@ export interface Database {
95
120
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
96
121
  query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
97
122
  execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
123
+ /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
124
+ executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
98
125
  sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
99
126
  drop(callback: SimpleCallback): void;
100
127
  escape(value: any): string;
@@ -108,6 +135,7 @@ export interface Database {
108
135
  // Result metadata is only available through the callback API.
109
136
  queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
110
137
  executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
138
+ executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
111
139
  sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
112
140
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
113
141
  transactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
@@ -132,6 +160,8 @@ export interface Transaction {
132
160
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
133
161
  query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
134
162
  execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
163
+ /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
164
+ executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
135
165
  sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
136
166
  commit(callback?: SimpleCallback): void;
137
167
  commitRetaining(callback?: SimpleCallback): void;
@@ -141,6 +171,7 @@ export interface Transaction {
141
171
  // Promise / async-await API
142
172
  queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
143
173
  executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
174
+ executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
144
175
  sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
145
176
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
146
177
  newStatementAsync(query: string): Promise<Statement>;
@@ -159,8 +190,12 @@ export interface Statement {
159
190
  fetchScroll(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset: number, count: number, callback: QueryCallback): void;
160
191
  fetchAll(transaction: Transaction, callback: QueryCallback): void;
161
192
 
193
+ /** Execute this prepared statement once per row (Firebird 4.0+ batch API). */
194
+ executeBatch(transaction: Transaction, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
195
+
162
196
  // Promise / async-await API
163
197
  executeAsync(transaction: Transaction, params?: any[], options?: QueryOptions): Promise<any>;
198
+ executeBatchAsync(transaction: Transaction, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
164
199
  fetchAsync(transaction: Transaction, count: number | 'all'): Promise<any>;
165
200
  fetchScrollAsync(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset?: number, count?: number): Promise<any>;
166
201
  fetchAllAsync(transaction: Transaction): Promise<any>;
@@ -246,6 +281,19 @@ export interface Options {
246
281
  * expected Firebird server response time under load.
247
282
  */
248
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;
249
297
  /**
250
298
  * **Firebird 6.0+ only (Protocol 20+)**
251
299
  *
@@ -289,10 +337,30 @@ export interface SvcMgrOptions extends Options {
289
337
  manager: true; // Attach to ServiceManager
290
338
  }
291
339
 
340
+ export type PoolEvent = 'connect' | 'acquire' | 'release' | 'remove' | 'error';
341
+
292
342
  export interface ConnectionPool {
293
343
  get(callback: DatabaseCallback): void;
294
344
  destroy(callback?: SimpleCallback): void;
295
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
+
296
364
  // Promise / async-await API
297
365
  getAsync(): Promise<Database>;
298
366
  destroyAsync(): Promise<void>;