node-firebird 2.12.0 → 2.14.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
@@ -14,7 +14,7 @@
14
14
  - [Connection types](#connection-types) — connection options, `firebird://` URIs and traditional connection strings, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
16
  - [Examples](#examples) — parametrized queries, tagged-template queries (sql), named placeholders, nested result tables (nestTables), row-key transforms (transformKeys), result metadata / affected rows (withMeta), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
- - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
17
+ - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts incl. BLOBs), bulk-insert stream (batchStream), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
18
18
  - [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
19
19
  - [FAQ](#faq)
20
20
  - [Contributing](#contributing) · [Contributors](#contributors)
@@ -39,16 +39,31 @@ and 26 against Firebird 3, 4, 5 and 6).
39
39
 
40
40
  ## Usage
41
41
 
42
+ CommonJS and ESM are both first-class (conditional `exports`):
43
+
42
44
  ```js
43
- var Firebird = require('node-firebird');
45
+ // CommonJS
46
+ const Firebird = require('node-firebird');
47
+
48
+ // ESM — default and named imports both work
49
+ import Firebird from 'node-firebird';
50
+ import { attach, pool, GDSCode, SQL_TYPES } from 'node-firebird';
44
51
  ```
45
52
 
53
+ The documented subpaths keep working in both module systems
54
+ (`require('node-firebird/lib/gdscodes')`, …).
55
+
46
56
  TypeScript is fully supported — the driver itself is written in TypeScript and
47
- ships its own type declarations:
57
+ ships its own type declarations, with generics on the query APIs:
48
58
 
49
59
  ```ts
50
60
  import * as Firebird from 'node-firebird';
51
61
  import type { Options, Database } from 'node-firebird';
62
+
63
+ interface Emp { ID: number; NAME: string }
64
+ const rows = await db.queryAsync<Emp>('SELECT ID, NAME FROM EMP'); // Emp[]
65
+ db.query<Emp>('SELECT ID, NAME FROM EMP', [], (err, rows) => { /* rows: Emp[] */ });
66
+ const r = await db.queryAsync<Emp>('SELECT ...', [], { withMeta: true }); // QueryResult<Emp>
52
67
  ```
53
68
 
54
69
  ### Developing the driver
@@ -340,6 +355,48 @@ console.log({
340
355
  is attached, so existing applications keep working unchanged.
341
356
  - Metrics are plain getters — reading them has no side effects.
342
357
 
358
+ #### Multi-host pooling (PoolCluster)
359
+
360
+ For primary/replica topologies (Firebird 4+ logical replication) or plain
361
+ redundancy, `Firebird.poolCluster` manages one pool per named node with
362
+ pattern-based selection and automatic failover — the mysql2 `PoolCluster`
363
+ model:
364
+
365
+ ```js
366
+ const cluster = Firebird.poolCluster({
367
+ defaults: { user: 'SYSDBA', password: 'masterkey', database: '/data/app.fdb', connectTimeout: 5000 },
368
+ nodes: {
369
+ primary: { host: 'db-primary' },
370
+ replica1: { host: 'db-replica-1' },
371
+ replica2: { host: 'db-replica-2' },
372
+ },
373
+ max: 4, // per-node pool size
374
+ selector: 'rr', // 'rr' | 'random' | 'order' (first online match)
375
+ removeNodeErrorCount: 5, // offline a node after N consecutive connection failures
376
+ restoreNodeTimeout: 30000, // put it back into rotation after 30s (0 = manual restore())
377
+ });
378
+
379
+ // writes go to the primary, reads round-robin across replicas
380
+ await cluster.withConnection('primary', db => db.queryAsync('UPDATE ...'));
381
+ const rows = await cluster.withConnection('replica*', db => db.queryAsync('SELECT ...'));
382
+
383
+ // or bind a pattern once (mysql2's cluster.of)
384
+ const replicas = cluster.of('replica*', 'rr');
385
+ const db = await replicas.getAsync(); // release with db.detach(), as with a plain pool
386
+ ```
387
+
388
+ A failed connection attempt marks the node and **fails over** to the next
389
+ matching online node; only when every candidate has failed does the call
390
+ error (set `connectTimeout` in `defaults` so dead-but-routable hosts fail
391
+ fast). Nodes taken offline emit `'offline'`, restorations emit
392
+ `'online'`, and `cluster.status()` returns per-node
393
+ `{ online, errorCount, totalCount, idleCount, activeCount, waitingCount }`
394
+ for monitoring. Each node's pool is a regular
395
+ [connection pool](#pool-events-and-metrics) — health checks, idle
396
+ reaping, `maxUses`/`maxLifetimeMillis` recycling and keepalive all apply
397
+ per node. `add(name, overrides)` / `remove(name)` manage nodes at
398
+ runtime; `destroy()` closes everything.
399
+
343
400
  #### Advanced Pooling Features
344
401
 
345
402
  The pool implementation includes several safeguards for reliability:
@@ -1209,7 +1266,11 @@ Firebird.attach(options, function (err, db) {
1209
1266
  });
1210
1267
 
1211
1268
  db.on('error', function (err) {
1212
- // connection-level errors (socket errors, closed connection, etc.)
1269
+ // connection-level errors (socket errors, closed connection, etc.).
1270
+ // Delivered to listeners only: without one, background failures (e.g.
1271
+ // a failed automatic reconnect) are NOT re-thrown as uncaught
1272
+ // exceptions — the operations they affect still receive the error
1273
+ // through their own callbacks/promises.
1213
1274
  });
1214
1275
 
1215
1276
  db.on('transaction', function (options) {
@@ -1506,7 +1567,27 @@ Commonly used Firebird character sets are automatically mapped to their correspo
1506
1567
  | `ASCII` | `ascii` | 7-bit ASCII. |
1507
1568
  | `NONE` | `latin1` | Raw/unspecified character set. Treated as binary-safe 8-bit characters. |
1508
1569
 
1509
- Accented characters and fixed-length `CHAR(N)` column whitespace/truncation are handled automatically matching the connection character set width definitions.
1570
+ Beyond Node's native encodings, the driver ships **codepage codecs** for the
1571
+ single-byte charsets (decode *and* encode — columns, parameters, SQL
1572
+ literals and `blobAsText` blobs all transcode):
1573
+
1574
+ > `WIN1250`–`WIN1258` (Central European, Cyrillic, Greek, Turkish, Hebrew,
1575
+ > Arabic, Baltic, Vietnamese), `ISO8859_2`–`ISO8859_9`, `ISO8859_13`,
1576
+ > `KOI8R`, `KOI8U`, `DOS866`
1577
+
1578
+ ```js
1579
+ const options = { /* ... */ encoding: 'WIN1251' };
1580
+ await db.queryAsync('INSERT INTO T VALUES (?)', ['Привет']); // encoded as cp1251
1581
+ ```
1582
+
1583
+ The codecs are built from Node's ICU tables at first use (present in every
1584
+ official Node build). `attachOrCreate`/`create` honour `options.encoding`
1585
+ for the new database's default charset too. Accented characters and
1586
+ fixed-length `CHAR(N)` whitespace/truncation are handled automatically per
1587
+ the charset width — and single-byte columns (including charset `NONE`) are
1588
+ readable in full under the default UTF8 connection (the declared fetch
1589
+ lengths are widened per the charset-width ratio, fixing the
1590
+ `string right truncation` errors of issue [#422](https://github.com/hgourvest/node-firebird/issues/422)).
1510
1591
 
1511
1592
  #### Custom Charset Connection Example
1512
1593
  ```js
@@ -1770,11 +1851,43 @@ Notes:
1770
1851
  - Values are encoded from the statement's own parameter metadata, so
1771
1852
  NUMERIC/DECIMAL scale, `BIGINT`/`INT128` (pass `BigInt`), `BOOLEAN`,
1772
1853
  `TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE` and `DECFLOAT` all round-trip
1773
- exactly. BLOB and ARRAY parameters are not supported in batches yet.
1854
+ exactly.
1855
+ - `BLOB` columns accept Buffers, strings, JSON-able objects, or
1856
+ pre-created blob quad ids: values are uploaded as transaction blobs
1857
+ first — all initiated back-to-back so the blob ops pipeline on the
1858
+ wire — and the batch messages reference their ids. `ARRAY` parameters
1859
+ are not supported.
1774
1860
  - Oversized `CHAR`/`VARCHAR` values fail the batch client-side before
1775
1861
  anything is sent; server-side record errors (constraint violations,
1776
1862
  truncation…) are reported per record.
1777
1863
 
1864
+ ### Bulk-insert stream (batchStream, Firebird 4.0+)
1865
+
1866
+ `db.batchStream(sql, options)` is the COPY FROM analogue: an object-mode
1867
+ `Writable` that flushes parameter-array rows in chunks through one
1868
+ prepared statement using the batch API. The Database form runs its own
1869
+ transaction — **committed on finish, rolled back on error or destroy**,
1870
+ all-or-nothing for the whole stream:
1871
+
1872
+ ```js
1873
+ const { pipeline } = require('stream/promises');
1874
+
1875
+ const stream = db.batchStream('INSERT INTO EVENTS VALUES (?, ?, ?)', {
1876
+ flushRows: 1000, // rows buffered per batch flush (default 1000)
1877
+ });
1878
+
1879
+ await pipeline(mySourceOfRowArrays, stream); // e.g. a CSV parser
1880
+ console.log(stream.recordCount, stream.affectedRows); // totals after 'finish'
1881
+ ```
1882
+
1883
+ Backpressure is the `Writable` machinery itself: writes pause while a
1884
+ chunk is in flight, so an arbitrarily large source never accumulates in
1885
+ memory beyond `flushRows`. BLOB columns accept Buffers/strings per the
1886
+ batch rules above. `transaction.batchStream(sql, options)` runs inside an
1887
+ existing transaction and leaves commit/rollback to you. The remaining
1888
+ options (`chunkSize`, `bufferSize`, …) pass through to
1889
+ [executeBatch](#batch-execution-firebird-40).
1890
+
1778
1891
  ### Statement Timeouts (Firebird 4.0+)
1779
1892
  Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
1780
1893
  ```js
@@ -2110,6 +2223,25 @@ app.get('/users/:id/picture', withConnection(pool, function (db, req, res, done)
2110
2223
 
2111
2224
  Answers to recurring questions from the [issue tracker](https://github.com/hgourvest/node-firebird/issues).
2112
2225
 
2226
+ #### Text comes back as `������` with a WIN1250/1251/1253/1257 database (issue [#319](https://github.com/hgourvest/node-firebird/issues/319))
2227
+
2228
+ Resolved — the driver now ships codepage codecs for the single-byte
2229
+ charsets (`WIN1250`–`WIN1258`, `ISO8859_2`–`9`/`13`, `KOI8R`/`KOI8U`,
2230
+ `DOS866`): just set the matching connection encoding and both reads and
2231
+ writes transcode correctly, including parameters, SQL literals and
2232
+ `blobAsText` blobs:
2233
+
2234
+ ```js
2235
+ const options = { /* ... */ encoding: 'WIN1253' };
2236
+ ```
2237
+
2238
+ See [§ Character Set & Encoding Support](#character-set--encoding-support).
2239
+ For a charset *outside* that list (e.g. `DOS437`/`DOS850`), the
2240
+ [iconv-lite](https://www.npmjs.com/package/iconv-lite) escape hatch still
2241
+ works: connect with `encoding: 'NONE'`, read raw bytes via `latin1` in a
2242
+ [`typeCast` hook](#custom-type-parsers-typecast) and decode with the real
2243
+ codepage; write already-encoded bytes as Buffer parameters.
2244
+
2113
2245
  #### Can I use aggregate functions like `LIST()`? I get "no database to handle" when I call the result.
2114
2246
 
2115
2247
  Yes — `LIST()` is plain SQL and needs no special driver support. The error happens because `LIST()` returns a text blob (subtype 1), and blob columns come back from `db.query`/`transaction.query` as **async reader functions** bound to the transaction the query ran in (see [Reading Blobs](#reading-blobs-asynchronous)). Calling that function without a transaction — or with a different one — is what throws "no database to handle".
package/lib/callback.d.ts CHANGED
@@ -28,6 +28,13 @@ export declare function toError(err: any): Error;
28
28
  * Run a callback-style operation and return a Promise for its result.
29
29
  * Usage: fromCallback<Database>(cb => attach(options, cb))
30
30
  */
31
+ /**
32
+ * Run `work` with a pooled connection and always return it to its pool
33
+ * (detach) when the promise settles — a detach hiccup never masks the
34
+ * outcome of `work`. Shared by Pool.withConnection and
35
+ * PoolCluster.withConnection so the release semantics cannot drift.
36
+ */
37
+ export declare function withPooledConnection<T>(getAsync: () => Promise<any>, work: (db: any) => Promise<T> | T): Promise<T>;
31
38
  export declare function fromCallback<T = any>(executor: (cb: Callback<T>) => void): Promise<T>;
32
39
  export declare function doError(obj: any, callback?: (...args: any[]) => void): void;
33
40
  export declare function doCallback<T>(obj: T, callback?: Callback<T>): void;
package/lib/callback.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toError = toError;
4
+ exports.withPooledConnection = withPooledConnection;
4
5
  exports.fromCallback = fromCallback;
5
6
  exports.doError = doError;
6
7
  exports.doCallback = doCallback;
@@ -23,6 +24,21 @@ function toError(err) {
23
24
  * Run a callback-style operation and return a Promise for its result.
24
25
  * Usage: fromCallback<Database>(cb => attach(options, cb))
25
26
  */
27
+ /**
28
+ * Run `work` with a pooled connection and always return it to its pool
29
+ * (detach) when the promise settles — a detach hiccup never masks the
30
+ * outcome of `work`. Shared by Pool.withConnection and
31
+ * PoolCluster.withConnection so the release semantics cannot drift.
32
+ */
33
+ async function withPooledConnection(getAsync, work) {
34
+ const db = await getAsync();
35
+ try {
36
+ return await work(db);
37
+ }
38
+ finally {
39
+ await new Promise(function (resolve) { db.detach(function () { resolve(); }); });
40
+ }
41
+ }
26
42
  function fromCallback(executor) {
27
43
  return new Promise(function (resolve, reject) {
28
44
  executor(function (err, result) {
package/lib/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import Connection from './wire/connection';
2
+ import PoolCluster from './pool-cluster';
3
+ import type { PoolClusterOptions } from './pool-cluster';
2
4
  import { escape as escapeValue } from './utils';
3
5
  import { parseConnectionUri, parseConnectionString } from './uri';
4
6
  import type { Options, SvcMgrOptions, DatabaseCallback, ServiceManagerCallback, SimpleCallback, ConnectionPool, Database, ServiceManager } from './types';
@@ -41,6 +43,14 @@ export declare function drop(options: Options | string, callback: SimpleCallback
41
43
  export declare function create(options: Options | string, callback: DatabaseCallback): void;
42
44
  export declare function attachOrCreate(options: Options | string, callback: DatabaseCallback): void;
43
45
  export declare function pool(max: number, options: Options | string): ConnectionPool;
46
+ /**
47
+ * Multi-host pooling (primaries/replicas, failover): named nodes, each
48
+ * backed by a regular pool, selected by glob pattern + 'rr'/'random'/
49
+ * 'order' selector, with connection-failure failover and error-based
50
+ * node offlining. See README § Multi-host pooling.
51
+ */
52
+ export declare function poolCluster(options?: PoolClusterOptions): PoolCluster;
53
+ export type { PoolClusterOptions, ClusterSelector } from './pool-cluster';
44
54
  export { parseConnectionUri, parseConnectionString };
45
55
  export { parseNamedPlaceholders } from './named-params';
46
56
  export declare function attachAsync(options: SvcMgrOptions): Promise<ServiceManager>;
package/lib/index.js CHANGED
@@ -23,6 +23,7 @@ exports.drop = drop;
23
23
  exports.create = create;
24
24
  exports.attachOrCreate = attachOrCreate;
25
25
  exports.pool = pool;
26
+ exports.poolCluster = poolCluster;
26
27
  exports.attachAsync = attachAsync;
27
28
  exports.createAsync = createAsync;
28
29
  exports.attachOrCreateAsync = attachOrCreateAsync;
@@ -31,6 +32,7 @@ const const_1 = __importDefault(require("./wire/const"));
31
32
  const callback_1 = require("./callback");
32
33
  const connection_1 = __importDefault(require("./wire/connection"));
33
34
  const pool_1 = __importDefault(require("./pool"));
35
+ const pool_cluster_1 = __importDefault(require("./pool-cluster"));
34
36
  const utils_1 = require("./utils");
35
37
  const uri_1 = require("./uri");
36
38
  Object.defineProperty(exports, "parseConnectionUri", { enumerable: true, get: function () { return uri_1.parseConnectionUri; } });
@@ -178,6 +180,17 @@ function attachOrCreate(options, callback) {
178
180
  function pool(max, options) {
179
181
  return new pool_1.default(attach, max, Object.assign({}, (0, uri_1.normalizeOptions)(options), { isPool: true }));
180
182
  }
183
+ /**
184
+ * Multi-host pooling (primaries/replicas, failover): named nodes, each
185
+ * backed by a regular pool, selected by glob pattern + 'rr'/'random'/
186
+ * 'order' selector, with connection-failure failover and error-based
187
+ * node offlining. See README § Multi-host pooling.
188
+ */
189
+ function poolCluster(options) {
190
+ const normalized = { ...(options || {}) };
191
+ normalized.defaults = (0, uri_1.normalizeOptions)(normalized.defaults || {});
192
+ return new pool_cluster_1.default(attach, normalized);
193
+ }
181
194
  var named_params_1 = require("./named-params");
182
195
  Object.defineProperty(exports, "parseNamedPlaceholders", { enumerable: true, get: function () { return named_params_1.parseNamedPlaceholders; } });
183
196
  function attachAsync(options) {
@@ -0,0 +1,89 @@
1
+ /***************************************
2
+ *
3
+ * PoolCluster — multi-host pooling (primaries/replicas, failover)
4
+ *
5
+ * The mysql2 PoolCluster model on top of this driver's Pool: named
6
+ * nodes, each backed by a regular connection pool (health checks,
7
+ * recycling and metrics included), selected by glob pattern +
8
+ * selector. Consecutive connection failures take a node offline
9
+ * (with optional timed restoration), and get() fails over to the
10
+ * next matching online node.
11
+ *
12
+ ***************************************/
13
+ import Events from 'events';
14
+ import type { Callback } from './callback';
15
+ type AttachFn = (options: any, callback: Callback) => void;
16
+ export type ClusterSelector = 'rr' | 'random' | 'order';
17
+ export interface PoolClusterOptions {
18
+ /** Options shared by every node (user, password, database, …). */
19
+ defaults?: any;
20
+ /** name → per-node option overrides (host, port, …). */
21
+ nodes?: Record<string, any>;
22
+ /** Per-node pool size (default 4). */
23
+ max?: number;
24
+ /** Default selector for get()/of() (default 'rr'). */
25
+ selector?: ClusterSelector;
26
+ /**
27
+ * Consecutive connection failures after which a node goes offline
28
+ * (default 5; 0 disables offlining).
29
+ */
30
+ removeNodeErrorCount?: number;
31
+ /**
32
+ * Milliseconds after which an offline node is restored and probed
33
+ * again (default 30000; 0 = stay offline until restore()/remove()).
34
+ */
35
+ restoreNodeTimeout?: number;
36
+ }
37
+ /**
38
+ * Events: 'online' (name) — node restored; 'offline' (name) — node taken
39
+ * out of rotation after too many connection failures; 'remove' (name) —
40
+ * node removed via remove().
41
+ */
42
+ declare class PoolCluster extends Events.EventEmitter {
43
+ private attach;
44
+ private nodes;
45
+ private rrIndex;
46
+ private max;
47
+ private defaults;
48
+ private selector;
49
+ private removeNodeErrorCount;
50
+ private restoreNodeTimeout;
51
+ private _destroyed;
52
+ constructor(attach: AttachFn, options?: PoolClusterOptions);
53
+ /** Register a node; its pool is created lazily-safe right away. */
54
+ add(name: string, overrides?: any): this;
55
+ /** Remove a node for good, destroying its pool. */
56
+ remove(name: string, callback?: (err?: any) => void): void;
57
+ /** Bring an offline node back into rotation immediately. */
58
+ restore(name: string): void;
59
+ /** name → { online, errorCount, pool metrics } for every node. */
60
+ status(): Record<string, any>;
61
+ private matching;
62
+ private pick;
63
+ private noteFailure;
64
+ /**
65
+ * Acquire a connection from a node matching `pattern` (default '*').
66
+ * Connection failures mark the node and FAIL OVER to the next
67
+ * matching online node; only when every candidate has failed does the
68
+ * callback receive the last error. Release connections with
69
+ * db.detach(), exactly like a plain pool.
70
+ */
71
+ get(pattern: string | Callback, selector?: ClusterSelector | Callback, callback?: Callback): void;
72
+ getAsync(pattern?: string, selector?: ClusterSelector): Promise<any>;
73
+ /**
74
+ * A pool-like facade bound to a pattern (mysql2's cluster.of):
75
+ * { get, getAsync, withConnection } routed through the cluster's
76
+ * selection and failover.
77
+ */
78
+ of(pattern: string, selector?: ClusterSelector): {
79
+ get(callback: Callback): void;
80
+ getAsync(): Promise<any>;
81
+ withConnection<T>(work: (db: any) => Promise<T> | T): Promise<T>;
82
+ };
83
+ /** Run `work` with a connection from a matching node, always released. */
84
+ withConnection<T>(pattern: string, work: (db: any) => Promise<T> | T, selector?: ClusterSelector): Promise<T>;
85
+ /** Destroy every node's pool. */
86
+ destroy(callback?: (err?: any) => void): void;
87
+ destroyAsync(): Promise<void>;
88
+ }
89
+ export default PoolCluster;
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ /***************************************
3
+ *
4
+ * PoolCluster — multi-host pooling (primaries/replicas, failover)
5
+ *
6
+ * The mysql2 PoolCluster model on top of this driver's Pool: named
7
+ * nodes, each backed by a regular connection pool (health checks,
8
+ * recycling and metrics included), selected by glob pattern +
9
+ * selector. Consecutive connection failures take a node offline
10
+ * (with optional timed restoration), and get() fails over to the
11
+ * next matching online node.
12
+ *
13
+ ***************************************/
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ const events_1 = __importDefault(require("events"));
19
+ const callback_1 = require("./callback");
20
+ const uri_1 = require("./uri");
21
+ const pool_1 = __importDefault(require("./pool"));
22
+ function patternToRegExp(pattern) {
23
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
24
+ return new RegExp('^' + escaped + '$');
25
+ }
26
+ /**
27
+ * Events: 'online' (name) — node restored; 'offline' (name) — node taken
28
+ * out of rotation after too many connection failures; 'remove' (name) —
29
+ * node removed via remove().
30
+ */
31
+ class PoolCluster extends events_1.default.EventEmitter {
32
+ constructor(attach, options) {
33
+ super();
34
+ this.nodes = new Map();
35
+ this.rrIndex = new Map();
36
+ this._destroyed = false;
37
+ options = options || {};
38
+ this.attach = attach;
39
+ this.defaults = options.defaults || {};
40
+ this.max = options.max && options.max > 0 ? options.max : 4;
41
+ this.selector = options.selector || 'rr';
42
+ this.removeNodeErrorCount = options.removeNodeErrorCount !== undefined ? options.removeNodeErrorCount : 5;
43
+ this.restoreNodeTimeout = options.restoreNodeTimeout !== undefined ? options.restoreNodeTimeout : 30000;
44
+ for (const [name, overrides] of Object.entries(options.nodes || {})) {
45
+ this.add(name, overrides);
46
+ }
47
+ }
48
+ /** Register a node; its pool is created lazily-safe right away. */
49
+ add(name, overrides) {
50
+ if (this._destroyed) {
51
+ throw new Error('PoolCluster has been destroyed');
52
+ }
53
+ if (this.nodes.has(name)) {
54
+ throw new Error('PoolCluster node already exists: ' + name);
55
+ }
56
+ // a connection-string override must be parsed, not object-spread
57
+ // into character-indexed garbage
58
+ if (typeof overrides === 'string') {
59
+ overrides = (0, uri_1.parseConnectionString)(overrides);
60
+ }
61
+ const nodeOptions = { ...this.defaults, ...(overrides || {}) };
62
+ this.nodes.set(name, {
63
+ name,
64
+ options: nodeOptions,
65
+ pool: new pool_1.default(this.attach, nodeOptions.max || this.max, { ...nodeOptions, isPool: true }),
66
+ online: true,
67
+ errorCount: 0,
68
+ restoreTimer: null,
69
+ });
70
+ return this;
71
+ }
72
+ /** Remove a node for good, destroying its pool. */
73
+ remove(name, callback) {
74
+ const node = this.nodes.get(name);
75
+ if (!node) {
76
+ if (callback)
77
+ callback();
78
+ return;
79
+ }
80
+ this.nodes.delete(name);
81
+ if (node.restoreTimer) {
82
+ clearTimeout(node.restoreTimer);
83
+ }
84
+ this.emit('remove', name);
85
+ node.pool.destroy(callback);
86
+ }
87
+ /** Bring an offline node back into rotation immediately. */
88
+ restore(name) {
89
+ const node = this.nodes.get(name);
90
+ if (!node || node.online) {
91
+ return;
92
+ }
93
+ if (node.restoreTimer) {
94
+ clearTimeout(node.restoreTimer);
95
+ node.restoreTimer = null;
96
+ }
97
+ node.online = true;
98
+ node.errorCount = 0;
99
+ this.emit('online', name);
100
+ }
101
+ /** name → { online, errorCount, pool metrics } for every node. */
102
+ status() {
103
+ const out = {};
104
+ for (const node of this.nodes.values()) {
105
+ out[node.name] = {
106
+ online: node.online,
107
+ errorCount: node.errorCount,
108
+ totalCount: node.pool.totalCount,
109
+ idleCount: node.pool.idleCount,
110
+ activeCount: node.pool.activeCount,
111
+ waitingCount: node.pool.waitingCount,
112
+ };
113
+ }
114
+ return out;
115
+ }
116
+ matching(pattern) {
117
+ const re = patternToRegExp(pattern);
118
+ const out = [];
119
+ for (const node of this.nodes.values()) {
120
+ if (re.test(node.name)) {
121
+ out.push(node);
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ pick(pattern, selector, exclude) {
127
+ const candidates = this.matching(pattern).filter((n) => n.online && !exclude.has(n.name));
128
+ if (!candidates.length) {
129
+ return null;
130
+ }
131
+ if (selector === 'random') {
132
+ return candidates[Math.floor(Math.random() * candidates.length)];
133
+ }
134
+ if (selector === 'order') {
135
+ return candidates[0];
136
+ }
137
+ // round-robin per pattern; only the FIRST pick of a get() advances
138
+ // the counter — failover re-picks reuse it, or a run of failovers
139
+ // would skew the distribution toward nodes after the failing ones
140
+ const index = this.rrIndex.get(pattern) || 0;
141
+ if (exclude.size === 0) {
142
+ this.rrIndex.set(pattern, index + 1);
143
+ }
144
+ return candidates[index % candidates.length];
145
+ }
146
+ noteFailure(node) {
147
+ // a node removed while a get was in flight must not accumulate
148
+ // counters, emit 'offline', or arm a restore timer nobody clears
149
+ if (!this.nodes.has(node.name)) {
150
+ return;
151
+ }
152
+ node.errorCount++;
153
+ if (!this.removeNodeErrorCount || node.errorCount < this.removeNodeErrorCount || !node.online) {
154
+ return;
155
+ }
156
+ node.online = false;
157
+ this.emit('offline', node.name);
158
+ if (this.restoreNodeTimeout > 0) {
159
+ node.restoreTimer = setTimeout(() => {
160
+ node.restoreTimer = null;
161
+ this.restore(node.name);
162
+ }, this.restoreNodeTimeout);
163
+ if (node.restoreTimer.unref) {
164
+ node.restoreTimer.unref();
165
+ }
166
+ }
167
+ }
168
+ /**
169
+ * Acquire a connection from a node matching `pattern` (default '*').
170
+ * Connection failures mark the node and FAIL OVER to the next
171
+ * matching online node; only when every candidate has failed does the
172
+ * callback receive the last error. Release connections with
173
+ * db.detach(), exactly like a plain pool.
174
+ */
175
+ get(pattern, selector, callback) {
176
+ if (typeof pattern === 'function') {
177
+ callback = pattern;
178
+ pattern = '*';
179
+ }
180
+ if (typeof selector === 'function') {
181
+ callback = selector;
182
+ selector = undefined;
183
+ }
184
+ if (this._destroyed) {
185
+ callback(new Error('PoolCluster has been destroyed'), null);
186
+ return;
187
+ }
188
+ const sel = selector || this.selector;
189
+ const tried = new Set();
190
+ const self = this;
191
+ const attempt = (lastError) => {
192
+ const node = self.pick(pattern, sel, tried);
193
+ if (!node) {
194
+ callback(lastError || new Error('PoolCluster: no online node matches pattern "' + pattern + '"'), null);
195
+ return;
196
+ }
197
+ tried.add(node.name);
198
+ node.pool.get((err, db) => {
199
+ if (err) {
200
+ self.noteFailure(node);
201
+ attempt(err);
202
+ return;
203
+ }
204
+ node.errorCount = 0;
205
+ callback(null, db);
206
+ });
207
+ };
208
+ attempt();
209
+ }
210
+ getAsync(pattern, selector) {
211
+ const self = this;
212
+ return (0, callback_1.fromCallback)((cb) => self.get(pattern || '*', selector, cb));
213
+ }
214
+ /**
215
+ * A pool-like facade bound to a pattern (mysql2's cluster.of):
216
+ * { get, getAsync, withConnection } routed through the cluster's
217
+ * selection and failover.
218
+ */
219
+ of(pattern, selector) {
220
+ const self = this;
221
+ return {
222
+ get(callback) {
223
+ self.get(pattern, selector, callback);
224
+ },
225
+ getAsync() {
226
+ return self.getAsync(pattern, selector);
227
+ },
228
+ withConnection(work) {
229
+ return self.withConnection(pattern, work, selector);
230
+ },
231
+ };
232
+ }
233
+ /** Run `work` with a connection from a matching node, always released. */
234
+ withConnection(pattern, work, selector) {
235
+ return (0, callback_1.withPooledConnection)(() => this.getAsync(pattern, selector), work);
236
+ }
237
+ /** Destroy every node's pool. */
238
+ destroy(callback) {
239
+ this._destroyed = true;
240
+ const nodes = [...this.nodes.values()];
241
+ this.nodes.clear();
242
+ let remaining = nodes.length;
243
+ if (!remaining) {
244
+ if (callback)
245
+ callback();
246
+ return;
247
+ }
248
+ let firstError = null;
249
+ for (const node of nodes) {
250
+ if (node.restoreTimer) {
251
+ clearTimeout(node.restoreTimer);
252
+ }
253
+ node.pool.destroy((err) => {
254
+ if (err && !firstError)
255
+ firstError = err;
256
+ if (--remaining === 0 && callback)
257
+ callback(firstError);
258
+ });
259
+ }
260
+ }
261
+ destroyAsync() {
262
+ const self = this;
263
+ return (0, callback_1.fromCallback)((cb) => self.destroy(cb));
264
+ }
265
+ }
266
+ exports.default = PoolCluster;
package/lib/pool.js CHANGED
@@ -352,16 +352,8 @@ class Pool extends events_1.default.EventEmitter {
352
352
  * Run `work` with a connection from the pool, returning it to the pool
353
353
  * (detach) when the returned promise settles — success or failure.
354
354
  */
355
- async withConnection(work) {
356
- const db = await this.getAsync();
357
- try {
358
- return await work(db);
359
- }
360
- finally {
361
- // A pooled detach only returns the connection to the pool; do not
362
- // let a detach hiccup mask the outcome of `work`.
363
- await new Promise(function (resolve) { db.detach(function () { resolve(); }); });
364
- }
355
+ withConnection(work) {
356
+ return (0, callback_1.withPooledConnection)(() => this.getAsync(), work);
365
357
  }
366
358
  }
367
359
  module.exports = Pool;