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 +138 -6
- package/lib/callback.d.ts +7 -0
- package/lib/callback.js +16 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +13 -0
- package/lib/pool-cluster.d.ts +89 -0
- package/lib/pool-cluster.js +266 -0
- package/lib/pool.js +2 -10
- package/lib/types.d.ts +38 -5
- package/lib/utils.d.ts +12 -0
- package/lib/utils.js +20 -2
- package/lib/wire/batch-stream.d.ts +26 -0
- package/lib/wire/batch-stream.js +109 -0
- package/lib/wire/codepages.d.ts +23 -0
- package/lib/wire/codepages.js +137 -0
- package/lib/wire/connection.d.ts +38 -4
- package/lib/wire/connection.js +294 -44
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +13 -6
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +10 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +6 -0
- package/lib/wire/transaction.js +27 -2
- package/lib/wire/xsqlvar.d.ts +56 -1
- package/lib/wire/xsqlvar.js +107 -29
- package/package.json +19 -1
- package/src/callback.ts +15 -0
- package/src/index.ts +15 -0
- package/src/pool-cluster.ts +319 -0
- package/src/pool.ts +3 -10
- package/src/types.ts +40 -5
- package/src/utils.ts +19 -1
- package/src/wire/batch-stream.ts +121 -0
- package/src/wire/codepages.ts +147 -0
- package/src/wire/connection.ts +315 -49
- package/src/wire/database.ts +15 -7
- package/src/wire/serialize.ts +11 -0
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +28 -2
- package/src/wire/xsqlvar.ts +129 -30
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
356
|
-
|
|
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;
|