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 +23 -5
- package/lib/types.d.ts +4 -0
- package/lib/uri.js +1 -1
- package/lib/wire/eventConnection.js +10 -8
- package/lib/wire/fbEventManager.d.ts +13 -0
- package/lib/wire/fbEventManager.js +153 -11
- package/package.json +1 -1
- package/src/types.ts +4 -0
- package/src/uri.ts +1 -1
- package/src/wire/eventConnection.ts +10 -8
- package/src/wire/fbEventManager.ts +161 -11
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.
|
|
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
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self.emgr.events
|
|
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('
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
212
|
-
self._handleAsyncError(err || new Error('
|
|
228
|
+
if (err || !ret) {
|
|
229
|
+
self._handleAsyncError(err || new Error('Missing event packet'));
|
|
213
230
|
return;
|
|
214
231
|
}
|
|
215
|
-
ret.
|
|
216
|
-
self.
|
|
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
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
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
self.emgr.events
|
|
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('
|
|
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
|
-
|
|
218
|
-
|
|
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
|
|
228
|
-
self._handleAsyncError(err || new Error('
|
|
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
|
-
|
|
233
|
-
self.
|
|
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
|