node-firebird 2.16.2 → 2.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,6 +181,7 @@ same way `PGUSER`/`PGPASSWORD` work with pg.
181
181
  var options = {};
182
182
 
183
183
  options.host = '127.0.0.1';
184
+ options.eventHost = undefined; // optional; override the server-advertised host for the auxiliary event connection
184
185
  options.port = 3050;
185
186
  options.database = 'database.fdb';
186
187
  options.user = 'SYSDBA';
@@ -1403,6 +1404,12 @@ Firebird.attach(options, function (err, db) {
1403
1404
  db.attachEvent(function (err, evtmgr) {
1404
1405
  if (err) throw err;
1405
1406
 
1407
+ // Auxiliary socket/protocol failures after attachment are reported here.
1408
+ // Install this listener before registering events.
1409
+ evtmgr.on('error', function (err) {
1410
+ console.error('event connection failed:', err);
1411
+ });
1412
+
1406
1413
  // 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
1407
1414
  // PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
1408
1415
  evtmgr.registerEvent(['MY_EVENT'], function (err) {
@@ -1455,6 +1462,11 @@ If the server reports `0.0.0.0` or `::` as the aux address — usual when it lis
1455
1462
  interfaces — the driver dials the host from your connection options instead, so that host must
1456
1463
  be the one reaching the aux port.
1457
1464
 
1465
+ Behind NAT, a tunnel, container networking, or a load balancer, the advertised
1466
+ address may not be reachable from the client. Set `options.eventHost` to override
1467
+ only the host used for the auxiliary event connection. The auxiliary port is
1468
+ still selected by Firebird; use `RemoteAuxPort` when it also needs to be fixed.
1469
+
1458
1470
  Since **2.16.2** a failed dial is reported to the attachment callback as an `Error`, exactly
1459
1471
  once, carrying Node's socket `code`. Earlier versions recorded it internally and never called
1460
1472
  back, so `attachEvent()` and `attachEventAsync()` hung indefinitely:
@@ -1473,9 +1485,20 @@ db.attachEvent(function (err, evtmgr) {
1473
1485
  });
1474
1486
  ```
1475
1487
 
1476
- Errors on the aux socket *after* it connects are not delivered to this callback. Poll
1477
- `evtmgr.getState().isEventConnectionOpen` if you need to detect an aux connection that dies
1478
- mid-subscription.
1488
+ Errors on the aux socket *after* it connects are delivered through the manager's `error`
1489
+ event. Install the listener before calling `registerEvent()`:
1490
+
1491
+ ```js
1492
+ evtmgr.on('error', function (err) {
1493
+ // The manager is now CLOSED. Reattach explicitly if the application wants
1494
+ // to resume event delivery; the driver does not reconnect automatically.
1495
+ console.error('event connection failed:', err);
1496
+ });
1497
+ ```
1498
+
1499
+ When no manager error listener is installed, the driver forwards the failure to guarded
1500
+ database `error` listeners when present. It never emits an unhandled EventEmitter `error`.
1501
+ The primary database attachment remains available for ordinary queries.
1479
1502
 
1480
1503
  ### Escaping Query values
1481
1504
 
package/lib/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Readable, Writable } from 'stream';
2
+ import type { EventEmitter } from 'events';
2
3
  import type { SqlTag } from './sql-template';
3
4
  export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
4
5
  export type DatabaseCallback = (err: any, db: Database) => void;
@@ -6,6 +7,27 @@ export type TransactionCallback = (err: any, transaction: Transaction) => void;
6
7
  export type QueryCallback = (err: any, result: any[]) => void;
7
8
  export type SimpleCallback = (err: any) => void;
8
9
  export type SequentialCallback = (row: any, index: number, next?: (err?: any) => void) => void | Promise<void>;
10
+ export interface FbEventState {
11
+ state: 'IDLE' | 'SUBSCRIBED' | 'CLOSED';
12
+ hasActiveSubscription: boolean;
13
+ registeredEvents: Record<string, number>;
14
+ eventId: number;
15
+ isEventConnectionOpen: boolean;
16
+ isDatabaseConnectionClosed: boolean;
17
+ }
18
+ export interface FbEventManager extends EventEmitter {
19
+ readonly eventid: number;
20
+ readonly events: Record<string, number>;
21
+ registerEvent(events: string[], callback: SimpleCallback): void;
22
+ unregisterEvent(events: string[], callback: SimpleCallback): void;
23
+ close(callback?: SimpleCallback): void;
24
+ getState(): FbEventState;
25
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
26
+ on(event: 'error', listener: (error: Error) => void): this;
27
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
28
+ once(event: 'error', listener: (error: Error) => void): this;
29
+ }
30
+ export type FbEventManagerCallback = (err: any, manager?: FbEventManager) => void;
9
31
  /**
10
32
  * Describes a single column in a prepared statement's result set or
11
33
  * parameter list. The properties here are populated by the
@@ -250,7 +272,7 @@ export interface Database {
250
272
  batchStream(query: string, options?: BatchStreamOptions): BatchStream;
251
273
  drop(callback: SimpleCallback): void;
252
274
  escape(value: any): string;
253
- attachEvent(callback: any): this;
275
+ attachEvent(callback: FbEventManagerCallback): this;
254
276
  createTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
255
277
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
256
278
  dropTablespace(name: string, callback?: QueryCallback): Database;
@@ -271,7 +293,7 @@ export interface Database {
271
293
  newStatementAsync(query: string): Promise<Statement>;
272
294
  detachAsync(force?: boolean): Promise<void>;
273
295
  dropAsync(): Promise<void>;
274
- attachEventAsync(): Promise<any>;
296
+ attachEventAsync(): Promise<FbEventManager>;
275
297
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
276
298
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
277
299
  /**
@@ -352,6 +374,8 @@ export interface Statement {
352
374
  export type SupportedCharacterSet = 'NONE' | 'CP943C' | 'DOS737' | 'DOS775' | 'DOS858' | 'DOS862' | 'DOS864' | 'DOS866' | 'DOS869' | 'GB18030' | 'GBK' | 'ISO8859_1' | 'ISO8859_2' | 'ISO8859_3' | 'ISO8859_4' | 'ISO8859_5' | 'ISO8859_6' | 'ISO8859_7' | 'ISO8859_8' | 'ISO8859_9' | 'ISO8859_13' | 'KOI8R' | 'KOI8U' | 'TIS620' | 'UTF8' | 'WIN1251' | 'WIN1252' | 'WIN1253' | 'WIN1254' | 'WIN1255' | 'WIN1256' | 'WIN1257' | 'WIN1258' | 'WIN_1258';
353
375
  export interface Options {
354
376
  host?: string;
377
+ /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
378
+ eventHost?: string;
355
379
  port?: number;
356
380
  database?: string;
357
381
  user?: string;
package/lib/utils.d.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { FbStatusItem } from './callback';
2
+ /** Resolve the address used for Firebird's auxiliary event connection. */
3
+ export declare function resolveEventHost(options: {
4
+ host?: string;
5
+ eventHost?: string;
6
+ }, advertisedHost: string): string;
2
7
  /**
3
8
  * Parse date from string
4
9
  */
package/lib/utils.js CHANGED
@@ -4,10 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.escape = exports.lookupMessages = exports.batchResultToError = exports.parseDate = void 0;
7
+ exports.resolveEventHost = resolveEventHost;
7
8
  exports.noop = noop;
8
9
  const firebird_msg_json_1 = __importDefault(require("./firebird.msg.json"));
9
10
  const const_1 = __importDefault(require("./wire/const"));
10
11
  const MessagesError = firebird_msg_json_1.default;
12
+ /** Resolve the address used for Firebird's auxiliary event connection. */
13
+ function resolveEventHost(options, advertisedHost) {
14
+ if (options.eventHost)
15
+ return options.eventHost;
16
+ if (advertisedHost === '0.0.0.0' || advertisedHost === '::')
17
+ return options.host || const_1.default.DEFAULT_HOST;
18
+ return advertisedHost;
19
+ }
11
20
  /**
12
21
  * Parse date from string
13
22
  */
@@ -390,6 +390,13 @@ class Database extends events_1.default.EventEmitter {
390
390
  attachEvent(callback) {
391
391
  var self = this;
392
392
  const eventid = self.eventid++;
393
+ let completed = false;
394
+ const complete = function (err, manager) {
395
+ if (completed)
396
+ return;
397
+ completed = true;
398
+ callback(err, manager);
399
+ };
393
400
  if (process.env.FIREBIRD_DEBUG) {
394
401
  console.log('[fb-debug] Database.attachEvent: calling auxConnection, eventid=%d queue=%d', eventid, self.connection._queue.length);
395
402
  }
@@ -398,21 +405,19 @@ class Database extends events_1.default.EventEmitter {
398
405
  if (process.env.FIREBIRD_DEBUG) {
399
406
  console.log('[fb-debug] Database.attachEvent: auxConnection error:', err.message);
400
407
  }
401
- (0, callback_1.doError)(err, callback);
408
+ (0, callback_1.doError)(err, complete);
402
409
  return;
403
410
  }
404
411
  if (process.env.FIREBIRD_DEBUG) {
405
412
  console.log('[fb-debug] Database.attachEvent: auxConnection ok, connecting to aux port %s:%d', socket_info.host, socket_info.port);
406
413
  }
407
- const host = (socket_info.host === '0.0.0.0' || socket_info.host === '::')
408
- ? self.connection.options.host
409
- : socket_info.host;
414
+ const host = (0, utils_1.resolveEventHost)(self.connection.options, socket_info.host);
410
415
  const eventConnection = new eventConnection_1.default(host, socket_info.port, function (err) {
411
416
  if (err) {
412
417
  if (process.env.FIREBIRD_DEBUG) {
413
418
  console.log('[fb-debug] Database.attachEvent: EventConnection error:', err.message);
414
419
  }
415
- (0, callback_1.doError)(err, callback);
420
+ (0, callback_1.doError)(err, complete);
416
421
  return;
417
422
  }
418
423
  if (process.env.FIREBIRD_DEBUG) {
@@ -420,13 +425,13 @@ class Database extends events_1.default.EventEmitter {
420
425
  }
421
426
  const evt = new fbEventManager_1.default(self, eventConnection, eventid, function (err) {
422
427
  if (err) {
423
- (0, callback_1.doError)(err, callback);
428
+ (0, callback_1.doError)(err, complete);
424
429
  return;
425
430
  }
426
431
  if (process.env.FIREBIRD_DEBUG) {
427
432
  console.log('[fb-debug] Database.attachEvent: FbEventManager ready, eventid=%d', evt.eventid);
428
433
  }
429
- callback(err, evt);
434
+ complete(err, evt);
430
435
  });
431
436
  }, self);
432
437
  });
@@ -10,6 +10,8 @@ declare class EventConnection {
10
10
  error: any;
11
11
  eventcallback: ((err: any, ret?: any) => void) | null;
12
12
  _connectSettled: boolean;
13
+ _terminalErrorReported: boolean;
14
+ _intentionalClose: boolean;
13
15
  constructor(host: string, port: number, callback: ((err?: Error) => void) | undefined, db: any);
14
16
  _bind_events(host: string, port: number, callback?: (err?: Error) => void): void;
15
17
  throwClosed(callback?: (err: any) => void): this;
@@ -14,6 +14,8 @@ class EventConnection {
14
14
  this._isClosed = false;
15
15
  this._isOpened = false;
16
16
  this._connectSettled = false;
17
+ this._terminalErrorReported = false;
18
+ this._intentionalClose = false;
17
19
  this._socket = net_1.default.createConnection(port, host);
18
20
  this._bind_events(host, port, callback);
19
21
  this.error = null;
@@ -28,19 +30,41 @@ class EventConnection {
28
30
  if (callback)
29
31
  callback(err);
30
32
  }
33
+ function reportTerminalError(err) {
34
+ if (self._terminalErrorReported)
35
+ return;
36
+ self._terminalErrorReported = true;
37
+ const wasOpened = self._isOpened;
38
+ self.error = err;
39
+ self._isClosed = true;
40
+ self._isOpened = false;
41
+ // An error during a caller-initiated shutdown is part of tearing
42
+ // down the auxiliary socket, not a database error.
43
+ if (!self._intentionalClose && !wasOpened) {
44
+ finishConnect(err);
45
+ }
46
+ else if (!self._intentionalClose && self.eventcallback) {
47
+ self.eventcallback(err);
48
+ }
49
+ else if (!self._intentionalClose && self.db && self.db.connection &&
50
+ typeof self.db.connection._emitError === 'function') {
51
+ self.db.connection._emitError(err);
52
+ }
53
+ if (!self._socket.destroyed)
54
+ self._socket.destroy();
55
+ }
31
56
  self._socket.on('close', function () {
32
57
  self._isClosed = true;
33
58
  if (!self._isOpened) {
34
59
  finishConnect(self.error || new Error(`Event connection to ${host}:${port} closed before connecting.`));
35
60
  }
61
+ else if (!self._intentionalClose && self.eventcallback && !self._terminalErrorReported) {
62
+ reportTerminalError(new Error(`Event connection to ${host}:${port} closed unexpectedly.`));
63
+ }
64
+ self._isOpened = false;
36
65
  });
37
66
  self._socket.on('error', function (e) {
38
- self.error = e;
39
- if (!self._isOpened) {
40
- if (!self._socket.destroyed)
41
- self._socket.destroy();
42
- finishConnect(e);
43
- }
67
+ reportTerminalError(e);
44
68
  });
45
69
  self._socket.on('connect', function () {
46
70
  self._isClosed = false;
@@ -64,6 +88,7 @@ class EventConnection {
64
88
  try {
65
89
  var tmp_event;
66
90
  while (xdr.pos < xdr.buffer.length) {
91
+ op_pos = xdr.pos;
67
92
  do {
68
93
  var r = xdr.readInt();
69
94
  } while (r === const_1.default.op_dummy);
@@ -80,7 +105,9 @@ class EventConnection {
80
105
  var eventcount = 0;
81
106
  var pos = 1;
82
107
  while (pos < buf.length) {
83
- var len = buf.readInt8(pos++);
108
+ var len = buf.readUInt8(pos++);
109
+ if (pos + len + 4 > buf.length)
110
+ throw new RangeError('Incomplete event payload');
84
111
  eventname = buf.toString(DEFAULT_ENCODING, pos, pos + len);
85
112
  var prevcount = self.emgr.events[eventname] || 0;
86
113
  pos += len;
@@ -105,7 +132,7 @@ class EventConnection {
105
132
  self.eventcallback(null, { eventid: event_id, events: lst_event });
106
133
  break;
107
134
  default:
108
- // Unknown opcode on the event connection stop processing.
135
+ reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
109
136
  return;
110
137
  }
111
138
  }
@@ -117,14 +144,13 @@ class EventConnection {
117
144
  self._xdr = xdr;
118
145
  }
119
146
  else {
120
- throw err;
147
+ reportTerminalError(err instanceof Error ? err : new Error(String(err)));
121
148
  }
122
149
  }
123
150
  });
124
151
  }
125
152
  throwClosed(callback) {
126
153
  var err = new Error('Event Connection is closed.');
127
- this.db.emit('error', err);
128
154
  if (callback)
129
155
  callback(err);
130
156
  return this;
@@ -6,7 +6,16 @@ declare class FbEventManager extends Events.EventEmitter {
6
6
  eventid: number;
7
7
  _subscriptionVersion: number;
8
8
  _hasActiveSubscription: boolean;
9
+ _readySettled: boolean;
10
+ _terminalErrorReported: boolean;
11
+ _readyCallback: (err: any, ret?: any) => void;
9
12
  constructor(db: any, eventconnection: any, eventid: number, callback: (err: any, ret?: any) => void);
13
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
14
+ on(event: 'error', listener: (error: Error) => void): this;
15
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
16
+ once(event: 'error', listener: (error: Error) => void): this;
17
+ _finishReady(err?: Error): void;
18
+ _handleAsyncError(err: any): void;
10
19
  /**
11
20
  * Returns a snapshot of the current state for debugging.
12
21
  * Useful for tracing the state machine during development.
@@ -36,7 +45,7 @@ declare class FbEventManager extends Events.EventEmitter {
36
45
  isEventConnectionOpen: boolean;
37
46
  isDatabaseConnectionClosed: boolean;
38
47
  };
39
- _createEventLoop(callback: (err: any, ret?: any) => void): void;
48
+ _createEventLoop(): void;
40
49
  _changeEvent(callback: (err: any, ret?: any) => void): void;
41
50
  registerEvent(events: string[], callback: (err: any, ret?: any) => void): any;
42
51
  unregisterEvent(events: string[], callback: (err: any, ret?: any) => void): any;
@@ -64,6 +64,7 @@
64
64
  // Asynchronous notifications on the AUX (EventConnection) socket
65
65
  // ───────────────────────────────────────────────────────────────
66
66
  // Server → Client : op_event (fired by Firebird POST_EVENT trigger)
67
+ // Error / unexpected close → CLOSED; emit manager 'error' once
67
68
  var __importDefault = (this && this.__importDefault) || function (mod) {
68
69
  return (mod && mod.__esModule) ? mod : { "default": mod };
69
70
  };
@@ -83,7 +84,50 @@ class FbEventManager extends events_1.default.EventEmitter {
83
84
  // main connection (so close() and _changeEvent know whether to send
84
85
  // op_cancel_events before tearing down or re-subscribing).
85
86
  this._hasActiveSubscription = false;
86
- this._createEventLoop(callback);
87
+ this._readySettled = false;
88
+ this._terminalErrorReported = false;
89
+ this._readyCallback = callback;
90
+ this._createEventLoop();
91
+ process.nextTick(() => this._finishReady());
92
+ }
93
+ on(event, listener) {
94
+ return super.on(event, listener);
95
+ }
96
+ once(event, listener) {
97
+ return super.once(event, listener);
98
+ }
99
+ _finishReady(err) {
100
+ if (this._readySettled)
101
+ return;
102
+ this._readySettled = true;
103
+ if (err)
104
+ (0, callback_1.doError)(err, this._readyCallback);
105
+ else
106
+ this._readyCallback(null);
107
+ }
108
+ _handleAsyncError(err) {
109
+ if (this._terminalErrorReported)
110
+ return;
111
+ this._terminalErrorReported = true;
112
+ const error = err instanceof Error ? err : new Error(String(err));
113
+ this._hasActiveSubscription = false;
114
+ this._subscriptionVersion++;
115
+ this.eventconnection._isClosed = true;
116
+ this.eventconnection._isOpened = false;
117
+ this.eventconnection.eventcallback = null;
118
+ if (this.eventconnection._socket && !this.eventconnection._socket.destroyed &&
119
+ typeof this.eventconnection._socket.destroy === 'function') {
120
+ this.eventconnection._socket.destroy();
121
+ }
122
+ if (!this._readySettled) {
123
+ this._finishReady(error);
124
+ }
125
+ else if (this.listenerCount('error') > 0) {
126
+ this.emit('error', error);
127
+ }
128
+ else if (this.db.connection && typeof this.db.connection._emitError === 'function') {
129
+ this.db.connection._emitError(error);
130
+ }
87
131
  }
88
132
  /**
89
133
  * Returns a snapshot of the current state for debugging.
@@ -136,7 +180,7 @@ class FbEventManager extends events_1.default.EventEmitter {
136
180
  isDatabaseConnectionClosed: dbConnClosed,
137
181
  };
138
182
  }
139
- _createEventLoop(callback) {
183
+ _createEventLoop() {
140
184
  var self = this;
141
185
  var cnx = this.db.connection;
142
186
  this.eventconnection.emgr = this;
@@ -157,15 +201,15 @@ class FbEventManager extends events_1.default.EventEmitter {
157
201
  }
158
202
  cnx.queEvents(self.events, self.eventid, function (err) {
159
203
  if (err) {
160
- (0, callback_1.doError)(err, callback);
204
+ self._handleAsyncError(err);
161
205
  return;
162
206
  }
163
207
  // subscription renewed, nothing else to do
164
208
  });
165
209
  }
166
210
  this.eventconnection.eventcallback = function (err, ret) {
167
- if (err || (self.eventid !== ret.eventid)) {
168
- (0, callback_1.doError)(err || new Error('Bad eventid'), callback);
211
+ if (err || !ret || (self.eventid !== ret.eventid)) {
212
+ self._handleAsyncError(err || new Error('Bad eventid'));
169
213
  return;
170
214
  }
171
215
  ret.events.forEach(function (event) {
@@ -173,11 +217,6 @@ class FbEventManager extends events_1.default.EventEmitter {
173
217
  });
174
218
  loop();
175
219
  };
176
- // Resolve attachEvent on the next tick – no subscription is needed
177
- // until the caller registers at least one event name via registerEvent().
178
- // process.nextTick ensures the outer `const evt = new FbEventManager(...)`
179
- // assignment in database.js completes before this callback fires.
180
- process.nextTick(function () { callback(null); });
181
220
  }
182
221
  _changeEvent(callback) {
183
222
  var self = this;
@@ -246,6 +285,7 @@ class FbEventManager extends events_1.default.EventEmitter {
246
285
  }
247
286
  // Prevent the event loop from re-queuing on stale op_event notifications
248
287
  // that may arrive between closeEvents and socket.end()
288
+ self.eventconnection._intentionalClose = true;
249
289
  self.eventconnection.eventcallback = null;
250
290
  // Gracefully close the event socket using a FIN (end()) rather than a RST
251
291
  // (destroy()), then wait for the 'close' event which confirms both sides have
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.16.2",
3
+ "version": "2.17.1",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/types.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  // published declaration files.
7
7
 
8
8
  import type { Readable, Writable } from 'stream';
9
+ import type { EventEmitter } from 'events';
9
10
  import type { SqlTag } from './sql-template';
10
11
 
11
12
  export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
@@ -16,6 +17,30 @@ export type QueryCallback = (err: any, result: any[]) => void;
16
17
  export type SimpleCallback = (err: any) => void;
17
18
  export type SequentialCallback = (row: any, index: number, next?: (err?: any) => void) => void | Promise<void>;
18
19
 
20
+ export interface FbEventState {
21
+ state: 'IDLE' | 'SUBSCRIBED' | 'CLOSED';
22
+ hasActiveSubscription: boolean;
23
+ registeredEvents: Record<string, number>;
24
+ eventId: number;
25
+ isEventConnectionOpen: boolean;
26
+ isDatabaseConnectionClosed: boolean;
27
+ }
28
+
29
+ export interface FbEventManager extends EventEmitter {
30
+ readonly eventid: number;
31
+ readonly events: Record<string, number>;
32
+ registerEvent(events: string[], callback: SimpleCallback): void;
33
+ unregisterEvent(events: string[], callback: SimpleCallback): void;
34
+ close(callback?: SimpleCallback): void;
35
+ getState(): FbEventState;
36
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
37
+ on(event: 'error', listener: (error: Error) => void): this;
38
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
39
+ once(event: 'error', listener: (error: Error) => void): this;
40
+ }
41
+
42
+ export type FbEventManagerCallback = (err: any, manager?: FbEventManager) => void;
43
+
19
44
  /**
20
45
  * Describes a single column in a prepared statement's result set or
21
46
  * parameter list. The properties here are populated by the
@@ -272,7 +297,7 @@ export interface Database {
272
297
  batchStream(query: string, options?: BatchStreamOptions): BatchStream;
273
298
  drop(callback: SimpleCallback): void;
274
299
  escape(value: any): string;
275
- attachEvent(callback: any): this;
300
+ attachEvent(callback: FbEventManagerCallback): this;
276
301
  createTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
277
302
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
278
303
  dropTablespace(name: string, callback?: QueryCallback): Database;
@@ -293,7 +318,7 @@ export interface Database {
293
318
  newStatementAsync(query: string): Promise<Statement>;
294
319
  detachAsync(force?: boolean): Promise<void>;
295
320
  dropAsync(): Promise<void>;
296
- attachEventAsync(): Promise<any>;
321
+ attachEventAsync(): Promise<FbEventManager>;
297
322
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
298
323
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
299
324
  /**
@@ -413,6 +438,8 @@ export type SupportedCharacterSet = |
413
438
 
414
439
  export interface Options {
415
440
  host?: string;
441
+ /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
442
+ eventHost?: string;
416
443
  port?: number;
417
444
  database?: string;
418
445
  user?: string;
package/src/utils.ts CHANGED
@@ -4,6 +4,13 @@ import type { FbStatusItem } from './callback';
4
4
 
5
5
  const MessagesError = messagesJson as Record<string, string>;
6
6
 
7
+ /** Resolve the address used for Firebird's auxiliary event connection. */
8
+ export function resolveEventHost(options: { host?: string; eventHost?: string }, advertisedHost: string): string {
9
+ if (options.eventHost) return options.eventHost;
10
+ if (advertisedHost === '0.0.0.0' || advertisedHost === '::') return options.host || Const.DEFAULT_HOST;
11
+ return advertisedHost;
12
+ }
13
+
7
14
  /**
8
15
  * Parse date from string
9
16
  */
@@ -1,6 +1,6 @@
1
1
  import Events from 'events';
2
2
  import { doError, fromCallback, type Callback, type SimpleCallback } from '../callback';
3
- import { batchResultToError, escape } from '../utils';
3
+ import { batchResultToError, escape, resolveEventHost } from '../utils';
4
4
  import Const from './const';
5
5
  import { makeSqlTag, type SqlTag } from '../sql-template';
6
6
  import { computeColumnKeys, nestCell, resolveKeyTransform, resolveNestTables } from './xsqlvar';
@@ -464,6 +464,12 @@ class Database extends Events.EventEmitter {
464
464
  attachEvent(callback: Callback<FbEventManager>): this {
465
465
  var self = this;
466
466
  const eventid = self.eventid++;
467
+ let completed = false;
468
+ const complete: Callback<FbEventManager> = function(err?: any, manager?: FbEventManager) {
469
+ if (completed) return;
470
+ completed = true;
471
+ callback(err, manager);
472
+ };
467
473
  if (process.env.FIREBIRD_DEBUG) {
468
474
  console.log('[fb-debug] Database.attachEvent: calling auxConnection, eventid=%d queue=%d', eventid, self.connection._queue.length);
469
475
  }
@@ -473,7 +479,7 @@ class Database extends Events.EventEmitter {
473
479
  if (process.env.FIREBIRD_DEBUG) {
474
480
  console.log('[fb-debug] Database.attachEvent: auxConnection error:', err.message);
475
481
  }
476
- doError(err, callback);
482
+ doError(err, complete);
477
483
  return;
478
484
  }
479
485
 
@@ -481,9 +487,7 @@ class Database extends Events.EventEmitter {
481
487
  console.log('[fb-debug] Database.attachEvent: auxConnection ok, connecting to aux port %s:%d', socket_info.host, socket_info.port);
482
488
  }
483
489
 
484
- const host = (socket_info.host === '0.0.0.0' || socket_info.host === '::')
485
- ? self.connection.options.host
486
- : socket_info.host;
490
+ const host = resolveEventHost(self.connection.options, socket_info.host);
487
491
 
488
492
  const eventConnection = new EventConnection(
489
493
  host, socket_info.port, function(err?: any) {
@@ -491,7 +495,7 @@ class Database extends Events.EventEmitter {
491
495
  if (process.env.FIREBIRD_DEBUG) {
492
496
  console.log('[fb-debug] Database.attachEvent: EventConnection error:', err.message);
493
497
  }
494
- doError(err, callback);
498
+ doError(err, complete);
495
499
  return;
496
500
  }
497
501
 
@@ -501,14 +505,14 @@ class Database extends Events.EventEmitter {
501
505
 
502
506
  const evt = new FbEventManager(self, eventConnection, eventid, function (err: any) {
503
507
  if (err) {
504
- doError(err, callback);
508
+ doError(err, complete);
505
509
  return;
506
510
  }
507
511
 
508
512
  if (process.env.FIREBIRD_DEBUG) {
509
513
  console.log('[fb-debug] Database.attachEvent: FbEventManager ready, eventid=%d', evt.eventid);
510
514
  }
511
- callback(err, evt);
515
+ complete(err, evt);
512
516
  });
513
517
  }, self);
514
518
  });
@@ -14,6 +14,8 @@ class EventConnection {
14
14
  error: any;
15
15
  eventcallback: ((err: any, ret?: any) => void) | null;
16
16
  _connectSettled: boolean;
17
+ _terminalErrorReported: boolean;
18
+ _intentionalClose: boolean;
17
19
 
18
20
  constructor(host: string, port: number, callback: ((err?: Error) => void) | undefined, db: any) {
19
21
  var self = this;
@@ -22,6 +24,8 @@ class EventConnection {
22
24
  this._isClosed = false;
23
25
  this._isOpened = false;
24
26
  this._connectSettled = false;
27
+ this._terminalErrorReported = false;
28
+ this._intentionalClose = false;
25
29
  this._socket = net.createConnection(port, host);
26
30
  this._bind_events(host, port, callback);
27
31
  this.error = null;
@@ -37,19 +41,40 @@ class EventConnection {
37
41
  if (callback) callback(err);
38
42
  }
39
43
 
44
+ function reportTerminalError(err: Error) {
45
+ if (self._terminalErrorReported) return;
46
+ self._terminalErrorReported = true;
47
+ const wasOpened = self._isOpened;
48
+ self.error = err;
49
+ self._isClosed = true;
50
+ self._isOpened = false;
51
+
52
+ // An error during a caller-initiated shutdown is part of tearing
53
+ // down the auxiliary socket, not a database error.
54
+ if (!self._intentionalClose && !wasOpened) {
55
+ finishConnect(err);
56
+ } else if (!self._intentionalClose && self.eventcallback) {
57
+ self.eventcallback(err);
58
+ } else if (!self._intentionalClose && self.db && self.db.connection &&
59
+ typeof self.db.connection._emitError === 'function') {
60
+ self.db.connection._emitError(err);
61
+ }
62
+
63
+ if (!self._socket.destroyed) self._socket.destroy();
64
+ }
65
+
40
66
  self._socket.on('close', function () {
41
67
  self._isClosed = true;
42
68
  if (!self._isOpened) {
43
69
  finishConnect(self.error || new Error(`Event connection to ${host}:${port} closed before connecting.`));
70
+ } else if (!self._intentionalClose && self.eventcallback && !self._terminalErrorReported) {
71
+ reportTerminalError(new Error(`Event connection to ${host}:${port} closed unexpectedly.`));
44
72
  }
73
+ self._isOpened = false;
45
74
  })
46
75
 
47
76
  self._socket.on('error', function (e) {
48
- self.error = e;
49
- if (!self._isOpened) {
50
- if (!self._socket.destroyed) self._socket.destroy();
51
- finishConnect(e);
52
- }
77
+ reportTerminalError(e);
53
78
  })
54
79
 
55
80
  self._socket.on('connect', function () {
@@ -78,6 +103,7 @@ class EventConnection {
78
103
 
79
104
  var tmp_event: Record<string, number>;
80
105
  while (xdr.pos < xdr.buffer.length) {
106
+ op_pos = xdr.pos;
81
107
  do {
82
108
  var r = xdr.readInt();
83
109
  } while (r === Const.op_dummy);
@@ -95,7 +121,8 @@ class EventConnection {
95
121
  var eventcount = 0;
96
122
  var pos = 1;
97
123
  while (pos < buf.length) {
98
- var len = buf.readInt8(pos++);
124
+ var len = buf.readUInt8(pos++);
125
+ if (pos + len + 4 > buf.length) throw new RangeError('Incomplete event payload');
99
126
  eventname = buf.toString(DEFAULT_ENCODING, pos, pos + len);
100
127
  var prevcount = self.emgr.events[eventname] || 0;
101
128
  pos += len;
@@ -120,7 +147,7 @@ class EventConnection {
120
147
  self.eventcallback(null, { eventid: event_id, events: lst_event });
121
148
  break;
122
149
  default:
123
- // Unknown opcode on the event connection stop processing.
150
+ reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
124
151
  return;
125
152
  }
126
153
  }
@@ -130,7 +157,7 @@ class EventConnection {
130
157
  xdr.pos = 0;
131
158
  self._xdr = xdr;
132
159
  } else {
133
- throw err;
160
+ reportTerminalError(err instanceof Error ? err : new Error(String(err)));
134
161
  }
135
162
  }
136
163
  })
@@ -138,7 +165,6 @@ class EventConnection {
138
165
 
139
166
  throwClosed(callback?: (err: any) => void): this {
140
167
  var err = new Error('Event Connection is closed.');
141
- this.db.emit('error', err);
142
168
  if (callback)
143
169
  callback(err);
144
170
  return this;
@@ -63,6 +63,7 @@
63
63
  // Asynchronous notifications on the AUX (EventConnection) socket
64
64
  // ───────────────────────────────────────────────────────────────
65
65
  // Server → Client : op_event (fired by Firebird POST_EVENT trigger)
66
+ // Error / unexpected close → CLOSED; emit manager 'error' once
66
67
 
67
68
  import Events from 'events';
68
69
  import { doError } from '../callback';
@@ -74,6 +75,9 @@ class FbEventManager extends Events.EventEmitter {
74
75
  eventid: number;
75
76
  _subscriptionVersion: number;
76
77
  _hasActiveSubscription: boolean;
78
+ _readySettled: boolean;
79
+ _terminalErrorReported: boolean;
80
+ _readyCallback: (err: any, ret?: any) => void;
77
81
 
78
82
  constructor(db: any, eventconnection: any, eventid: number, callback: (err: any, ret?: any) => void) {
79
83
  super();
@@ -88,7 +92,53 @@ class FbEventManager extends Events.EventEmitter {
88
92
  // main connection (so close() and _changeEvent know whether to send
89
93
  // op_cancel_events before tearing down or re-subscribing).
90
94
  this._hasActiveSubscription = false;
91
- this._createEventLoop(callback);
95
+ this._readySettled = false;
96
+ this._terminalErrorReported = false;
97
+ this._readyCallback = callback;
98
+ this._createEventLoop();
99
+ process.nextTick(() => this._finishReady());
100
+ }
101
+
102
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
103
+ on(event: 'error', listener: (error: Error) => void): this;
104
+ on(event: string | symbol, listener: (...args: any[]) => void): this {
105
+ return super.on(event, listener);
106
+ }
107
+
108
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
109
+ once(event: 'error', listener: (error: Error) => void): this;
110
+ once(event: string | symbol, listener: (...args: any[]) => void): this {
111
+ return super.once(event, listener);
112
+ }
113
+
114
+ _finishReady(err?: Error): void {
115
+ if (this._readySettled) return;
116
+ this._readySettled = true;
117
+ if (err) doError(err, this._readyCallback);
118
+ else this._readyCallback(null);
119
+ }
120
+
121
+ _handleAsyncError(err: any): void {
122
+ if (this._terminalErrorReported) return;
123
+ this._terminalErrorReported = true;
124
+ const error = err instanceof Error ? err : new Error(String(err));
125
+ this._hasActiveSubscription = false;
126
+ this._subscriptionVersion++;
127
+ this.eventconnection._isClosed = true;
128
+ this.eventconnection._isOpened = false;
129
+ this.eventconnection.eventcallback = null;
130
+ if (this.eventconnection._socket && !this.eventconnection._socket.destroyed &&
131
+ typeof this.eventconnection._socket.destroy === 'function') {
132
+ this.eventconnection._socket.destroy();
133
+ }
134
+
135
+ if (!this._readySettled) {
136
+ this._finishReady(error);
137
+ } else if (this.listenerCount('error') > 0) {
138
+ this.emit('error', error);
139
+ } else if (this.db.connection && typeof this.db.connection._emitError === 'function') {
140
+ this.db.connection._emitError(error);
141
+ }
92
142
  }
93
143
 
94
144
  /**
@@ -143,7 +193,7 @@ class FbEventManager extends Events.EventEmitter {
143
193
  };
144
194
  }
145
195
 
146
- _createEventLoop(callback: (err: any, ret?: any) => void): void {
196
+ _createEventLoop(): void {
147
197
  var self = this;
148
198
  var cnx = this.db.connection;
149
199
  this.eventconnection.emgr = this;
@@ -166,7 +216,7 @@ class FbEventManager extends Events.EventEmitter {
166
216
  }
167
217
  cnx.queEvents(self.events, self.eventid, function (err: any) {
168
218
  if (err) {
169
- doError(err, callback);
219
+ self._handleAsyncError(err);
170
220
  return;
171
221
  }
172
222
  // subscription renewed, nothing else to do
@@ -174,8 +224,8 @@ class FbEventManager extends Events.EventEmitter {
174
224
  }
175
225
 
176
226
  this.eventconnection.eventcallback = function (err: any, ret?: any) {
177
- if (err || (self.eventid !== ret.eventid)) {
178
- doError(err || new Error('Bad eventid'), callback);
227
+ if (err || !ret || (self.eventid !== ret.eventid)) {
228
+ self._handleAsyncError(err || new Error('Bad eventid'));
179
229
  return;
180
230
  }
181
231
 
@@ -186,11 +236,6 @@ class FbEventManager extends Events.EventEmitter {
186
236
  loop();
187
237
  };
188
238
 
189
- // Resolve attachEvent on the next tick – no subscription is needed
190
- // until the caller registers at least one event name via registerEvent().
191
- // process.nextTick ensures the outer `const evt = new FbEventManager(...)`
192
- // assignment in database.js completes before this callback fires.
193
- process.nextTick(function() { callback(null); });
194
239
  }
195
240
 
196
241
  _changeEvent(callback: (err: any, ret?: any) => void): void {
@@ -271,6 +316,7 @@ class FbEventManager extends Events.EventEmitter {
271
316
 
272
317
  // Prevent the event loop from re-queuing on stale op_event notifications
273
318
  // that may arrive between closeEvents and socket.end()
319
+ self.eventconnection._intentionalClose = true;
274
320
  self.eventconnection.eventcallback = null;
275
321
 
276
322
  // Gracefully close the event socket using a FIN (end()) rather than a RST