node-firebird 2.11.0 → 2.12.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.
@@ -6,6 +6,8 @@ const callback_1 = require("../callback");
6
6
  const named_params_1 = require("../named-params");
7
7
  const utils_1 = require("../utils");
8
8
  const const_1 = __importDefault(require("./const"));
9
+ const sql_template_1 = require("../sql-template");
10
+ const xsqlvar_1 = require("./xsqlvar");
9
11
  const query_stream_1 = __importDefault(require("./query-stream"));
10
12
  /***************************************
11
13
  *
@@ -44,9 +46,73 @@ function hookAbortSignal(connection, signal, callback) {
44
46
  }
45
47
  class Transaction {
46
48
  constructor(connection) {
49
+ /** Current savepoint nesting depth (names savepoints, see savepoint()). */
50
+ this._savepointDepth = 0;
47
51
  this.connection = connection;
48
52
  this.db = connection.db;
49
53
  }
54
+ /**
55
+ * Tagged-template query API: tx.sql`SELECT ... ${value}` (see README).
56
+ * Built lazily — transactions are created per-query internally, and
57
+ * those throwaway instances must not pay for the tag. The compiled text
58
+ * is positional-only, so the namedPlaceholders rewriter is disabled:
59
+ * any `:token` in the template is PSQL (EXECUTE BLOCK), not a
60
+ * placeholder.
61
+ */
62
+ get sql() {
63
+ return this._sql || (this._sql = (0, sql_template_1.makeSqlTag)((text, params, options) => this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
64
+ }
65
+ /**
66
+ * Run `work` inside a savepoint (Firebird 1.5+): on resolve the
67
+ * savepoint is released, on reject the transaction rolls back TO the
68
+ * savepoint — undoing only work's changes — and the error is rethrown,
69
+ * leaving the transaction itself usable. Nestable (each call generates
70
+ * a fresh NF_SP_n name), mirroring db.withTransaction's style and
71
+ * Postgres.js's sql.savepoint().
72
+ *
73
+ * Do NOT run sibling savepoints concurrently on one transaction
74
+ * (Promise.all): Firebird's RELEASE SAVEPOINT also releases every
75
+ * savepoint created after it, so interleaved siblings release each
76
+ * other. Nested (awaited) savepoints are fine.
77
+ */
78
+ async savepoint(work) {
79
+ if (typeof work !== 'function') {
80
+ throw new Error('savepoint(work) expects a function');
81
+ }
82
+ // named by nesting depth, not a global counter: sequential
83
+ // savepoints at the same depth reuse the same three SQL strings, so
84
+ // the statement cache serves them instead of accumulating
85
+ // single-use entries (redefining a released savepoint name is legal)
86
+ const name = 'NF_SP_' + (++this._savepointDepth);
87
+ try {
88
+ await this.queryAsync('SAVEPOINT ' + name);
89
+ let result;
90
+ try {
91
+ result = await work(this);
92
+ }
93
+ catch (err) {
94
+ // only a work() failure rolls back to the savepoint — a
95
+ // RELEASE failure below must NOT undo work's successful
96
+ // changes
97
+ try {
98
+ await this.queryAsync('ROLLBACK TO SAVEPOINT ' + name);
99
+ }
100
+ catch (rollbackErr) {
101
+ // the original failure matters more; keep the rollback
102
+ // failure attached for diagnosis
103
+ if (err && typeof err === 'object') {
104
+ err.savepointRollbackError = rollbackErr;
105
+ }
106
+ }
107
+ throw err;
108
+ }
109
+ await this.queryAsync('RELEASE SAVEPOINT ' + name);
110
+ return result;
111
+ }
112
+ finally {
113
+ this._savepointDepth--;
114
+ }
115
+ }
50
116
  /** Per-call options.namedPlaceholders overrides the connection option. */
51
117
  namedPlaceholdersEnabled(options) {
52
118
  if (options && options.namedPlaceholders !== undefined)
@@ -116,6 +182,66 @@ class Transaction {
116
182
  dropError(err);
117
183
  return;
118
184
  }
185
+ // withMeta applies to query/execute only: in streaming mode
186
+ // (sequentially/queryStream, which spread user options) rows
187
+ // bypass fetchAll's array, so a result object here would
188
+ // carry rows: [] and a meaningless affectedRows
189
+ var withMeta = Boolean(options && typeof options === 'object' &&
190
+ options.withMeta && !options.asStream);
191
+ // Deliver the historic result shape, or — when options.withMeta
192
+ // is set — request the per-verb DML row counts while the
193
+ // statement handle is still open and wrap everything in a
194
+ // { rows, fields, affectedRows, recordCounts, warnings } object.
195
+ function deliver(rows, isSelect, plainDml) {
196
+ if (!withMeta) {
197
+ statement.release();
198
+ if (callback) {
199
+ if (plainDml) {
200
+ // plain DML historically calls back with no args
201
+ callback();
202
+ }
203
+ else {
204
+ callback(undefined, rows, statement.output, isSelect);
205
+ }
206
+ }
207
+ return;
208
+ }
209
+ var execWarnings = (ret && ret.warnings) || [];
210
+ var finalize = function (counts) {
211
+ statement.release();
212
+ if (!callback) {
213
+ return;
214
+ }
215
+ // DML: what the server actually changed; SELECT: rows
216
+ // returned (pg's rowCount convention)
217
+ var affectedRows = counts
218
+ ? counts.insertCount + counts.updateCount + counts.deleteCount
219
+ : (Array.isArray(rows) ? rows.length : (rows !== undefined ? 1 : 0));
220
+ callback(undefined, {
221
+ rows: rows,
222
+ fields: (0, xsqlvar_1.describeFields)(statement.output),
223
+ affectedRows: affectedRows,
224
+ recordCounts: counts,
225
+ warnings: execWarnings,
226
+ }, statement.output, isSelect);
227
+ };
228
+ var t = statement.type;
229
+ var isDml = t === const_1.default.isc_info_sql_stmt_insert ||
230
+ t === const_1.default.isc_info_sql_stmt_update ||
231
+ t === const_1.default.isc_info_sql_stmt_delete ||
232
+ t === const_1.default.isc_info_sql_stmt_exec_procedure;
233
+ if (!isDml) {
234
+ finalize();
235
+ return;
236
+ }
237
+ self.connection.statementInfo(statement, const_1.default.RECORDS_INFO, function (err, info) {
238
+ if (err) {
239
+ dropError(err);
240
+ return;
241
+ }
242
+ finalize((0, xsqlvar_1.parseRecordCounts)(info && info.buffer));
243
+ });
244
+ }
119
245
  switch (statement.type) {
120
246
  case const_1.default.isc_info_sql_stmt_select:
121
247
  statement.fetchAll(self, function (err, r) {
@@ -123,35 +249,27 @@ class Transaction {
123
249
  dropError(err);
124
250
  return;
125
251
  }
126
- statement.release();
127
- if (callback)
128
- callback(undefined, r, statement.output, true);
252
+ deliver(r, true);
129
253
  });
130
254
  break;
131
255
  case const_1.default.isc_info_sql_stmt_exec_procedure:
132
256
  if (ret && ret.data && ret.data.length > 0) {
133
- statement.release();
134
- if (callback)
135
- callback(undefined, ret.data[0], statement.output, true);
257
+ deliver(ret.data[0], true);
136
258
  break;
137
259
  }
138
260
  else if (statement.output.length) {
139
- statement.fetch(self, 1, function (err, ret) {
261
+ statement.fetch(self, 1, function (err, fret) {
140
262
  if (err) {
141
263
  dropError(err);
142
264
  return;
143
265
  }
144
- statement.release();
145
- if (callback)
146
- callback(undefined, ret.data[0], statement.output, false);
266
+ deliver(fret.data[0], false);
147
267
  });
148
268
  break;
149
269
  }
150
270
  // Fall through is normal
151
271
  default:
152
- statement.release();
153
- if (callback)
154
- callback();
272
+ deliver(undefined, false, true);
155
273
  break;
156
274
  }
157
275
  }, options);
@@ -1,4 +1,5 @@
1
1
  import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
2
+ import type { RecordCounts } from '../types';
2
3
  /**
3
4
  * Common shape of all SQLVar descriptor objects. The metadata properties
4
5
  * are populated externally (in connection.ts) from the op_prepare_statement
@@ -41,7 +42,23 @@ export interface ColumnKey {
41
42
  * decoder and by fetchBlobSyncRow, which must agree on where each column
42
43
  * landed in the row.
43
44
  */
44
- export declare function computeColumnKeys(output: SQLVarBase[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined): ColumnKey[];
45
+ export declare function computeColumnKeys(output: SQLVarBase[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined, transform?: (key: string) => string): ColumnKey[];
46
+ /** transformKeys option value: the built-in 'camel', or a custom mapper. */
47
+ export type KeyTransform = 'camel' | ((key: string) => string);
48
+ /** FIRST_NAME → firstName (the transformKeys: 'camel' built-in). */
49
+ export declare function camelizeKey(key: string): string;
50
+ /**
51
+ * Resolve the effective transformKeys value (per-query wins over the
52
+ * connection option) into a callable mapper, or undefined when off.
53
+ * A custom mapper is guarded like the typeCast hook: a throw inside the
54
+ * row-decode loop would be mistaken for an incomplete packet and desync
55
+ * the response queue, so failures fall back to the untransformed key.
56
+ */
57
+ export declare function resolveKeyTransform(queryOptions: {
58
+ transformKeys?: KeyTransform;
59
+ } | undefined, connectionOptions: {
60
+ transformKeys?: KeyTransform;
61
+ } | undefined): ((key: string) => string) | undefined;
45
62
  /**
46
63
  * Resolve the effective nestTables value: the per-query option wins over
47
64
  * the connection option. The decoder and fetchBlobSyncRow both use this —
@@ -60,6 +77,50 @@ export declare function resolveNestTables(queryOptions: {
60
77
  * cell by ColumnKey must resolve it through here.
61
78
  */
62
79
  export declare function nestCell(row: any, table: string | undefined): any;
80
+ /** Human-readable names for the SQL_* wire type codes. */
81
+ export declare const SQL_TYPE_NAMES: Record<number, string>;
82
+ /**
83
+ * Public column-metadata shape for one output descriptor: the vocabulary
84
+ * both the typeCast hook and withMeta `fields` deliver. Keep the two in
85
+ * lockstep by building both through here.
86
+ */
87
+ export declare function describeField(meta: Partial<SQLVarBase>): {
88
+ type: number;
89
+ typeName: string;
90
+ subType: number | undefined;
91
+ scale: number | undefined;
92
+ length: number | undefined;
93
+ nullable: boolean | undefined;
94
+ field: string | undefined;
95
+ relation: string | undefined;
96
+ relationAlias: string | undefined;
97
+ relationSchema: string | undefined;
98
+ alias: string | undefined;
99
+ };
100
+ /**
101
+ * Map a statement's output descriptors to the column-metadata array
102
+ * delivered in withMeta results ({ rows, fields, ... }).
103
+ */
104
+ export declare function describeFields(output: SQLVarBase[]): {
105
+ type: number;
106
+ typeName: string;
107
+ subType: number | undefined;
108
+ scale: number | undefined;
109
+ length: number | undefined;
110
+ nullable: boolean | undefined;
111
+ field: string | undefined;
112
+ relation: string | undefined;
113
+ relationAlias: string | undefined;
114
+ relationSchema: string | undefined;
115
+ alias: string | undefined;
116
+ }[];
117
+ /**
118
+ * Parse the op_info_sql response buffer of a Const.RECORDS_INFO request
119
+ * into per-verb row counts. The buffer holds an isc_info_sql_records
120
+ * cluster (2-byte total length, then nested isc_info_req_*_count items,
121
+ * each 2-byte length + little-endian integer) terminated by isc_info_end.
122
+ */
123
+ export declare function parseRecordCounts(buffer: Buffer | undefined): RecordCounts;
63
124
  export declare class SQLVarText extends SQLVarBase {
64
125
  decode(data: XdrReader, lowerV13: boolean, options?: any): string | Buffer<ArrayBufferLike> | null | undefined;
65
126
  calcBlr(blr: BlrWriter): void;
@@ -3,12 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
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;
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.SQL_TYPE_NAMES = exports.SQLVarBase = void 0;
7
7
  exports.computeColumnKeys = computeColumnKeys;
8
+ exports.camelizeKey = camelizeKey;
9
+ exports.resolveKeyTransform = resolveKeyTransform;
8
10
  exports.resolveNestTables = resolveNestTables;
9
11
  exports.nestCell = nestCell;
12
+ exports.describeField = describeField;
13
+ exports.describeFields = describeFields;
14
+ exports.parseRecordCounts = parseRecordCounts;
10
15
  exports.encodeDateTimeParts = encodeDateTimeParts;
11
16
  const const_1 = __importDefault(require("./const"));
17
+ const serialize_1 = require("./serialize");
12
18
  /***************************************
13
19
  *
14
20
  * SQLVar
@@ -84,12 +90,15 @@ exports.SQLVarBase = SQLVarBase;
84
90
  * decoder and by fetchBlobSyncRow, which must agree on where each column
85
91
  * landed in the row.
86
92
  */
87
- function computeColumnKeys(output, nestTables, lowercaseKeys) {
93
+ function computeColumnKeys(output, nestTables, lowercaseKeys, transform) {
88
94
  return output.map((column) => {
89
95
  let key = column.alias || '';
90
96
  if (lowercaseKeys) {
91
97
  key = key.toLowerCase();
92
98
  }
99
+ if (transform) {
100
+ key = transform(key);
101
+ }
93
102
  if (nestTables !== true && typeof nestTables !== 'string') {
94
103
  return { key };
95
104
  }
@@ -97,12 +106,63 @@ function computeColumnKeys(output, nestTables, lowercaseKeys) {
97
106
  if (lowercaseKeys) {
98
107
  table = table.toLowerCase();
99
108
  }
109
+ if (transform) {
110
+ table = transform(table);
111
+ }
100
112
  if (nestTables === true) {
101
113
  return { table, key };
102
114
  }
103
115
  return { key: table + nestTables + key };
104
116
  });
105
117
  }
118
+ /** FIRST_NAME → firstName (the transformKeys: 'camel' built-in). */
119
+ function camelizeKey(key) {
120
+ const parts = String(key).toLowerCase().split('_');
121
+ let out = parts[0] || '';
122
+ for (let i = 1; i < parts.length; i++) {
123
+ const part = parts[i];
124
+ if (part) {
125
+ out += part.charAt(0).toUpperCase() + part.slice(1);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ /**
131
+ * Resolve the effective transformKeys value (per-query wins over the
132
+ * connection option) into a callable mapper, or undefined when off.
133
+ * A custom mapper is guarded like the typeCast hook: a throw inside the
134
+ * row-decode loop would be mistaken for an incomplete packet and desync
135
+ * the response queue, so failures fall back to the untransformed key.
136
+ */
137
+ function resolveKeyTransform(queryOptions, connectionOptions) {
138
+ const value = resolveQueryOption('transformKeys', queryOptions, connectionOptions);
139
+ if (value === 'camel') {
140
+ return camelizeKey;
141
+ }
142
+ if (typeof value !== 'function') {
143
+ return undefined;
144
+ }
145
+ return (key) => {
146
+ try {
147
+ return String(value(key));
148
+ }
149
+ catch (err) {
150
+ console.warn('[node-firebird] transformKeys mapper threw for key "%s": %s — using the untransformed key', key, err && err.message);
151
+ return key;
152
+ }
153
+ };
154
+ }
155
+ /**
156
+ * Shared precedence rule for per-query-overridable connection options:
157
+ * the per-query value wins whenever it is present (even if falsy), the
158
+ * connection option applies otherwise.
159
+ */
160
+ function resolveQueryOption(name, queryOptions, connectionOptions) {
161
+ if (queryOptions && queryOptions[name] !== undefined) {
162
+ return queryOptions[name];
163
+ }
164
+ return connectionOptions ? connectionOptions[name] : undefined;
165
+ }
106
166
  /**
107
167
  * Resolve the effective nestTables value: the per-query option wins over
108
168
  * the connection option. The decoder and fetchBlobSyncRow both use this —
@@ -110,10 +170,7 @@ function computeColumnKeys(output, nestTables, lowercaseKeys) {
110
170
  * up in the wrong place.
111
171
  */
112
172
  function resolveNestTables(queryOptions, connectionOptions) {
113
- if (queryOptions && queryOptions.nestTables !== undefined) {
114
- return queryOptions.nestTables;
115
- }
116
- return connectionOptions && connectionOptions.nestTables;
173
+ return resolveQueryOption('nestTables', queryOptions, connectionOptions);
117
174
  }
118
175
  /**
119
176
  * The object a column's value lives in: the row itself, or — when the
@@ -128,6 +185,108 @@ function nestCell(row, table) {
128
185
  return row[table] || (row[table] = {});
129
186
  }
130
187
  //------------------------------------------------------
188
+ /** Human-readable names for the SQL_* wire type codes. */
189
+ exports.SQL_TYPE_NAMES = {
190
+ [const_1.default.SQL_TEXT]: 'TEXT',
191
+ [const_1.default.SQL_VARYING]: 'VARYING',
192
+ [const_1.default.SQL_SHORT]: 'SHORT',
193
+ [const_1.default.SQL_LONG]: 'LONG',
194
+ [const_1.default.SQL_FLOAT]: 'FLOAT',
195
+ [const_1.default.SQL_DOUBLE]: 'DOUBLE',
196
+ [const_1.default.SQL_D_FLOAT]: 'D_FLOAT',
197
+ [const_1.default.SQL_TIMESTAMP]: 'TIMESTAMP',
198
+ [const_1.default.SQL_BLOB]: 'BLOB',
199
+ [const_1.default.SQL_ARRAY]: 'ARRAY',
200
+ [const_1.default.SQL_QUAD]: 'QUAD',
201
+ [const_1.default.SQL_TYPE_TIME]: 'TIME',
202
+ [const_1.default.SQL_TYPE_DATE]: 'DATE',
203
+ [const_1.default.SQL_INT64]: 'INT64',
204
+ [const_1.default.SQL_INT128]: 'INT128',
205
+ [const_1.default.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
206
+ [const_1.default.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
207
+ [const_1.default.SQL_TIME_TZ]: 'TIME_TZ',
208
+ [const_1.default.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
209
+ [const_1.default.SQL_DEC16]: 'DEC16',
210
+ [const_1.default.SQL_DEC34]: 'DEC34',
211
+ [const_1.default.SQL_BOOLEAN]: 'BOOLEAN',
212
+ [const_1.default.SQL_NULL]: 'NULL',
213
+ };
214
+ /**
215
+ * Public column-metadata shape for one output descriptor: the vocabulary
216
+ * both the typeCast hook and withMeta `fields` deliver. Keep the two in
217
+ * lockstep by building both through here.
218
+ */
219
+ function describeField(meta) {
220
+ return {
221
+ type: meta.type,
222
+ typeName: exports.SQL_TYPE_NAMES[meta.type] || 'UNKNOWN',
223
+ subType: meta.subType,
224
+ scale: meta.scale,
225
+ length: meta.length,
226
+ nullable: meta.nullable,
227
+ field: meta.field,
228
+ relation: meta.relation,
229
+ relationAlias: meta.relationAlias,
230
+ relationSchema: meta.relationSchema,
231
+ alias: meta.alias,
232
+ };
233
+ }
234
+ /**
235
+ * Map a statement's output descriptors to the column-metadata array
236
+ * delivered in withMeta results ({ rows, fields, ... }).
237
+ */
238
+ function describeFields(output) {
239
+ return (output || []).map(describeField);
240
+ }
241
+ /**
242
+ * Parse the op_info_sql response buffer of a Const.RECORDS_INFO request
243
+ * into per-verb row counts. The buffer holds an isc_info_sql_records
244
+ * cluster (2-byte total length, then nested isc_info_req_*_count items,
245
+ * each 2-byte length + little-endian integer) terminated by isc_info_end.
246
+ */
247
+ function parseRecordCounts(buffer) {
248
+ const counts = { selectCount: 0, insertCount: 0, updateCount: 0, deleteCount: 0 };
249
+ if (!buffer || !buffer.length) {
250
+ return counts;
251
+ }
252
+ // this runs inside a response callback — a malformed/truncated buffer
253
+ // must yield partial counts, never a throw
254
+ try {
255
+ const br = new serialize_1.BlrReader(buffer);
256
+ while (br.pos < br.buffer.length) {
257
+ const item = br.readByteCode();
258
+ if (item === const_1.default.isc_info_end || item === const_1.default.isc_info_truncated) {
259
+ break;
260
+ }
261
+ if (item === const_1.default.isc_info_sql_records) {
262
+ br.pos += 2; // skip the cluster's total length; nested items follow
263
+ continue;
264
+ }
265
+ switch (item) {
266
+ case const_1.default.isc_info_req_select_count:
267
+ counts.selectCount = br.readInt() || 0;
268
+ break;
269
+ case const_1.default.isc_info_req_insert_count:
270
+ counts.insertCount = br.readInt() || 0;
271
+ break;
272
+ case const_1.default.isc_info_req_update_count:
273
+ counts.updateCount = br.readInt() || 0;
274
+ break;
275
+ case const_1.default.isc_info_req_delete_count:
276
+ counts.deleteCount = br.readInt() || 0;
277
+ break;
278
+ default:
279
+ // unknown item: its 2-byte length prefix tells us how far to skip
280
+ br.pos += 2 + br.buffer.readUInt16LE(br.pos);
281
+ }
282
+ }
283
+ }
284
+ catch (e) {
285
+ // fall through with whatever was parsed so far
286
+ }
287
+ return counts;
288
+ }
289
+ //------------------------------------------------------
131
290
  class SQLVarText extends SQLVarBase {
132
291
  decode(data, lowerV13, options) {
133
292
  let ret;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/pool.ts CHANGED
@@ -26,7 +26,12 @@ type AttachFn = (options: any, callback: Callback) => void;
26
26
  *
27
27
  * Options: max (factory argument), options.min (floor the reaper never
28
28
  * shrinks below), options.idleTimeoutMillis (close idle connections after
29
- * this many ms; 0/absent = never), options.connectTimeout.
29
+ * this many ms; 0/absent = never), options.connectTimeout,
30
+ * options.maxUses (retire a connection after this many checkouts — pg's
31
+ * maxUses), options.maxLifetimeMillis (retire a connection this many ms
32
+ * after it was created — Postgres.js's max_lifetime). Retirement happens
33
+ * when the connection is returned to the pool, and the sweep also closes
34
+ * over-lifetime idle connections; a replacement is created on demand.
30
35
  */
31
36
  class Pool extends Events.EventEmitter {
32
37
  attach: AttachFn;
@@ -37,6 +42,8 @@ class Pool extends Events.EventEmitter {
37
42
  max: number;
38
43
  min: number;
39
44
  idleTimeoutMillis: number;
45
+ maxUses: number;
46
+ maxLifetimeMillis: number;
40
47
  pending: Callback[];
41
48
  options: any;
42
49
  _destroyed: boolean;
@@ -52,22 +59,50 @@ class Pool extends Events.EventEmitter {
52
59
  this.max = max || 4;
53
60
  this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
54
61
  this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
62
+ this.maxUses = (options && options.maxUses > 0) ? options.maxUses : 0;
63
+ this.maxLifetimeMillis = (options && options.maxLifetimeMillis > 0) ? options.maxLifetimeMillis : 0;
55
64
  this.pending = []; // callbacks waiting for a free slot
56
65
  this.options = options;
57
66
  this._destroyed = false; // true after destroy() — prevents further use
58
67
  this._reaper = null;
59
68
 
60
- if (this.idleTimeoutMillis) {
69
+ // the sweep serves both idle eviction and lifetime retirement of
70
+ // idle connections; base its cadence on the tightest configured limit
71
+ var sweepBasis = Math.min(this.idleTimeoutMillis || Infinity, this.maxLifetimeMillis || Infinity);
72
+ if (sweepBasis !== Infinity) {
61
73
  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
74
+ // Sweep at half the basis (bounded to 100ms..30s) so a
75
+ // connection lives at most ~1.5x its limit. unref() keeps
64
76
  // the timer from holding the process open.
65
- var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
77
+ var interval = Math.min(Math.max(sweepBasis / 2, 100), 30000);
66
78
  this._reaper = setInterval(function() { self._reap(); }, interval);
67
79
  if (this._reaper.unref) this._reaper.unref();
68
80
  }
69
81
  }
70
82
 
83
+ /** True when the connection exceeded maxUses / maxLifetimeMillis.
84
+ * Both stamps are set unconditionally when the pool creates the
85
+ * connection, so they can be read bare here. */
86
+ _isExpired(db: any): boolean {
87
+ if (this.maxUses > 0 && db.__poolUseCount >= this.maxUses) return true;
88
+ if (this.maxLifetimeMillis > 0 && Date.now() - db.__poolCreatedAt >= this.maxLifetimeMillis) return true;
89
+ return false;
90
+ }
91
+
92
+ /** Close a healthy pooled connection for good (reaper/retirement path). */
93
+ _retire(db: any): void {
94
+ var self = this;
95
+ this._forget(db);
96
+ db.connection._pooled = false;
97
+ try {
98
+ db.detach(function(err?: any) {
99
+ if (err) self._emitError(err, db);
100
+ });
101
+ } catch (e) {
102
+ self._emitError(e, db);
103
+ }
104
+ }
105
+
71
106
  /** Physical connections owned by the pool (idle + in use). */
72
107
  get totalCount(): number {
73
108
  return this.internaldb.length;
@@ -124,19 +159,18 @@ class Pool extends Events.EventEmitter {
124
159
  self._forget(db);
125
160
  return;
126
161
  }
162
+ // lifetime retirement applies even below min — recycling is the
163
+ // point; replacements are created on demand
164
+ if (self._isExpired(db)) {
165
+ self._retire(db);
166
+ return;
167
+ }
168
+ if (!self.idleTimeoutMillis) return;
127
169
  if (self.internaldb.length <= self.min) return;
128
170
  var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
129
171
  if (now - idleSince < self.idleTimeoutMillis) return;
130
172
 
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
- }
173
+ self._retire(db);
140
174
  });
141
175
  }
142
176
 
@@ -175,6 +209,7 @@ class Pool extends Events.EventEmitter {
175
209
  }
176
210
  // Idle connection available — hand it out immediately.
177
211
  self.dbinuse++;
212
+ db.__poolUseCount = (db.__poolUseCount || 0) + 1;
178
213
  self.emit('acquire', db);
179
214
  cb(null, db);
180
215
  } else {
@@ -235,6 +270,8 @@ class Pool extends Events.EventEmitter {
235
270
 
236
271
  if (!err) {
237
272
  self.dbinuse++;
273
+ db.__poolCreatedAt = Date.now();
274
+ db.__poolUseCount = 1;
238
275
  self.internaldb.push(db);
239
276
  db.on('detach', function () {
240
277
  // also in pool (could be a twice call to detach)
@@ -244,6 +281,12 @@ class Pool extends Events.EventEmitter {
244
281
  if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
245
282
  self.internaldb.splice(self.internaldb.indexOf(db), 1);
246
283
  self.emit('remove', db);
284
+ } else if (self._isExpired(db)) {
285
+ // worn out (maxUses / maxLifetimeMillis): close it
286
+ // for good instead of returning it to the idle
287
+ // pool. The re-fired detach event exits early via
288
+ // the internaldb guard above.
289
+ self._retire(db);
247
290
  } else {
248
291
  db.__poolIdleSince = Date.now();
249
292
  self.pooldb.push(db);