postgrejs 3.0.1 → 3.0.2
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/CHANGELOG.md +20 -1
- package/README.md +1 -1
- package/connection/connection.d.ts +17 -3
- package/connection/connection.js +31 -16
- package/connection/intl-connection.d.ts +45 -2
- package/connection/intl-connection.js +109 -36
- package/connection/prepared-statement.js +12 -12
- package/data-types/numeric-type.js +10 -5
- package/interfaces/command-result.d.ts +2 -1
- package/interfaces/database-connection-params.d.ts +13 -0
- package/interfaces/query-options.d.ts +6 -0
- package/interfaces/script-execute-options.d.ts +6 -0
- package/interfaces/script-result.d.ts +3 -2
- package/package.json +1 -1
- package/protocol/frontend.js +13 -5
- package/protocol/sasl.js +1 -1
- package/util/abort-signal.d.ts +10 -0
- package/util/abort-signal.js +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.0.
|
|
3
|
+
### [v3.0.2](https://github.com/panates/postgrejs/compare/v3.0.1...v3.0.2) -
|
|
4
|
+
|
|
5
|
+
#### 🚀 New Features
|
|
6
|
+
|
|
7
|
+
- feat: add Docker setup for PostgreSQL with SSL support @Eray Hanoğlu
|
|
8
|
+
- feat: support nested startTransaction()/commit() and savepoint()/releaseSavepoint() @Eray Hanoğlu
|
|
9
|
+
- feat: make execute()/query() timing an opt-in, default-off option @Eray Hanoğlu
|
|
10
|
+
|
|
11
|
+
#### 🪲 Fixes
|
|
12
|
+
|
|
13
|
+
- fix: raise warmup for Simple Query benchmark, regenerate BENCHMARKS.md @Eray Hanoğlu
|
|
14
|
+
- fix: enlarge benchmark charts so GitHub's Mermaid overlay doesn't swallow them @Eray Hanoğlu
|
|
15
|
+
- fix: replace deprecated substr() with substring() @Eray Hanoğlu
|
|
16
|
+
|
|
17
|
+
#### ⚡️ Performance Improvments
|
|
18
|
+
|
|
19
|
+
- perf: remove needless async wrapping and coercion from execute()/query() hot path @Eray Hanoğlu
|
|
20
|
+
- perf: hoist loop-invariant reads in Frontend/numeric encoders @Eray Hanoğlu
|
|
21
|
+
|
|
22
|
+
### [v3.0.1](https://github.com/panates/postgrejs/compare/v3.0.0...v3.0.1) - 7 September 2026
|
|
4
23
|
|
|
5
24
|
- Dev
|
|
6
25
|
- Dev
|
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
|
|
|
126
126
|
| Per-query type mapping | ✅ | ❌ <sup>6</sup> | ❌ <sup>6</sup> |
|
|
127
127
|
| Query cancellation | ✅ AbortSignal | ✅ | ✅ |
|
|
128
128
|
| Per-query timeout | ✅ AbortSignal | ✅ | ❌ <sup>7</sup> |
|
|
129
|
-
| Reference counters | Connection
|
|
129
|
+
| Reference counters | Connection / Statement | ❌ | ❌ |
|
|
130
130
|
| Caller kept in async error stacks | ✅ | 🟡 <sup>8</sup> | 🟡 <sup>9</sup> |
|
|
131
131
|
| Error located in the SQL text | ✅ line and mark | 🟡 offset | 🟡 offset |
|
|
132
132
|
| TC39 Explicit Resource Management | ✅ | ❌ | ❌ |
|
|
@@ -157,9 +157,11 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
|
|
|
157
157
|
*/
|
|
158
158
|
startTransaction(): Promise<void>;
|
|
159
159
|
/**
|
|
160
|
-
* Commits current transaction
|
|
160
|
+
* Commits current transaction.
|
|
161
|
+
* @param immediate - Commits right away, ignoring how many nested
|
|
162
|
+
* startTransaction() calls are still unmatched by a commit().
|
|
161
163
|
*/
|
|
162
|
-
commit(): Promise<void>;
|
|
164
|
+
commit(immediate?: boolean): Promise<void>;
|
|
163
165
|
/**
|
|
164
166
|
* Rolls back current transaction
|
|
165
167
|
*/
|
|
@@ -206,8 +208,11 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
|
|
|
206
208
|
/**
|
|
207
209
|
* Releases savepoint
|
|
208
210
|
* @param name {string} - Name of the savepoint
|
|
211
|
+
* @param immediate - Releases right away, ignoring how many nested
|
|
212
|
+
* savepoint() calls under this name are still unmatched by a
|
|
213
|
+
* releaseSavepoint().
|
|
209
214
|
*/
|
|
210
|
-
releaseSavepoint(name: string): Promise<void>;
|
|
215
|
+
releaseSavepoint(name: string, immediate?: boolean): Promise<void>;
|
|
211
216
|
listen(channel: string, callback: NotificationCallback): Promise<void>;
|
|
212
217
|
unListen(channel: string): Promise<void>;
|
|
213
218
|
unListenAll(): Promise<void>;
|
|
@@ -242,6 +247,15 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
|
|
|
242
247
|
* `asyncErrorHandling` is the per-call override (execute()/query()'s own
|
|
243
248
|
* option of the same name); when omitted, the connection's own
|
|
244
249
|
* `DatabaseConnectionParams.asyncErrorHandling` decides.
|
|
250
|
+
*
|
|
251
|
+
* Not `async` itself, deliberately: every branch below already returns a
|
|
252
|
+
* promise directly (`promise` itself, or `promise.catch(...)`'s own
|
|
253
|
+
* derived one) with no `await` in between, so there is nothing here that
|
|
254
|
+
* needs the function to be a coroutine. Marking it `async` anyway would
|
|
255
|
+
* still be correct, but the spec requires wrapping whatever an async
|
|
256
|
+
* function returns through a promise-resolve step, a genuine extra
|
|
257
|
+
* microtask tick on top of the one `promise`/`.catch()` already goes
|
|
258
|
+
* through - paid on every single query this wraps, disabled or not.
|
|
245
259
|
*/
|
|
246
260
|
protected _captureErrorStack<T>(promise: Promise<T>, entry?: (...args: any[]) => any, asyncErrorHandling?: boolean): Promise<T>;
|
|
247
261
|
[Symbol.asyncDispose](): Promise<void>;
|
package/connection/connection.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { coerceToBoolean } from 'putil-varhelpers';
|
|
2
1
|
import { ConnectionState, DataTypeOIDs } from '../constants.js';
|
|
3
2
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
4
3
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
@@ -141,7 +140,8 @@ export class Connection extends SafeEventEmitter {
|
|
|
141
140
|
async execute(sql, options) {
|
|
142
141
|
if (typeof sql === 'object' && sql instanceof QueryRequest)
|
|
143
142
|
sql = sql.stringify({ ...options, typeMap: options?.typeMap });
|
|
144
|
-
this.
|
|
143
|
+
if (this.listenerCount('execute'))
|
|
144
|
+
this.emit('execute', sql, options);
|
|
145
145
|
return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._captureErrorStack(this._intlCon.execute(sql, options), this.execute, options?.asyncErrorHandling).catch((e) => {
|
|
146
146
|
throw this._handleError(e, sql);
|
|
147
147
|
}));
|
|
@@ -164,7 +164,8 @@ export class Connection extends SafeEventEmitter {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
/* c8 ignore stop */
|
|
167
|
-
this.
|
|
167
|
+
if (this.listenerCount('query'))
|
|
168
|
+
this.emit('query', sql, options);
|
|
168
169
|
return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._query(sql, options));
|
|
169
170
|
}
|
|
170
171
|
/**
|
|
@@ -198,7 +199,8 @@ export class Connection extends SafeEventEmitter {
|
|
|
198
199
|
});
|
|
199
200
|
}
|
|
200
201
|
/* c8 ignore stop */
|
|
201
|
-
this.
|
|
202
|
+
if (this.listenerCount('execute'))
|
|
203
|
+
this.emit('execute', sql);
|
|
202
204
|
return await this._captureErrorStack(this._intlCon.copyTo(sql)).catch((e) => {
|
|
203
205
|
throw this._handleError(e, sql);
|
|
204
206
|
});
|
|
@@ -231,7 +233,8 @@ export class Connection extends SafeEventEmitter {
|
|
|
231
233
|
});
|
|
232
234
|
}
|
|
233
235
|
/* c8 ignore stop */
|
|
234
|
-
this.
|
|
236
|
+
if (this.listenerCount('execute'))
|
|
237
|
+
this.emit('execute', sql);
|
|
235
238
|
return await this._captureErrorStack(this._intlCon.copyFrom(sql)).catch((e) => {
|
|
236
239
|
throw this._handleError(e, sql);
|
|
237
240
|
});
|
|
@@ -313,10 +316,12 @@ export class Connection extends SafeEventEmitter {
|
|
|
313
316
|
return this._captureErrorStack(this._intlCon.startTransaction());
|
|
314
317
|
}
|
|
315
318
|
/**
|
|
316
|
-
* Commits current transaction
|
|
319
|
+
* Commits current transaction.
|
|
320
|
+
* @param immediate - Commits right away, ignoring how many nested
|
|
321
|
+
* startTransaction() calls are still unmatched by a commit().
|
|
317
322
|
*/
|
|
318
|
-
commit() {
|
|
319
|
-
return this._captureErrorStack(this._intlCon.commit());
|
|
323
|
+
commit(immediate) {
|
|
324
|
+
return this._captureErrorStack(this._intlCon.commit(immediate));
|
|
320
325
|
}
|
|
321
326
|
/**
|
|
322
327
|
* Rolls back current transaction
|
|
@@ -378,9 +383,12 @@ export class Connection extends SafeEventEmitter {
|
|
|
378
383
|
/**
|
|
379
384
|
* Releases savepoint
|
|
380
385
|
* @param name {string} - Name of the savepoint
|
|
386
|
+
* @param immediate - Releases right away, ignoring how many nested
|
|
387
|
+
* savepoint() calls under this name are still unmatched by a
|
|
388
|
+
* releaseSavepoint().
|
|
381
389
|
*/
|
|
382
|
-
releaseSavepoint(name) {
|
|
383
|
-
return this._captureErrorStack(this._intlCon.releaseSavepoint(name));
|
|
390
|
+
releaseSavepoint(name, immediate) {
|
|
391
|
+
return this._captureErrorStack(this._intlCon.releaseSavepoint(name, immediate));
|
|
384
392
|
}
|
|
385
393
|
async listen(channel, callback) {
|
|
386
394
|
if (!/^[A-Z]\w+$/i.test(channel))
|
|
@@ -420,8 +428,8 @@ export class Connection extends SafeEventEmitter {
|
|
|
420
428
|
? options.autoCommit
|
|
421
429
|
: this._intlCon.config.autoCommit;
|
|
422
430
|
if (!options?.cursor &&
|
|
423
|
-
|
|
424
|
-
|
|
431
|
+
effectiveAutoCommit !== false &&
|
|
432
|
+
!this._intlCon.inTransaction) {
|
|
425
433
|
const params = options?.params?.map(prm => prm instanceof BindParam ? prm.value : prm);
|
|
426
434
|
return await this._captureErrorStack(this._intlCon.queryOnce(sql, paramTypes, params, options || {}), this.query, options?.asyncErrorHandling).catch((e) => {
|
|
427
435
|
throw this._handleError(e, sql);
|
|
@@ -509,11 +517,18 @@ export class Connection extends SafeEventEmitter {
|
|
|
509
517
|
* `asyncErrorHandling` is the per-call override (execute()/query()'s own
|
|
510
518
|
* option of the same name); when omitted, the connection's own
|
|
511
519
|
* `DatabaseConnectionParams.asyncErrorHandling` decides.
|
|
520
|
+
*
|
|
521
|
+
* Not `async` itself, deliberately: every branch below already returns a
|
|
522
|
+
* promise directly (`promise` itself, or `promise.catch(...)`'s own
|
|
523
|
+
* derived one) with no `await` in between, so there is nothing here that
|
|
524
|
+
* needs the function to be a coroutine. Marking it `async` anyway would
|
|
525
|
+
* still be correct, but the spec requires wrapping whatever an async
|
|
526
|
+
* function returns through a promise-resolve step, a genuine extra
|
|
527
|
+
* microtask tick on top of the one `promise`/`.catch()` already goes
|
|
528
|
+
* through - paid on every single query this wraps, disabled or not.
|
|
512
529
|
*/
|
|
513
|
-
|
|
514
|
-
const enabled = asyncErrorHandling
|
|
515
|
-
? asyncErrorHandling
|
|
516
|
-
: coerceToBoolean(this._intlCon.config.asyncErrorHandling, true);
|
|
530
|
+
_captureErrorStack(promise, entry, asyncErrorHandling) {
|
|
531
|
+
const enabled = asyncErrorHandling ?? this._intlCon.config.asyncErrorHandling ?? true;
|
|
517
532
|
if (!enabled)
|
|
518
533
|
return promise;
|
|
519
534
|
const stackHolder = {};
|
|
@@ -13,6 +13,8 @@ import { CopyFromStream, CopyToStream } from './copy-stream.js';
|
|
|
13
13
|
export declare class IntlConnection extends SafeEventEmitter {
|
|
14
14
|
private _executeReusedParserCache;
|
|
15
15
|
protected _refCount: number;
|
|
16
|
+
protected _transactionDepth: number;
|
|
17
|
+
protected _savepointDepths: Map<string, number>;
|
|
16
18
|
protected _config: ConnectionConfiguration;
|
|
17
19
|
protected _onErrorSavePoint: string;
|
|
18
20
|
transactionStatus: string;
|
|
@@ -30,9 +32,45 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
30
32
|
connect(): Promise<void>;
|
|
31
33
|
close(): Promise<void>;
|
|
32
34
|
execute(sql: string, options?: ScriptExecuteOptions, cb?: (event: string, ...args: any[]) => void): Promise<ScriptResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Starts a transaction, or - if one is already running - marks a nested
|
|
37
|
+
* level of it. Each call increments `_transactionDepth`; only the
|
|
38
|
+
* outermost one actually sends BEGIN (`inTransaction` is already false at
|
|
39
|
+
* that point). A matching number of commit() calls is then needed to
|
|
40
|
+
* actually commit - see commit().
|
|
41
|
+
*/
|
|
33
42
|
startTransaction(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Starts a savepoint, or - if one under the same `name` is already open -
|
|
45
|
+
* marks a nested level of it. Mirrors startTransaction(): only the
|
|
46
|
+
* outermost call for a given name actually sends SAVEPOINT: a savepoint
|
|
47
|
+
* already exists under that name, re-declaring it would just stack a
|
|
48
|
+
* second, independent one with the same name on the server rather than
|
|
49
|
+
* nesting the existing one.
|
|
50
|
+
*/
|
|
34
51
|
savepoint(name: string): Promise<void>;
|
|
35
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Commits the transaction started by startTransaction() - or, when nested,
|
|
54
|
+
* just unwinds one level of `_transactionDepth`. The actual COMMIT is only
|
|
55
|
+
* sent once the depth reaches zero, i.e. once every startTransaction()
|
|
56
|
+
* call has a matching commit().
|
|
57
|
+
* @param immediate - Ignores `_transactionDepth` and commits right away,
|
|
58
|
+
* regardless of how many nested levels are still open.
|
|
59
|
+
*/
|
|
60
|
+
commit(immediate?: boolean): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Releases the savepoint created by savepoint() - or, when nested, just
|
|
63
|
+
* unwinds one level of that name's depth. The actual RELEASE SAVEPOINT is
|
|
64
|
+
* only sent once that depth reaches zero.
|
|
65
|
+
* @param name - Name of the savepoint.
|
|
66
|
+
* @param immediate - Ignores the tracked depth and releases right away.
|
|
67
|
+
*/
|
|
68
|
+
releaseSavepoint(name: string, immediate?: boolean): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Rolls back the whole transaction, regardless of `_transactionDepth` -
|
|
71
|
+
* a rollback ends the transaction outright, so there is nothing left for
|
|
72
|
+
* any pending, still-nested commit() call to unwind.
|
|
73
|
+
*/
|
|
36
74
|
rollback(): Promise<void>;
|
|
37
75
|
/**
|
|
38
76
|
* Ends the current transaction as a prepared one: it stops being tied to
|
|
@@ -53,8 +91,13 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
53
91
|
commitPrepared(name: string): Promise<void>;
|
|
54
92
|
/** Discards a transaction left waiting by prepareTransaction(). */
|
|
55
93
|
rollbackPrepared(name: string): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* Rolls back to the given savepoint, regardless of its tracked depth - a
|
|
96
|
+
* rollback discards it (and everything after it) outright, so there is
|
|
97
|
+
* nothing left for any pending, still-nested releaseSavepoint() call to
|
|
98
|
+
* unwind.
|
|
99
|
+
*/
|
|
56
100
|
rollbackToSavepoint(name: string): Promise<void>;
|
|
57
|
-
releaseSavepoint(name: string): Promise<void>;
|
|
58
101
|
/** Asks the server to cancel whatever this session is running. */
|
|
59
102
|
cancel(): Promise<void>;
|
|
60
103
|
ref(): void;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
|
-
import { coerceToBoolean } from 'putil-varhelpers';
|
|
3
2
|
import { ConnectionState, DEFAULT_COLUMN_FORMAT } from '../constants.js';
|
|
4
3
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
5
4
|
import { PgSocket } from '../protocol/pg-socket.js';
|
|
@@ -29,6 +28,8 @@ function columnFormatsEqual(a, b) {
|
|
|
29
28
|
export class IntlConnection extends SafeEventEmitter {
|
|
30
29
|
_executeReusedParserCache = new WeakMap();
|
|
31
30
|
_refCount = 0;
|
|
31
|
+
_transactionDepth = 0;
|
|
32
|
+
_savepointDepths = new Map();
|
|
32
33
|
_config;
|
|
33
34
|
_onErrorSavePoint;
|
|
34
35
|
transactionStatus = 'I';
|
|
@@ -109,58 +110,122 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
109
110
|
});
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
|
-
async execute(sql, options, cb) {
|
|
113
|
+
async execute(sql, options = {}, cb) {
|
|
113
114
|
this.assertConnected();
|
|
114
115
|
const transactionCommand = sql.match(/^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i);
|
|
115
116
|
let beginFirst = false;
|
|
116
117
|
let commitLast = false;
|
|
118
|
+
const { autoCommit } = options;
|
|
117
119
|
if (!transactionCommand) {
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
? options?.autoCommit
|
|
121
|
-
: this.config.autoCommit) === false) {
|
|
120
|
+
if ((autoCommit != null ? autoCommit : this.config.autoCommit) === false &&
|
|
121
|
+
!this.inTransaction) {
|
|
122
122
|
beginFirst = true;
|
|
123
123
|
}
|
|
124
|
-
if (this.inTransaction
|
|
124
|
+
if (autoCommit && this.inTransaction)
|
|
125
125
|
commitLast = true;
|
|
126
126
|
}
|
|
127
127
|
if (beginFirst)
|
|
128
128
|
await this._execute('BEGIN');
|
|
129
|
+
// this.inTransaction goes first here, before the config fallback: the
|
|
130
|
+
// result is only ever consulted below when it's true, so checking it
|
|
131
|
+
// first short-circuits that read on every call made outside of a
|
|
132
|
+
// transaction, the common case. Below, `rollbackOnError &&
|
|
133
|
+
// this.inTransaction` deliberately puts it last instead - re-checking
|
|
134
|
+
// live state a plain local boolean can't capture (the awaited call in
|
|
135
|
+
// between may have committed, rolled back, or errored the transaction
|
|
136
|
+
// out from under it) - but ordered so the cheap local read still
|
|
137
|
+
// short-circuits the getter whenever rollbackOnError is already
|
|
138
|
+
// false, which it is outside a transaction.
|
|
129
139
|
const rollbackOnError = !transactionCommand &&
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (this.inTransaction && rollbackOnError)
|
|
140
|
+
this.inTransaction &&
|
|
141
|
+
(options.rollbackOnError ?? this.config.rollbackOnError ?? true);
|
|
142
|
+
if (rollbackOnError && this.inTransaction)
|
|
134
143
|
await this._execute('SAVEPOINT ' + this._onErrorSavePoint);
|
|
135
144
|
try {
|
|
136
145
|
const result = await this._execute(sql, options, cb);
|
|
137
146
|
if (commitLast)
|
|
138
147
|
await this._execute('COMMIT');
|
|
139
|
-
else if (this.inTransaction
|
|
148
|
+
else if (rollbackOnError && this.inTransaction) {
|
|
140
149
|
await this._execute('RELEASE ' + this._onErrorSavePoint + ';');
|
|
141
150
|
}
|
|
142
151
|
return result;
|
|
143
152
|
}
|
|
144
153
|
catch (e) {
|
|
145
|
-
if (this.inTransaction
|
|
154
|
+
if (rollbackOnError && this.inTransaction)
|
|
146
155
|
await this._execute('ROLLBACK TO ' + this._onErrorSavePoint + ';');
|
|
147
156
|
throw e;
|
|
148
157
|
}
|
|
149
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Starts a transaction, or - if one is already running - marks a nested
|
|
161
|
+
* level of it. Each call increments `_transactionDepth`; only the
|
|
162
|
+
* outermost one actually sends BEGIN (`inTransaction` is already false at
|
|
163
|
+
* that point). A matching number of commit() calls is then needed to
|
|
164
|
+
* actually commit - see commit().
|
|
165
|
+
*/
|
|
150
166
|
async startTransaction() {
|
|
167
|
+
this._transactionDepth++;
|
|
151
168
|
if (!this.inTransaction)
|
|
152
169
|
await this.execute('BEGIN');
|
|
153
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Starts a savepoint, or - if one under the same `name` is already open -
|
|
173
|
+
* marks a nested level of it. Mirrors startTransaction(): only the
|
|
174
|
+
* outermost call for a given name actually sends SAVEPOINT: a savepoint
|
|
175
|
+
* already exists under that name, re-declaring it would just stack a
|
|
176
|
+
* second, independent one with the same name on the server rather than
|
|
177
|
+
* nesting the existing one.
|
|
178
|
+
*/
|
|
154
179
|
async savepoint(name) {
|
|
155
180
|
if (!(name && name.match(/^[a-zA-Z]\w+$/)))
|
|
156
181
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
157
|
-
|
|
182
|
+
const depth = (this._savepointDepths.get(name) || 0) + 1;
|
|
183
|
+
this._savepointDepths.set(name, depth);
|
|
184
|
+
if (depth === 1)
|
|
185
|
+
await this.execute('BEGIN; SAVEPOINT ' + name);
|
|
158
186
|
}
|
|
159
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Commits the transaction started by startTransaction() - or, when nested,
|
|
189
|
+
* just unwinds one level of `_transactionDepth`. The actual COMMIT is only
|
|
190
|
+
* sent once the depth reaches zero, i.e. once every startTransaction()
|
|
191
|
+
* call has a matching commit().
|
|
192
|
+
* @param immediate - Ignores `_transactionDepth` and commits right away,
|
|
193
|
+
* regardless of how many nested levels are still open.
|
|
194
|
+
*/
|
|
195
|
+
async commit(immediate) {
|
|
196
|
+
if (!immediate && this._transactionDepth > 1) {
|
|
197
|
+
this._transactionDepth--;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this._transactionDepth = 0;
|
|
160
201
|
if (this.inTransaction)
|
|
161
202
|
await this.execute('COMMIT');
|
|
162
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Releases the savepoint created by savepoint() - or, when nested, just
|
|
206
|
+
* unwinds one level of that name's depth. The actual RELEASE SAVEPOINT is
|
|
207
|
+
* only sent once that depth reaches zero.
|
|
208
|
+
* @param name - Name of the savepoint.
|
|
209
|
+
* @param immediate - Ignores the tracked depth and releases right away.
|
|
210
|
+
*/
|
|
211
|
+
async releaseSavepoint(name, immediate) {
|
|
212
|
+
if (!(name && name.match(/^[a-zA-Z]\w+$/)))
|
|
213
|
+
throw new Error(`Invalid savepoint "${name}"`);
|
|
214
|
+
const depth = this._savepointDepths.get(name) || 0;
|
|
215
|
+
if (!immediate && depth > 1) {
|
|
216
|
+
this._savepointDepths.set(name, depth - 1);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this._savepointDepths.delete(name);
|
|
220
|
+
await this.execute('RELEASE SAVEPOINT ' + name, { autoCommit: false });
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Rolls back the whole transaction, regardless of `_transactionDepth` -
|
|
224
|
+
* a rollback ends the transaction outright, so there is nothing left for
|
|
225
|
+
* any pending, still-nested commit() call to unwind.
|
|
226
|
+
*/
|
|
163
227
|
async rollback() {
|
|
228
|
+
this._transactionDepth = 0;
|
|
164
229
|
if (this.inTransaction)
|
|
165
230
|
await this.execute('ROLLBACK');
|
|
166
231
|
}
|
|
@@ -189,16 +254,18 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
189
254
|
async rollbackPrepared(name) {
|
|
190
255
|
await this._execute('ROLLBACK PREPARED ' + escapeLiteral(name));
|
|
191
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Rolls back to the given savepoint, regardless of its tracked depth - a
|
|
259
|
+
* rollback discards it (and everything after it) outright, so there is
|
|
260
|
+
* nothing left for any pending, still-nested releaseSavepoint() call to
|
|
261
|
+
* unwind.
|
|
262
|
+
*/
|
|
192
263
|
async rollbackToSavepoint(name) {
|
|
193
264
|
if (!(name && name.match(/^[a-zA-Z]\w+$/)))
|
|
194
265
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
266
|
+
this._savepointDepths.delete(name);
|
|
195
267
|
await this.execute('ROLLBACK TO SAVEPOINT ' + name, { autoCommit: false });
|
|
196
268
|
}
|
|
197
|
-
async releaseSavepoint(name) {
|
|
198
|
-
if (!(name && name.match(/^[a-zA-Z]\w+$/)))
|
|
199
|
-
throw new Error(`Invalid savepoint "${name}"`);
|
|
200
|
-
await this.execute('RELEASE SAVEPOINT ' + name, { autoCommit: false });
|
|
201
|
-
}
|
|
202
269
|
/** Asks the server to cancel whatever this session is running. */
|
|
203
270
|
cancel() {
|
|
204
271
|
return this.socket.cancel();
|
|
@@ -257,22 +324,21 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
257
324
|
await stream.waitStarted();
|
|
258
325
|
return stream;
|
|
259
326
|
}
|
|
260
|
-
async _execute(sql, options, cb) {
|
|
327
|
+
async _execute(sql, options = {}, cb) {
|
|
261
328
|
this.ref();
|
|
262
329
|
try {
|
|
263
|
-
const
|
|
330
|
+
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
331
|
+
const startTime = timingEnabled ? performance.now() : 0;
|
|
264
332
|
const result = {
|
|
265
333
|
totalCommands: 0,
|
|
266
|
-
totalTime: 0,
|
|
267
334
|
results: [],
|
|
268
335
|
};
|
|
269
|
-
const opts = options || {};
|
|
270
336
|
let currentStart = startTime;
|
|
271
337
|
let parsers;
|
|
272
338
|
let current = { command: undefined };
|
|
273
339
|
let fields;
|
|
274
340
|
let error;
|
|
275
|
-
const typeMap =
|
|
341
|
+
const typeMap = options.typeMap || GlobalTypeMap;
|
|
276
342
|
this.runningQueryCount++;
|
|
277
343
|
return await this.socket.sendQueryMessage(sql, (code, msg, done) => {
|
|
278
344
|
switch (code) {
|
|
@@ -301,9 +367,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
301
367
|
break;
|
|
302
368
|
case Protocol.BackendMessageCode.DataRow:
|
|
303
369
|
{
|
|
304
|
-
const row =
|
|
305
|
-
? parseObjectRow(parsers, msg.data, msg.columnCount,
|
|
306
|
-
: parseRow(parsers, msg.data, msg.columnCount,
|
|
370
|
+
const row = options.objectRows && current.fields
|
|
371
|
+
? parseObjectRow(parsers, msg.data, msg.columnCount, options, current.fields)
|
|
372
|
+
: parseRow(parsers, msg.data, msg.columnCount, options);
|
|
307
373
|
if (cb)
|
|
308
374
|
cb('row', row);
|
|
309
375
|
current.rows = current.rows || [];
|
|
@@ -318,15 +384,17 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
318
384
|
current.command === 'UPDATE') {
|
|
319
385
|
current.rowsAffected = msg.rowCount;
|
|
320
386
|
}
|
|
321
|
-
|
|
387
|
+
if (timingEnabled)
|
|
388
|
+
current.executeTime = performance.now() - currentStart;
|
|
322
389
|
if (current.rows)
|
|
323
390
|
current.rowType =
|
|
324
|
-
|
|
391
|
+
options.objectRows && current.fields ? 'object' : 'array';
|
|
325
392
|
result.results.push(current);
|
|
326
393
|
if (cb)
|
|
327
394
|
cb('command-complete', current);
|
|
328
395
|
current = { command: undefined };
|
|
329
|
-
|
|
396
|
+
if (timingEnabled)
|
|
397
|
+
currentStart = performance.now();
|
|
330
398
|
break;
|
|
331
399
|
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
332
400
|
this.transactionStatus = msg.status;
|
|
@@ -334,7 +402,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
334
402
|
done(error);
|
|
335
403
|
break;
|
|
336
404
|
}
|
|
337
|
-
|
|
405
|
+
if (timingEnabled)
|
|
406
|
+
result.totalTime = performance.now() - startTime;
|
|
338
407
|
// Ignore COMMIT command that we added to sql
|
|
339
408
|
result.totalCommands = result.results.length;
|
|
340
409
|
done(undefined, result);
|
|
@@ -361,7 +430,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
361
430
|
this.ref();
|
|
362
431
|
try {
|
|
363
432
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
364
|
-
const
|
|
433
|
+
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
434
|
+
const startTime = timingEnabled ? performance.now() : 0;
|
|
365
435
|
const result = { command: undefined };
|
|
366
436
|
const rows = [];
|
|
367
437
|
let parsers;
|
|
@@ -428,7 +498,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
428
498
|
result.command === 'UPDATE') {
|
|
429
499
|
result.rowsAffected = commandTag?.rowCount;
|
|
430
500
|
}
|
|
431
|
-
|
|
501
|
+
if (timingEnabled)
|
|
502
|
+
result.executeTime = performance.now() - startTime;
|
|
432
503
|
return result;
|
|
433
504
|
}
|
|
434
505
|
finally {
|
|
@@ -498,7 +569,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
498
569
|
this.ref();
|
|
499
570
|
try {
|
|
500
571
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
501
|
-
const
|
|
572
|
+
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
573
|
+
const startTime = timingEnabled ? performance.now() : 0;
|
|
502
574
|
const result = { command: undefined };
|
|
503
575
|
const rows = [];
|
|
504
576
|
let parsers;
|
|
@@ -591,7 +663,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
591
663
|
result.command === 'UPDATE') {
|
|
592
664
|
result.rowsAffected = commandTag?.rowCount;
|
|
593
665
|
}
|
|
594
|
-
|
|
666
|
+
if (timingEnabled)
|
|
667
|
+
result.executeTime = performance.now() - startTime;
|
|
595
668
|
return result;
|
|
596
669
|
}
|
|
597
670
|
finally {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { coerceToBoolean } from 'putil-varhelpers';
|
|
2
1
|
import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
|
|
3
2
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
4
3
|
import { Protocol } from '../protocol/protocol.js';
|
|
@@ -60,35 +59,36 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
60
59
|
const transactionCommand = this.sql.match(/^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i);
|
|
61
60
|
let beginFirst = false;
|
|
62
61
|
let commitLast = false;
|
|
62
|
+
const autoCommit = options?.autoCommit;
|
|
63
63
|
if (!transactionCommand) {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: intlCon.config.autoCommit) === false) {
|
|
64
|
+
if ((autoCommit != null ? autoCommit : intlCon.config.autoCommit) ===
|
|
65
|
+
false &&
|
|
66
|
+
!intlCon.inTransaction) {
|
|
68
67
|
beginFirst = true;
|
|
69
68
|
}
|
|
70
|
-
if (intlCon.inTransaction
|
|
69
|
+
if (autoCommit && intlCon.inTransaction)
|
|
71
70
|
commitLast = true;
|
|
72
71
|
}
|
|
73
72
|
if (beginFirst)
|
|
74
73
|
await intlCon.execute('BEGIN');
|
|
74
|
+
// See IntlConnection.execute()'s own rollbackOnError for why
|
|
75
|
+
// intlCon.inTransaction goes first here but last in the checks below.
|
|
75
76
|
const rollbackOnError = !transactionCommand &&
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (intlCon.inTransaction && rollbackOnError)
|
|
77
|
+
intlCon.inTransaction &&
|
|
78
|
+
(options?.rollbackOnError ?? intlCon.config.rollbackOnError ?? true);
|
|
79
|
+
if (rollbackOnError && intlCon.inTransaction)
|
|
80
80
|
await intlCon.execute('SAVEPOINT ' + this._onErrorSavePoint);
|
|
81
81
|
try {
|
|
82
82
|
const result = await this._execute(options);
|
|
83
83
|
if (commitLast)
|
|
84
84
|
await intlCon.execute('COMMIT');
|
|
85
|
-
else if (intlCon.inTransaction
|
|
85
|
+
else if (rollbackOnError && intlCon.inTransaction) {
|
|
86
86
|
await intlCon.execute('RELEASE ' + this._onErrorSavePoint + ';');
|
|
87
87
|
}
|
|
88
88
|
return result;
|
|
89
89
|
}
|
|
90
90
|
catch (e) {
|
|
91
|
-
if (intlCon.inTransaction
|
|
91
|
+
if (rollbackOnError && intlCon.inTransaction) {
|
|
92
92
|
await intlCon.execute('ROLLBACK TO ' + this._onErrorSavePoint + ';');
|
|
93
93
|
}
|
|
94
94
|
throw e;
|
|
@@ -59,9 +59,12 @@ export const NumericType = {
|
|
|
59
59
|
const fracPad = (DEC_DIGITS - (fracPart.length % DEC_DIGITS)) % DEC_DIGITS;
|
|
60
60
|
fracPart = fracPart + '0'.repeat(fracPad);
|
|
61
61
|
const digits = [];
|
|
62
|
-
|
|
62
|
+
let i;
|
|
63
|
+
let l = intPart.length;
|
|
64
|
+
for (i = 0; i < l; i += DEC_DIGITS)
|
|
63
65
|
digits.push(+intPart.substring(i, i + DEC_DIGITS));
|
|
64
|
-
|
|
66
|
+
l = fracPart.length;
|
|
67
|
+
for (i = 0; i < l; i += DEC_DIGITS)
|
|
65
68
|
digits.push(+fracPart.substring(i, i + DEC_DIGITS));
|
|
66
69
|
// weight counts groups before the point, less one; leading zero groups
|
|
67
70
|
// are dropped and lower it, trailing ones are simply dropped.
|
|
@@ -75,8 +78,10 @@ export const NumericType = {
|
|
|
75
78
|
if (!digits.length)
|
|
76
79
|
weight = 0;
|
|
77
80
|
writeHeader(digits.length, weight, sign, dscale);
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
l = digits.length;
|
|
82
|
+
for (i = 0; i < l; i++) {
|
|
83
|
+
buf.writeInt16BE(digits[i]);
|
|
84
|
+
}
|
|
80
85
|
},
|
|
81
86
|
decodeBinary(v, offset = 0) {
|
|
82
87
|
const len = v.readInt16BE(offset);
|
|
@@ -190,7 +195,7 @@ export function numberBytesToString(digits, scale, weight, sign) {
|
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
const extra = (i - scale) % DEC_DIGITS;
|
|
193
|
-
return out.
|
|
198
|
+
return out.substring(0, out.length - extra);
|
|
194
199
|
}
|
|
195
200
|
/* https://github.com/pgjdbc/pgjdbc/blob/3eca3a76aa4a04cb28cb960ed674cb67db30b5e3/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java */
|
|
196
201
|
/**
|
|
@@ -18,7 +18,8 @@ export interface CommandResult {
|
|
|
18
18
|
*/
|
|
19
19
|
rowType?: 'array' | 'object';
|
|
20
20
|
/**
|
|
21
|
-
* Time elapsed to execute command
|
|
21
|
+
* Time elapsed to execute command - only measured when `timing` is
|
|
22
|
+
* enabled (off by default; see `DatabaseConnectionParams.timing`).
|
|
22
23
|
*/
|
|
23
24
|
executeTime?: number;
|
|
24
25
|
/**
|
|
@@ -91,6 +91,19 @@ export interface DatabaseConnectionParams {
|
|
|
91
91
|
* @default true
|
|
92
92
|
*/
|
|
93
93
|
asyncErrorHandling?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether `query()`/`execute()` (etc.) measure how long the call took -
|
|
96
|
+
* `CommandResult.executeTime` and `ScriptResult.totalTime`.
|
|
97
|
+
*
|
|
98
|
+
* Off by default: each measurement is a `performance.now()` call, and
|
|
99
|
+
* unlike `asyncErrorHandling` there is no way to compute it lazily only
|
|
100
|
+
* once something actually reads it - a real, if small, cost paid on
|
|
101
|
+
* every call whether or not the result is ever inspected, and one a
|
|
102
|
+
* client that does not offer this at all does not pay. Set to `true` to
|
|
103
|
+
* turn it on, connection-wide or for one call.
|
|
104
|
+
* @default false
|
|
105
|
+
*/
|
|
106
|
+
timing?: boolean;
|
|
94
107
|
debugLogger?: DebugLogger;
|
|
95
108
|
}
|
|
96
109
|
export type TargetSessionAttrs = 'read-write' | 'read-only' | 'primary' | 'standby' | 'prefer-standby';
|
|
@@ -60,4 +60,10 @@ export interface QueryOptions extends DataMappingOptions {
|
|
|
60
60
|
* @default the connection's own setting
|
|
61
61
|
*/
|
|
62
62
|
asyncErrorHandling?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Overrides the connection's own `timing` for this call only - see
|
|
65
|
+
* `DatabaseConnectionParams.timing`.
|
|
66
|
+
* @default the connection's own setting
|
|
67
|
+
*/
|
|
68
|
+
timing?: boolean;
|
|
63
69
|
}
|
|
@@ -40,4 +40,10 @@ export interface ScriptExecuteOptions extends DataMappingOptions {
|
|
|
40
40
|
* @default the connection's own setting
|
|
41
41
|
*/
|
|
42
42
|
asyncErrorHandling?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Overrides the connection's own `timing` for this call only - see
|
|
45
|
+
* `DatabaseConnectionParams.timing`.
|
|
46
|
+
* @default the connection's own setting
|
|
47
|
+
*/
|
|
48
|
+
timing?: boolean;
|
|
43
49
|
}
|
|
@@ -9,7 +9,8 @@ export interface ScriptResult {
|
|
|
9
9
|
*/
|
|
10
10
|
totalCommands: number;
|
|
11
11
|
/**
|
|
12
|
-
* Total execution time
|
|
12
|
+
* Total execution time - only measured when `timing` is enabled (off by
|
|
13
|
+
* default; see `DatabaseConnectionParams.timing`).
|
|
13
14
|
*/
|
|
14
|
-
totalTime
|
|
15
|
+
totalTime?: number;
|
|
15
16
|
}
|
package/package.json
CHANGED
package/protocol/frontend.js
CHANGED
|
@@ -52,7 +52,12 @@ export class Frontend {
|
|
|
52
52
|
.writeInt32BE(0) // Preserve length
|
|
53
53
|
.writeInt16BE(Protocol.VERSION_MAJOR)
|
|
54
54
|
.writeInt16BE(Protocol.VERSION_MINOR);
|
|
55
|
-
|
|
55
|
+
const entries = Object.entries(args);
|
|
56
|
+
const l = entries.length;
|
|
57
|
+
let k;
|
|
58
|
+
let v;
|
|
59
|
+
for (let i = 0; i < l; i++) {
|
|
60
|
+
[k, v] = entries[i];
|
|
56
61
|
if (k !== 'client_encoding')
|
|
57
62
|
io.writeCString(k, 'utf8').writeCString(v, 'utf8');
|
|
58
63
|
}
|
|
@@ -105,16 +110,19 @@ export class Frontend {
|
|
|
105
110
|
getParseMessage(args) {
|
|
106
111
|
if (args.statement && args.statement.length > 63)
|
|
107
112
|
throw new Error('Query name length must be lower than 63');
|
|
113
|
+
const { paramTypes } = args;
|
|
108
114
|
const io = this._io
|
|
109
115
|
.start()
|
|
110
116
|
.writeInt8(Protocol.FrontendMessageCode.Parse)
|
|
111
117
|
.writeInt32BE(0) // Preserve header
|
|
112
118
|
.writeCString(args.statement || '', 'utf8')
|
|
113
119
|
.writeCString(args.sql, 'utf8')
|
|
114
|
-
.writeUInt16BE(
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
.writeUInt16BE(paramTypes?.length || 0);
|
|
121
|
+
if (paramTypes) {
|
|
122
|
+
let i;
|
|
123
|
+
const l = paramTypes.length;
|
|
124
|
+
for (i = 0; i < l; i++) {
|
|
125
|
+
io.writeUInt32BE(paramTypes[i] || 0);
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
return setLengthAndFlush(io, 1);
|
package/protocol/sasl.js
CHANGED
|
@@ -96,7 +96,7 @@ export var SASL;
|
|
|
96
96
|
const arr = data.split(',');
|
|
97
97
|
for (const s of arr) {
|
|
98
98
|
if (s[0] === 'v')
|
|
99
|
-
serverSignature = s.
|
|
99
|
+
serverSignature = s.substring(2);
|
|
100
100
|
}
|
|
101
101
|
if (serverSignature !== session.serverSignature)
|
|
102
102
|
throw new Error('SASL: Server signature does not match');
|
package/util/abort-signal.d.ts
CHANGED
|
@@ -20,5 +20,15 @@ export declare function abortError(signal: AbortSignal, cause?: unknown): Error;
|
|
|
20
20
|
*
|
|
21
21
|
* The listener is always removed again: one long-lived signal shared by many
|
|
22
22
|
* queries would otherwise accumulate a listener per query.
|
|
23
|
+
*
|
|
24
|
+
* Not `async` itself: the overwhelmingly common case (no signal passed) just
|
|
25
|
+
* returns `run()`'s own promise straight through. Marking this function
|
|
26
|
+
* `async` would still be correct, but every `async function` return goes
|
|
27
|
+
* through the spec's promise-resolve machinery - a real extra microtask
|
|
28
|
+
* tick, even for a value that is already the right kind of promise - and
|
|
29
|
+
* this sits on every query's hot path, signal or not. The slow path (a
|
|
30
|
+
* signal actually given) is factored into its own async helper below, where
|
|
31
|
+
* the cost is negligible next to the abort-listener bookkeeping it already
|
|
32
|
+
* does.
|
|
23
33
|
*/
|
|
24
34
|
export declare function withAbortSignal<T>(signal: AbortSignal | undefined, cancel: () => Promise<void>, run: () => Promise<T>): Promise<T>;
|
package/util/abort-signal.js
CHANGED
|
@@ -36,13 +36,26 @@ export function abortError(signal, cause) {
|
|
|
36
36
|
*
|
|
37
37
|
* The listener is always removed again: one long-lived signal shared by many
|
|
38
38
|
* queries would otherwise accumulate a listener per query.
|
|
39
|
+
*
|
|
40
|
+
* Not `async` itself: the overwhelmingly common case (no signal passed) just
|
|
41
|
+
* returns `run()`'s own promise straight through. Marking this function
|
|
42
|
+
* `async` would still be correct, but every `async function` return goes
|
|
43
|
+
* through the spec's promise-resolve machinery - a real extra microtask
|
|
44
|
+
* tick, even for a value that is already the right kind of promise - and
|
|
45
|
+
* this sits on every query's hot path, signal or not. The slow path (a
|
|
46
|
+
* signal actually given) is factored into its own async helper below, where
|
|
47
|
+
* the cost is negligible next to the abort-listener bookkeeping it already
|
|
48
|
+
* does.
|
|
39
49
|
*/
|
|
40
|
-
export
|
|
50
|
+
export function withAbortSignal(signal, cancel, run) {
|
|
41
51
|
if (!signal)
|
|
42
52
|
return run();
|
|
43
53
|
// Already aborted: nothing has been sent, so nothing needs cancelling.
|
|
44
54
|
if (signal.aborted)
|
|
45
|
-
|
|
55
|
+
return Promise.reject(abortError(signal));
|
|
56
|
+
return runWithAbortSignal(signal, cancel, run);
|
|
57
|
+
}
|
|
58
|
+
async function runWithAbortSignal(signal, cancel, run) {
|
|
46
59
|
const onAbort = () => {
|
|
47
60
|
// A failed cancel (the session already gone, say) must not replace the
|
|
48
61
|
// error the query itself is about to report.
|