node-firebird 2.17.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -182,6 +182,7 @@ var options = {};
182
182
 
183
183
  options.host = '127.0.0.1';
184
184
  options.eventHost = undefined; // optional; override the server-advertised host for the auxiliary event connection
185
+ options.eventBaseline = false; // optional; emit the first event counter snapshot as 'baseline' instead of 'post_event'
185
186
  options.port = 3050;
186
187
  options.database = 'database.fdb';
187
188
  options.user = 'SYSDBA';
@@ -1396,6 +1397,19 @@ Firebird database events are **asynchronous** notifications triggered by `POST_E
1396
1397
  triggers or stored procedures. They travel over a separate "aux" connection (opened via
1397
1398
  `db.attachEvent()`) and are managed through a `FbEventManager` instance.
1398
1399
 
1400
+ By default, the first event notification after registration follows the existing
1401
+ `post_event` behavior. Set `options.eventBaseline = true` to emit that initial
1402
+ counter snapshot as `baseline` instead, without a `post_event` for that packet.
1403
+ Each register/unregister reconfiguration starts a new baseline. A `baseline`
1404
+ listener receives a snapshot of the currently registered event counters; install
1405
+ it, and any `post_event` listener, **before** calling `registerEvent()` because
1406
+ the auxiliary packet can arrive before the registration callback. A real event
1407
+ committed during registration can be included in that first snapshot, so an
1408
+ application using this option should query its current database state on
1409
+ `baseline` rather than assume every individual change will generate a later
1410
+ `post_event`. The default remains unchanged for applications that use the first
1411
+ `post_event` as a startup refresh signal.
1412
+
1399
1413
  ```js
