node-firebird 2.17.1 → 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;
@@ -1410,16 +1424,20 @@ Firebird.attach(options, function (err, db) {
1410
1424
  console.error('event connection failed:', err);
1411
1425
  });
1412
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
+
1413
1435
  // 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
1414
1436
  // PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
1415
1437
  evtmgr.registerEvent(['MY_EVENT'], function (err) {
1416
1438
  if (err) throw err;
1417
1439
 
1418
- // 3. Listen for POST_EVENT notifications
1419
- evtmgr.on('post_event', function (name, count) {
1420
- // name === event name string (e.g. 'MY_EVENT')
1421
- // count === cumulative trigger count since last notification
1422
- });
1440
+ // 3. Subscription acknowledged. Notifications may already have arrived.
1423
1441
  });
1424
1442
 
1425
1443
  // 4. Unsubscribe from one or more events. Passing all currently registered names cancels
package/lib/types.d.ts CHANGED
@@ -22,8 +22,10 @@ export interface FbEventManager extends EventEmitter {
22
22
  unregisterEvent(events: string[], callback: SimpleCallback): void;
23
23
  close(callback?: SimpleCallback): void;
24
24
  getState(): FbEventState;
25
+ on(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
25
26
  on(event: 'post_event', listener: (name: string, count: number) => void): this;
26
27
  on(event: 'error', listener: (error: Error) => void): this;
28
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
27
29
  once(event: 'post_event', listener: (name: string, count: number) => void): this;
28
30
  once(event: 'error', listener: (error: Error) => void): this;
29
31
  }
@@ -376,6 +378,8 @@ export interface Options {
376
378
  host?: string;
377
379
  /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
378
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;
379
383
  port?: number;
380
384
  database?: string;
381
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([
@@ -119,17 +119,19 @@ class EventConnection {
119
119
  }
120
120
  xdr.readInt64(); // ignore AST INFO
121
121
  var event_id = xdr.readInt();
122
- // set the new count in global event hash
123
- // Only update events that are still registered; do not
124
- // re-add events that unregisterEvent() has deleted, since
125
- // that would cause subscribe() to re-subscribe for them.
126
- for (var evt in tmp_event) {
127
- if (Object.prototype.hasOwnProperty.call(self.emgr.events, evt)) {
128
- 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
+ }
129
131
  }
130
132
  }
131
133
  if (self.eventcallback)
132
- self.eventcallback(null, { eventid: event_id, events: lst_event });
134
+ self.eventcallback(null, { eventid: event_id, events: lst_event, counts: tmp_event });
133
135
  break;
134
136
  default:
135
137
  reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
@@ -5,13 +5,24 @@ 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>;
9
18
  _readySettled: boolean;
10
19
  _terminalErrorReported: boolean;
11
20
  _readyCallback: (err: any, ret?: any) => void;
12
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;
13
23
  on(event: 'post_event', listener: (name: string, count: number) => void): this;
14
24
  on(event: 'error', listener: (error: Error) => void): this;
25
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
15
26
  once(event: 'post_event', listener: (name: string, count: number) => void): this;
16
27
  once(event: 'error', listener: (error: Error) => void): this;
17
28
  _finishReady(err?: Error): void;
@@ -47,6 +58,8 @@ declare class FbEventManager extends Events.EventEmitter {
47
58
  };
48
59
  _createEventLoop(): void;
49
60
  _changeEvent(callback: (err: any, ret?: any) => void): void;
61
+ _isRetiredEventId(eventId: number): boolean;
62
+ _changeEventWithBaseline(callback: (err: any, ret?: any) => void): void;
50
63
  registerEvent(events: string[], callback: (err: any, ret?: any) => void): any;
51
64
  unregisterEvent(events: string[], callback: (err: any, ret?: any) => void): any;
52
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)
@@ -80,10 +81,23 @@ class FbEventManager extends events_1.default.EventEmitter {
80
81
  // Guards _hasActiveSubscription against late callbacks from an older
81
82
  // register/unregister cycle after a newer subscription change started.
82
83
  this._subscriptionVersion = 0;
84
+ this._activeSubscriptionVersion = 0;
83
85
  // True when an op_que_events subscription is currently active on the
84
86
  // main connection (so close() and _changeEvent know whether to send
85
87
  // op_cancel_events before tearing down or re-subscribing).
86
88
  this._hasActiveSubscription = false;
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 = [];
87
101
  this._readySettled = false;
88
102
  this._terminalErrorReported = false;
89
103
  this._readyCallback = callback;
@@ -111,6 +125,7 @@ class FbEventManager extends events_1.default.EventEmitter {
111
125
  this._terminalErrorReported = true;
112
126
  const error = err instanceof Error ? err : new Error(String(err));
113
127
  this._hasActiveSubscription = false;
128
+ this._baselinePending = false;
114
129
  this._subscriptionVersion++;
115
130
  this.eventconnection._isClosed = true;
116
131
  this.eventconnection._isOpened = false;
@@ -196,11 +211,13 @@ class FbEventManager extends events_1.default.EventEmitter {
196
211
  // op_event arriving on the event connection after closeEvents can
197
212
  // trigger queEvents({}) which Firebird never acknowledges,
198
213
  // permanently blocking the main connection queue.
199
- 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)) {
200
216
  return;
201
217
  }
202
- cnx.queEvents(self.events, self.eventid, function (err) {
203
- if (err) {
218
+ const eventId = self.eventid;
219
+ cnx.queEvents(self.events, eventId, function (err) {
220
+ if (err && (!self._eventBaseline || !self._isRetiredEventId(eventId))) {
204
221
  self._handleAsyncError(err);
205
222
  return;
206
223
  }
@@ -208,17 +225,48 @@ class FbEventManager extends events_1.default.EventEmitter {
208
225
  });
209
226
  }
210
227
  this.eventconnection.eventcallback = function (err, ret) {
211
- if (err || !ret || (self.eventid !== ret.eventid)) {
212
- self._handleAsyncError(err || new Error('Bad eventid'));
228
+ if (err || !ret) {
229
+ self._handleAsyncError(err || new Error('Missing event packet'));
213
230
  return;
214
231
  }
215
- ret.events.forEach(function (event) {
216
- self.emit('post_event', event.name, event.count);
217
- });
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
+ }
218
262
  loop();
219
263
  };
220
264
  }
221
265
  _changeEvent(callback) {
266
+ if (this._eventBaseline) {
267
+ this._changeEventWithBaseline(callback);
268
+ return;
269
+ }
222
270
  var self = this;
223
271
  const changeVersion = ++self._subscriptionVersion;
224
272
  function subscribe() {
@@ -228,6 +276,7 @@ class FbEventManager extends events_1.default.EventEmitter {
228
276
  // permanently block the main connection queue.
229
277
  if (Object.keys(self.events).length === 0) {
230
278
  self._hasActiveSubscription = false;
279
+ self._baselinePending = false;
231
280
  callback(null);
232
281
  return;
233
282
  }
@@ -240,6 +289,7 @@ class FbEventManager extends events_1.default.EventEmitter {
240
289
  if (err) {
241
290
  if (self._subscriptionVersion === changeVersion) {
242
291
  self._hasActiveSubscription = false;
292
+ self._baselinePending = false;
243
293
  }
244
294
  (0, callback_1.doError)(err, callback);
245
295
  return;
@@ -264,22 +314,113 @@ class FbEventManager extends events_1.default.EventEmitter {
264
314
  subscribe();
265
315
  }
266
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
+ }
267
402
  registerEvent(events, callback) {
268
403
  var self = this;
269
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
404
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
270
405
  return self.eventconnection.throwClosed(callback);
271
406
  events.forEach((event) => self.events[event] = self.events[event] || 0);
272
407
  self._changeEvent(callback);
273
408
  }
274
409
  unregisterEvent(events, callback) {
275
410
  var self = this;
276
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
411
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
277
412
  return self.eventconnection.throwClosed(callback);
278
413
  events.forEach(function (event) { delete self.events[event]; });
279
414
  self._changeEvent(callback);
280
415
  }
281
416
  close(callback) {
282
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
+ }
283
424
  if (process.env.FIREBIRD_DEBUG) {
284
425
  console.log('[fb-debug] FbEventManager.close() called, _hasActiveSubscription=%s eventid=%d', self._hasActiveSubscription, self.eventid);
285
426
  }
@@ -287,6 +428,7 @@ class FbEventManager extends events_1.default.EventEmitter {
287
428
  // that may arrive between closeEvents and socket.end()
288
429
  self.eventconnection._intentionalClose = true;
289
430
  self.eventconnection.eventcallback = null;
431
+ self._baselinePending = false;
290
432
  // Gracefully close the event socket using a FIN (end()) rather than a RST
291
433
  // (destroy()), then wait for the 'close' event which confirms both sides have
292
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.1",
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
@@ -33,8 +33,10 @@ export interface FbEventManager extends EventEmitter {
33
33
  unregisterEvent(events: string[], callback: SimpleCallback): void;
34
34
  close(callback?: SimpleCallback): void;
35
35
  getState(): FbEventState;
36
+ on(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
36
37
  on(event: 'post_event', listener: (name: string, count: number) => void): this;
37
38
  on(event: 'error', listener: (error: Error) => void): this;
39
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
38
40
  once(event: 'post_event', listener: (name: string, count: number) => void): this;
39
41
  once(event: 'error', listener: (error: Error) => void): this;
40
42
  }
@@ -440,6 +442,8 @@ export interface Options {
440
442
  host?: string;
441
443
  /** Override the server-advertised auxiliary event host (for NAT, tunnels and load balancers). */
442
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;
443
447
  port?: number;
444
448
  database?: string;
445
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. */
@@ -134,17 +134,19 @@ class EventConnection {
134
134
  }
135
135
  xdr.readInt64(); // ignore AST INFO
136
136
  var event_id = xdr.readInt();
137
- // set the new count in global event hash
138
- // Only update events that are still registered; do not
139
- // re-add events that unregisterEvent() has deleted, since
140
- // that would cause subscribe() to re-subscribe for them.
141
- for (var evt in tmp_event) {
142
- if (Object.prototype.hasOwnProperty.call(self.emgr.events, evt)) {
143
- 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
+ }
144
146
  }
145
147
  }
146
148
  if (self.eventcallback)
147
- self.eventcallback(null, { eventid: event_id, events: lst_event });
149
+ self.eventcallback(null, { eventid: event_id, events: lst_event, counts: tmp_event });
148
150
  break;
149
151
  default:
150
152
  reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
@@ -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)
@@ -74,7 +75,16 @@ class FbEventManager extends Events.EventEmitter {
74
75
  events: Record<string, number>;
75
76
  eventid: number;
76
77
  _subscriptionVersion: number;
78
+ _activeSubscriptionVersion: number;
77
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>;
78
88
  _readySettled: boolean;
79
89
  _terminalErrorReported: boolean;
80
90
  _readyCallback: (err: any, ret?: any) => void;
@@ -88,10 +98,23 @@ class FbEventManager extends Events.EventEmitter {
88
98
  // Guards _hasActiveSubscription against late callbacks from an older
89
99
  // register/unregister cycle after a newer subscription change started.
90
100
  this._subscriptionVersion = 0;
101
+ this._activeSubscriptionVersion = 0;
91
102
  // True when an op_que_events subscription is currently active on the
92
103
  // main connection (so close() and _changeEvent know whether to send
93
104
  // op_cancel_events before tearing down or re-subscribing).
94
105
  this._hasActiveSubscription = false;
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 = [];
95
118
  this._readySettled = false;
96
119
  this._terminalErrorReported = false;
97
120
  this._readyCallback = callback;
@@ -99,12 +122,14 @@ class FbEventManager extends Events.EventEmitter {
99
122
  process.nextTick(() => this._finishReady());
100
123
  }
101
124
 
125
+ on(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
102
126
  on(event: 'post_event', listener: (name: string, count: number) => void): this;
103
127
  on(event: 'error', listener: (error: Error) => void): this;
104
128
  on(event: string | symbol, listener: (...args: any[]) => void): this {
105
129
  return super.on(event, listener);
106
130
  }
107
131
 
132
+ once(event: 'baseline', listener: (counts: Readonly<Record<string, number>>) => void): this;
108
133
  once(event: 'post_event', listener: (name: string, count: number) => void): this;
109
134
  once(event: 'error', listener: (error: Error) => void): this;
110
135
  once(event: string | symbol, listener: (...args: any[]) => void): this {
@@ -123,6 +148,7 @@ class FbEventManager extends Events.EventEmitter {
123
148
  this._terminalErrorReported = true;
124
149
  const error = err instanceof Error ? err : new Error(String(err));
125
150
  this._hasActiveSubscription = false;
151
+ this._baselinePending = false;
126
152
  this._subscriptionVersion++;
127
153
  this.eventconnection._isClosed = true;
128
154
  this.eventconnection._isOpened = false;
@@ -211,11 +237,13 @@ class FbEventManager extends Events.EventEmitter {
211
237
  // op_event arriving on the event connection after closeEvents can
212
238
  // trigger queEvents({}) which Firebird never acknowledges,
213
239
  // permanently blocking the main connection queue.
214
- 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)) {
215
242
  return;
216
243
  }
217
- cnx.queEvents(self.events, self.eventid, function (err: any) {
218
- if (err) {
244
+ const eventId = self.eventid;
245
+ cnx.queEvents(self.events, eventId, function (err: any) {
246
+ if (err && (!self._eventBaseline || !self._isRetiredEventId(eventId))) {
219
247
  self._handleAsyncError(err);
220
248
  return;
221
249
  }
@@ -224,14 +252,42 @@ class FbEventManager extends Events.EventEmitter {
224
252
  }
225
253
 
226
254
  this.eventconnection.eventcallback = function (err: any, ret?: any) {
227
- if (err || !ret || (self.eventid !== ret.eventid)) {
228
- self._handleAsyncError(err || new Error('Bad eventid'));
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'));
229
262
  return;
230
263
  }
231
264
 
232
- ret.events.forEach(function (event: { name: string; count: number }) {
233
- self.emit('post_event', event.name, event.count);
234
- });
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
+ }
235
291
 
236
292
  loop();
237
293
  };
@@ -239,6 +295,10 @@ class FbEventManager extends Events.EventEmitter {
239
295
  }
240
296
 
241
297
  _changeEvent(callback: (err: any, ret?: any) => void): void {
298
+ if (this._eventBaseline) {
299
+ this._changeEventWithBaseline(callback);
300
+ return;
301
+ }
242
302
  var self = this;
243
303
  const changeVersion = ++self._subscriptionVersion;
244
304
 
@@ -249,6 +309,7 @@ class FbEventManager extends Events.EventEmitter {
249
309
  // permanently block the main connection queue.
250
310
  if (Object.keys(self.events).length === 0) {
251
311
  self._hasActiveSubscription = false;
312
+ self._baselinePending = false;
252
313
  callback(null);
253
314
  return;
254
315
  }
@@ -262,6 +323,7 @@ class FbEventManager extends Events.EventEmitter {
262
323
  if (err) {
263
324
  if (self._subscriptionVersion === changeVersion) {
264
325
  self._hasActiveSubscription = false;
326
+ self._baselinePending = false;
265
327
  }
266
328
  doError(err, callback);
267
329
  return;
@@ -287,10 +349,91 @@ class FbEventManager extends Events.EventEmitter {
287
349
  }
288
350
  }
289
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
+
290
433
  registerEvent(events: string[], callback: (err: any, ret?: any) => void): any {
291
434
  var self = this;
292
435
 
293
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
436
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
294
437
  return self.eventconnection.throwClosed(callback);
295
438
 
296
439
  events.forEach((event) => self.events[event] = self.events[event] || 0);
@@ -300,7 +443,7 @@ class FbEventManager extends Events.EventEmitter {
300
443
  unregisterEvent(events: string[], callback: (err: any, ret?: any) => void): any {
301
444
  var self = this;
302
445
 
303
- if (self.db.connection._isClosed || self.eventconnection._isClosed)
446
+ if (self.db.connection._isClosed || self.eventconnection._isClosed || self._baselineCloseRequested)
304
447
  return self.eventconnection.throwClosed(callback);
305
448
 
306
449
  events.forEach(function (event) { delete self.events[event] });
@@ -310,6 +453,12 @@ class FbEventManager extends Events.EventEmitter {
310
453
  close(callback?: (err?: any) => void): void {
311
454
  var self = this;
312
455
 
456
+ if (self._eventBaseline && self._baselineChangeInProgress) {
457
+ self._baselineCloseRequested = true;
458
+ if (callback) self._baselineCloseCallbacks.push(callback);
459
+ return;
460
+ }
461
+
313
462
  if (process.env.FIREBIRD_DEBUG) {
314
463
  console.log('[fb-debug] FbEventManager.close() called, _hasActiveSubscription=%s eventid=%d', self._hasActiveSubscription, self.eventid);
315
464
  }
@@ -318,6 +467,7 @@ class FbEventManager extends Events.EventEmitter {
318
467
  // that may arrive between closeEvents and socket.end()
319
468
  self.eventconnection._intentionalClose = true;
320
469
  self.eventconnection.eventcallback = null;
470
+ self._baselinePending = false;
321
471
 
322
472
  // Gracefully close the event socket using a FIN (end()) rather than a RST
323
473
  // (destroy()), then wait for the 'close' event which confirms both sides have