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 +43 -8
- package/lib/types.d.ts +28 -2
- package/lib/uri.js +1 -1
- package/lib/wire/database.js +11 -4
- package/lib/wire/eventConnection.d.ts +2 -0
- package/lib/wire/eventConnection.js +46 -18
- package/lib/wire/fbEventManager.d.ts +23 -1
- package/lib/wire/fbEventManager.js +201 -19
- package/package.json +1 -1
- package/src/types.ts +31 -2
- package/src/uri.ts +1 -1
- package/src/wire/database.ts +10 -4
- package/src/wire/eventConnection.ts +45 -17
- package/src/wire/fbEventManager.ts +215 -19
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.
|
|
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
|
|
1483
|
-
|
|
1484
|
-
|
|
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:
|
|
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<
|
|
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([
|
package/lib/wire/database.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
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
|
+
}
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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('
|
|
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.
|
|
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(
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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 ||
|
|
168
|
-
|
|
228
|
+
if (err || !ret) {
|
|
229
|
+
self._handleAsyncError(err || new Error('Missing event packet'));
|
|
169
230
|
return;
|
|
170
231
|
}
|
|
171
|
-
ret.
|
|
172
|
-
self.
|
|
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
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:
|
|
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<
|
|
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. */
|
package/src/wire/database.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
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
|
+
}
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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(
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 ||
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
self.
|
|
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
|