node-firebird 2.5.0 → 2.6.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 CHANGED
@@ -14,7 +14,7 @@
14
14
  - [Connection types](#connection-types) — connection options, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
16
  - [Examples](#examples) — parametrized queries, BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
- - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
17
+ - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
18
18
  - [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
19
19
  - [FAQ](#faq)
20
20
  - [Contributing](#contributing) · [Contributors](#contributors)
@@ -1151,6 +1151,47 @@ Firebird.attach({
1151
1151
  });
1152
1152
  ```
1153
1153
 
1154
+ ### Query Cancellation with AbortSignal (Firebird 2.5+)
1155
+
1156
+ Any query can be cancelled while it is executing on the server. Pass an
1157
+ `AbortSignal` in the query options — when it fires, the driver sends an
1158
+ out-of-band `op_cancel` packet and the running statement fails with
1159
+ `err.gdscode === GDSCode.CANCELLED`. The connection itself stays healthy and
1160
+ can run further queries immediately.
1161
+
1162
+ ```js
1163
+ const { GDSCode } = require('node-firebird/lib/gdscodes');
1164
+
1165
+ const controller = new AbortController();
1166
+ setTimeout(() => controller.abort(), 5000); // or req.on('close', ...) in Express
1167
+
1168
+ try {
1169
+ const rows = await db.queryAsync('SELECT /* expensive */ ...', [], { signal: controller.signal });
1170
+ } catch (err) {
1171
+ if (err.gdscode === GDSCode.CANCELLED) {
1172
+ // cancelled server-side; the connection remains usable
1173
+ } else if (err.name === 'AbortError') {
1174
+ // the signal was already aborted — the query was never sent
1175
+ } else {
1176
+ throw err;
1177
+ }
1178
+ }
1179
+ ```
1180
+
1181
+ The option works with the callback API as well (`db.query(sql, params, cb,
1182
+ { signal })`) and on transaction-level queries. A running operation can also
1183
+ be cancelled manually from anywhere with `db.cancel()` / `await
1184
+ db.cancelAsync()`.
1185
+
1186
+ Notes:
1187
+
1188
+ - Cancellation is **per attachment** (that is how the Firebird protocol
1189
+ defines it): it cancels whatever is currently executing on that
1190
+ connection. With a pool this is naturally scoped to the request holding
1191
+ the connection.
1192
+ - A signal that is already aborted rejects immediately with an `AbortError`
1193
+ without contacting the server; cancelling an idle connection is harmless.
1194
+
1154
1195
  ### Statement Timeouts (Firebird 4.0+)
1155
1196
  Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
1156
1197
  ```js
package/lib/types.d.ts CHANGED
@@ -67,6 +67,15 @@ export type TransactionOptions = {
67
67
  export type QueryOptions = {
68
68
  timeout?: number;
69
69
  scrollable?: boolean;
70
+ /**
71
+ * Abort the query when the signal fires (Firebird 2.5+ / protocol 12+).
72
+ * If the signal is already aborted the query is not sent at all and the
73
+ * callback/promise fails with an `AbortError`. If it fires mid-flight an
74
+ * out-of-band op_cancel is sent and the query fails with
75
+ * `err.gdscode === GDSCode.CANCELLED`. Cancellation is per-attachment:
76
+ * it cancels whatever is currently executing on the connection.
77
+ */
78
+ signal?: AbortSignal;
70
79
  };
71
80
  export interface Database {
72
81
  detach(callback?: SimpleCallback): Database;
@@ -94,6 +103,14 @@ export interface Database {
94
103
  attachEventAsync(): Promise<any>;
95
104
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
96
105
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
106
+ /**
107
+ * Cancel the operation currently executing on this connection
108
+ * (Firebird 2.5+ / protocol 12+). The cancelled operation fails through
109
+ * its own callback/promise with `err.gdscode === GDSCode.CANCELLED`.
110
+ */
111
+ cancel(callback?: SimpleCallback): Database;
112
+ cancel(kind: number, callback?: SimpleCallback): Database;
113
+ cancelAsync(kind?: number): Promise<void>;
97
114
  }
98
115
  export interface Transaction {
99
116
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
@@ -57,6 +57,13 @@ declare class Connection {
57
57
  sendOpContAuth(authData: any, authDataEnc: any, pluginName: any): void;
58
58
  sendOpCrypt(encryptPlugin: any): void;
59
59
  sendOpCryptKeyCallback(pluginData: any): void;
60
+ /**
61
+ * Send an out-of-band op_cancel packet (protocol 12+ / Firebird 2.5+).
62
+ * The server reads it asynchronously while an operation is executing and
63
+ * makes that operation fail with isc_cancelled (GDSCode.CANCELLED); the
64
+ * op_cancel packet itself has no response, so nothing is queued here.
65
+ */
66
+ cancelOperation(kind: any, callback: any): this;
60
67
  _queueEvent(callback: any, defer?: boolean): void;
61
68
  connect(options: any, callback: any): void;
62
69
  attach(options: any, callback?: any, db?: any): void;
@@ -294,6 +294,32 @@ class Connection {
294
294
  msg.addBlr(pluginData); // Send the callback response data as a buffer
295
295
  this._socket.write(msg.getData());
296
296
  }
297
+ /**
298
+ * Send an out-of-band op_cancel packet (protocol 12+ / Firebird 2.5+).
299
+ * The server reads it asynchronously while an operation is executing and
300
+ * makes that operation fail with isc_cancelled (GDSCode.CANCELLED); the
301
+ * op_cancel packet itself has no response, so nothing is queued here.
302
+ */
303
+ cancelOperation(kind, callback) {
304
+ if (typeof kind === 'function') {
305
+ callback = kind;
306
+ kind = undefined;
307
+ }
308
+ kind = kind || const_1.default.fb_cancel_raise;
309
+ if (this._isClosed)
310
+ return this.throwClosed(callback);
311
+ if (!this.accept || this.accept.protocolVersion < const_1.default.PROTOCOL_VERSION12) {
312
+ (0, callback_1.doError)(new Error('Query cancellation requires protocol 12+ (Firebird 2.5 or newer)'), callback);
313
+ return;
314
+ }
315
+ var msg = this._msg;
316
+ msg.pos = 0;
317
+ msg.addInt(const_1.default.op_cancel);
318
+ msg.addInt(kind);
319
+ this._socket.write(msg.getData());
320
+ if (callback)
321
+ callback();
322
+ }
297
323
  _queueEvent(callback, defer = false) {
298
324
  var self = this;
299
325
  if (self._isClosed) {
@@ -104,6 +104,10 @@ declare const Const: Readonly<{
104
104
  DSQL_close: number;
105
105
  DSQL_drop: number;
106
106
  DSQL_unprepare: number;
107
+ fb_cancel_disable: number;
108
+ fb_cancel_enable: number;
109
+ fb_cancel_raise: number;
110
+ fb_cancel_abort: number;
107
111
  fetch_next: number;
108
112
  fetch_prior: number;
109
113
  fetch_first: number;
package/lib/wire/const.js CHANGED
@@ -117,6 +117,13 @@ const dsql = {
117
117
  DSQL_drop: 2,
118
118
  DSQL_unprepare: 4, // >: 2.5
119
119
  };
120
+ // fb_cancel_operation kinds sent with op_cancel (protocol 12+ / Firebird 2.5+)
121
+ const cancelKind = {
122
+ fb_cancel_disable: 1, // disable execution of fb_cancel_raise requests
123
+ fb_cancel_enable: 2, // re-enable delivery of cancel requests
124
+ fb_cancel_raise: 3, // cancel the operation currently executing on the attachment
125
+ fb_cancel_abort: 4, // forcibly abort the attachment's current activity
126
+ };
120
127
  const fetchOp = {
121
128
  fetch_next: 0,
122
129
  fetch_prior: 1,
@@ -794,6 +801,7 @@ const Const = Object.freeze({
794
801
  ...blr,
795
802
  ...blobType,
796
803
  ...buffer,
804
+ ...cancelKind,
797
805
  ...cnct,
798
806
  ...common,
799
807
  ...connect,
@@ -12,6 +12,14 @@ declare class Database extends Events.EventEmitter {
12
12
  sequentially(query: string, params?: any, on?: any, callback?: any, options?: any): this;
13
13
  query(query: string, params?: any, callback?: any, options?: any): this;
14
14
  drop(callback?: (err?: any) => void): void;
15
+ /**
16
+ * Cancel the operation currently executing on this connection by sending
17
+ * an out-of-band op_cancel (Firebird 2.5+ / protocol 12+). The cancelled
18
+ * operation fails through its own callback with err.gdscode ===
19
+ * GDSCode.CANCELLED. `kind` defaults to fb_cancel_raise; cancellation is
20
+ * per-attachment, not per-statement.
21
+ */
22
+ cancel(kind?: number | ((err?: any) => void), callback?: (err?: any) => void): this;
15
23
  attachEvent(callback: (err: any, evt?: any) => void): this;
16
24
  /**
17
25
  * Create a physical tablespace.
@@ -62,6 +70,7 @@ declare class Database extends Events.EventEmitter {
62
70
  detachAsync(force?: boolean): Promise<void>;
63
71
  dropAsync(): Promise<void>;
64
72
  attachEventAsync(): Promise<any>;
73
+ cancelAsync(kind?: number): Promise<void>;
65
74
  /**
66
75
  * Run `work` inside a transaction: commits when the returned promise
67
76
  * resolves, rolls back when it rejects (the original error is rethrown,
@@ -273,6 +273,21 @@ class Database extends events_1.default.EventEmitter {
273
273
  drop(callback) {
274
274
  return this.connection.dropDatabase(callback);
275
275
  }
276
+ /**
277
+ * Cancel the operation currently executing on this connection by sending
278
+ * an out-of-band op_cancel (Firebird 2.5+ / protocol 12+). The cancelled
279
+ * operation fails through its own callback with err.gdscode ===
280
+ * GDSCode.CANCELLED. `kind` defaults to fb_cancel_raise; cancellation is
281
+ * per-attachment, not per-statement.
282
+ */
283
+ cancel(kind, callback) {
284
+ if (typeof kind === 'function') {
285
+ callback = kind;
286
+ kind = undefined;
287
+ }
288
+ this.connection.cancelOperation(kind, callback);
289
+ return this;
290
+ }
276
291
  attachEvent(callback) {
277
292
  var self = this;
278
293
  const eventid = self.eventid++;
@@ -423,6 +438,10 @@ class Database extends events_1.default.EventEmitter {
423
438
  var self = this;
424
439
  return (0, callback_1.fromCallback)(function (cb) { self.attachEvent(cb); });
425
440
  }
441
+ cancelAsync(kind) {
442
+ var self = this;
443
+ return (0, callback_1.fromCallback)(function (cb) { self.cancel(kind, cb); });
444
+ }
426
445
  /**
427
446
  * Run `work` inside a transaction: commits when the returned promise
428
447
  * resolves, rolls back when it rejects (the original error is rethrown,
@@ -1,8 +1,3 @@
1
- /***************************************
2
- *
3
- * Transaction
4
- *
5
- ***************************************/
6
1
  declare class Transaction {
7
2
  connection: any;
8
3
  db: any;
@@ -10,6 +10,36 @@ const const_1 = __importDefault(require("./const"));
10
10
  * Transaction
11
11
  *
12
12
  ***************************************/
13
+ /** The error delivered when options.signal was already aborted on entry. */
14
+ function abortError(signal) {
15
+ if (signal && signal.reason instanceof Error)
16
+ return signal.reason;
17
+ var err = new Error('The operation was aborted');
18
+ err.name = 'AbortError';
19
+ err.code = 'ABORT_ERR';
20
+ return err;
21
+ }
22
+ /**
23
+ * Wire an AbortSignal to a running statement: on abort, send an out-of-band
24
+ * op_cancel so the server fails the executing operation with isc_cancelled
25
+ * (surfaced through the statement's own callback as err.gdscode ===
26
+ * GDSCode.CANCELLED). Returns the wrapped callback that detaches the
27
+ * listener once the operation settles.
28
+ */
29
+ function hookAbortSignal(connection, signal, callback) {
30
+ var settled = false;
31
+ var onAbort = function () {
32
+ if (!settled)
33
+ connection.cancelOperation(const_1.default.fb_cancel_raise);
34
+ };
35
+ signal.addEventListener('abort', onAbort, { once: true });
36
+ return function (err, result, meta, isSelect) {
37
+ settled = true;
38
+ signal.removeEventListener('abort', onAbort);
39
+ if (callback)
40
+ callback(err, result, meta, isSelect);
41
+ };
42
+ }
13
43
  class Transaction {
14
44
  constructor(connection) {
15
45
  this.connection = connection;
@@ -32,6 +62,14 @@ class Transaction {
32
62
  callback = params;
33
63
  params = undefined;
34
64
  }
65
+ var signal = options && options.signal;
66
+ if (signal) {
67
+ if (signal.aborted) {
68
+ (0, callback_1.doError)(abortError(signal), callback);
69
+ return;
70
+ }
71
+ callback = hookAbortSignal(this.connection, signal, callback);
72
+ }
35
73
  var self = this;
36
74
  this.newStatement(query, function (err, statement) {
37
75
  if (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
package/src/types.ts CHANGED
@@ -78,6 +78,15 @@ export type TransactionOptions = {
78
78
  export type QueryOptions = {
79
79
  timeout?: number;
80
80
  scrollable?: boolean;
81
+ /**
82
+ * Abort the query when the signal fires (Firebird 2.5+ / protocol 12+).
83
+ * If the signal is already aborted the query is not sent at all and the
84
+ * callback/promise fails with an `AbortError`. If it fires mid-flight an
85
+ * out-of-band op_cancel is sent and the query fails with
86
+ * `err.gdscode === GDSCode.CANCELLED`. Cancellation is per-attachment:
87
+ * it cancels whatever is currently executing on the connection.
88
+ */
89
+ signal?: AbortSignal;
81
90
  }
82
91
 
83
92
  export interface Database {
@@ -109,6 +118,14 @@ export interface Database {
109
118
  attachEventAsync(): Promise<any>;
110
119
  /** Starts a transaction, commits when `work` resolves, rolls back when it rejects. */
111
120
  withTransaction<T>(work: (transaction: Transaction) => Promise<T> | T, options?: TransactionOptions | Isolation): Promise<T>;
121
+ /**
122
+ * Cancel the operation currently executing on this connection
123
+ * (Firebird 2.5+ / protocol 12+). The cancelled operation fails through
124
+ * its own callback/promise with `err.gdscode === GDSCode.CANCELLED`.
125
+ */
126
+ cancel(callback?: SimpleCallback): Database;
127
+ cancel(kind: number, callback?: SimpleCallback): Database;
128
+ cancelAsync(kind?: number): Promise<void>;
112
129
  }
113
130
 
114
131
  export interface Transaction {
@@ -369,6 +369,38 @@ class Connection {
369
369
  }
370
370
 
371
371
 
372
+ /**
373
+ * Send an out-of-band op_cancel packet (protocol 12+ / Firebird 2.5+).
374
+ * The server reads it asynchronously while an operation is executing and
375
+ * makes that operation fail with isc_cancelled (GDSCode.CANCELLED); the
376
+ * op_cancel packet itself has no response, so nothing is queued here.
377
+ */
378
+ cancelOperation(kind, callback) {
379
+ if (typeof kind === 'function') {
380
+ callback = kind;
381
+ kind = undefined;
382
+ }
383
+ kind = kind || Const.fb_cancel_raise;
384
+
385
+ if (this._isClosed)
386
+ return this.throwClosed(callback);
387
+
388
+ if (!this.accept || this.accept.protocolVersion < Const.PROTOCOL_VERSION12) {
389
+ doError(new Error('Query cancellation requires protocol 12+ (Firebird 2.5 or newer)'), callback);
390
+ return;
391
+ }
392
+
393
+ var msg = this._msg;
394
+ msg.pos = 0;
395
+ msg.addInt(Const.op_cancel);
396
+ msg.addInt(kind);
397
+ this._socket.write(msg.getData());
398
+
399
+ if (callback)
400
+ callback();
401
+ }
402
+
403
+
372
404
  _queueEvent(callback, defer = false) {
373
405
  var self = this;
374
406
 
package/src/wire/const.ts CHANGED
@@ -132,6 +132,14 @@ const dsql = {
132
132
  DSQL_unprepare : 4, // >: 2.5
133
133
  };
134
134
 
135
+ // fb_cancel_operation kinds sent with op_cancel (protocol 12+ / Firebird 2.5+)
136
+ const cancelKind = {
137
+ fb_cancel_disable : 1, // disable execution of fb_cancel_raise requests
138
+ fb_cancel_enable : 2, // re-enable delivery of cancel requests
139
+ fb_cancel_raise : 3, // cancel the operation currently executing on the attachment
140
+ fb_cancel_abort : 4, // forcibly abort the attachment's current activity
141
+ };
142
+
135
143
  const fetchOp = {
136
144
  fetch_next : 0,
137
145
  fetch_prior : 1,
@@ -859,6 +867,7 @@ const Const = Object.freeze({
859
867
  ...blr,
860
868
  ...blobType,
861
869
  ...buffer,
870
+ ...cancelKind,
862
871
  ...cnct,
863
872
  ...common,
864
873
  ...connect,
@@ -322,6 +322,22 @@ class Database extends Events.EventEmitter {
322
322
  return this.connection.dropDatabase(callback);
323
323
  }
324
324
 
325
+ /**
326
+ * Cancel the operation currently executing on this connection by sending
327
+ * an out-of-band op_cancel (Firebird 2.5+ / protocol 12+). The cancelled
328
+ * operation fails through its own callback with err.gdscode ===
329
+ * GDSCode.CANCELLED. `kind` defaults to fb_cancel_raise; cancellation is
330
+ * per-attachment, not per-statement.
331
+ */
332
+ cancel(kind?: number | ((err?: any) => void), callback?: (err?: any) => void): this {
333
+ if (typeof kind === 'function') {
334
+ callback = kind;
335
+ kind = undefined;
336
+ }
337
+ this.connection.cancelOperation(kind, callback);
338
+ return this;
339
+ }
340
+
325
341
  attachEvent(callback: (err: any, evt?: any) => void): this {
326
342
  var self = this;
327
343
  const eventid = self.eventid++;
@@ -496,6 +512,11 @@ class Database extends Events.EventEmitter {
496
512
  return fromCallback(function(cb) { self.attachEvent(cb); });
497
513
  }
498
514
 
515
+ cancelAsync(kind?: number): Promise<void> {
516
+ var self = this;
517
+ return fromCallback(function(cb) { self.cancel(kind, cb); });
518
+ }
519
+
499
520
  /**
500
521
  * Run `work` inside a transaction: commits when the returned promise
501
522
  * resolves, rolls back when it rejects (the original error is rethrown,
@@ -8,6 +8,38 @@ import Const from './const';
8
8
  *
9
9
  ***************************************/
10
10
 
11
+ /** The error delivered when options.signal was already aborted on entry. */
12
+ function abortError(signal: any): Error {
13
+ if (signal && signal.reason instanceof Error)
14
+ return signal.reason;
15
+ var err: any = new Error('The operation was aborted');
16
+ err.name = 'AbortError';
17
+ err.code = 'ABORT_ERR';
18
+ return err;
19
+ }
20
+
21
+ /**
22
+ * Wire an AbortSignal to a running statement: on abort, send an out-of-band
23
+ * op_cancel so the server fails the executing operation with isc_cancelled
24
+ * (surfaced through the statement's own callback as err.gdscode ===
25
+ * GDSCode.CANCELLED). Returns the wrapped callback that detaches the
26
+ * listener once the operation settles.
27
+ */
28
+ function hookAbortSignal(connection: any, signal: any, callback: any): any {
29
+ var settled = false;
30
+ var onAbort = function() {
31
+ if (!settled)
32
+ connection.cancelOperation(Const.fb_cancel_raise);
33
+ };
34
+ signal.addEventListener('abort', onAbort, { once: true });
35
+ return function(err?: any, result?: any, meta?: any, isSelect?: boolean) {
36
+ settled = true;
37
+ signal.removeEventListener('abort', onAbort);
38
+ if (callback)
39
+ callback(err, result, meta, isSelect);
40
+ };
41
+ }
42
+
11
43
  class Transaction {
12
44
  connection: any;
13
45
  db: any;
@@ -38,6 +70,15 @@ class Transaction {
38
70
  params = undefined;
39
71
  }
40
72
 
73
+ var signal = options && options.signal;
74
+ if (signal) {
75
+ if (signal.aborted) {
76
+ doError(abortError(signal), callback);
77
+ return;
78
+ }
79
+ callback = hookAbortSignal(this.connection, signal, callback);
80
+ }
81
+
41
82
  var self = this;
42
83
  this.newStatement(query, function(err: any, statement: any) {
43
84