node-firebird 2.0.2 → 2.3.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/.github/workflows/node.js.yml +20 -0
- package/BIGINT_MIGRATION.md +374 -0
- package/CI_DEBUGGING_GUIDE.md +148 -0
- package/FIREBIRD_LOG_FEATURE.md +145 -0
- package/MINIMAL_CHANGES_SUMMARY.md +136 -0
- package/PR_SUMMARY.md +88 -131
- package/README.md +142 -22
- package/ROADMAP.md +223 -0
- package/SRP_PROTOCOL.md +482 -0
- package/lib/ieee754-decimal.js +500 -0
- package/lib/index.d.ts +10 -6
- package/lib/srp.js +65 -50
- package/lib/wire/connection.js +257 -56
- package/lib/wire/const.js +29 -0
- package/lib/wire/database.js +78 -9
- package/lib/wire/eventConnection.js +6 -3
- package/lib/wire/fbEventManager.js +234 -41
- package/lib/wire/serialize.js +39 -0
- package/lib/wire/service.js +102 -94
- package/lib/wire/statement.js +4 -4
- package/lib/wire/transaction.js +27 -9
- package/lib/wire/xsqlvar.js +176 -0
- package/package.json +5 -5
- package/vitest.config.js +12 -1
- package/Roadmap.md +0 -80
package/lib/wire/const.js
CHANGED
|
@@ -186,6 +186,14 @@ const protocol = {
|
|
|
186
186
|
|
|
187
187
|
// Protocol 15 has support for conditional accept with early wire encryption.
|
|
188
188
|
PROTOCOL_VERSION15 : (FB_PROTOCOL_FLAG | 15),
|
|
189
|
+
|
|
190
|
+
// Protocol 16 has support for statement timeouts, DECFLOAT data types,
|
|
191
|
+
// and extended metadata identifiers (up to 63 characters).
|
|
192
|
+
// Introduced in Firebird 4.0.
|
|
193
|
+
PROTOCOL_VERSION16 : (FB_PROTOCOL_FLAG | 16),
|
|
194
|
+
|
|
195
|
+
// Protocol 17 continues Firebird 4.0 enhancements.
|
|
196
|
+
PROTOCOL_VERSION17 : (FB_PROTOCOL_FLAG | 17),
|
|
189
197
|
};
|
|
190
198
|
|
|
191
199
|
// Protocols types (accept_type)
|
|
@@ -205,6 +213,8 @@ const SUPPORTED_PROTOCOL = [
|
|
|
205
213
|
[protocol.PROTOCOL_VERSION13, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 4],
|
|
206
214
|
[protocol.PROTOCOL_VERSION14, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 5],
|
|
207
215
|
[protocol.PROTOCOL_VERSION15, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 6],
|
|
216
|
+
[protocol.PROTOCOL_VERSION16, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 7],
|
|
217
|
+
[protocol.PROTOCOL_VERSION17, connect.ARCHITECTURE_GENERIC, acceptType.ptype_lazy_send, acceptType.ptype_lazy_send, 8],
|
|
208
218
|
];
|
|
209
219
|
|
|
210
220
|
const authPlugin = {
|
|
@@ -240,8 +250,18 @@ const sqlType = {
|
|
|
240
250
|
SQL_TYPE_DATE : 570,
|
|
241
251
|
SQL_INT64 : 580,
|
|
242
252
|
SQL_INT128: 32752, // >= 4.0
|
|
253
|
+
SQL_TIMESTAMP_TZ : 32754, // >= 4.0 - timestamp with time zone
|
|
254
|
+
SQL_TIMESTAMP_TZ_EX : 32748, // >= 4.0 - extended timestamp with time zone
|
|
255
|
+
SQL_TIME_TZ : 32756, // >= 4.0 - time with time zone
|
|
256
|
+
SQL_TIME_TZ_EX : 32750, // >= 4.0 - extended time with time zone
|
|
257
|
+
SQL_DEC16 : 32760, // >= 4.0 - DECFLOAT(16) - 8 bytes, IEEE 754 Decimal64
|
|
258
|
+
SQL_DEC34 : 32762, // >= 4.0 - DECFLOAT(34) - 16 bytes, IEEE 754 Decimal128
|
|
243
259
|
SQL_BOOLEAN : 32764, // >: 3.0
|
|
244
260
|
SQL_NULL : 32766, // >= 2.5
|
|
261
|
+
SQL_TIMESTAMP_TZ_EX : 32748,
|
|
262
|
+
SQL_TIME_TZ_EX : 32750,
|
|
263
|
+
SQL_TIMESTAMP_TZ : 32754,
|
|
264
|
+
SQL_TIME_TZ : 32756,
|
|
245
265
|
};
|
|
246
266
|
|
|
247
267
|
const blobType = {
|
|
@@ -271,6 +291,10 @@ const blr = {
|
|
|
271
291
|
blr_sql_time : 13,
|
|
272
292
|
blr_int64 : 16,
|
|
273
293
|
blr_int128 : 26, // >: 4.0
|
|
294
|
+
blr_dec64 : 24, // >= 4.0 - DECFLOAT(16), IEEE 754 Decimal64
|
|
295
|
+
blr_dec128 : 25, // >= 4.0 - DECFLOAT(34), IEEE 754 Decimal128
|
|
296
|
+
blr_timestamp_tz : 28, // >= 4.0 - timestamp with time zone
|
|
297
|
+
blr_time_tz : 29, // >= 4.0 - time with time zone
|
|
274
298
|
blr_blob2 : 17, // >: 2.0
|
|
275
299
|
blr_domain_name : 18, // >: 2.1
|
|
276
300
|
blr_domain_name2 : 19, // >: 2.1
|
|
@@ -279,6 +303,11 @@ const blr = {
|
|
|
279
303
|
blr_column_name2 : 22, // >: 2.5
|
|
280
304
|
blr_bool : 23, // >: 3.0
|
|
281
305
|
|
|
306
|
+
blr_sql_time_tz : 28, // >: 4.0
|
|
307
|
+
blr_timestamp_tz : 29, // >: 4.0
|
|
308
|
+
blr_ex_time_tz : 30, // >: 4.0
|
|
309
|
+
blr_ex_timestamp_tz : 31, // >: 4.0
|
|
310
|
+
|
|
282
311
|
blr_version4 : 4,
|
|
283
312
|
blr_version5 : 5, // dialect 3
|
|
284
313
|
blr_eoc : 76,
|
package/lib/wire/database.js
CHANGED
|
@@ -9,6 +9,35 @@ const FbEventManager = require('./fbEventManager');
|
|
|
9
9
|
*
|
|
10
10
|
* Database
|
|
11
11
|
*
|
|
12
|
+
* Driver events (emitted on the Database instance itself)
|
|
13
|
+
* --------------------------------------------------------
|
|
14
|
+
* These are synchronous notifications from the driver about connection-level
|
|
15
|
+
* operations. Subscribe with db.on(eventName, handler).
|
|
16
|
+
*
|
|
17
|
+
* 'attach' – fired synchronously after the user callback returns,
|
|
18
|
+
* once the database is attached.
|
|
19
|
+
* 'detach' – fired when the database connection is detached.
|
|
20
|
+
* 'reconnect' – fired after the driver successfully reconnects a dropped socket.
|
|
21
|
+
* 'error' – fired for connection-level errors (socket errors, closed
|
|
22
|
+
* connection attempts, etc.).
|
|
23
|
+
* 'transaction' – fired when a transaction is started (before server response),
|
|
24
|
+
* with the resolved transaction options object as the argument.
|
|
25
|
+
* 'commit' – fired when a transaction commit is sent (before server response).
|
|
26
|
+
* 'rollback' – fired when a transaction rollback is sent (before server response).
|
|
27
|
+
* 'query' – fired with the SQL string when a statement is prepared.
|
|
28
|
+
* 'row' – fired with each individual row as it is decoded.
|
|
29
|
+
* 'result' – fired with the full rows array once all rows are fetched.
|
|
30
|
+
*
|
|
31
|
+
* Firebird database events (POST_EVENT)
|
|
32
|
+
* ----------------------------------------
|
|
33
|
+
* Real Firebird asynchronous notifications triggered by POST_EVENT inside
|
|
34
|
+
* PSQL triggers or stored procedures are handled through a separate channel:
|
|
35
|
+
* 1. Call db.attachEvent(callback) to obtain a FbEventManager instance.
|
|
36
|
+
* 2. Call evtmgr.registerEvent(names, callback) to subscribe to event names.
|
|
37
|
+
* 3. Listen for evtmgr.on('post_event', (name, count) => {}) to receive them.
|
|
38
|
+
* 4. Call evtmgr.unregisterEvent(names, callback) to cancel a subscription.
|
|
39
|
+
* 5. Call evtmgr.close(callback) when done to release the aux connection.
|
|
40
|
+
*
|
|
12
41
|
***************************************/
|
|
13
42
|
|
|
14
43
|
function readblob(blob, callback) {
|
|
@@ -161,9 +190,9 @@ class Database extends Events.EventEmitter {
|
|
|
161
190
|
return this;
|
|
162
191
|
}
|
|
163
192
|
|
|
164
|
-
execute(query, params, callback,
|
|
193
|
+
execute(query, params, callback, options) {
|
|
165
194
|
if (params instanceof Function) {
|
|
166
|
-
|
|
195
|
+
options = callback;
|
|
167
196
|
callback = params;
|
|
168
197
|
params = undefined;
|
|
169
198
|
}
|
|
@@ -191,15 +220,15 @@ class Database extends Events.EventEmitter {
|
|
|
191
220
|
callback(err, result, meta, isSelect);
|
|
192
221
|
});
|
|
193
222
|
|
|
194
|
-
},
|
|
223
|
+
}, options);
|
|
195
224
|
});
|
|
196
225
|
|
|
197
226
|
return self;
|
|
198
227
|
}
|
|
199
228
|
|
|
200
|
-
sequentially(query, params, on, callback,
|
|
229
|
+
sequentially(query, params, on, callback, options = {}) {
|
|
201
230
|
if (params instanceof Function) {
|
|
202
|
-
|
|
231
|
+
options = callback;
|
|
203
232
|
callback = on;
|
|
204
233
|
on = params;
|
|
205
234
|
params = undefined;
|
|
@@ -210,7 +239,7 @@ class Database extends Events.EventEmitter {
|
|
|
210
239
|
}
|
|
211
240
|
|
|
212
241
|
if (callback instanceof Boolean) {
|
|
213
|
-
|
|
242
|
+
options = callback;
|
|
214
243
|
callback = undefined;
|
|
215
244
|
}
|
|
216
245
|
|
|
@@ -252,18 +281,37 @@ class Database extends Events.EventEmitter {
|
|
|
252
281
|
});
|
|
253
282
|
};
|
|
254
283
|
|
|
255
|
-
|
|
284
|
+
// back compatibility - options parameter is a boolean
|
|
285
|
+
if (typeof options === 'boolean') {
|
|
286
|
+
options = { asObject: !options, asStream: true, on: _on };
|
|
287
|
+
} else {
|
|
288
|
+
options = {
|
|
289
|
+
asObject: true,
|
|
290
|
+
asStream: true,
|
|
291
|
+
on: _on,
|
|
292
|
+
...options,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
self.execute(query, params, callback, options);
|
|
256
297
|
return self;
|
|
257
298
|
}
|
|
258
299
|
|
|
259
|
-
query(query, params, callback) {
|
|
300
|
+
query(query, params, callback, options = {}) {
|
|
260
301
|
if (params instanceof Function) {
|
|
302
|
+
options = callback || {};
|
|
261
303
|
callback = params;
|
|
262
304
|
params = undefined;
|
|
263
305
|
}
|
|
264
306
|
|
|
307
|
+
options = {
|
|
308
|
+
asObject: true,
|
|
309
|
+
asStream: callback === undefined || callback === null,
|
|
310
|
+
...options
|
|
311
|
+
};
|
|
312
|
+
|
|
265
313
|
var self = this;
|
|
266
|
-
self.execute(query, params, callback,
|
|
314
|
+
self.execute(query, params, callback, options);
|
|
267
315
|
return self;
|
|
268
316
|
}
|
|
269
317
|
|
|
@@ -273,13 +321,23 @@ class Database extends Events.EventEmitter {
|
|
|
273
321
|
|
|
274
322
|
attachEvent(callback) {
|
|
275
323
|
var self = this;
|
|
324
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
325
|
+
console.log('[fb-debug] Database.attachEvent: calling auxConnection, eventid=%d queue=%d', self.eventid, self.connection._queue.length);
|
|
326
|
+
}
|
|
276
327
|
this.connection.auxConnection(function (err, socket_info) {
|
|
277
328
|
|
|
278
329
|
if (err) {
|
|
330
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
331
|
+
console.log('[fb-debug] Database.attachEvent: auxConnection error:', err.message);
|
|
332
|
+
}
|
|
279
333
|
doError(err, callback);
|
|
280
334
|
return;
|
|
281
335
|
}
|
|
282
336
|
|
|
337
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
338
|
+
console.log('[fb-debug] Database.attachEvent: auxConnection ok, connecting to aux port %s:%d', socket_info.host, socket_info.port);
|
|
339
|
+
}
|
|
340
|
+
|
|
283
341
|
const host = (socket_info.host === '0.0.0.0' || socket_info.host === '::')
|
|
284
342
|
? self.connection.options.host
|
|
285
343
|
: socket_info.host;
|
|
@@ -287,16 +345,27 @@ class Database extends Events.EventEmitter {
|
|
|
287
345
|
const eventConnection = new EventConnection(
|
|
288
346
|
host, socket_info.port, function(err) {
|
|
289
347
|
if (err) {
|
|
348
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
349
|
+
console.log('[fb-debug] Database.attachEvent: EventConnection error:', err.message);
|
|
350
|
+
}
|
|
290
351
|
doError(err, callback);
|
|
291
352
|
return;
|
|
292
353
|
}
|
|
293
354
|
|
|
355
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
356
|
+
console.log('[fb-debug] Database.attachEvent: EventConnection connected, creating FbEventManager eventid=%d', self.eventid);
|
|
357
|
+
}
|
|
358
|
+
|
|
294
359
|
const evt = new FbEventManager(self, eventConnection, self.eventid++, function (err) {
|
|
295
360
|
if (err) {
|
|
296
361
|
doError(err, callback);
|
|
297
362
|
return;
|
|
298
363
|
}
|
|
299
364
|
|
|
365
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
366
|
+
console.log('[fb-debug] Database.attachEvent: FbEventManager ready, eventid=%d', evt.eventid);
|
|
367
|
+
}
|
|
368
|
+
|
|
300
369
|
callback(err, evt);
|
|
301
370
|
});
|
|
302
371
|
}, self);
|
|
@@ -87,10 +87,11 @@ class EventConnection {
|
|
|
87
87
|
self.emgr.events[evt] = tmp_event[evt];
|
|
88
88
|
}
|
|
89
89
|
if (self.eventcallback)
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
self.eventcallback(null, { eventid: event_id, events: lst_event });
|
|
91
|
+
break;
|
|
92
92
|
default:
|
|
93
|
-
|
|
93
|
+
// Unknown opcode on the event connection – stop processing.
|
|
94
|
+
return;
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
} catch (err) {
|
|
@@ -98,6 +99,8 @@ class EventConnection {
|
|
|
98
99
|
xdr.buffer = xdr.buffer = xdr.buffer.slice(op_pos);
|
|
99
100
|
xdr.pos = 0;
|
|
100
101
|
self._xdr = xdr;
|
|
102
|
+
} else {
|
|
103
|
+
throw err;
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
})
|
|
@@ -1,3 +1,69 @@
|
|
|
1
|
+
// FbEventManager – Firebird POST_EVENT subscription manager
|
|
2
|
+
//
|
|
3
|
+
// State machine overview
|
|
4
|
+
// ──────────────────────
|
|
5
|
+
//
|
|
6
|
+
// ┌──────────────────────────────────────────────────────────────────────┐
|
|
7
|
+
// │ FbEventManager states │
|
|
8
|
+
// └──────────────────────────────────────────────────────────────────────┘
|
|
9
|
+
//
|
|
10
|
+
// attachEvent()
|
|
11
|
+
// │
|
|
12
|
+
// ▼
|
|
13
|
+
// ┌─────────────────────────────────────────────────────────┐
|
|
14
|
+
// │ IDLE │
|
|
15
|
+
// │ _hasActiveSubscription = false │
|
|
16
|
+
// │ events = {} │
|
|
17
|
+
// │ eventcallback = loop fn (set but subscription absent) │
|
|
18
|
+
// └──────────────┬────────────────────────────┬────────────┘
|
|
19
|
+
// │ registerEvent([...]) │ close()
|
|
20
|
+
// ▼ ▼
|
|
21
|
+
// ┌──────────────────────────┐ ┌───────────────────────┐
|
|
22
|
+
// │ SUBSCRIBING │ │ CLOSING │
|
|
23
|
+
// │ queEvents() sent │ │ endAndWaitForClose() │
|
|
24
|
+
// │ waiting for op_response │ │ sock.end() + wait │
|
|
25
|
+
// └──────────┬───────────────┘ └───────────┬───────────┘
|
|
26
|
+
// │ op_response ok │ 'close' event
|
|
27
|
+
// ▼ ▼
|
|
28
|
+
// ┌──────────────────────────┐ ┌───────────────────────┐
|
|
29
|
+
// │ SUBSCRIBED │ │ CLOSED / DONE │
|
|
30
|
+
// │ _hasActiveSubscription │ │ eventconnection gone │
|
|
31
|
+
// │ = true │ └───────────────────────┘
|
|
32
|
+
// │ eventcallback active │
|
|
33
|
+
// └───┬───────────┬──────────┘
|
|
34
|
+
// │ │
|
|
35
|
+
// │ op_event │ unregisterEvent() (all removed) or close()
|
|
36
|
+
// │ received │
|
|
37
|
+
// │ ▼
|
|
38
|
+
// │ ┌──────────────────────────────────────────────────┐
|
|
39
|
+
// │ │ CANCELLING │
|
|
40
|
+
// │ │ closeEvents() sent (op_cancel_events) │
|
|
41
|
+
// │ │ waiting for op_response │
|
|
42
|
+
// │ └──────────┬───────────────────────────────────────┘
|
|
43
|
+
// │ │ op_response ok
|
|
44
|
+
// │ ▼
|
|
45
|
+
// │ ┌──────────────────────────────────────────────────┐
|
|
46
|
+
// │ │ IDLE (or CLOSING if called from close()) │
|
|
47
|
+
// │ └──────────────────────────────────────────────────┘
|
|
48
|
+
// │
|
|
49
|
+
// │ emit('post_event', name, count)
|
|
50
|
+
// └──────────────────────┐
|
|
51
|
+
// ▼
|
|
52
|
+
// loop() → SUBSCRIBING (re-subscribe)
|
|
53
|
+
//
|
|
54
|
+
// Wire-protocol messages on the MAIN connection
|
|
55
|
+
// ──────────────────────────────────────────────
|
|
56
|
+
// Client → Server : op_connect_request (attachEvent / auxConnection)
|
|
57
|
+
// Server → Client : op_response (socket address of AUX port)
|
|
58
|
+
// Client → Server : op_que_events (registerEvent / loop)
|
|
59
|
+
// Server → Client : op_response (confirms event ID)
|
|
60
|
+
// Client → Server : op_cancel_events (unregisterEvent / close)
|
|
61
|
+
// Server → Client : op_response
|
|
62
|
+
//
|
|
63
|
+
// Asynchronous notifications on the AUX (EventConnection) socket
|
|
64
|
+
// ───────────────────────────────────────────────────────────────
|
|
65
|
+
// Server → Client : op_event (fired by Firebird POST_EVENT trigger)
|
|
66
|
+
|
|
1
67
|
const Events = require('events');
|
|
2
68
|
const { doError } = require('../callback');
|
|
3
69
|
|
|
@@ -8,95 +74,172 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
8
74
|
this.eventconnection = eventconnection;
|
|
9
75
|
this.events = {};
|
|
10
76
|
this.eventid = eventid;
|
|
77
|
+
// True when an op_que_events subscription is currently active on the
|
|
78
|
+
// main connection (so close() and _changeEvent know whether to send
|
|
79
|
+
// op_cancel_events before tearing down or re-subscribing).
|
|
80
|
+
this._hasActiveSubscription = false;
|
|
11
81
|
this._createEventLoop(callback);
|
|
12
82
|
}
|
|
13
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Returns a snapshot of the current state for debugging.
|
|
86
|
+
* Useful for tracing the state machine during development.
|
|
87
|
+
*
|
|
88
|
+
* Stable states: 'IDLE', 'SUBSCRIBED', 'CLOSED'.
|
|
89
|
+
* Transient states (SUBSCRIBING, CANCELLING, CLOSING) occur while waiting
|
|
90
|
+
* for op_response on the main connection or for the socket to close; they
|
|
91
|
+
* are not tracked with dedicated flags to keep the implementation simple,
|
|
92
|
+
* but they can be inferred: if the socket is open and _hasActiveSubscription
|
|
93
|
+
* disagrees with what the caller expects, a transitional operation is in
|
|
94
|
+
* progress.
|
|
95
|
+
*
|
|
96
|
+
* @returns {{
|
|
97
|
+
* state: string,
|
|
98
|
+
* hasActiveSubscription: boolean,
|
|
99
|
+
* registeredEvents: Object,
|
|
100
|
+
* eventId: number,
|
|
101
|
+
* isEventConnectionOpen: boolean,
|
|
102
|
+
* isDatabaseConnectionClosed: boolean
|
|
103
|
+
* }}
|
|
104
|
+
*/
|
|
105
|
+
getState() {
|
|
106
|
+
const evtConnOpen = this.eventconnection
|
|
107
|
+
? !this.eventconnection._isClosed
|
|
108
|
+
: false;
|
|
109
|
+
const dbConnClosed = this.db.connection
|
|
110
|
+
? this.db.connection._isClosed
|
|
111
|
+
: true;
|
|
112
|
+
|
|
113
|
+
// Derive a human-readable stable-state label.
|
|
114
|
+
// Transitional states (SUBSCRIBING / CANCELLING / CLOSING) are not
|
|
115
|
+
// individually flagged; callers that need finer granularity can
|
|
116
|
+
// inspect hasActiveSubscription and isEventConnectionOpen together.
|
|
117
|
+
let state;
|
|
118
|
+
if (dbConnClosed || !evtConnOpen) {
|
|
119
|
+
state = 'CLOSED';
|
|
120
|
+
} else if (this._hasActiveSubscription) {
|
|
121
|
+
state = 'SUBSCRIBED';
|
|
122
|
+
} else {
|
|
123
|
+
state = 'IDLE';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
state,
|
|
128
|
+
hasActiveSubscription: this._hasActiveSubscription,
|
|
129
|
+
registeredEvents: Object.assign({}, this.events),
|
|
130
|
+
eventId: this.eventid,
|
|
131
|
+
isEventConnectionOpen: evtConnOpen,
|
|
132
|
+
isDatabaseConnectionClosed: dbConnClosed,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
14
136
|
_createEventLoop(callback) {
|
|
15
137
|
var self = this;
|
|
16
138
|
var cnx = this.db.connection;
|
|
17
139
|
this.eventconnection.emgr = this;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
140
|
+
|
|
141
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
142
|
+
console.log('[fb-debug] FbEventManager._createEventLoop: eventid=%d', self.eventid);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Re-subscribe after each op_event notification so that further
|
|
146
|
+
// trigger fires continue to be delivered.
|
|
147
|
+
function loop() {
|
|
148
|
+
// Guard: do not send queEvents if the subscription has been
|
|
149
|
+
// cancelled (e.g. via unregisterEvent removing all events) or if
|
|
150
|
+
// there are no registered events. Without this check, a late
|
|
151
|
+
// op_event arriving on the event connection after closeEvents can
|
|
152
|
+
// trigger queEvents({}) which Firebird never acknowledges,
|
|
153
|
+
// permanently blocking the main connection queue.
|
|
154
|
+
if (!self._hasActiveSubscription || Object.keys(self.events).length === 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
cnx.queEvents(self.events, self.eventid, function (err) {
|
|
24
158
|
if (err) {
|
|
25
|
-
console.log('[EventManager] queEvents error:', err);
|
|
26
159
|
doError(err, callback);
|
|
27
160
|
return;
|
|
28
161
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
callback(null);
|
|
32
|
-
}
|
|
33
|
-
})
|
|
162
|
+
// subscription renewed, nothing else to do
|
|
163
|
+
});
|
|
34
164
|
}
|
|
35
165
|
|
|
36
166
|
this.eventconnection.eventcallback = function (err, ret) {
|
|
37
|
-
console.log('[EventManager] eventcallback invoked, err:', err, 'eventid match:', self.eventid === ret?.eventid);
|
|
38
167
|
if (err || (self.eventid !== ret.eventid)) {
|
|
39
168
|
doError(err || new Error('Bad eventid'), callback);
|
|
40
169
|
return;
|
|
41
170
|
}
|
|
42
171
|
|
|
43
172
|
ret.events.forEach(function (event) {
|
|
44
|
-
self.emit('post_event', event.name, event.count)
|
|
45
|
-
})
|
|
173
|
+
self.emit('post_event', event.name, event.count);
|
|
174
|
+
});
|
|
46
175
|
|
|
47
|
-
loop(
|
|
48
|
-
}
|
|
176
|
+
loop();
|
|
177
|
+
};
|
|
49
178
|
|
|
50
|
-
|
|
179
|
+
// Resolve attachEvent on the next tick – no subscription is needed
|
|
180
|
+
// until the caller registers at least one event name via registerEvent().
|
|
181
|
+
// process.nextTick ensures the outer `const evt = new FbEventManager(...)`
|
|
182
|
+
// assignment in database.js completes before this callback fires.
|
|
183
|
+
process.nextTick(function() { callback(null); });
|
|
51
184
|
}
|
|
52
185
|
|
|
53
186
|
_changeEvent(callback) {
|
|
54
187
|
var self = this;
|
|
55
188
|
|
|
56
|
-
console.log('[EventManager] _changeEvent called, eventid:', this.eventid, 'events:', Object.keys(self.events));
|
|
57
189
|
// Temporarily suppress the event loop to prevent stale op_event
|
|
58
190
|
// notifications from pushing spurious queEvents onto the main
|
|
59
|
-
// connection queue while
|
|
191
|
+
// connection queue while we're reconfiguring the subscription.
|
|
60
192
|
var savedCallback = self.eventconnection.eventcallback;
|
|
61
193
|
self.eventconnection.eventcallback = null;
|
|
62
|
-
console.log('[EventManager] Suppressed event callback, calling closeEvents');
|
|
63
194
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
195
|
+
function subscribe() {
|
|
196
|
+
// If no events remain, mark subscription as inactive and return.
|
|
197
|
+
// Sending queEvents with an empty EPB after op_cancel_events does
|
|
198
|
+
// not receive op_response from some Firebird versions, which would
|
|
199
|
+
// permanently block the main connection queue.
|
|
200
|
+
if (Object.keys(self.events).length === 0) {
|
|
201
|
+
self._hasActiveSubscription = false;
|
|
67
202
|
self.eventconnection.eventcallback = savedCallback;
|
|
68
|
-
|
|
203
|
+
callback(null);
|
|
69
204
|
return;
|
|
70
205
|
}
|
|
71
|
-
|
|
72
|
-
console.log('[EventManager] closeEvents success, calling queEvents');
|
|
206
|
+
|
|
73
207
|
self.db.connection.queEvents(self.events, self.eventid, function (err, ret) {
|
|
74
|
-
console.log('[EventManager] queEvents callback in _changeEvent, err:', err, 'ret:', ret);
|
|
75
|
-
// Restore the event loop now that our queEvents is safely queued
|
|
76
208
|
self.eventconnection.eventcallback = savedCallback;
|
|
77
|
-
console.log('[EventManager] Restored event callback');
|
|
78
209
|
if (err) {
|
|
79
|
-
console.log('[EventManager] queEvents error in _changeEvent:', err);
|
|
80
210
|
doError(err, callback);
|
|
81
211
|
return;
|
|
82
212
|
}
|
|
83
|
-
|
|
213
|
+
self._hasActiveSubscription = true;
|
|
84
214
|
callback(null, ret);
|
|
85
215
|
});
|
|
86
|
-
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (self._hasActiveSubscription) {
|
|
219
|
+
// Cancel the current subscription before setting up a new one.
|
|
220
|
+
self.db.connection.closeEvents(this.eventid, function (err) {
|
|
221
|
+
if (err) {
|
|
222
|
+
self.eventconnection.eventcallback = savedCallback;
|
|
223
|
+
doError(err, callback);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
self._hasActiveSubscription = false;
|
|
227
|
+
subscribe();
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
// No active subscription yet (first registerEvent call, or after
|
|
231
|
+
// all events were unregistered) – go straight to subscribing.
|
|
232
|
+
subscribe();
|
|
233
|
+
}
|
|
87
234
|
}
|
|
88
235
|
|
|
89
236
|
registerEvent(events, callback) {
|
|
90
237
|
var self = this;
|
|
91
238
|
|
|
92
|
-
console.log('[EventManager] registerEvent called with events:', events);
|
|
93
|
-
console.log('[EventManager] Connection closed?', self.db.connection._isClosed, 'Event connection closed?', self.eventconnection._isClosed);
|
|
94
|
-
|
|
95
239
|
if (self.db.connection._isClosed || self.eventconnection._isClosed)
|
|
96
240
|
return self.eventconnection.throwClosed(callback);
|
|
97
241
|
|
|
98
242
|
events.forEach((event) => self.events[event] = self.events[event] || 0);
|
|
99
|
-
console.log('[EventManager] Events registered, current events:', Object.keys(self.events));
|
|
100
243
|
self._changeEvent(callback);
|
|
101
244
|
}
|
|
102
245
|
|
|
@@ -113,21 +256,71 @@ class FbEventManager extends Events.EventEmitter {
|
|
|
113
256
|
close(callback) {
|
|
114
257
|
var self = this;
|
|
115
258
|
|
|
259
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
260
|
+
console.log('[fb-debug] FbEventManager.close() called, _hasActiveSubscription=%s eventid=%d', self._hasActiveSubscription, self.eventid);
|
|
261
|
+
}
|
|
262
|
+
|
|
116
263
|
// Prevent the event loop from re-queuing on stale op_event notifications
|
|
117
264
|
// that may arrive between closeEvents and socket.end()
|
|
118
265
|
self.eventconnection.eventcallback = null;
|
|
119
266
|
|
|
267
|
+
// Gracefully close the event socket using a FIN (end()) rather than a RST
|
|
268
|
+
// (destroy()), then wait for the 'close' event which confirms both sides have
|
|
269
|
+
// exchanged FINs. This gives Firebird (all versions 3/4/5) time to fully
|
|
270
|
+
// process the previous event connection's teardown before the next
|
|
271
|
+
// op_connect_request or op_que_events arrives on the main connection.
|
|
272
|
+
// destroy() (RST) is faster but causes Firebird 3 to get confused on subsequent
|
|
273
|
+
// queEvents calls – the server internally fails on the RST error and does not
|
|
274
|
+
// clean up its event state in time for the next subscription request.
|
|
275
|
+
// A 200 ms safety timer fires as a fallback if Firebird never sends its FIN.
|
|
276
|
+
function endAndWaitForClose(cb) {
|
|
277
|
+
var sock = self.eventconnection && self.eventconnection._socket;
|
|
278
|
+
if (!sock || sock.destroyed) {
|
|
279
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
280
|
+
console.log('[fb-debug] endAndWaitForClose: socket already destroyed, calling back immediately');
|
|
281
|
+
}
|
|
282
|
+
if (cb) cb();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
var fired = false;
|
|
286
|
+
var timer;
|
|
287
|
+
function done(source) {
|
|
288
|
+
if (!fired) {
|
|
289
|
+
fired = true;
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
292
|
+
console.log('[fb-debug] endAndWaitForClose done() via %s, eventid=%d', source, self.eventid);
|
|
293
|
+
}
|
|
294
|
+
if (cb) cb();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
sock.once('close', function() { done('close-event'); });
|
|
298
|
+
if (process.env.FIREBIRD_DEBUG) {
|
|
299
|
+
console.log('[fb-debug] endAndWaitForClose: calling sock.end(), eventid=%d sock.destroyed=%s', self.eventid, sock.destroyed);
|
|
300
|
+
}
|
|
301
|
+
sock.end();
|
|
302
|
+
// Safety fallback: if Firebird never sends its FIN (e.g. an error
|
|
303
|
+
// occurs on the server side), resolve after 200 ms so tests don't hang.
|
|
304
|
+
timer = setTimeout(function() { done('200ms-timer'); }, 200);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!self._hasActiveSubscription) {
|
|
308
|
+
// No active subscription (attachEvent without registerEvent, or
|
|
309
|
+
// all events were unregistered) – nothing to cancel.
|
|
310
|
+
endAndWaitForClose(callback);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
120
314
|
self.db.connection.closeEvents(this.eventid, function (err) {
|
|
121
315
|
if (err) {
|
|
122
316
|
doError(err, callback);
|
|
123
317
|
return;
|
|
124
318
|
}
|
|
125
319
|
|
|
126
|
-
self.
|
|
127
|
-
|
|
128
|
-
callback();
|
|
320
|
+
self._hasActiveSubscription = false;
|
|
321
|
+
endAndWaitForClose(callback);
|
|
129
322
|
});
|
|
130
323
|
}
|
|
131
324
|
}
|
|
132
325
|
|
|
133
|
-
module.exports = FbEventManager;
|
|
326
|
+
module.exports = FbEventManager;
|
package/lib/wire/serialize.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var Long = require('long');
|
|
2
|
+
const { encodeDecimal64, decodeDecimal64, encodeDecimal128, decodeDecimal128 } = require('../ieee754-decimal');
|
|
2
3
|
|
|
3
4
|
function align(n) {
|
|
4
5
|
return (n + 3) & ~3;
|
|
@@ -297,6 +298,26 @@ class XdrWriter {
|
|
|
297
298
|
this.pos += 8;
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
addDecFloat16(value) {
|
|
302
|
+
// DECFLOAT(16) - IEEE 754 Decimal64 - 8 bytes
|
|
303
|
+
// Full IEEE 754-2008 Decimal64 implementation
|
|
304
|
+
this.ensure(8);
|
|
305
|
+
|
|
306
|
+
const encoded = encodeDecimal64(value);
|
|
307
|
+
encoded.copy(this.buffer, this.pos, 0, 8);
|
|
308
|
+
this.pos += 8;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
addDecFloat34(value) {
|
|
312
|
+
// DECFLOAT(34) - IEEE 754 Decimal128 - 16 bytes
|
|
313
|
+
// Full IEEE 754-2008 Decimal128 implementation
|
|
314
|
+
this.ensure(16);
|
|
315
|
+
|
|
316
|
+
const encoded = encodeDecimal128(value);
|
|
317
|
+
encoded.copy(this.buffer, this.pos, 0, 16);
|
|
318
|
+
this.pos += 16;
|
|
319
|
+
}
|
|
320
|
+
|
|
300
321
|
addUInt(value) {
|
|
301
322
|
this.ensure(4);
|
|
302
323
|
this.buffer.writeUInt32BE(value, this.pos);
|
|
@@ -406,6 +427,24 @@ class XdrReader {
|
|
|
406
427
|
return (BigInt(high) << BigInt(64)) + BigInt(low)
|
|
407
428
|
}
|
|
408
429
|
|
|
430
|
+
readDecFloat16() {
|
|
431
|
+
// DECFLOAT(16) - IEEE 754 Decimal64 - 8 bytes
|
|
432
|
+
// Full IEEE 754-2008 Decimal64 implementation
|
|
433
|
+
const buf = this.buffer.slice(this.pos, this.pos + 8);
|
|
434
|
+
this.pos += 8;
|
|
435
|
+
|
|
436
|
+
return decodeDecimal64(buf);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
readDecFloat34() {
|
|
440
|
+
// DECFLOAT(34) - IEEE 754 Decimal128 - 16 bytes
|
|
441
|
+
// Full IEEE 754-2008 Decimal128 implementation
|
|
442
|
+
const buf = this.buffer.slice(this.pos, this.pos + 16);
|
|
443
|
+
this.pos += 16;
|
|
444
|
+
|
|
445
|
+
return decodeDecimal128(buf);
|
|
446
|
+
}
|
|
447
|
+
|
|
409
448
|
readShort() {
|
|
410
449
|
var r = this.buffer.readInt16BE(this.pos);
|
|
411
450
|
this.pos += 2;
|