node-firebird 2.3.1 → 2.3.3
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 +218 -31
- package/lib/index.d.ts +22 -0
- package/lib/pool.js +97 -10
- package/lib/srp.js +12 -1
- package/lib/wire/connection.js +40 -7
- package/lib/wire/socket.js +13 -1
- package/lib/wire/xsqlvar.js +69 -6
- package/package.json +1 -1
- package/poc/README.md +160 -0
- package/poc/helpers.js +59 -0
- package/poc/node_modules/.package-lock.json +14 -0
- package/poc/node_modules/node-firebird/.eslintrc.json +12 -0
- package/poc/node_modules/node-firebird/.github/workflows/codeql.yml +76 -0
- package/poc/node_modules/node-firebird/.github/workflows/node.js.yml +95 -0
- package/poc/node_modules/node-firebird/BIGINT_MIGRATION.md +374 -0
- package/poc/node_modules/node-firebird/CI_DEBUGGING_GUIDE.md +148 -0
- package/poc/node_modules/node-firebird/ENCRYPTION_CALLBACK.md +152 -0
- package/poc/node_modules/node-firebird/FIREBIRD_LOG_FEATURE.md +145 -0
- package/poc/node_modules/node-firebird/LICENSE +373 -0
- package/poc/node_modules/node-firebird/MINIMAL_CHANGES_SUMMARY.md +136 -0
- package/poc/node_modules/node-firebird/PR_SUMMARY.md +96 -0
- package/poc/node_modules/node-firebird/README.md +794 -0
- package/poc/node_modules/node-firebird/ROADMAP.md +223 -0
- package/poc/node_modules/node-firebird/SRP_PROTOCOL.md +482 -0
- package/poc/node_modules/node-firebird/lib/callback.js +38 -0
- package/poc/node_modules/node-firebird/lib/firebird.msg +0 -0
- package/poc/node_modules/node-firebird/lib/firebird.msg.json +1371 -0
- package/poc/node_modules/node-firebird/lib/gdscodes.d.ts +1524 -0
- package/poc/node_modules/node-firebird/lib/gdscodes.js +1531 -0
- package/poc/node_modules/node-firebird/lib/ieee754-decimal.js +500 -0
- package/poc/node_modules/node-firebird/lib/index.d.ts +316 -0
- package/poc/node_modules/node-firebird/lib/index.js +128 -0
- package/poc/node_modules/node-firebird/lib/messages.js +162 -0
- package/poc/node_modules/node-firebird/lib/pool.js +108 -0
- package/poc/node_modules/node-firebird/lib/srp.js +299 -0
- package/poc/node_modules/node-firebird/lib/unix-crypt.js +343 -0
- package/poc/node_modules/node-firebird/lib/utils.js +164 -0
- package/poc/node_modules/node-firebird/lib/wire/connection.js +2510 -0
- package/poc/node_modules/node-firebird/lib/wire/const.js +807 -0
- package/poc/node_modules/node-firebird/lib/wire/database.js +378 -0
- package/poc/node_modules/node-firebird/lib/wire/eventConnection.js +118 -0
- package/poc/node_modules/node-firebird/lib/wire/fbEventManager.js +326 -0
- package/poc/node_modules/node-firebird/lib/wire/serialize.js +588 -0
- package/poc/node_modules/node-firebird/lib/wire/service.js +1058 -0
- package/poc/node_modules/node-firebird/lib/wire/socket.js +175 -0
- package/poc/node_modules/node-firebird/lib/wire/statement.js +48 -0
- package/poc/node_modules/node-firebird/lib/wire/transaction.js +206 -0
- package/poc/node_modules/node-firebird/lib/wire/xsqlvar.js +703 -0
- package/poc/node_modules/node-firebird/package.json +38 -0
- package/poc/node_modules/node-firebird/vitest.config.js +24 -0
- package/poc/package-lock.json +21 -0
- package/poc/package.json +12 -0
- package/poc/reproduce-fixed.js +150 -0
- package/poc/reproduce.js +133 -0
- package/vitest.config.js +4 -1
package/README.md
CHANGED
|
@@ -67,11 +67,14 @@ options.role = null; // default
|
|
|
67
67
|
options.pageSize = 4096; // default when creating database
|
|
68
68
|
options.retryConnectionInterval = 1000; // reconnect interval in case of connection drop
|
|
69
69
|
options.blobAsText = false; // set to true to get blob as text, only affects blob subtype 1
|
|
70
|
+
options.blobChunkSize = 1024; // segment size in bytes used when WRITING blobs (default 1024, max 65535)
|
|
71
|
+
options.blobReadChunkSize = 1024; // buffer size in bytes requested per op_get_segment when READING blobs (default 1024, max 65535)
|
|
70
72
|
options.encoding = 'UTF8'; // default encoding for connection is UTF-8
|
|
71
73
|
options.wireCompression = false; // set to true to enable firebird compression on the wire (works only on FB >= 3 and compression is enabled on server (WireCompression = true in firebird.conf))
|
|
72
74
|
options.wireCrypt = Firebird.WIRE_CRYPT_ENABLE; // default; set to Firebird.WIRE_CRYPT_DISABLE to disable wire encryption (FB >= 3)
|
|
73
75
|
options.pluginName = undefined; // optional, auto-negotiated; can be set to Firebird.AUTH_PLUGIN_SRP256, Firebird.AUTH_PLUGIN_SRP, or Firebird.AUTH_PLUGIN_LEGACY
|
|
74
76
|
options.dbCryptConfig = undefined; // optional; database encryption key for encrypted databases. Use 'base64:<value>' for base64-encoded keys or plain text
|
|
77
|
+
options.connectTimeout = 10000; // optional; timeout in ms for a single pool.get() attach operation (default: no timeout)
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
### Classic
|
|
@@ -109,6 +112,60 @@ pool.get(function (err, db) {
|
|
|
109
112
|
pool.destroy();
|
|
110
113
|
```
|
|
111
114
|
|
|
115
|
+
#### Advanced Pooling Features
|
|
116
|
+
|
|
117
|
+
The pool implementation includes several safeguards for reliability:
|
|
118
|
+
|
|
119
|
+
1. **Connection Timeout**: Use `options.connectTimeout` to prevent the pool from hanging if a server accepts the TCP connection but fails to respond to the Firebird wire protocol (e.g., during high load or authentication stalls).
|
|
120
|
+
2. **Pool Destruction**: Calling `pool.destroy()` now immediately drains the `pending` queue, notifying all waiting callers with an error. It also prevents any further `pool.get()` calls.
|
|
121
|
+
3. **Slot Recovery**: If a connection attempt times out, the pool slot is correctly freed so subsequent requests can be served. Late-arriving connections are automatically discarded to prevent resource leaks.
|
|
122
|
+
|
|
123
|
+
#### Pool Lifecycle State Diagram
|
|
124
|
+
|
|
125
|
+
```mermaid
|
|
126
|
+
stateDiagram-v2
|
|
127
|
+
[*] --> Active
|
|
128
|
+
Active --> Destroying: pool.destroy()
|
|
129
|
+
Destroying --> Destroyed: all connections detached
|
|
130
|
+
Destroyed --> [*]
|
|
131
|
+
|
|
132
|
+
state Active {
|
|
133
|
+
[*] --> Idle
|
|
134
|
+
Idle --> Creating: pool.get() [no idle db]
|
|
135
|
+
Creating --> InUse: attach() success
|
|
136
|
+
Creating --> Idle: attach() failure/timeout
|
|
137
|
+
Idle --> InUse: pool.get() [idle db exists]
|
|
138
|
+
InUse --> Idle: db.detach()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
state Destroying {
|
|
142
|
+
[*] --> DrainingPending
|
|
143
|
+
DrainingPending --> DetachingIdle
|
|
144
|
+
DetachingIdle --> WaitingForInUse
|
|
145
|
+
WaitingForInUse --> [*]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### Connect Timeout Sequence
|
|
150
|
+
|
|
151
|
+
```mermaid
|
|
152
|
+
sequenceDiagram
|
|
153
|
+
participant User
|
|
154
|
+
participant Pool
|
|
155
|
+
participant Firebird
|
|
156
|
+
User->>Pool: pool.get()
|
|
157
|
+
Pool->>Pool: increment _creating
|
|
158
|
+
Pool->>Pool: start timer (connectTimeout)
|
|
159
|
+
Pool->>Firebird: attach(options)
|
|
160
|
+
Note over Firebird: Server accepts TCP but hangs
|
|
161
|
+
Pool-->>Pool: timer expires
|
|
162
|
+
Pool->>Pool: decrement _creating
|
|
163
|
+
Pool-->>User: callback(Error: Connection timeout)
|
|
164
|
+
Note over Firebird: Server eventually responds
|
|
165
|
+
Firebird-->>Pool: attach callback(db)
|
|
166
|
+
Pool->>Firebird: db.detach() (discard late connection)
|
|
167
|
+
```
|
|
168
|
+
|
|
112
169
|
## Database object (db)
|
|
113
170
|
|
|
114
171
|
### Database Methods
|
|
@@ -317,6 +374,34 @@ Firebird.attach(options, (err, db) => {
|
|
|
317
374
|
});
|
|
318
375
|
```
|
|
319
376
|
|
|
377
|
+
### Optimizing BLOB Read/Write Chunk Sizes
|
|
378
|
+
|
|
379
|
+
When working with large blobs (especially over remote or high-latency connections), you can configure the chunk/segment sizes to minimize the number of network round-trips:
|
|
380
|
+
|
|
381
|
+
* **blobChunkSize**: The segment size in bytes used when writing blobs (default: `1024`, maximum: `65535`).
|
|
382
|
+
* **blobReadChunkSize**: The buffer size in bytes requested per segment read operation when reading blobs (default: `1024`, maximum: `65535`).
|
|
383
|
+
|
|
384
|
+
For example, setting `blobReadChunkSize: 65535` requests 64KB segments at a time, resulting in up to 64x fewer network packets/round-trips when reading large blobs.
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
var options = {
|
|
388
|
+
host: '127.0.0.1',
|
|
389
|
+
port: 3050,
|
|
390
|
+
database: 'database.fdb',
|
|
391
|
+
user: 'SYSDBA',
|
|
392
|
+
password: 'masterkey',
|
|
393
|
+
blobChunkSize: 65535, // Minimize write round-trips
|
|
394
|
+
blobReadChunkSize: 65535 // Minimize read round-trips
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
Firebird.attach(options, function (err, db) {
|
|
398
|
+
if (err) throw err;
|
|
399
|
+
|
|
400
|
+
// Insert/Read operations will use the configured 64KB chunk sizes
|
|
401
|
+
db.detach();
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
320
405
|
### Streaming a big data
|
|
321
406
|
|
|
322
407
|
```js
|
|
@@ -642,15 +727,47 @@ fb.attach(_connection, function (err, svc) {
|
|
|
642
727
|
});
|
|
643
728
|
```
|
|
644
729
|
|
|
645
|
-
###
|
|
730
|
+
### Character Set & Encoding Support
|
|
731
|
+
|
|
732
|
+
Node-Firebird defaults to `UTF-8` for database connections, but fully supports custom client character sets. You can set the connection encoding by specifying `options.encoding` (e.g. `'UTF8'`, `'WIN1252'`, `'ISO8859_1'`, `'LATIN1'`, `'ASCII'`, or `'NONE'`).
|
|
733
|
+
|
|
734
|
+
Commonly used Firebird character sets are automatically mapped to their corresponding Node.js Buffer encodings:
|
|
735
|
+
|
|
736
|
+
| Firebird Character Set | Node.js Buffer Encoding | Description / Notes |
|
|
737
|
+
| ---------------------- | ----------------------- | ------------------- |
|
|
738
|
+
| `UTF8`, `UNICODE_FSS` | `utf8` | Unicode. Handles character-level truncation automatically based on charset width. |
|
|
739
|
+
| `WIN1252`, `ISO8859_1`, `LATIN1` | `latin1` | 8-bit European encodings. Safely decodes special accented characters. |
|
|
740
|
+
| `ASCII` | `ascii` | 7-bit ASCII. |
|
|
741
|
+
| `NONE` | `latin1` | Raw/unspecified character set. Treated as binary-safe 8-bit characters. |
|
|
646
742
|
|
|
647
|
-
|
|
743
|
+
Accented characters and fixed-length `CHAR(N)` column whitespace/truncation are handled automatically matching the connection character set width definitions.
|
|
648
744
|
|
|
745
|
+
#### Custom Charset Connection Example
|
|
649
746
|
```js
|
|
650
|
-
|
|
651
|
-
|
|
747
|
+
var options = {
|
|
748
|
+
host: '127.0.0.1',
|
|
749
|
+
port: 3050,
|
|
750
|
+
database: 'win1252_db.fdb',
|
|
751
|
+
user: 'SYSDBA',
|
|
752
|
+
password: 'masterkey',
|
|
753
|
+
encoding: 'WIN1252' // Automatically maps to 'latin1' under the hood
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
Firebird.attach(options, function (err, db) {
|
|
757
|
+
if (err) throw err;
|
|
652
758
|
|
|
653
|
-
|
|
759
|
+
// Writes 'Ç Ã É Ú Ñ' correctly using Windows-1252 encoding
|
|
760
|
+
db.query('INSERT INTO ACCENTED_TEST (ID, NAME) VALUES (?, ?)', [1, 'Ç Ã É Ú Ñ'], function (err) {
|
|
761
|
+
if (err) throw err;
|
|
762
|
+
|
|
763
|
+
db.query('SELECT NAME FROM ACCENTED_TEST WHERE ID = 1', function (err, rows) {
|
|
764
|
+
if (err) throw err;
|
|
765
|
+
console.log(rows[0].name); // 'Ç Ã É Ú Ñ' (perfectly decoded)
|
|
766
|
+
db.detach();
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
```
|
|
654
771
|
|
|
655
772
|
### Firebird 3.0+ Support
|
|
656
773
|
|
|
@@ -704,17 +821,18 @@ Firebird.attach({
|
|
|
704
821
|
- Empty or undefined values send an empty response to the callback
|
|
705
822
|
- This feature requires Firebird 3.0.1+ (protocol 14/15) for encrypted databases
|
|
706
823
|
|
|
707
|
-
### Firebird 4.0 and 5.0
|
|
824
|
+
### Firebird 4.0 and 5.0 Support
|
|
708
825
|
|
|
709
|
-
Firebird 4 wire protocol (versions 16 and 17) is
|
|
826
|
+
Firebird 4.0+ wire protocol (versions 16 and 17) is fully supported, including:
|
|
710
827
|
|
|
711
|
-
- **Protocol versions 16 and 17** — Full support for Firebird 4.0+ wire
|
|
712
|
-
- **DECFLOAT data types** —
|
|
713
|
-
- **INT128 data type** — Native support for 128-bit integers
|
|
714
|
-
- **
|
|
715
|
-
- **
|
|
828
|
+
- **Protocol versions 16 and 17** — Full support for Firebird 4.0+ and 5.0+ wire protocols (automatic fallback/negotiation).
|
|
829
|
+
- **DECFLOAT data types** — Production-ready support for `DECFLOAT(16)` (Decimal64, 8 bytes) and `DECFLOAT(34)` (Decimal128, 16 bytes) complying with the full IEEE 754-2008 standard using BID (Binary Integer Decimal) encoding. Supports special values such as `NaN`, `+Infinity`, `-Infinity`, `+0`, and `-0`.
|
|
830
|
+
- **INT128 data type** — Native support for 128-bit integers using Node.js `BigInt`.
|
|
831
|
+
- **Statement Timeout** — Support for query and statement-level execution timeouts (Protocol 16+).
|
|
832
|
+
- **Time Zone Support** — Native support for `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME ZONE` (represented as JavaScript `Date` objects).
|
|
833
|
+
- **Extended metadata identifiers** — Support for identifiers up to 63 characters.
|
|
716
834
|
|
|
717
|
-
The driver will automatically negotiate the best protocol version supported by both the client and server.
|
|
835
|
+
No configuration changes are required for Firebird 4.0 or 5.0 servers. The driver will automatically negotiate the best protocol version supported by both the client and server.
|
|
718
836
|
|
|
719
837
|
```js
|
|
720
838
|
Firebird.attach({
|
|
@@ -728,27 +846,12 @@ Firebird.attach({
|
|
|
728
846
|
|
|
729
847
|
// DECFLOAT and INT128 types are automatically supported
|
|
730
848
|
db.query('SELECT CAST(123.456 AS DECFLOAT(16)) AS df16, CAST(9876543210 AS INT128) AS i128 FROM RDB$DATABASE', function(err, result) {
|
|
731
|
-
console.log(result); // { df16: 123.456, i128:
|
|
849
|
+
console.log(result); // { df16: 123.456, i128: 9876543210n }
|
|
732
850
|
db.detach();
|
|
733
851
|
});
|
|
734
852
|
});
|
|
735
853
|
```
|
|
736
854
|
|
|
737
|
-
**Important Notes:**
|
|
738
|
-
|
|
739
|
-
1. **Statement Timeouts:** Statement timeouts (a Protocol 16 feature) are not yet implemented. This will be added in a future release.
|
|
740
|
-
|
|
741
|
-
2. **DECFLOAT Encoding:** The current DECFLOAT implementation uses a simplified encoding/decoding mechanism and does NOT implement proper IEEE 754 Decimal64/Decimal128 formats. For production use with DECFLOAT types requiring full precision and standards compliance, consider:
|
|
742
|
-
- Using a proper IEEE 754 Decimal library (e.g., `decimal128` npm package)
|
|
743
|
-
- Contributing a full IEEE 754 implementation to this driver
|
|
744
|
-
- Working with DECFLOAT values as strings or Buffers to preserve exact precision
|
|
745
|
-
|
|
746
|
-
For legacy Firebird 4 servers with SRP authentication only, use the following configuration in `firebird.conf`:
|
|
747
|
-
Firebird 4 wire protocol (versions 16 and 17) is partially supported, including:
|
|
748
|
-
- **Time Zone Support**: Native support for `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME ZONE` (Protocol 16+).
|
|
749
|
-
- **INT128 support**: Native support for 128-bit integers.
|
|
750
|
-
- **Statement Timeout**: Support for statement-level timeouts.
|
|
751
|
-
|
|
752
855
|
#### Using Timezones (FB 4.0+)
|
|
753
856
|
|
|
754
857
|
Columns of type `TIMESTAMP WITH TIME ZONE` and `TIME WITH TIME ZONE` are automatically mapped to JavaScript `Date` objects. Values are read as UTC and represented in the local timezone of the Node.js process.
|
|
@@ -765,8 +868,7 @@ db.query('INSERT INTO FB4_TABLE (TS_TZ_COL) VALUES (?)', [new Date()], function(
|
|
|
765
868
|
});
|
|
766
869
|
```
|
|
767
870
|
|
|
768
|
-
|
|
769
|
-
so you only need the following minimal configuration in `firebird.conf`:
|
|
871
|
+
For legacy Firebird 4 servers with SRP authentication only, use the following configuration in `firebird.conf`:
|
|
770
872
|
|
|
771
873
|
```bash
|
|
772
874
|
AuthServer = Srp256, Srp
|
|
@@ -781,6 +883,91 @@ For more details see:
|
|
|
781
883
|
- [Firebird 5 migration guide — authorization](https://ib-aid.com/download/docs/fb5migrationguide.html#_authorization_from_firebird_2_5_client_libraries)
|
|
782
884
|
|
|
783
885
|
|
|
886
|
+
## Extensive Examples
|
|
887
|
+
|
|
888
|
+
### Firebird 4.0+ DECFLOAT & INT128 Usage
|
|
889
|
+
```js
|
|
890
|
+
Firebird.attach({
|
|
891
|
+
host: '127.0.0.1',
|
|
892
|
+
database: '/path/to/fb4.fdb',
|
|
893
|
+
user: 'SYSDBA',
|
|
894
|
+
password: 'masterkey',
|
|
895
|
+
}, function(err, db) {
|
|
896
|
+
if (err) throw err;
|
|
897
|
+
|
|
898
|
+
// Insert DECFLOAT and INT128 types
|
|
899
|
+
db.query(
|
|
900
|
+
'INSERT INTO INVENTORY (ID, PRICE, SERIAL_NUMBER) VALUES (?, ?, ?)',
|
|
901
|
+
[1n, '12.34567890123456', 987654321098765432109876543210n],
|
|
902
|
+
function(err) {
|
|
903
|
+
if (err) throw err;
|
|
904
|
+
|
|
905
|
+
// Select them back
|
|
906
|
+
db.query('SELECT PRICE, SERIAL_NUMBER FROM INVENTORY WHERE ID = 1', function(err, result) {
|
|
907
|
+
if (err) throw err;
|
|
908
|
+
console.log(typeof result[0].price); // 'string' (e.g. '12.34567890123456')
|
|
909
|
+
console.log(typeof result[0].serial_number); // 'bigint' (e.g. 987654321098765432109876543210n)
|
|
910
|
+
db.detach();
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
);
|
|
914
|
+
});
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### Statement Timeouts (Firebird 4.0+)
|
|
918
|
+
Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
|
|
919
|
+
```js
|
|
920
|
+
Firebird.attach(options, function(err, db) {
|
|
921
|
+
if (err) throw err;
|
|
922
|
+
|
|
923
|
+
// Specify a statement-level execution timeout of 1000ms
|
|
924
|
+
db.query(
|
|
925
|
+
'SELECT * FROM MY_LARGE_TABLE',
|
|
926
|
+
[],
|
|
927
|
+
function(err, result) {
|
|
928
|
+
if (err) {
|
|
929
|
+
if (err.message.includes('timeout')) {
|
|
930
|
+
console.error('Query timed out!');
|
|
931
|
+
} else {
|
|
932
|
+
console.error('Error:', err);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
db.detach();
|
|
936
|
+
},
|
|
937
|
+
{ timeout: 1000 } // timeout option passed to query/execute
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
### Advanced Connection Pooling & Life-cycle
|
|
943
|
+
```js
|
|
944
|
+
var pool = Firebird.pool(10, {
|
|
945
|
+
host: '127.0.0.1',
|
|
946
|
+
database: 'db.fdb',
|
|
947
|
+
user: 'SYSDBA',
|
|
948
|
+
password: 'masterkey',
|
|
949
|
+
connectTimeout: 5000 // 5 seconds connect timeout for pool.get()
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Retrieve a connection
|
|
953
|
+
pool.get(function(err, db) {
|
|
954
|
+
if (err) {
|
|
955
|
+
console.error('Could not get connection from pool:', err);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
db.query('SELECT * FROM TABLE', function(err, result) {
|
|
960
|
+
// Return connection back to the pool
|
|
961
|
+
db.detach();
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Close all pool connections and reject pending requests
|
|
966
|
+
process.on('SIGTERM', function() {
|
|
967
|
+
pool.destroy();
|
|
968
|
+
});
|
|
969
|
+
```
|
|
970
|
+
|
|
784
971
|
## Contributors
|
|
785
972
|
|
|
786
973
|
- Henri Gourvest, <https://github.com/hgourvest>
|
package/lib/index.d.ts
CHANGED
|
@@ -114,10 +114,32 @@ declare module 'node-firebird' {
|
|
|
114
114
|
retryConnectionInterval?: number;
|
|
115
115
|
encoding?: SupportedCharacterSet;
|
|
116
116
|
blobAsText?: boolean; // only affects for blob subtype 1
|
|
117
|
+
/**
|
|
118
|
+
* Segment size in bytes used when WRITING blobs (op_batch_segments).
|
|
119
|
+
* Default 1024, max 65535. Use 65535 to minimize round-trips on
|
|
120
|
+
* remote/high-latency connections.
|
|
121
|
+
*/
|
|
122
|
+
blobChunkSize?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Buffer size in bytes requested per op_get_segment when READING
|
|
125
|
+
* blobs. Default 1024, max 65535. Use 65535 to minimize round-trips
|
|
126
|
+
* on remote/high-latency connections (~64x fewer round-trips).
|
|
127
|
+
*/
|
|
128
|
+
blobReadChunkSize?: number;
|
|
117
129
|
wireCrypt?: number; // WIRE_CRYPT_DISABLE or WIRE_CRYPT_ENABLE
|
|
118
130
|
wireCompression?: boolean;
|
|
119
131
|
pluginName?: string;
|
|
120
132
|
dbCryptConfig?: string; // Database encryption key callback config (base64: prefix for base64, or plain string)
|
|
133
|
+
/**
|
|
134
|
+
* Timeout in milliseconds for a single pool.get() attach operation.
|
|
135
|
+
* If attach() does not complete within this time the slot is freed,
|
|
136
|
+
* the caller receives an error, and any late-arriving connection is
|
|
137
|
+
* safely discarded. Set to 0 or omit to disable (default: no timeout).
|
|
138
|
+
*
|
|
139
|
+
* Recommended value: 5000–10000 ms depending on network latency and
|
|
140
|
+
* expected Firebird server response time under load.
|
|
141
|
+
*/
|
|
142
|
+
connectTimeout?: number;
|
|
121
143
|
}
|
|
122
144
|
|
|
123
145
|
export interface SvcMgrOptions extends Options {
|
package/lib/pool.js
CHANGED
|
@@ -6,17 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
class Pool {
|
|
8
8
|
constructor(attach, max, options) {
|
|
9
|
-
this.attach
|
|
10
|
-
this.internaldb = []; //
|
|
11
|
-
this.pooldb
|
|
12
|
-
this.dbinuse
|
|
13
|
-
this._creating = 0;
|
|
14
|
-
this.max
|
|
15
|
-
this.pending
|
|
16
|
-
this.options
|
|
9
|
+
this.attach = attach;
|
|
10
|
+
this.internaldb = []; // connections created by the pool (for destroy)
|
|
11
|
+
this.pooldb = []; // connections available in the pool (idle)
|
|
12
|
+
this.dbinuse = 0; // connections currently handed out to callers
|
|
13
|
+
this._creating = 0; // connections currently being created (attach in flight)
|
|
14
|
+
this.max = max || 4;
|
|
15
|
+
this.pending = []; // callbacks waiting for a free slot
|
|
16
|
+
this.options = options;
|
|
17
|
+
this._destroyed = false; // true after destroy() — prevents further use
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
get(callback) {
|
|
21
|
+
// [Fix 2] Reject immediately if the pool has already been destroyed.
|
|
22
|
+
if (this._destroyed) {
|
|
23
|
+
callback(new Error('Pool has been destroyed'), null);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
20
26
|
var self = this;
|
|
21
27
|
self.pending.push(callback);
|
|
22
28
|
self.check();
|
|
@@ -25,6 +31,10 @@ class Pool {
|
|
|
25
31
|
|
|
26
32
|
check() {
|
|
27
33
|
var self = this;
|
|
34
|
+
|
|
35
|
+
// [Fix 2] Do not serve requests on a destroyed pool.
|
|
36
|
+
if (self._destroyed) return self;
|
|
37
|
+
|
|
28
38
|
if ((self.dbinuse + self._creating) >= self.max)
|
|
29
39
|
return self;
|
|
30
40
|
|
|
@@ -32,12 +42,74 @@ class Pool {
|
|
|
32
42
|
if (!cb)
|
|
33
43
|
return self;
|
|
34
44
|
if (self.pooldb.length) {
|
|
45
|
+
var db = self.pooldb.shift();
|
|
46
|
+
// Discard connections that have been closed or destroyed while idle
|
|
47
|
+
if (db.connection && (db.connection._isClosed || db.connection._isDetach || !db.connection._socket || db.connection._socket.destroyed)) {
|
|
48
|
+
var idx = self.internaldb.indexOf(db);
|
|
49
|
+
if (idx !== -1) self.internaldb.splice(idx, 1);
|
|
50
|
+
self.pending.unshift(cb);
|
|
51
|
+
setImmediate(function () { self.check(); });
|
|
52
|
+
return self;
|
|
53
|
+
}
|
|
54
|
+
// Idle connection available — hand it out immediately.
|
|
35
55
|
self.dbinuse++;
|
|
36
|
-
cb(null,
|
|
56
|
+
cb(null, db);
|
|
37
57
|
} else {
|
|
58
|
+
// No idle connection — create a new one via attach().
|
|
38
59
|
self._creating++;
|
|
60
|
+
|
|
61
|
+
var timedOut = false;
|
|
62
|
+
var timer = null;
|
|
63
|
+
|
|
64
|
+
// [Fix 1] Optional per-attach timeout.
|
|
65
|
+
// If attach() does not call back within connectTimeout ms (e.g. because
|
|
66
|
+
// the server accepted TCP but stalled on the Firebird wire protocol), we
|
|
67
|
+
// free the slot and notify the caller. Any connection that arrives late
|
|
68
|
+
// is discarded in the attach() callback below.
|
|
69
|
+
if (self.options.connectTimeout > 0) {
|
|
70
|
+
timer = setTimeout(function () {
|
|
71
|
+
timedOut = true;
|
|
72
|
+
self._creating--;
|
|
73
|
+
cb(new Error(
|
|
74
|
+
'Connection timeout after ' + self.options.connectTimeout + 'ms'
|
|
75
|
+
), null);
|
|
76
|
+
// Free the slot so the next pending request can be served.
|
|
77
|
+
setImmediate(function () { self.check(); });
|
|
78
|
+
}, self.options.connectTimeout);
|
|
79
|
+
}
|
|
80
|
+
|
|
39
81
|
this.attach(self.options, function (err, db) {
|
|
82
|
+
|
|
83
|
+
// [Fix 1] Timeout already fired — discard this late connection.
|
|
84
|
+
// Without this guard the socket would stay open until the OS-level
|
|
85
|
+
// TCP timeout (potentially minutes), leaking a file descriptor.
|
|
86
|
+
if (timedOut) {
|
|
87
|
+
if (db) {
|
|
88
|
+
try {
|
|
89
|
+
// _pooled = false forces a real op_detach / socket close
|
|
90
|
+
// instead of a silent pool-return emit.
|
|
91
|
+
if (db.connection) db.connection._pooled = false;
|
|
92
|
+
db.detach();
|
|
93
|
+
} catch (e) { /* ignore cleanup errors */ }
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (timer) clearTimeout(timer);
|
|
40
99
|
self._creating--;
|
|
100
|
+
|
|
101
|
+
// [Fix 3] Pool was destroyed while attach() was in flight.
|
|
102
|
+
if (self._destroyed) {
|
|
103
|
+
if (db) {
|
|
104
|
+
try {
|
|
105
|
+
if (db.connection) db.connection._pooled = false;
|
|
106
|
+
db.detach();
|
|
107
|
+
} catch (e) { /* ignore cleanup errors */ }
|
|
108
|
+
}
|
|
109
|
+
cb(new Error('Pool has been destroyed'), null);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
41
113
|
if (!err) {
|
|
42
114
|
self.dbinuse++;
|
|
43
115
|
self.internaldb.push(db);
|
|
@@ -45,7 +117,7 @@ class Pool {
|
|
|
45
117
|
// also in pool (could be a twice call to detach)
|
|
46
118
|
if (self.pooldb.indexOf(db) !== -1 || self.internaldb.indexOf(db) === -1)
|
|
47
119
|
return;
|
|
48
|
-
// if not usable don't put
|
|
120
|
+
// if not usable don't put it back in the pool
|
|
49
121
|
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false)
|
|
50
122
|
self.internaldb.splice(self.internaldb.indexOf(db), 1);
|
|
51
123
|
else
|
|
@@ -68,6 +140,16 @@ class Pool {
|
|
|
68
140
|
|
|
69
141
|
destroy(callback) {
|
|
70
142
|
var self = this;
|
|
143
|
+
self._destroyed = true;
|
|
144
|
+
|
|
145
|
+
// [Fix 4] Drain pending callbacks so callers are not left hanging.
|
|
146
|
+
// This is critical when destroy() is called as a recovery measure after
|
|
147
|
+
// a timeout: without draining, every concurrent pool.get() that had not
|
|
148
|
+
// yet received a slot would hang until the process exits.
|
|
149
|
+
var draining = self.pending.splice(0);
|
|
150
|
+
draining.forEach(function (cb) {
|
|
151
|
+
try { cb(new Error('Pool is being destroyed'), null); } catch (e) { /* ignore */ }
|
|
152
|
+
});
|
|
71
153
|
|
|
72
154
|
var connectionCount = this.internaldb.length;
|
|
73
155
|
|
|
@@ -100,6 +182,11 @@ class Pool {
|
|
|
100
182
|
self.pooldb.splice(_db_in_pool, 1);
|
|
101
183
|
db.connection._pooled = false;
|
|
102
184
|
db.detach(detachCallback);
|
|
185
|
+
} else {
|
|
186
|
+
// [Fix 5] Connection is currently in use (dbinuse > 0).
|
|
187
|
+
// The caller is responsible for releasing it via detach().
|
|
188
|
+
// Count it down here so the destroy() callback is not blocked forever.
|
|
189
|
+
detachCallback();
|
|
103
190
|
}
|
|
104
191
|
});
|
|
105
192
|
}
|
package/lib/srp.js
CHANGED
|
@@ -24,7 +24,11 @@ const PRIME = {
|
|
|
24
24
|
* @param a BigInt Client private key.
|
|
25
25
|
* @returns {{private: BigInt, public: BigInt}}
|
|
26
26
|
*/
|
|
27
|
-
exports.clientSeed = function(a = toBigInt(crypto.randomBytes(SRP_KEY_SIZE))) {
|
|
27
|
+
exports.clientSeed = function(a = toBigInt(crypto.randomBytes(SRP_KEY_SIZE)) % PRIME.N) {
|
|
28
|
+
// a must be in [0, N): clientSession() reduces the exponent (a + ux) mod N
|
|
29
|
+
// to match the Firebird engine, but A = g^a is computed from the raw a.
|
|
30
|
+
// When random a >= N (~10% of 1024-bit values), the proof diverges from
|
|
31
|
+
// the server's session key, causing sporadic auth failures.
|
|
28
32
|
var A = modPow(PRIME.g, a, PRIME.N);
|
|
29
33
|
|
|
30
34
|
dump('a', a);
|
|
@@ -127,6 +131,13 @@ function hexPad(hex) {
|
|
|
127
131
|
}
|
|
128
132
|
exports.hexPad = hexPad;
|
|
129
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The SRP prime modulus N (1024-bit, same value used by Firebird).
|
|
136
|
+
* Exported so tests and tooling can assert key-size invariants without
|
|
137
|
+
* duplicating the constant.
|
|
138
|
+
*/
|
|
139
|
+
exports.PRIME_N = PRIME.N;
|
|
140
|
+
|
|
130
141
|
/**
|
|
131
142
|
* Pad key with SRP_KEY_SIZE.
|
|
132
143
|
*
|
package/lib/wire/connection.js
CHANGED
|
@@ -40,6 +40,7 @@ class Connection {
|
|
|
40
40
|
this._isUsed = false;
|
|
41
41
|
this._pooled = options.isPool||false;
|
|
42
42
|
if (options && options.blobChunkSize > 65535) options.blobChunkSize = 65535;
|
|
43
|
+
if (options && options.blobReadChunkSize > 65535) options.blobReadChunkSize = 65535;
|
|
43
44
|
this.options = options;
|
|
44
45
|
this._bind_events(host, port, callback);
|
|
45
46
|
this.error;
|
|
@@ -155,6 +156,7 @@ class Connection {
|
|
|
155
156
|
|
|
156
157
|
while (xdr.pos < xdr.buffer.length) {
|
|
157
158
|
var cb = self._queue[0], pos = xdr.pos;
|
|
159
|
+
var lazySnapshot = cb ? cb.lazy_count : undefined;
|
|
158
160
|
|
|
159
161
|
decodeResponse(xdr, cb, self, self._lowercase_keys, function (err, obj) {
|
|
160
162
|
|
|
@@ -171,8 +173,17 @@ class Connection {
|
|
|
171
173
|
xdr.buffer.length, pos, self._queue.length);
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
// Restore lazy_count to its value before this (failed)
|
|
177
|
+
// decode attempt. Forcing lazy_count = 2 here made the
|
|
178
|
+
// decoder expect a second op_response that never arrives
|
|
179
|
+
// whenever a single large response (e.g. op_get_segment
|
|
180
|
+
// with a big buffer) spans multiple TCP packets.
|
|
181
|
+
if (cb) {
|
|
182
|
+
if (lazySnapshot === undefined) {
|
|
183
|
+
delete cb.lazy_count;
|
|
184
|
+
} else {
|
|
185
|
+
cb.lazy_count = lazySnapshot;
|
|
186
|
+
}
|
|
176
187
|
}
|
|
177
188
|
} else {
|
|
178
189
|
// Any other error (truly unknown opcode not handled above).
|
|
@@ -1287,9 +1298,22 @@ class Connection {
|
|
|
1287
1298
|
}
|
|
1288
1299
|
|
|
1289
1300
|
if (ret && ret.data && ret.data.length) {
|
|
1290
|
-
|
|
1301
|
+
// Read blobs sequentially instead of in parallel to avoid
|
|
1302
|
+
// exceeding Firebird's per-connection open-blob-handle limit,
|
|
1303
|
+
// which causes a server-side deadlock when many rows contain
|
|
1304
|
+
// BLOBs and blobAsText is true. See issue #387.
|
|
1305
|
+
const arrBlobFns = ret.arrBlob || [];
|
|
1306
|
+
const readBlobsSequentially = (index, results) => {
|
|
1307
|
+
if (index >= arrBlobFns.length) {
|
|
1308
|
+
return Promise.resolve(results);
|
|
1309
|
+
}
|
|
1310
|
+
return arrBlobFns[index](transaction).then((v) => {
|
|
1311
|
+
results.push(v);
|
|
1312
|
+
return readBlobsSequentially(index + 1, results);
|
|
1313
|
+
});
|
|
1314
|
+
};
|
|
1291
1315
|
|
|
1292
|
-
|
|
1316
|
+
readBlobsSequentially(0, []).then((arrBlob) => {
|
|
1293
1317
|
for (let i = 0; i < arrBlob.length; i++) {
|
|
1294
1318
|
const blob = arrBlob[i];
|
|
1295
1319
|
ret.data[blob.row][blob.column] = blob.value;
|
|
@@ -1354,7 +1378,7 @@ class Connection {
|
|
|
1354
1378
|
msg.pos = 0;
|
|
1355
1379
|
msg.addInt(Const.op_get_segment);
|
|
1356
1380
|
msg.addInt(blob.handle);
|
|
1357
|
-
msg.addInt(1024); // buffer length
|
|
1381
|
+
msg.addInt(this.options.blobReadChunkSize || 1024); // buffer length (max 65535)
|
|
1358
1382
|
msg.addInt(0); // ???
|
|
1359
1383
|
this._queueEvent(callback);
|
|
1360
1384
|
}
|
|
@@ -1718,7 +1742,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
|
|
|
1718
1742
|
_xdrpos = data.pos;
|
|
1719
1743
|
const key = custom.asObject ? data.fcols[data.fcolumn] : data.fcolumn;
|
|
1720
1744
|
const row = data.frows.length;
|
|
1721
|
-
let value = item.decode(data, lowerV13);
|
|
1745
|
+
let value = item.decode(data, lowerV13, cnx.options);
|
|
1722
1746
|
|
|
1723
1747
|
if (item.type === Const.SQL_BLOB && value !== null) {
|
|
1724
1748
|
if (item.subType === Const.isc_blob_text && cnx.options.blobAsText) {
|
|
@@ -1808,7 +1832,15 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
|
|
|
1808
1832
|
|
|
1809
1833
|
if (is_authenticated === 0) {
|
|
1810
1834
|
if (cnx.options.pluginName && cnx.options.pluginName !== accept.pluginName) {
|
|
1811
|
-
|
|
1835
|
+
var errPlugin = new Error('Server don\'t accept plugin : ' + cnx.options.pluginName + ', but support : ' + accept.pluginName);
|
|
1836
|
+
doError(errPlugin, callback);
|
|
1837
|
+
return cb(errPlugin);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
if (Const.AUTH_PLUGIN_SRP_LIST.indexOf(accept.pluginName) !== -1 && !cnx.clientKeys) {
|
|
1841
|
+
var errPlugin = new Error('Server accepted plugin : ' + accept.pluginName + ', but client did not initialize SRP keys');
|
|
1842
|
+
doError(errPlugin, callback);
|
|
1843
|
+
return cb(errPlugin);
|
|
1812
1844
|
}
|
|
1813
1845
|
|
|
1814
1846
|
if (Const.AUTH_PLUGIN_SRP_LIST.indexOf(accept.pluginName) !== -1) {
|
|
@@ -1976,6 +2008,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
|
|
|
1976
2008
|
// Database encryption key callback
|
|
1977
2009
|
// Read server data (plugin data sent by server)
|
|
1978
2010
|
var serverPluginData = data.readArray();
|
|
2011
|
+
data.readInt(); // p_cc_reply
|
|
1979
2012
|
|
|
1980
2013
|
// Get client response from dbCryptConfig option
|
|
1981
2014
|
var clientPluginData = parseDbCryptConfig(cnx.options.dbCryptConfig);
|
package/lib/wire/socket.js
CHANGED
|
@@ -55,6 +55,7 @@ class Socket {
|
|
|
55
55
|
constructor(port, host) {
|
|
56
56
|
this._socket = net.createConnection(port, host);
|
|
57
57
|
this._socket.setNoDelay(true);
|
|
58
|
+
this._socket.setKeepAlive(true, 60000); // 1 minute delay to detect dead/stale connections
|
|
58
59
|
this.compressor = null;
|
|
59
60
|
this.compressorBuffer = [];
|
|
60
61
|
this.decompressor = null;
|
|
@@ -99,7 +100,14 @@ class Socket {
|
|
|
99
100
|
*/
|
|
100
101
|
write(data, defer = false) {
|
|
101
102
|
if (defer) {
|
|
102
|
-
|
|
103
|
+
// Accumulate deferred packets instead of overwriting. Multiple
|
|
104
|
+
// deferred ops (e.g. op_close_blob followed immediately by
|
|
105
|
+
// op_free_statement) must all be kept until the next flush;
|
|
106
|
+
// overwriting the buffer silently drops packets and desynchronises
|
|
107
|
+
// the request/response queue, causing the connection to hang.
|
|
108
|
+
this.buffer = this.buffer
|
|
109
|
+
? Buffer.concat([this.buffer, Buffer.from(data)])
|
|
110
|
+
: Buffer.from(data);
|
|
103
111
|
return;
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -167,6 +175,10 @@ class Socket {
|
|
|
167
175
|
return this[field].bind(this);
|
|
168
176
|
}
|
|
169
177
|
|
|
178
|
+
if (typeof target[field] === 'function') {
|
|
179
|
+
return target[field].bind(target);
|
|
180
|
+
}
|
|
181
|
+
|
|
170
182
|
return target[field];
|
|
171
183
|
}
|
|
172
184
|
}
|