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/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,
@@ -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, custom) {
193
+ execute(query, params, callback, options) {
165
194
  if (params instanceof Function) {
166
- custom = callback;
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
- }, custom);
223
+ }, options);
195
224
  });
196
225
 
197
226
  return self;
198
227
  }
199
228
 
200
- sequentially(query, params, on, callback, asArray) {
229
+ sequentially(query, params, on, callback, options = {}) {
201
230
  if (params instanceof Function) {
202
- asArray = callback;
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
- asArray = callback;
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
- self.execute(query, params, callback, { asObject: !asArray, asStream: true, on: _on });
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, { asObject: true, asStream: callback === undefined || callback === null });
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
- return self.eventcallback(null, { eventid: event_id, events: lst_event });
91
-
90
+ self.eventcallback(null, { eventid: event_id, events: lst_event });
91
+ break;
92
92
  default:
93
- return cb(new Error('Unexpected:' + r));
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
- console.log('[EventManager] Creating event loop, eventid:', this.eventid);
19
- // create the loop
20
- function loop(first) {
21
- console.log('[EventManager] Calling queEvents, first:', first, 'events:', Object.keys(self.events));
22
- cnx.queEvents(self.events, self.eventid, function (err, ret) {
23
- console.log('[EventManager] queEvents callback, err:', err, 'ret:', ret, 'first:', first);
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
- if (first) {
30
- console.log('[EventManager] First loop complete, calling callback');
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(false);
48
- }
176
+ loop();
177
+ };
49
178
 
50
- loop(true);
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 closeEvents + queEvents are in flight
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
- self.db.connection.closeEvents(this.eventid, function (err) {
65
- console.log('[EventManager] closeEvents callback, err:', err);
66
- if (err) {
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
- doError(err, callback);
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
- console.log('[EventManager] _changeEvent complete, calling final callback');
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.eventconnection._socket.end();
127
- if (callback)
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;
@@ -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;