node-firebird 2.17.0 → 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
@@ -1404,6 +1404,12 @@ Firebird.attach(options, function (err, db) {
1404
1404
  db.attachEvent(function (err, evtmgr) {
1405
1405
  if (err) throw err;
1406
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
+
1407
1413
  // 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
1408
1414
  // PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
1409
1415
  evtmgr.registerEvent(['MY_EVENT'], function (err) {
@@ -1479,9 +1485,20 @@ db.attachEvent(function (err, evtmgr) {
1479
1485
  });
1480
1486
  ```
1481
1487
 
1482
- Errors on the aux socket *after* it connects are not delivered to this callback. Poll
1483
- `evtmgr.getState().isEventConnectionOpen` if you need to detect an aux connection that dies
1484
- 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.
1485
1502
 
1486
1503
  ### Escaping Query values
1487
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
  /**
@@ -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,7 +405,7 @@ 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) {
@@ -410,7 +417,7 @@ class Database extends events_1.default.EventEmitter {
410
417
  if (process.env.FIREBIRD_DEBUG) {
411
418
  console.log('[fb-debug] Database.attachEvent: EventConnection error:', err.message);
412
419
  }
413
- (0, callback_1.doError)(err, callback);
420
+ (0, callback_1.doError)(err, complete);
414
421
  return;
415
422
  }
416
423
  if (process.env.FIREBIRD_DEBUG) {
@@ -418,13 +425,13 @@ class Database extends events_1.default.EventEmitter {
418
425
  }
419
426
  const evt = new fbEventManager_1.default(self, eventConnection, eventid, function (err) {
420
427
  if (err) {
421
- (0, callback_1.doError)(err, callback);
428
+ (0, callback_1.doError)(err, complete);
422
429
  return;
423
430
  }
424
431
  if (process.env.FIREBIRD_DEBUG) {
425
432
  console.log('[fb-debug] Database.attachEvent: FbEventManager ready, eventid=%d', evt.eventid);
426
433
  }
427
- callback(err, evt);
434
+ complete(err, evt);
428
435
  });
429
436
  }, self);
430
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.17.0",
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
  /**
@@ -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
 
@@ -489,7 +495,7 @@ class Database extends Events.EventEmitter {
489
495
  if (process.env.FIREBIRD_DEBUG) {
490
496
  console.log('[fb-debug] Database.attachEvent: EventConnection error:', err.message);
491
497
  }
492
- doError(err, callback);
498
+ doError(err, complete);
493
499
  return;
494
500
  }
495
501
 
@@ -499,14 +505,14 @@ class Database extends Events.EventEmitter {
499
505
 
500
506
  const evt = new FbEventManager(self, eventConnection, eventid, function (err: any) {
501
507
  if (err) {
502
- doError(err, callback);
508
+ doError(err, complete);
503
509
  return;
504
510
  }
505
511
 
506
512
  if (process.env.FIREBIRD_DEBUG) {
507
513
  console.log('[fb-debug] Database.attachEvent: FbEventManager ready, eventid=%d', evt.eventid);
508
514
  }
509
- callback(err, evt);
515
+ complete(err, evt);
510
516
  });
511
517
  }, self);
512
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