1400
1414
  Firebird.attach(options, function (err, db) {
1401
1415
  if (err) throw err;
@@ -1404,16 +1418,26 @@ Firebird.attach(options, function (err, db) {
1404
1418
  db.attachEvent(function (err, evtmgr) {
1405
1419
  if (err) throw err;
1406
1420
 
1421
+ // Auxiliary socket/protocol failures after attachment are reported here.
1422
+ // Install this listener before registering events.
1423
+ evtmgr.on('error', function (err) {
1424
+ console.error('event connection failed:', err);
1425
+ });
1426
+
1427
+ // With options.eventBaseline = true, initialize from the current DB state.
1428
+ // evtmgr.on('baseline', function (counts) { refreshFromDatabase(); });
1429
+
1430
+ evtmgr.on('post_event', function (name, count) {
1431
+ // name === event name string (e.g. 'MY_EVENT')
1432
+ // count === cumulative trigger count since last notification
1433
+ });
1434
+
1407
1435
  // 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
1408
1436
  // PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
1409
1437
  evtmgr.registerEvent(['MY_EVENT'], function (err) {
1410
1438
  if (err) throw err;
1411
1439
 
1412
- // 3. Listen for POST_EVENT notifications
1413
- evtmgr.on('post_event', function (name, count) {
1414
- // name === event name string (e.g. 'MY_EVENT')
1415
- // count === cumulative trigger count since last notification
1416
- });
1440
+ // 3. Subscription acknowledged. Notifications may already have arrived.
1417
1441
  });
1418
1442
 
1419
1443
  // 4. Unsubscribe from one or more events. Passing all currently registered names cancels
@@ -1479,9 +1503,20 @@ db.attachEvent(function (err, evtmgr) {
1479
1503
  });
1480
1504
  ```
1481
1505
 
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.
1506
+ Errors on the aux socket *after* it connects are delivered through the manager's `error`
1507
+ event. Install the listener before calling `registerEvent()`:
1508
+
1509
+ ```js
1510
+ evtmgr.on('error', function (err) {
1511
+ // The manager is now CLOSED. Reattach explicitly if the application wants
1512
+ // to resume event delivery; the driver does not reconnect automatically.
1513
+ console.error('event connection failed:', err);
1514
+ });
1515
+ ```
1516
+
1517
+ When no manager error listener is installed, the driver forwards the failure to guarded
1518
+ database `error` listeners when present. It never emits an unhandled EventEmitter `error`.
1519
+ The primary database attachment remains available for ordinary queries.
1485
1520
 
1486
1521
  ### Escaping Query values
1487
1522
 
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,29 @@ 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: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
26
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
27
+ on(event: 'error', listener: (error: Error) => void): this;
28
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
29
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
30
+ once(event: 'error', listener: (error: Error) => void): this;
31
+ }
32
+ export type FbEventManagerCallback = (err: any, manager?: FbEventManager) => void;
9
33
  /**
10
34
  * Describes a single column in a prepared statement's result set or
11
35
  * parameter list. The properties here are populated by the
@@ -250,7 +274,7 @@ export interface Database {
250
274
  batchStream(query: string, options?: BatchStreamOptions): BatchStream;
251
275
  drop(callback: SimpleCallback): void;
252
276
  escape(value: any): string;
253
- attachEvent(callback: any): this;
277
+ attachEvent(callback: FbEventManagerCallback): this;
254
278
  createTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
255
279
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
256
280
  dropTablespace(name: string, callback?: QueryCallback): Database;
@@ -271,7 +295,7 @@ export interface Database {
271
295
  newStatementAsync(query: string): Promise<Statement>;
272
296
  detachAsync(force?: boolean): Promise<void>;
273
297
  dropAsync(): Promise<void>;
274
- attachEventAsync(): Promise<any>;
298
+ attachEventAsync(): Promise<FbEventManager>;
275
299
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
276
300
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
277
301
  /**
@@ -354,6 +378,8 @@ export interface Options {
354
378
  host?: string;
355
379
  /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
356
380
  eventHost?: string;
381
+ /** Emit the first counter snapshot of each event subscription as 'baseline' instead of 'post_event'. Off by default. */
382
+ eventBaseline?: boolean;
357
383
  port?: number;
358
384
  database?: string;
359
385
  user?: string;
package/lib/uri.js CHANGED
@@ -15,7 +15,7 @@ exports.normalizeOptions = normalizeOptions;
15
15
  */
16
16
  const BOOLEAN_KEYS = new Set([
17
17
  'lowercase_keys', 'blobAsText', 'wireCompression', 'manager',
18
- 'namedPlaceholders', 'enableKeepAlive',
18
+ 'namedPlaceholders', 'enableKeepAlive', 'eventBaseline',
19
19
  ]);
20
20
  /** Option keys coerced to number when they arrive as URI query parameters. */
21
21
  const NUMBER_KEYS = new Set([
@@ -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;
@@ -92,20 +119,22 @@ class EventConnection {
92
119
  }
93
120
  xdr.readInt64(); // ignore AST INFO
94
121
  var event_id = xdr.readInt();
95
- // set the new count in global event hash
96
- // Only update events that are still registered; do not
97
- // re-add events that unregisterEvent() has deleted, since
98
- // that would cause subscribe() to re-subscribe for them.
99
- for (var evt in tmp_event) {
100
- if (Object.prototype.hasOwnProperty.call(self.emgr.events, evt)) {
101
- self.emgr.events[evt] = tmp_event[evt];
122
+ // In the default mode, retain the existing parser-side
123
+ // counter update. Baseline mode lets the manager apply
124
+ // counts only after rejecting cancelled subscriptions.
125
+ // Never re-add an event removed by unregisterEvent().
126
+ if (!self.emgr._eventBaseline) {
127
+ for (var evt in tmp_event) {
128
+ if (Object.prototype.hasOwnProperty.call(self.emgr.events, evt)) {
129
+ self.emgr.events[evt] = tmp_event[evt];
130
+ }
102
131
  }
103
132
  }
104
133
  if (self.eventcallback)
105
- self.eventcallback(null, { eventid: event_id, events: lst_event });
134
+ self.eventcallback(null, { eventid: event_id, events: lst_event, counts: tmp_event });
106
135
  break;
107
136
  default:
108
- // Unknown opcode on the event connection – stop processing.
137
+ reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
109
138
  return;
110
139
  }
111
140
  }
@@ -117,14 +146,13 @@ class EventConnection {
117
146
  self._xdr = xdr;
118
147
  }
119
148
  else {
120
- throw err;
149
+ reportTerminalError(err instanceof Error ? err : new Error(String(err)));
121
150
  }
122
151
  }
123
152
  });
124
153
  }
125
154
  throwClosed(callback) {
126
155
  var err = new Error('Event Connection is closed.');
127
- this.db.emit('error', err);
128
156
  if (callback)
129
157
  callback(err);
130
158
  return this;
@@ -5,8 +5,28 @@ declare class FbEventManager extends Events.EventEmitter {
5
5
  events: Record<string, number>;
6
6
  eventid: number;
7
7
  _subscriptionVersion: number;
8
+ _activeSubscriptionVersion: number;
8
9
  _hasActiveSubscription: boolean;
10
+ _baselinePending: boolean;
11
+ _eventBaseline: boolean;
12
+ _baselineChangeInProgress: boolean;
13
+ _baselineCallbacks: Array<(err: any, ret?: any) => void>;
14
+ _hasQueuedBaseline: boolean;
15
+ _retiredEventIdLimit: number;
16
+ _baselineCloseRequested: boolean;
17
+ _baselineCloseCallbacks: Array<(err?: any) => void>;
18
+ _readySettled: boolean;
19
+ _terminalErrorReported: boolean;
20
+ _readyCallback: (err: any, ret?: any) => void;
9
21
  constructor(db: any, eventconnection: any, eventid: number, callback: (err: any, ret?: any) => void);
22
+ on(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
23
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
24
+ on(event: 'error', listener: (error: Error) => void): this;
25
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
26
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
27
+ once(event: 'error', listener: (error: Error) => void): this;
28
+ _finishReady(err?: Error): void;
29
+ _handleAsyncError(err: any): void;
10
30
  /**
11
31
  * Returns a snapshot of the current state for debugging.
12
32
  * Useful for tracing the state machine during development.
@@ -36,8 +56,10 @@ declare class FbEventManager extends Events.EventEmitter {
36
56
  isEventConnectionOpen: boolean;
37
57
  isDatabaseConnectionClosed: boolean;
38
58
  };
39
- _createEventLoop(callback: (err: any, ret?: any) => void): void;
59
+ _createEventLoop(): void;
40
60
  _changeEvent(callback: (err: any, ret?: any) => void): void;
61
+ _isRetiredEventId(eventId: number): boolean;
62
+ _changeEventWithBaseline(callback: (err: any, ret?: any) => void): void;
41
63
  registerEvent(events: string[], callback: (err: any, ret?: any) => void): any;
42
64
  unregisterEvent(events: string[], callback: (err: any, ret?: any) => void): any;
43
65
  close(callback?: (err?: any) => void): void;
@@ -47,7 +47,8 @@
47
47
  // │ │ IDLE (or CLOSING if called from close()) │
48
48
  // │ └──────────────────────────────────────────────────┘
49
49
  // │
50
- // │ emit('post_event', name, count)
50
+ // │ eventBaseline: first op_event → emit('baseline', counts)
51
+ // │ otherwise → emit('post_event', name, count)
51
52
  // └──────────────────────┐
52
53
  // ▼
53
54
  // loop() → SUBSCRIBING (re-subscribe)
@@ -64,6 +65,7 @@
64
65
  // Asynchronous notifications on the AUX (EventConnection) socket
65
66
  // ───────────────────────────────────────────────────────────────
66
67
  // Server → Client : op_event (fired by Firebird POST_EVENT trigger)
68
+ // Error / unexpected close → CLOSED; emit manager 'error' once
67
69
  var __importDefault = (this && this.__importDefault) || function (mod) {
68
70
  return (mod && mod.__esModule) ? mod : { "default": mod };
69
71
  };
@@ -79,11 +81,68 @@ class FbEventManager extends events_1.default.EventEmitter {
79
81
  // Guards _hasActiveSubscription against late callbacks from an older
80
82
  // register/unregister cycle after a newer subscription change started.
81
83
  this._subscriptionVersion = 0;
84
+ this._activeSubscriptionVersion = 0;
82
85
  // True when an op_que_events subscription is currently active on the
83
86
  // main connection (so close() and _changeEvent know whether to send
84
87
  // op_cancel_events before tearing down or re-subscribing).
85
88
  this._hasActiveSubscription = false;
86
- this._createEventLoop(callback);
89
+ this._baselinePending = false;
90
+ this._eventBaseline = db.connection.options?.eventBaseline === true;
91
+ this._baselineChangeInProgress = false;
92
+ this._baselineCallbacks = [];
93
+ this._hasQueuedBaseline = false;
94
+ // Highest event ID retired by a baseline reconfiguration. IDs come
95
+ // from db.eventid++ and the current one is always retired before a
96
+ // new one is allocated, so every ID <= this value is stale. A single
97
+ // watermark avoids keeping one entry per reconfiguration forever.
98
+ this._retiredEventIdLimit = 0;
99
+ this._baselineCloseRequested = false;
100
+ this._baselineCloseCallbacks = [];
101
+ this._readySettled = false;
102
+ this._terminalErrorReported = false;
103
+ this._readyCallback = callback;
104
+ this._createEventLoop();
105
+ process.nextTick(() => this._finishReady());
106
+ }
107
+ on(event, listener) {
108
+ return super.on(event, listener);
109
+ }
110
+ once(event, listener) {
111
+ return super.once(event, listener);
112
+ }
113
+ _finishReady(err) {
114
+ if (this._readySettled)
115
+ return;
116
+ this._readySettled = true;
117
+ if (err)
118
+ (0, callback_1.doError)(err, this._readyCallback);
119
+ else
120
+ this._readyCallback(null);
121
+ }
122
+ _handleAsyncError(err) {
123
+ if (this._terminalErrorReported)
124
+ return;
125
+ this._terminalErrorReported = true;
126
+ const error = err instanceof Error ? err : new Error(String(err));
127
+ this._hasActiveSubscription = false;
128
+ this._baselinePending = false;
129
+ this._subscriptionVersion++;
130
+ this.eventconnection._isClosed = true;
131
+ this.eventconnection._isOpened = false;
132
+ this.eventconnection.eventcallback = null;
133
+ if (this.eventconnection._socket && !this.eventconnection._socket.destroyed &&
134
+ typeof this.eventconnection._socket.destroy === 'function') {
135
+ this.eventconnection._socket.destroy();
136
+ }
137
+ if (!this._readySettled) {
138
+ this._finishReady(error);
139
+ }
140
+ else if (this.listenerCount('error') > 0) {
141
+ this.emit('error', error);
142
+ }
143
+ else if (this.db.connection && typeof this.db.connection._emitError === 'function') {
144
+ this.db.connection._emitError(error);
145
+ }
87
146
  }
88
147
  /**
89
148
  * Returns a snapshot of the current state for debugging.
@@ -136,7 +195,7 @@ class FbEventManager extends events_1.default.EventEmitter {
136
195
  isDatabaseConnectionClosed: dbConnClosed,
137
196
  };
138
197
  }
139
- _createEventLoop(callback) {
198
+ _createEventLoop() {
140
199
  var self = this;
141
200
  var cnx = this.db.connection;
142
201
  this.eventconnection.emgr = this;
@@ -152,34 +211,62 @@ class FbEventManager extends events_1.default.EventEmitter {
152
211
  // op_event arriving on the event connection after closeEvents can
153
212
  // trigger queEvents({}) which Firebird never acknowledges,
154
213
  // permanently blocking the main connection queue.
155
- if (!self._hasActiveSubscription || Object.keys(self.events).length === 0) {
214
+ if (!self._hasActiveSubscription || Object.keys(self.events).length === 0 ||
215
+ (self._eventBaseline && self._activeSubscriptionVersion !== self._subscriptionVersion)) {
156
216
  return;
157
217
  }
158
- cnx.queEvents(self.events, self.eventid, function (err) {
159
- if (err) {
160
- (0, callback_1.doError)(err, callback);
218
+ const eventId = self.eventid;
219
+ cnx.queEvents(self.events, eventId, function (err) {
220
+ if (err && (!self._eventBaseline || !self._isRetiredEventId(eventId))) {
221
+ self._handleAsyncError(err);
161
222
  return;
162
223
  }
163
224
  // subscription renewed, nothing else to do
164
225
  });
165
226
  }
166
227
  this.eventconnection.eventcallback = function (err, ret) {
167
- if (err || (self.eventid !== ret.eventid)) {
168
- (0, callback_1.doError)(err || new Error('Bad eventid'), callback);
228
+ if (err || !ret) {
229
+ self._handleAsyncError(err || new Error('Missing event packet'));
169
230
  return;
170
231
  }
171
- ret.events.forEach(function (event) {
172
- self.emit('post_event', event.name, event.count);
173
- });
232
+ if (self.eventid !== ret.eventid) {
233
+ if (self._eventBaseline && self._isRetiredEventId(ret.eventid))
234
+ return;
235
+ self._handleAsyncError(new Error('Bad eventid'));
236
+ return;
237
+ }
238
+ if (self._eventBaseline &&
239
+ (self._baselineCloseRequested || !self._hasActiveSubscription ||
240
+ self._activeSubscriptionVersion !== self._subscriptionVersion ||
241
+ Object.keys(ret.counts).some(name => !Object.prototype.hasOwnProperty.call(self.events, name)))) {
242
+ // Ignore a packet from a cancelled subscription (including
243
+ // one carrying an event that has since been unregistered).
244
+ return;
245
+ }
246
+ if (self._eventBaseline) {
247
+ for (const [name, count] of Object.entries(ret.counts)) {
248
+ self.events[name] = count;
249
+ }
250
+ }
251
+ if (self._eventBaseline && self._baselinePending && self._hasActiveSubscription) {
252
+ self._baselinePending = false;
253
+ // Give callers their own snapshot, never the mutable
254
+ // subscription object.
255
+ self.emit('baseline', Object.freeze({ ...self.events }));
256
+ }
257
+ else {
258
+ ret.events.forEach(function (event) {
259
+ self.emit('post_event', event.name, event.count);
260
+ });
261
+ }
174
262
  loop();
175
263
  };
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
264
  }
182
265
  _changeEvent(callback) {
266
+ if (this._eventBaseline) {
267
+ this._changeEventWithBaseline(callback);
268
+ return;
269
+ }
183
270
  var self = this;
184
271
  const changeVersion = ++self._subscriptionVersion;
185
272
  function subscribe() {
@@ -189,6 +276,7 @@ class FbEventManager extends events_1.default.EventEmitter {
189
276
  // permanently block the main connection queue.
190
277
  if (Object.keys(self.events).length === 0) {
191
278
  self._hasActiveSubscription = false;
279
+ self._baselinePending = false;
192
280
  callback(null);
193
281
  return;
194
282
  }
@@ -201,6 +289,7 @@ class FbEventManager extends events_1.default.EventEmitter {
201
289
  if (err) {
202
290
  if (self._subscriptionVersion === changeVersion) {
203
291
  self._hasActiveSubscription = false;
292
+ self._baselinePending = false;
204
293
  }
205
294
  (0, callback_1.doError)(err, callback);
206
295
  return;
@@ -225,28 +314,121 @@ class FbEventManager extends events_1.default.EventEmitter {
225
314
  subscribe();
226
315
  }
227
316
  }
317
+ _isRetiredEventId(eventId) {
318
+ return eventId <= this._retiredEventIdLimit;
319
+ }
320
+ _changeEventWithBaseline(callback) {
321
+ const cnx = this.db.connection;
322
+ this._subscriptionVersion++;
323
+ this._baselineCallbacks.push(callback);
324
+ if (this._baselineChangeInProgress)
325
+ return;
326
+ this._baselineChangeInProgress = true;
327
+ const finish = (err, ret) => {
328
+ this._baselineChangeInProgress = false;
329
+ if (this._baselineCloseRequested && !err)
330
+ err = new Error('Event Connection is closed.');
331
+ const callbacks = this._baselineCallbacks.splice(0);
332
+ for (const done of callbacks) {
333
+ if (err)
334
+ (0, callback_1.doError)(err, done);
335
+ else
336
+ done(null, ret);
337
+ }
338
+ if (this._baselineCloseRequested) {
339
+ this._baselineCloseRequested = false;
340
+ const closeCallbacks = this._baselineCloseCallbacks.splice(0);
341
+ this.close(closeError => {
342
+ for (const done of closeCallbacks)
343
+ done(closeError);
344
+ });
345
+ }
346
+ };
347
+ const subscribe = () => {
348
+ if (this._baselineCloseRequested) {
349
+ finish(new Error('Event Connection is closed.'));
350
+ return;
351
+ }
352
+ if (Object.keys(this.events).length === 0) {
353
+ this._hasActiveSubscription = false;
354
+ this._baselinePending = false;
355
+ finish(null);
356
+ return;
357
+ }
358
+ // p_event_rid is client-generated. A fresh ID makes a delayed
359
+ // notification from an older generation distinguishable even
360
+ // when both generations contain the same event name.
361
+ if (this._hasQueuedBaseline)
362
+ this.eventid = this.db.eventid++;
363
+ this._hasQueuedBaseline = true;
364
+ const version = this._subscriptionVersion;
365
+ this._activeSubscriptionVersion = version;
366
+ for (const name of Object.keys(this.events))
367
+ this.events[name] = 0;
368
+ this._baselinePending = true;
369
+ this._hasActiveSubscription = true;
370
+ cnx.queEvents(this.events, this.eventid, (err, ret) => {
371
+ if (err) {
372
+ this._hasActiveSubscription = false;
373
+ this._baselinePending = false;
374
+ finish(err);
375
+ }
376
+ else if (version !== this._subscriptionVersion) {
377
+ cycle();
378
+ }
379
+ else {
380
+ finish(null, ret);
381
+ }
382
+ });
383
+ };
384
+ const cycle = () => {
385
+ if (!this._hasActiveSubscription) {
386
+ subscribe();
387
+ return;
388
+ }
389
+ const oldId = this.eventid;
390
+ this._hasActiveSubscription = false;
391
+ this._baselinePending = false;
392
+ this._retiredEventIdLimit = Math.max(this._retiredEventIdLimit, oldId);
393
+ cnx.closeEvents(oldId, (err) => {
394
+ if (err)
395
+ finish(err);
396
+ else
397
+ subscribe();
398
+ });
399
+ };
400
+ cycle();
401
+ }
228
402
  registerEvent(events, callback) {
229
403
  var self = this;
230
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
404
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
231
405
  return self.eventconnection.throwClosed(callback);
232
406
  events.forEach((event) => self.events[event] = self.events[event] || 0);
233
407
  self._changeEvent(callback);
234
408
  }
235
409
  unregisterEvent(events, callback) {
236
410
  var self = this;
237
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
411
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
238
412
  return self.eventconnection.throwClosed(callback);
239
413
  events.forEach(function (event) { delete self.events[event]; });
240
414
  self._changeEvent(callback);
241
415
  }
242
416
  close(callback) {
243
417
  var self = this;
418
+ if (self._eventBaseline && self._baselineChangeInProgress) {
419
+ self._baselineCloseRequested = true;
420
+ if (callback)
421
+ self._baselineCloseCallbacks.push(callback);
422
+ return;
423
+ }
244
424
  if (process.env.FIREBIRD_DEBUG) {
245
425
  console.log('[fb-debug] FbEventManager.close() called, _hasActiveSubscription=%s eventid=%d', self._hasActiveSubscription, self.eventid);
246
426
  }
247
427
  // Prevent the event loop from re-queuing on stale op_event notifications
248
428
  // that may arrive between closeEvents and socket.end()
429
+ self.eventconnection._intentionalClose = true;
249
430
  self.eventconnection.eventcallback = null;
431
+ self._baselinePending = false;
250
432
  // Gracefully close the event socket using a FIN (end()) rather than a RST
251
433
  // (destroy()), then wait for the 'close' event which confirms both sides have
252
434
  // exchanged FINs. This gives Firebird (all versions 3/4/5) time to fully
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
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,32 @@ 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: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
37
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
38
+ on(event: 'error', listener: (error: Error) => void): this;
39
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
40
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
41
+ once(event: 'error', listener: (error: Error) => void): this;
42
+ }
43
+
44
+ export type FbEventManagerCallback = (err: any, manager?: FbEventManager) => void;
45
+
19
46
  /**
20
47
  * Describes a single column in a prepared statement's result set or
21
48
  * parameter list. The properties here are populated by the
@@ -272,7 +299,7 @@ export interface Database {
272
299
  batchStream(query: string, options?: BatchStreamOptions): BatchStream;
273
300
  drop(callback: SimpleCallback): void;
274
301
  escape(value: any): string;
275
- attachEvent(callback: any): this;
302
+ attachEvent(callback: FbEventManagerCallback): this;
276
303
  createTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
277
304
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
278
305
  dropTablespace(name: string, callback?: QueryCallback): Database;
@@ -293,7 +320,7 @@ export interface Database {
293
320
  newStatementAsync(query: string): Promise<Statement>;
294
321
  detachAsync(force?: boolean): Promise<void>;
295
322
  dropAsync(): Promise<void>;
296
- attachEventAsync(): Promise<any>;
323
+ attachEventAsync(): Promise<FbEventManager>;
297
324
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
298
325
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
299
326
  /**
@@ -415,6 +442,8 @@ export interface Options {
415
442
  host?: string;
416
443
  /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
417
444
  eventHost?: string;
445
+ /** Emit the first counter snapshot of each event subscription as 'baseline' instead of 'post_event'. Off by default. */
446
+ eventBaseline?: boolean;
418
447
  port?: number;
419
448
  database?: string;
420
449
  user?: string;
package/src/uri.ts CHANGED
@@ -12,7 +12,7 @@ import type { Options } from './types';
12
12
  */
13
13
  const BOOLEAN_KEYS = new Set([
14
14
  'lowercase_keys', 'blobAsText', 'wireCompression', 'manager',
15
- 'namedPlaceholders', 'enableKeepAlive',
15
+ 'namedPlaceholders', 'enableKeepAlive', 'eventBaseline',
16
16
  ]);
17
17
 
18
18
  /** Option keys coerced to number when they arrive as URI query parameters. */
@@ -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;
@@ -107,20 +134,22 @@ class EventConnection {
107
134
  }
108
135
  xdr.readInt64(); // ignore AST INFO
109
136
  var event_id = xdr.readInt();
110
- // set the new count in global event hash
111
- // Only update events that are still registered; do not
112
- // re-add events that unregisterEvent() has deleted, since
113
- // that would cause subscribe() to re-subscribe for them.
114
- for (var evt in tmp_event) {
115
- if (Object.prototype.hasOwnProperty.call(self.emgr.events, evt)) {
116
- self.emgr.events[evt] = tmp_event[evt];
137
+ // In the default mode, retain the existing parser-side
138
+ // counter update. Baseline mode lets the manager apply
139
+ // counts only after rejecting cancelled subscriptions.
140
+ // Never re-add an event removed by unregisterEvent().
141
+ if (!self.emgr._eventBaseline) {
142
+ for (var evt in tmp_event) {
143
+ if (Object.prototype.hasOwnProperty.call(self.emgr.events, evt)) {
144
+ self.emgr.events[evt] = tmp_event[evt];
145
+ }
117
146
  }
118
147
  }
119
148
  if (self.eventcallback)
120
- self.eventcallback(null, { eventid: event_id, events: lst_event });
149
+ self.eventcallback(null, { eventid: event_id, events: lst_event, counts: tmp_event });
121
150
  break;
122
151
  default:
123
- // Unknown opcode on the event connection – stop processing.
152
+ reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
124
153
  return;
125
154
  }
126
155
  }
@@ -130,7 +159,7 @@ class EventConnection {
130
159
  xdr.pos = 0;
131
160
  self._xdr = xdr;
132
161
  } else {
133
- throw err;
162
+ reportTerminalError(err instanceof Error ? err : new Error(String(err)));
134
163
  }
135
164
  }
136
165
  })
@@ -138,7 +167,6 @@ class EventConnection {
138
167
 
139
168
  throwClosed(callback?: (err: any) => void): this {
140
169
  var err = new Error('Event Connection is closed.');
141
- this.db.emit('error', err);
142
170
  if (callback)
143
171
  callback(err);
144
172
  return this;
@@ -46,7 +46,8 @@
46
46
  // │ │ IDLE (or CLOSING if called from close()) │
47
47
  // │ └──────────────────────────────────────────────────┘
48
48
  // │
49
- // │ emit('post_event', name, count)
49
+ // │ eventBaseline: first op_event → emit('baseline', counts)
50
+ // │ otherwise → emit('post_event', name, count)
50
51
  // └──────────────────────┐
51
52
  // ▼
52
53
  // loop() → SUBSCRIBING (re-subscribe)
@@ -63,6 +64,7 @@
63
64
  // Asynchronous notifications on the AUX (EventConnection) socket
64
65
  // ───────────────────────────────────────────────────────────────
65
66
  // Server → Client : op_event (fired by Firebird POST_EVENT trigger)
67
+ // Error / unexpected close → CLOSED; emit manager 'error' once
66
68
 
67
69
  import Events from 'events';
68
70
  import { doError } from '../callback';
@@ -73,7 +75,19 @@ class FbEventManager extends Events.EventEmitter {
73
75
  events: Record<string, number>;
74
76
  eventid: number;
75
77
  _subscriptionVersion: number;
78
+ _activeSubscriptionVersion: number;
76
79
  _hasActiveSubscription: boolean;
80
+ _baselinePending: boolean;
81
+ _eventBaseline: boolean;
82
+ _baselineChangeInProgress: boolean;
83
+ _baselineCallbacks: Array<(err: any, ret?: any) => void>;
84
+ _hasQueuedBaseline: boolean;
85
+ _retiredEventIdLimit: number;
86
+ _baselineCloseRequested: boolean;
87
+ _baselineCloseCallbacks: Array<(err?: any) => void>;
88
+ _readySettled: boolean;
89
+ _terminalErrorReported: boolean;
90
+ _readyCallback: (err: any, ret?: any) => void;
77
91
 
78
92
  constructor(db: any, eventconnection: any, eventid: number, callback: (err: any, ret?: any) => void) {
79
93
  super();
@@ -84,11 +98,73 @@ class FbEventManager extends Events.EventEmitter {
84
98
  // Guards _hasActiveSubscription against late callbacks from an older
85
99
  // register/unregister cycle after a newer subscription change started.
86
100
  this._subscriptionVersion = 0;
101
+ this._activeSubscriptionVersion = 0;
87
102
  // True when an op_que_events subscription is currently active on the
88
103
  // main connection (so close() and _changeEvent know whether to send
89
104
  // op_cancel_events before tearing down or re-subscribing).
90
105
  this._hasActiveSubscription = false;
91
- this._createEventLoop(callback);
106
+ this._baselinePending = false;
107
+ this._eventBaseline = db.connection.options?.eventBaseline === true;
108
+ this._baselineChangeInProgress = false;
109
+ this._baselineCallbacks = [];
110
+ this._hasQueuedBaseline = false;
111
+ // Highest event ID retired by a baseline reconfiguration. IDs come
112
+ // from db.eventid++ and the current one is always retired before a
113
+ // new one is allocated, so every ID <= this value is stale. A single
114
+ // watermark avoids keeping one entry per reconfiguration forever.
115
+ this._retiredEventIdLimit = 0;
116
+ this._baselineCloseRequested = false;
117
+ this._baselineCloseCallbacks = [];
118
+ this._readySettled = false;
119
+ this._terminalErrorReported = false;
120
+ this._readyCallback = callback;
121
+ this._createEventLoop();
122
+ process.nextTick(() => this._finishReady());
123
+ }
124
+
125
+ on(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
126
+ on(event: 'post_event', listener: (name: string, count: number) => void): this;
127
+ on(event: 'error', listener: (error: Error) => void): this;
128
+ on(event: string | symbol, listener: (...args: any[]) => void): this {
129
+ return super.on(event, listener);
130
+ }
131
+
132
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
133
+ once(event: 'post_event', listener: (name: string, count: number) => void): this;
134
+ once(event: 'error', listener: (error: Error) => void): this;
135
+ once(event: string | symbol, listener: (...args: any[]) => void): this {
136
+ return super.once(event, listener);
137
+ }
138
+
139
+ _finishReady(err?: Error): void {
140
+ if (this._readySettled) return;
141
+ this._readySettled = true;
142
+ if (err) doError(err, this._readyCallback);
143
+ else this._readyCallback(null);
144
+ }
145
+
146
+ _handleAsyncError(err: any): void {
147
+ if (this._terminalErrorReported) return;
148
+ this._terminalErrorReported = true;
149
+ const error = err instanceof Error ? err : new Error(String(err));
150
+ this._hasActiveSubscription = false;
151
+ this._baselinePending = false;
152
+ this._subscriptionVersion++;
153
+ this.eventconnection._isClosed = true;
154
+ this.eventconnection._isOpened = false;
155
+ this.eventconnection.eventcallback = null;
156
+ if (this.eventconnection._socket && !this.eventconnection._socket.destroyed &&
157
+ typeof this.eventconnection._socket.destroy === 'function') {
158
+ this.eventconnection._socket.destroy();
159
+ }
160
+
161
+ if (!this._readySettled) {
162
+ this._finishReady(error);
163
+ } else if (this.listenerCount('error') > 0) {
164
+ this.emit('error', error);
165
+ } else if (this.db.connection && typeof this.db.connection._emitError === 'function') {
166
+ this.db.connection._emitError(error);
167
+ }
92
168
  }
93
169
 
94
170
  /**
@@ -143,7 +219,7 @@ class FbEventManager extends Events.EventEmitter {
143
219
  };
144
220
  }
145
221
 
146
- _createEventLoop(callback: (err: any, ret?: any) => void): void {
222
+ _createEventLoop(): void {
147
223
  var self = this;
148
224
  var cnx = this.db.connection;
149
225
  this.eventconnection.emgr = this;
@@ -161,12 +237,14 @@ class FbEventManager extends Events.EventEmitter {
161
237
  // op_event arriving on the event connection after closeEvents can
162
238
  // trigger queEvents({}) which Firebird never acknowledges,
163
239
  // permanently blocking the main connection queue.
164
- if (!self._hasActiveSubscription || Object.keys(self.events).length === 0) {
240
+ if (!self._hasActiveSubscription || Object.keys(self.events).length === 0 ||
241
+ (self._eventBaseline && self._activeSubscriptionVersion !== self._subscriptionVersion)) {
165
242
  return;
166
243
  }
167
- cnx.queEvents(self.events, self.eventid, function (err: any) {
168
- if (err) {
169
- doError(err, callback);
244
+ const eventId = self.eventid;
245
+ cnx.queEvents(self.events, eventId, function (err: any) {
246
+ if (err && (!self._eventBaseline || !self._isRetiredEventId(eventId))) {
247
+ self._handleAsyncError(err);
170
248
  return;
171
249
  }
172
250
  // subscription renewed, nothing else to do
@@ -174,26 +252,53 @@ class FbEventManager extends Events.EventEmitter {
174
252
  }
175
253
 
176
254
  this.eventconnection.eventcallback = function (err: any, ret?: any) {
177
- if (err || (self.eventid !== ret.eventid)) {
178
- doError(err || new Error('Bad eventid'), callback);
255
+ if (err || !ret) {
256
+ self._handleAsyncError(err || new Error('Missing event packet'));
257
+ return;
258
+ }
259
+ if (self.eventid !== ret.eventid) {
260
+ if (self._eventBaseline && self._isRetiredEventId(ret.eventid)) return;
261
+ self._handleAsyncError(new Error('Bad eventid'));
179
262
  return;
180
263
  }
181
264
 
182
- ret.events.forEach(function (event: { name: string; count: number }) {
183
- self.emit('post_event', event.name, event.count);
184
- });
265
+ if (self._eventBaseline &&
266
+ (self._baselineCloseRequested || !self._hasActiveSubscription ||
267
+ self._activeSubscriptionVersion !== self._subscriptionVersion ||
268
+ Object.keys(ret.counts).some(name =>
269
+ !Object.prototype.hasOwnProperty.call(self.events, name)))) {
270
+ // Ignore a packet from a cancelled subscription (including
271
+ // one carrying an event that has since been unregistered).
272
+ return;
273
+ }
274
+
275
+ if (self._eventBaseline) {
276
+ for (const [name, count] of Object.entries(ret.counts as Record<string, number>)) {
277
+ self.events[name] = count;
278
+ }
279
+ }
280
+
281
+ if (self._eventBaseline && self._baselinePending && self._hasActiveSubscription) {
282
+ self._baselinePending = false;
283
+ // Give callers their own snapshot, never the mutable
284
+ // subscription object.
285
+ self.emit('baseline', Object.freeze({ ...self.events }));
286
+ } else {
287
+ ret.events.forEach(function (event: { name: string; count: number }) {
288
+ self.emit('post_event', event.name, event.count);
289
+ });
290
+ }
185
291
 
186
292
  loop();
187
293
  };
188
294
 
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
295
  }
195
296
 
196
297
  _changeEvent(callback: (err: any, ret?: any) => void): void {
298
+ if (this._eventBaseline) {
299
+ this._changeEventWithBaseline(callback);
300
+ return;
301
+ }
197
302
  var self = this;
198
303
  const changeVersion = ++self._subscriptionVersion;
199
304
 
@@ -204,6 +309,7 @@ class FbEventManager extends Events.EventEmitter {
204
309
  // permanently block the main connection queue.
205
310
  if (Object.keys(self.events).length === 0) {
206
311
  self._hasActiveSubscription = false;
312
+ self._baselinePending = false;
207
313
  callback(null);
208
314
  return;
209
315
  }
@@ -217,6 +323,7 @@ class FbEventManager extends Events.EventEmitter {
217
323
  if (err) {
218
324
  if (self._subscriptionVersion === changeVersion) {
219
325
  self._hasActiveSubscription = false;
326
+ self._baselinePending = false;
220
327
  }
221
328
  doError(err, callback);
222
329
  return;
@@ -242,10 +349,91 @@ class FbEventManager extends Events.EventEmitter {
242
349
  }
243
350
  }
244
351
 
352
+ _isRetiredEventId(eventId: number): boolean {
353
+ return eventId <= this._retiredEventIdLimit;
354
+ }
355
+
356
+ _changeEventWithBaseline(callback: (err: any, ret?: any) => void): void {
357
+ const cnx = this.db.connection;
358
+ this._subscriptionVersion++;
359
+ this._baselineCallbacks.push(callback);
360
+ if (this._baselineChangeInProgress) return;
361
+ this._baselineChangeInProgress = true;
362
+
363
+ const finish = (err: any, ret?: any) => {
364
+ this._baselineChangeInProgress = false;
365
+ if (this._baselineCloseRequested && !err) err = new Error('Event Connection is closed.');
366
+ const callbacks = this._baselineCallbacks.splice(0);
367
+ for (const done of callbacks) {
368
+ if (err) doError(err, done);
369
+ else done(null, ret);
370
+ }
371
+ if (this._baselineCloseRequested) {
372
+ this._baselineCloseRequested = false;
373
+ const closeCallbacks = this._baselineCloseCallbacks.splice(0);
374
+ this.close(closeError => {
375
+ for (const done of closeCallbacks) done(closeError);
376
+ });
377
+ }
378
+ };
379
+
380
+ const subscribe = () => {
381
+ if (this._baselineCloseRequested) {
382
+ finish(new Error('Event Connection is closed.'));
383
+ return;
384
+ }
385
+ if (Object.keys(this.events).length === 0) {
386
+ this._hasActiveSubscription = false;
387
+ this._baselinePending = false;
388
+ finish(null);
389
+ return;
390
+ }
391
+
392
+ // p_event_rid is client-generated. A fresh ID makes a delayed
393
+ // notification from an older generation distinguishable even
394
+ // when both generations contain the same event name.
395
+ if (this._hasQueuedBaseline) this.eventid = this.db.eventid++;
396
+ this._hasQueuedBaseline = true;
397
+ const version = this._subscriptionVersion;
398
+ this._activeSubscriptionVersion = version;
399
+ for (const name of Object.keys(this.events)) this.events[name] = 0;
400
+ this._baselinePending = true;
401
+ this._hasActiveSubscription = true;
402
+ cnx.queEvents(this.events, this.eventid, (err: any, ret?: any) => {
403
+ if (err) {
404
+ this._hasActiveSubscription = false;
405
+ this._baselinePending = false;
406
+ finish(err);
407
+ } else if (version !== this._subscriptionVersion) {
408
+ cycle();
409
+ } else {
410
+ finish(null, ret);
411
+ }
412
+ });
413
+ };
414
+
415
+ const cycle = () => {
416
+ if (!this._hasActiveSubscription) {
417
+ subscribe();
418
+ return;
419
+ }
420
+ const oldId = this.eventid;
421
+ this._hasActiveSubscription = false;
422
+ this._baselinePending = false;
423
+ this._retiredEventIdLimit = Math.max(this._retiredEventIdLimit, oldId);
424
+ cnx.closeEvents(oldId, (err: any) => {
425
+ if (err) finish(err);
426
+ else subscribe();
427
+ });
428
+ };
429
+
430
+ cycle();
431
+ }
432
+
245
433
  registerEvent(events: string[], callback: (err: any, ret?: any) => void): any {
246
434
  var self = this;
247
435
 
248
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
436
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
249
437
  return self.eventconnection.throwClosed(callback);
250
438
 
251
439
  events.forEach((event) => self.events[event] = self.events[event] || 0);
@@ -255,7 +443,7 @@ class FbEventManager extends Events.EventEmitter {
255
443
  unregisterEvent(events: string[], callback: (err: any, ret?: any) => void): any {
256
444
  var self = this;
257
445
 
258
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
446
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
259
447
  return self.eventconnection.throwClosed(callback);
260
448
 
261
449
  events.forEach(function (event) { delete self.events[event] });
@@ -265,13 +453,21 @@ class FbEventManager extends Events.EventEmitter {
265
453
  close(callback?: (err?: any) => void): void {
266
454
  var self = this;
267
455
 
456
+ if (self._eventBaseline && self._baselineChangeInProgress) {
457
+ self._baselineCloseRequested = true;
458
+ if (callback) self._baselineCloseCallbacks.push(callback);
459
+ return;
460
+ }
461
+
268
462
  if (process.env.FIREBIRD_DEBUG) {
269
463
  console.log('[fb-debug] FbEventManager.close() called, _hasActiveSubscription=%s eventid=%d', self._hasActiveSubscription, self.eventid);
270
464
  }
271
465
 
272
466
  // Prevent the event loop from re-queuing on stale op_event notifications
273
467
  // that may arrive between closeEvents and socket.end()
468
+ self.eventconnection._intentionalClose = true;
274
469
  self.eventconnection.eventcallback = null;
470
+ self._baselinePending = false;
275
471
 
276
472
  // Gracefully close the event socket using a FIN (end()) rather than a RST
277
473
  // (destroy()), then wait for the 'close' event which confirms both sides have