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.
Files changed (55) hide show
  1. package/README.md +218 -31
  2. package/lib/index.d.ts +22 -0
  3. package/lib/pool.js +97 -10
  4. package/lib/srp.js +12 -1
  5. package/lib/wire/connection.js +40 -7
  6. package/lib/wire/socket.js +13 -1
  7. package/lib/wire/xsqlvar.js +69 -6
  8. package/package.json +1 -1
  9. package/poc/README.md +160 -0
  10. package/poc/helpers.js +59 -0
  11. package/poc/node_modules/.package-lock.json +14 -0
  12. package/poc/node_modules/node-firebird/.eslintrc.json +12 -0
  13. package/poc/node_modules/node-firebird/.github/workflows/codeql.yml +76 -0
  14. package/poc/node_modules/node-firebird/.github/workflows/node.js.yml +95 -0
  15. package/poc/node_modules/node-firebird/BIGINT_MIGRATION.md +374 -0
  16. package/poc/node_modules/node-firebird/CI_DEBUGGING_GUIDE.md +148 -0
  17. package/poc/node_modules/node-firebird/ENCRYPTION_CALLBACK.md +152 -0
  18. package/poc/node_modules/node-firebird/FIREBIRD_LOG_FEATURE.md +145 -0
  19. package/poc/node_modules/node-firebird/LICENSE +373 -0
  20. package/poc/node_modules/node-firebird/MINIMAL_CHANGES_SUMMARY.md +136 -0
  21. package/poc/node_modules/node-firebird/PR_SUMMARY.md +96 -0
  22. package/poc/node_modules/node-firebird/README.md +794 -0
  23. package/poc/node_modules/node-firebird/ROADMAP.md +223 -0
  24. package/poc/node_modules/node-firebird/SRP_PROTOCOL.md +482 -0
  25. package/poc/node_modules/node-firebird/lib/callback.js +38 -0
  26. package/poc/node_modules/node-firebird/lib/firebird.msg +0 -0
  27. package/poc/node_modules/node-firebird/lib/firebird.msg.json +1371 -0
  28. package/poc/node_modules/node-firebird/lib/gdscodes.d.ts +1524 -0
  29. package/poc/node_modules/node-firebird/lib/gdscodes.js +1531 -0
  30. package/poc/node_modules/node-firebird/lib/ieee754-decimal.js +500 -0
  31. package/poc/node_modules/node-firebird/lib/index.d.ts +316 -0
  32. package/poc/node_modules/node-firebird/lib/index.js +128 -0
  33. package/poc/node_modules/node-firebird/lib/messages.js +162 -0
  34. package/poc/node_modules/node-firebird/lib/pool.js +108 -0
  35. package/poc/node_modules/node-firebird/lib/srp.js +299 -0
  36. package/poc/node_modules/node-firebird/lib/unix-crypt.js +343 -0
  37. package/poc/node_modules/node-firebird/lib/utils.js +164 -0
  38. package/poc/node_modules/node-firebird/lib/wire/connection.js +2510 -0
  39. package/poc/node_modules/node-firebird/lib/wire/const.js +807 -0
  40. package/poc/node_modules/node-firebird/lib/wire/database.js +378 -0
  41. package/poc/node_modules/node-firebird/lib/wire/eventConnection.js +118 -0
  42. package/poc/node_modules/node-firebird/lib/wire/fbEventManager.js +326 -0
  43. package/poc/node_modules/node-firebird/lib/wire/serialize.js +588 -0
  44. package/poc/node_modules/node-firebird/lib/wire/service.js +1058 -0
  45. package/poc/node_modules/node-firebird/lib/wire/socket.js +175 -0
  46. package/poc/node_modules/node-firebird/lib/wire/statement.js +48 -0
  47. package/poc/node_modules/node-firebird/lib/wire/transaction.js +206 -0
  48. package/poc/node_modules/node-firebird/lib/wire/xsqlvar.js +703 -0
  49. package/poc/node_modules/node-firebird/package.json +38 -0
  50. package/poc/node_modules/node-firebird/vitest.config.js +24 -0
  51. package/poc/package-lock.json +21 -0
  52. package/poc/package.json +12 -0
  53. package/poc/reproduce-fixed.js +150 -0
  54. package/poc/reproduce.js +133 -0
  55. 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
- ### Charset for database connection is always UTF-8
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
- Node Firebird uses UTF-8 as the default charset. If you want a different one, such as Latin1, you will need to go into the library and modify the default_encoding in the index.js file
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
- const default_encoding = 'latin1';
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
- This is why you should use **Firebird 2.5** server at least.
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 now supported with the following features:
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 protocol
712
- - **DECFLOAT data types** — Support for DECFLOAT(16) and DECFLOAT(34) using IEEE 754 Decimal64 and Decimal128 formats
713
- - **INT128 data type** — Native support for 128-bit integers
714
- - **Extended metadata identifiers** — Support for identifiers up to 63 characters
715
- - **Backward compatibility** — Automatically negotiates the best protocol version with the server
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. No configuration changes are required for Firebird 4.0 servers.
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: '9876543210' }
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
- Srp256 authentication and wire encryption are now supported natively,
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 = attach;
10
- this.internaldb = []; // connection created by the pool (for destroy)
11
- this.pooldb = []; // available connection in the pool
12
- this.dbinuse = 0; // connection currently in use into the pool
13
- this._creating = 0; // connections currently being created
14
- this.max = max || 4;
15
- this.pending = [];
16
- this.options = 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, self.pooldb.shift());
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 in again in the pool and remove reference on it
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
  *
@@ -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
- if (self.accept && self.accept.protocolMinimumType === Const.ptype_lazy_send && self._queue.length > 0) {
175
- self._queue[0].lazy_count = 2;
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
- const arrPromise = (ret.arrBlob || []).map((value) => value(transaction));
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
- Promise.all(arrPromise).then((arrBlob) => {
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
- doError(new Error('Server don\'t accept plugin : ' + cnx.options.pluginName + ', but support : ' + accept.pluginName), callback);
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);
@@ -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
- this.buffer = Buffer.from(data);
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
  }