node-firebird 2.17.0 → 2.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/lib/types.d.ts +24 -2
- package/lib/wire/database.js +11 -4
- package/lib/wire/eventConnection.d.ts +2 -0
- package/lib/wire/eventConnection.js +36 -10
- package/lib/wire/fbEventManager.d.ts +10 -1
- package/lib/wire/fbEventManager.js +50 -10
- package/package.json +1 -1
- package/src/types.ts +27 -2
- package/src/wire/database.ts +10 -4
- package/src/wire/eventConnection.ts +35 -9
- package/src/wire/fbEventManager.ts +56 -10
package/README.md
CHANGED
|
@@ -1404,6 +1404,12 @@ Firebird.attach(options, function (err, db) {
|
|
|
1404
1404
|
db.attachEvent(function (err, evtmgr) {
|
|
1405
1405
|
if (err) throw err;
|
|
1406
1406
|
|
|
1407
|
+
// Auxiliary socket/protocol failures after attachment are reported here.
|
|
1408
|
+
// Install this listener before registering events.
|
|
1409
|
+
evtmgr.on('error', function (err) {
|
|
1410
|
+
console.error('event connection failed:', err);
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1407
1413
|
// 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
|
|
1408
1414
|
// PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
|
|
1409
1415
|
evtmgr.registerEvent(['MY_EVENT'], function (err) {
|
|
@@ -1479,9 +1485,20 @@ db.attachEvent(function (err, evtmgr) {
|
|
|
1479
1485
|
});
|
|
1480
1486
|
```
|
|
1481
1487
|
|
|
1482
|
-
Errors on the aux socket *after* it connects are
|
|
1483
|
-
|
|
1484
|
-
|
|
1488
|
+
Errors on the aux socket *after* it connects are delivered through the manager's `error`
|
|
1489
|
+
event. Install the listener before calling `registerEvent()`:
|
|
1490
|
+
|
|
1491
|
+
```js
|
|
1492
|
+
evtmgr.on('error', function (err) {
|
|
1493
|
+
// The manager is now CLOSED. Reattach explicitly if the application wants
|
|
1494
|
+
// to resume event delivery; the driver does not reconnect automatically.
|
|
1495
|
+
console.error('event connection failed:', err);
|
|
1496
|
+
});
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
When no manager error listener is installed, the driver forwards the failure to guarded
|
|
1500
|
+
database `error` listeners when present. It never emits an unhandled EventEmitter `error`.
|
|
1501
|
+
The primary database attachment remains available for ordinary queries.
|
|
1485
1502
|
|
|
1486
1503
|
### Escaping Query values
|
|
1487
1504
|
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Readable, Writable } from 'stream';
|
|
2
|
+
import type { EventEmitter } from 'events';
|
|
2
3
|
import type { SqlTag } from './sql-template';
|
|
3
4
|
export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
|
|
4
5
|
export type DatabaseCallback = (err: any, db: Database) => void;
|
|
@@ -6,6 +7,27 @@ export type TransactionCallback = (err: any, transaction: Transaction) => void;
|
|
|
6
7
|
export type QueryCallback = (err: any, result: any[]) => void;
|
|
7
8
|
export type SimpleCallback = (err: any) => void;
|
|
8
9
|
export type SequentialCallback = (row: any, index: number, next?: (err?: any) => void) => void | Promise<void>;
|
|
10
|
+
export interface FbEventState {
|
|
11
|
+
state: 'IDLE' | 'SUBSCRIBED' | 'CLOSED';
|
|
12
|
+
hasActiveSubscription: boolean;
|
|
13
|
+
registeredEvents: Record<string, number>;
|
|
14
|
+
eventId: number;
|
|
15
|
+
isEventConnectionOpen: boolean;
|
|
16
|
+
isDatabaseConnectionClosed: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface FbEventManager extends EventEmitter {
|
|
19
|
+
readonly eventid: number;
|
|
20
|
+
readonly events: Record<string, number>;
|
|
21
|
+
registerEvent(events: string[], callback: SimpleCallback): void;
|
|
22
|
+
unregisterEvent(events: string[], callback: SimpleCallback): void;
|
|
23
|
+
close(callback?: SimpleCallback): void;
|
|
24
|
+
getState(): FbEventState;
|
|
25
|
+
on(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
26
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
27
|
+
once(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
28
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
29
|
+
}
|
|
30
|
+
export type FbEventManagerCallback = (err: any, manager?: FbEventManager) => void;
|
|
9
31
|
/**
|
|
10
32
|
* Describes a single column in a prepared statement's result set or
|
|
11
33
|
* parameter list. The properties here are populated by the
|
|
@@ -250,7 +272,7 @@ export interface Database {
|
|
|
250
272
|
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
251
273
|
drop(callback: SimpleCallback): void;
|
|
252
274
|
escape(value: any): string;
|
|
253
|
-
attachEvent(callback:
|
|
275
|
+
attachEvent(callback: FbEventManagerCallback): this;
|
|
254
276
|
createTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
|
|
255
277
|
alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
|
|
256
278
|
dropTablespace(name: string, callback?: QueryCallback): Database;
|
|
@@ -271,7 +293,7 @@ export interface Database {
|
|
|
271
293
|
newStatementAsync(query: string): Promise<Statement>;
|
|
272
294
|
detachAsync(force?: boolean): Promise<void>;
|
|
273
295
|
dropAsync(): Promise<void>;
|
|
274
|
-
attachEventAsync(): Promise<
|
|
296
|
+
attachEventAsync(): Promise<FbEventManager>;
|
|
275
297
|
/** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
|
|
276
298
|
withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
|
|
277
299
|
/**
|
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;
|
|
@@ -105,7 +132,7 @@ class EventConnection {
|
|
|
105
132
|
self.eventcallback(null, { eventid: event_id, events: lst_event });
|
|
106
133
|
break;
|
|
107
134
|
default:
|
|
108
|
-
|
|
135
|
+
reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
|
|
109
136
|
return;
|
|
110
137
|
}
|
|
111
138
|
}
|
|
@@ -117,14 +144,13 @@ class EventConnection {
|
|
|
117
144
|
self._xdr = xdr;
|
|
118
145
|
}
|
|
119
146
|
else {
|
|
120
|
-
|
|
147
|
+
reportTerminalError(err instanceof Error ? err : new Error(String(err)));
|
|
121
148
|
}
|
|
122
149
|
}
|
|
123
150
|
});
|
|
124
151
|
}
|
|
125
152
|
throwClosed(callback) {
|
|
126
153
|
var err = new Error('Event Connection is closed.');
|
|
127
|
-
this.db.emit('error', err);
|
|
128
154
|
if (callback)
|
|
129
155
|
callback(err);
|
|
130
156
|
return this;
|
|
@@ -6,7 +6,16 @@ declare class FbEventManager extends Events.EventEmitter {
|
|
|
6
6
|
eventid: number;
|
|
7
7
|
_subscriptionVersion: number;
|
|
8
8
|
_hasActiveSubscription: boolean;
|
|
9
|
+
_readySettled: boolean;
|
|
10
|
+
_terminalErrorReported: boolean;
|
|
11
|
+
_readyCallback: (err: any, ret?: any) => void;
|
|
9
12
|
constructor(db: any, eventconnection: any, eventid: number, callback: (err: any, ret?: any) => void);
|
|
13
|
+
on(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
14
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
15
|
+
once(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
16
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
17
|
+
_finishReady(err?: Error): void;
|
|
18
|
+
_handleAsyncError(err: any): void;
|
|
10
19
|
/**
|
|
11
20
|
* Returns a snapshot of the current state for debugging.
|
|
12
21
|
* Useful for tracing the state machine during development.
|
|
@@ -36,7 +45,7 @@ declare class FbEventManager extends Events.EventEmitter {
|
|
|
36
45
|
isEventConnectionOpen: boolean;
|
|
37
46
|
isDatabaseConnectionClosed: boolean;
|
|
38
47
|
};
|
|
39
|
-
_createEventLoop(
|
|
48
|
+
_createEventLoop(): void;
|
|
40
49
|
_changeEvent(callback: (err: any, ret?: any) => void): void;
|
|
41
50
|
registerEvent(events: string[], callback: (err: any, ret?: any) => void): any;
|
|
42
51
|
unregisterEvent(events: string[], callback: (err: any, ret?: any) => void): any;
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
// Asynchronous notifications on the AUX (EventConnection) socket
|
|
65
65
|
// ───────────────────────────────────────────────────────────────
|
|
66
66
|
// Server → Client : op_event (fired by Firebird POST_EVENT trigger)
|
|
67
|
+
// Error / unexpected close → CLOSED; emit manager 'error' once
|
|
67
68
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
68
69
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
69
70
|
};
|
|
@@ -83,7 +84,50 @@ class FbEventManager extends events_1.default.EventEmitter {
|
|
|
83
84
|
// main connection (so close() and _changeEvent know whether to send
|
|
84
85
|
// op_cancel_events before tearing down or re-subscribing).
|
|
85
86
|
this._hasActiveSubscription = false;
|
|
86
|
-
this.
|
|
87
|
+
this._readySettled = false;
|
|
88
|
+
this._terminalErrorReported = false;
|
|
89
|
+
this._readyCallback = callback;
|
|
90
|
+
this._createEventLoop();
|
|
91
|
+
process.nextTick(() => this._finishReady());
|
|
92
|
+
}
|
|
93
|
+
on(event, listener) {
|
|
94
|
+
return super.on(event, listener);
|
|
95
|
+
}
|
|
96
|
+
once(event, listener) {
|
|
97
|
+
return super.once(event, listener);
|
|
98
|
+
}
|
|
99
|
+
_finishReady(err) {
|
|
100
|
+
if (this._readySettled)
|
|
101
|
+
return;
|
|
102
|
+
this._readySettled = true;
|
|
103
|
+
if (err)
|
|
104
|
+
(0, callback_1.doError)(err, this._readyCallback);
|
|
105
|
+
else
|
|
106
|
+
this._readyCallback(null);
|
|
107
|
+
}
|
|
108
|
+
_handleAsyncError(err) {
|
|
109
|
+
if (this._terminalErrorReported)
|
|
110
|
+
return;
|
|
111
|
+
this._terminalErrorReported = true;
|
|
112
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
113
|
+
this._hasActiveSubscription = false;
|
|
114
|
+
this._subscriptionVersion++;
|
|
115
|
+
this.eventconnection._isClosed = true;
|
|
116
|
+
this.eventconnection._isOpened = false;
|
|
117
|
+
this.eventconnection.eventcallback = null;
|
|
118
|
+
if (this.eventconnection._socket && !this.eventconnection._socket.destroyed &&
|
|
119
|
+
typeof this.eventconnection._socket.destroy === 'function') {
|
|
120
|
+
this.eventconnection._socket.destroy();
|
|
121
|
+
}
|
|
122
|
+
if (!this._readySettled) {
|
|
123
|
+
this._finishReady(error);
|
|
124
|
+
}
|
|
125
|
+
else if (this.listenerCount('error') > 0) {
|
|
126
|
+
this.emit('error', error);
|
|
127
|
+
}
|
|
128
|
+
else if (this.db.connection && typeof this.db.connection._emitError === 'function') {
|
|
129
|
+
this.db.connection._emitError(error);
|
|
130
|
+
}
|
|
87
131
|
}
|
|
88
132
|
/**
|
|
89
133
|
* Returns a snapshot of the current state for debugging.
|
|
@@ -136,7 +180,7 @@ class FbEventManager extends events_1.default.EventEmitter {
|
|
|
136
180
|
isDatabaseConnectionClosed: dbConnClosed,
|
|
137
181
|
};
|
|
138
182
|
}
|
|
139
|
-
_createEventLoop(
|
|
183
|
+
_createEventLoop() {
|
|
140
184
|
var self = this;
|
|
141
185
|
var cnx = this.db.connection;
|
|
142
186
|
this.eventconnection.emgr = this;
|
|
@@ -157,15 +201,15 @@ class FbEventManager extends events_1.default.EventEmitter {
|
|
|
157
201
|
}
|
|
158
202
|
cnx.queEvents(self.events, self.eventid, function (err) {
|
|
159
203
|
if (err) {
|
|
160
|
-
|
|
204
|
+
self._handleAsyncError(err);
|
|
161
205
|
return;
|
|
162
206
|
}
|
|
163
207
|
// subscription renewed, nothing else to do
|
|
164
208
|
});
|
|
165
209
|
}
|
|
166
210
|
this.eventconnection.eventcallback = function (err, ret) {
|
|
167
|
-
if (err || (self.eventid !== ret.eventid)) {
|
|
168
|
-
|
|
211
|
+
if (err || !ret || (self.eventid !== ret.eventid)) {
|
|
212
|
+
self._handleAsyncError(err || new Error('Bad eventid'));
|
|
169
213
|
return;
|
|
170
214
|
}
|
|
171
215
|
ret.events.forEach(function (event) {
|
|
@@ -173,11 +217,6 @@ class FbEventManager extends events_1.default.EventEmitter {
|
|
|
173
217
|
});
|
|
174
218
|
loop();
|
|
175
219
|
};
|
|
176
|
-
// Resolve attachEvent on the next tick – no subscription is needed
|
|
177
|
-
// until the caller registers at least one event name via registerEvent().
|
|
178
|
-
// process.nextTick ensures the outer `const evt = new FbEventManager(...)`
|
|
179
|
-
// assignment in database.js completes before this callback fires.
|
|
180
|
-
process.nextTick(function () { callback(null); });
|
|
181
220
|
}
|
|
182
221
|
_changeEvent(callback) {
|
|
183
222
|
var self = this;
|
|
@@ -246,6 +285,7 @@ class FbEventManager extends events_1.default.EventEmitter {
|
|
|
246
285
|
}
|
|
247
286
|
// Prevent the event loop from re-queuing on stale op_event notifications
|
|
248
287
|
// that may arrive between closeEvents and socket.end()
|
|
288
|
+
self.eventconnection._intentionalClose = true;
|
|
249
289
|
self.eventconnection.eventcallback = null;
|
|
250
290
|
// Gracefully close the event socket using a FIN (end()) rather than a RST
|
|
251
291
|
// (destroy()), then wait for the 'close' event which confirms both sides have
|
package/package.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// published declaration files.
|
|
7
7
|
|
|
8
8
|
import type { Readable, Writable } from 'stream';
|
|
9
|
+
import type { EventEmitter } from 'events';
|
|
9
10
|
import type { SqlTag } from './sql-template';
|
|
10
11
|
|
|
11
12
|
export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
|
|
@@ -16,6 +17,30 @@ export type QueryCallback = (err: any, result: any[]) => void;
|
|
|
16
17
|
export type SimpleCallback = (err: any) => void;
|
|
17
18
|
export type SequentialCallback = (row: any, index: number, next?: (err?: any) => void) => void | Promise<void>;
|
|
18
19
|
|
|
20
|
+
export interface FbEventState {
|
|
21
|
+
state: 'IDLE' | 'SUBSCRIBED' | 'CLOSED';
|
|
22
|
+
hasActiveSubscription: boolean;
|
|
23
|
+
registeredEvents: Record<string, number>;
|
|
24
|
+
eventId: number;
|
|
25
|
+
isEventConnectionOpen: boolean;
|
|
26
|
+
isDatabaseConnectionClosed: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FbEventManager extends EventEmitter {
|
|
30
|
+
readonly eventid: number;
|
|
31
|
+
readonly events: Record<string, number>;
|
|
32
|
+
registerEvent(events: string[], callback: SimpleCallback): void;
|
|
33
|
+
unregisterEvent(events: string[], callback: SimpleCallback): void;
|
|
34
|
+
close(callback?: SimpleCallback): void;
|
|
35
|
+
getState(): FbEventState;
|
|
36
|
+
on(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
37
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
38
|
+
once(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
39
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FbEventManagerCallback = (err: any, manager?: FbEventManager) => void;
|
|
43
|
+
|
|
19
44
|
/**
|
|
20
45
|
* Describes a single column in a prepared statement's result set or
|
|
21
46
|
* parameter list. The properties here are populated by the
|
|
@@ -272,7 +297,7 @@ export interface Database {
|
|
|
272
297
|
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
273
298
|
drop(callback: SimpleCallback): void;
|
|
274
299
|
escape(value: any): string;
|
|
275
|
-
attachEvent(callback:
|
|
300
|
+
attachEvent(callback: FbEventManagerCallback): this;
|
|
276
301
|
createTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
|
|
277
302
|
alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
|
|
278
303
|
dropTablespace(name: string, callback?: QueryCallback): Database;
|
|
@@ -293,7 +318,7 @@ export interface Database {
|
|
|
293
318
|
newStatementAsync(query: string): Promise<Statement>;
|
|
294
319
|
detachAsync(force?: boolean): Promise<void>;
|
|
295
320
|
dropAsync(): Promise<void>;
|
|
296
|
-
attachEventAsync(): Promise<
|
|
321
|
+
attachEventAsync(): Promise<FbEventManager>;
|
|
297
322
|
/** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
|
|
298
323
|
withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
|
|
299
324
|
/**
|
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;
|
|
@@ -120,7 +147,7 @@ class EventConnection {
|
|
|
120
147
|
self.eventcallback(null, { eventid: event_id, events: lst_event });
|
|
121
148
|
break;
|
|
122
149
|
default:
|
|
123
|
-
|
|
150
|
+
reportTerminalError(new Error('Unexpected event connection opcode: ' + r));
|
|
124
151
|
return;
|
|
125
152
|
}
|
|
126
153
|
}
|
|
@@ -130,7 +157,7 @@ class EventConnection {
|
|
|
130
157
|
xdr.pos = 0;
|
|
131
158
|
self._xdr = xdr;
|
|
132
159
|
} else {
|
|
133
|
-
|
|
160
|
+
reportTerminalError(err instanceof Error ? err : new Error(String(err)));
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
})
|
|
@@ -138,7 +165,6 @@ class EventConnection {
|
|
|
138
165
|
|
|
139
166
|
throwClosed(callback?: (err: any) => void): this {
|
|
140
167
|
var err = new Error('Event Connection is closed.');
|
|
141
|
-
this.db.emit('error', err);
|
|
142
168
|
if (callback)
|
|
143
169
|
callback(err);
|
|
144
170
|
return this;
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
// Asynchronous notifications on the AUX (EventConnection) socket
|
|
64
64
|
// ───────────────────────────────────────────────────────────────
|
|
65
65
|
// Server → Client : op_event (fired by Firebird POST_EVENT trigger)
|
|
66
|
+
// Error / unexpected close → CLOSED; emit manager 'error' once
|
|
66
67
|
|
|
67
68
|
import Events from 'events';
|
|
68
69
|
import { doError } from '../callback';
|
|
@@ -74,6 +75,9 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
74
75
|
eventid: number;
|
|
75
76
|
_subscriptionVersion: number;
|
|
76
77
|
_hasActiveSubscription: boolean;
|
|
78
|
+
_readySettled: boolean;
|
|
79
|
+
_terminalErrorReported: boolean;
|
|
80
|
+
_readyCallback: (err: any, ret?: any) => void;
|
|
77
81
|
|
|
78
82
|
constructor(db: any, eventconnection: any, eventid: number, callback: (err: any, ret?: any) => void) {
|
|
79
83
|
super();
|
|
@@ -88,7 +92,53 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
88
92
|
// main connection (so close() and _changeEvent know whether to send
|
|
89
93
|
// op_cancel_events before tearing down or re-subscribing).
|
|
90
94
|
this._hasActiveSubscription = false;
|
|
91
|
-
this.
|
|
95
|
+
this._readySettled = false;
|
|
96
|
+
this._terminalErrorReported = false;
|
|
97
|
+
this._readyCallback = callback;
|
|
98
|
+
this._createEventLoop();
|
|
99
|
+
process.nextTick(() => this._finishReady());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
on(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
103
|
+
on(event: 'error', listener: (error: Error) => void): this;
|
|
104
|
+
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
|
105
|
+
return super.on(event, listener);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
once(event: 'post_event', listener: (name: string, count: number) => void): this;
|
|
109
|
+
once(event: 'error', listener: (error: Error) => void): this;
|
|
110
|
+
once(event: string | symbol, listener: (...args: any[]) => void): this {
|
|
111
|
+
return super.once(event, listener);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_finishReady(err?: Error): void {
|
|
115
|
+
if (this._readySettled) return;
|
|
116
|
+
this._readySettled = true;
|
|
117
|
+
if (err) doError(err, this._readyCallback);
|
|
118
|
+
else this._readyCallback(null);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_handleAsyncError(err: any): void {
|
|
122
|
+
if (this._terminalErrorReported) return;
|
|
123
|
+
this._terminalErrorReported = true;
|
|
124
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
125
|
+
this._hasActiveSubscription = false;
|
|
126
|
+
this._subscriptionVersion++;
|
|
127
|
+
this.eventconnection._isClosed = true;
|
|
128
|
+
this.eventconnection._isOpened = false;
|
|
129
|
+
this.eventconnection.eventcallback = null;
|
|
130
|
+
if (this.eventconnection._socket && !this.eventconnection._socket.destroyed &&
|
|
131
|
+
typeof this.eventconnection._socket.destroy === 'function') {
|
|
132
|
+
this.eventconnection._socket.destroy();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!this._readySettled) {
|
|
136
|
+
this._finishReady(error);
|
|
137
|
+
} else if (this.listenerCount('error') > 0) {
|
|
138
|
+
this.emit('error', error);
|
|
139
|
+
} else if (this.db.connection && typeof this.db.connection._emitError === 'function') {
|
|
140
|
+
this.db.connection._emitError(error);
|
|
141
|
+
}
|
|
92
142
|
}
|
|
93
143
|
|
|
94
144
|
/**
|
|
@@ -143,7 +193,7 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
143
193
|
};
|
|
144
194
|
}
|
|
145
195
|
|
|
146
|
-
_createEventLoop(
|
|
196
|
+
_createEventLoop(): void {
|
|
147
197
|
var self = this;
|
|
148
198
|
var cnx = this.db.connection;
|
|
149
199
|
this.eventconnection.emgr = this;
|
|
@@ -166,7 +216,7 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
166
216
|
}
|
|
167
217
|
cnx.queEvents(self.events, self.eventid, function (err: any) {
|
|
168
218
|
if (err) {
|
|
169
|
-
|
|
219
|
+
self._handleAsyncError(err);
|
|
170
220
|
return;
|
|
171
221
|
}
|
|
172
222
|
// subscription renewed, nothing else to do
|
|
@@ -174,8 +224,8 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
174
224
|
}
|
|
175
225
|
|
|
176
226
|
this.eventconnection.eventcallback = function (err: any, ret?: any) {
|
|
177
|
-
if (err || (self.eventid !== ret.eventid)) {
|
|
178
|
-
|
|
227
|
+
if (err || !ret || (self.eventid !== ret.eventid)) {
|
|
228
|
+
self._handleAsyncError(err || new Error('Bad eventid'));
|
|
179
229
|
return;
|
|
180
230
|
}
|
|
181
231
|
|
|
@@ -186,11 +236,6 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
186
236
|
loop();
|
|
187
237
|
};
|
|
188
238
|
|
|
189
|
-
// Resolve attachEvent on the next tick – no subscription is needed
|
|
190
|
-
// until the caller registers at least one event name via registerEvent().
|
|
191
|
-
// process.nextTick ensures the outer `const evt = new FbEventManager(...)`
|
|
192
|
-
// assignment in database.js completes before this callback fires.
|
|
193
|
-
process.nextTick(function() { callback(null); });
|
|
194
239
|
}
|
|
195
240
|
|
|
196
241
|
_changeEvent(callback: (err: any, ret?: any) => void): void {
|
|
@@ -271,6 +316,7 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
271
316
|
|
|
272
317
|
// Prevent the event loop from re-queuing on stale op_event notifications
|
|
273
318
|
// that may arrive between closeEvents and socket.end()
|
|
319
|
+
self.eventconnection._intentionalClose = true;
|
|
274
320
|
self.eventconnection.eventcallback = null;
|
|
275
321
|
|
|
276
322
|
// Gracefully close the event socket using a FIN (end()) rather than a RST
|