ts-server-lib 0.0.17
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/LICENSE +1 -0
- package/README.md +8 -0
- package/db/TSMongo.d.ts +103 -0
- package/db/TSMongo.js +483 -0
- package/db/TSRQW.d.ts +271 -0
- package/db/TSRQW.js +699 -0
- package/db/TSRedis.d.ts +174 -0
- package/db/TSRedis.js +602 -0
- package/package.json +85 -0
- package/ussd/TSUssdMenu.d.ts +139 -0
- package/ussd/TSUssdMenu.js +364 -0
- package/ussd/TSUssdScreen.d.ts +58 -0
- package/ussd/TSUssdScreen.js +218 -0
- package/ussd/index.d.ts +3 -0
- package/ussd/index.js +19 -0
- package/ussd/providers/AfricasTalking.d.ts +3 -0
- package/ussd/providers/AfricasTalking.js +17 -0
- package/ussd/providers/AirtelDRC.d.ts +9 -0
- package/ussd/providers/AirtelDRC.js +31 -0
- package/ussd/providers/OrangeDRC.d.ts +5 -0
- package/ussd/providers/OrangeDRC.js +213 -0
- package/ussd/providers/VodacomDRC.d.ts +9 -0
- package/ussd/providers/VodacomDRC.js +48 -0
- package/ussd/providers/_.d.ts +55 -0
- package/ussd/providers/_.js +83 -0
- package/ussd/providers/index.d.ts +13 -0
- package/ussd/providers/index.js +56 -0
- package/utils/TSFile.d.ts +36 -0
- package/utils/TSFile.js +244 -0
- package/utils/TSHash.d.ts +20 -0
- package/utils/TSHash.js +75 -0
- package/utils/TSRequest.d.ts +39 -0
- package/utils/TSRequest.js +256 -0
- package/utils/TSStub.d.ts +159 -0
- package/utils/TSStub.js +296 -0
- package/utils/abort.d.ts +18 -0
- package/utils/abort.js +97 -0
- package/utils/mime.json +11358 -0
package/utils/TSStub.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TSStub — generic driver stub utility for all at-prefixed provider packages.
|
|
4
|
+
*
|
|
5
|
+
* Provides the shared scaffolding every stub needs:
|
|
6
|
+
* - Latency injection (fixed, random range, or abortable timeout)
|
|
7
|
+
* - Error injection (network error, timeout, rate-limit exceeded)
|
|
8
|
+
* - Outcome simulation (success / failure / pending / processing)
|
|
9
|
+
* - Retry-N-then-succeed counter
|
|
10
|
+
* - Webhook signature validation
|
|
11
|
+
* - In-memory store with deterministic ID generation
|
|
12
|
+
* - Rate-limit capability declaration
|
|
13
|
+
* - OAuth token / session simulation (atsocial)
|
|
14
|
+
* - Exchange rate table lookup (atexchange)
|
|
15
|
+
* - Capability matrix helper (atsocial)
|
|
16
|
+
* - Health status simulation (atkyc, others)
|
|
17
|
+
* - Config field extractor (normalise simulate* + latency from raw config)
|
|
18
|
+
* - Noop links factory
|
|
19
|
+
*
|
|
20
|
+
* Each at-package stub only needs to handle its domain-specific response shapes.
|
|
21
|
+
* TSStub has zero dependencies on any at-package — purely generic infrastructure.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.TSStub = exports.TSStubRateLimitError = void 0;
|
|
25
|
+
const node_crypto_1 = require("node:crypto");
|
|
26
|
+
const abort_js_1 = require("./abort.js");
|
|
27
|
+
const TSHash_js_1 = require("./TSHash.js");
|
|
28
|
+
function createStore() {
|
|
29
|
+
const map = new Map();
|
|
30
|
+
let counter = 0;
|
|
31
|
+
return {
|
|
32
|
+
get: (id) => map.get(id),
|
|
33
|
+
set: (id, value) => { map.set(id, value); },
|
|
34
|
+
delete: (id) => map.delete(id),
|
|
35
|
+
list: () => Array.from(map.values()),
|
|
36
|
+
entries: () => Array.from(map.entries()),
|
|
37
|
+
clear: () => { map.clear(); counter = 0; },
|
|
38
|
+
nextId: (prefix = 'stub') => `${prefix}-${String(++counter).padStart(3, '0')}`,
|
|
39
|
+
get size() { return map.size; },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function createRetryCounter() {
|
|
43
|
+
const counts = new Map();
|
|
44
|
+
return {
|
|
45
|
+
shouldFail(key, n) {
|
|
46
|
+
const c = (counts.get(key) ?? 0) + 1;
|
|
47
|
+
counts.set(key, c);
|
|
48
|
+
return c <= n;
|
|
49
|
+
},
|
|
50
|
+
reset(key) {
|
|
51
|
+
if (key !== undefined)
|
|
52
|
+
counts.delete(key);
|
|
53
|
+
else
|
|
54
|
+
counts.clear();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
59
|
+
// Rate-limit exceeded response (generic shape)
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
61
|
+
class TSStubRateLimitError extends Error {
|
|
62
|
+
code = 'STUB_RATE_LIMIT_EXCEEDED';
|
|
63
|
+
retryAfterMs;
|
|
64
|
+
constructor(retryAfterMs = 1000) {
|
|
65
|
+
super('Rate limit exceeded (stub)');
|
|
66
|
+
this.name = 'TSStubRateLimitError';
|
|
67
|
+
this.retryAfterMs = retryAfterMs;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.TSStubRateLimitError = TSStubRateLimitError;
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
72
|
+
// TSStub — static API
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
74
|
+
class TSStub {
|
|
75
|
+
// ── Latency ────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Delay by `cfg.latencyMs` (fixed) or a uniform random value in
|
|
78
|
+
* `[cfg.latencyMsMin, cfg.latencyMsMax]`. Abortable via `signal`.
|
|
79
|
+
* If `cfg.simulateTimeoutMs` is set, waits that long then throws AbortError.
|
|
80
|
+
*/
|
|
81
|
+
static async maybeDelay(cfg, signal) {
|
|
82
|
+
(0, abort_js_1.throwIfAborted)(signal);
|
|
83
|
+
if (typeof cfg.simulateTimeoutMs === 'number') {
|
|
84
|
+
await (0, abort_js_1.abortableDelay)(cfg.simulateTimeoutMs, signal);
|
|
85
|
+
throw (0, abort_js_1.abortError)();
|
|
86
|
+
}
|
|
87
|
+
let ms = 0;
|
|
88
|
+
if (typeof cfg.latencyMsMin === 'number' &&
|
|
89
|
+
typeof cfg.latencyMsMax === 'number' &&
|
|
90
|
+
cfg.latencyMsMax >= cfg.latencyMsMin &&
|
|
91
|
+
cfg.latencyMsMax > 0) {
|
|
92
|
+
const min = Math.max(0, cfg.latencyMsMin);
|
|
93
|
+
const max = Math.max(min, cfg.latencyMsMax);
|
|
94
|
+
ms = min + Math.random() * (max - min);
|
|
95
|
+
}
|
|
96
|
+
else if (typeof cfg.latencyMs === 'number' && cfg.latencyMs > 0) {
|
|
97
|
+
ms = cfg.latencyMs;
|
|
98
|
+
}
|
|
99
|
+
if (ms > 0)
|
|
100
|
+
await (0, abort_js_1.abortableDelay)(ms, signal);
|
|
101
|
+
}
|
|
102
|
+
// ── Error injection ─────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* Call at the start of every stub method. Throws a network error or rate-limit
|
|
105
|
+
* error when the corresponding simulate flag is set.
|
|
106
|
+
* Also honours `simulateTimeoutMs` via `maybeDelay`.
|
|
107
|
+
*/
|
|
108
|
+
static async maybeInjectError(cfg, signal) {
|
|
109
|
+
(0, abort_js_1.throwIfAborted)(signal);
|
|
110
|
+
if (cfg.simulateNetworkError) {
|
|
111
|
+
throw new Error('network error (stub)');
|
|
112
|
+
}
|
|
113
|
+
if (cfg.simulateRateLimitExceeded) {
|
|
114
|
+
throw new TSStubRateLimitError();
|
|
115
|
+
}
|
|
116
|
+
if (cfg.simulateTimeoutMs) {
|
|
117
|
+
await (0, abort_js_1.abortableDelay)(cfg.simulateTimeoutMs, signal);
|
|
118
|
+
throw (0, abort_js_1.abortError)();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── Outcome helpers ─────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Return `cfg.simulateOutcome ?? fallback`, cast to T.
|
|
124
|
+
* Domain stubs pass their expected outcome union as T.
|
|
125
|
+
*/
|
|
126
|
+
static outcome(cfg, fallback) {
|
|
127
|
+
return cfg.simulateOutcome ?? fallback;
|
|
128
|
+
}
|
|
129
|
+
static isFailed(cfg) {
|
|
130
|
+
return cfg.simulateOutcome === 'failed';
|
|
131
|
+
}
|
|
132
|
+
static isPending(cfg) {
|
|
133
|
+
const o = cfg.simulateOutcome;
|
|
134
|
+
return o === 'pending' || o === 'processing';
|
|
135
|
+
}
|
|
136
|
+
static failureReason(cfg, fallback = 'simulated failure') {
|
|
137
|
+
return cfg.simulateFailureReason ?? fallback;
|
|
138
|
+
}
|
|
139
|
+
// ── Retry ────────────────────────────────────────────────────────
|
|
140
|
+
/** Factory — create one per stub instance; call `reset()` in test teardown. */
|
|
141
|
+
static createRetryCounter() {
|
|
142
|
+
return createRetryCounter();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check retry counter. Returns true when the call should fail.
|
|
146
|
+
* Usage inside a stub method:
|
|
147
|
+
* if (retry.shouldFail(operationKey, cfg.simulateFailFirstN ?? 0)) throw ...
|
|
148
|
+
*/
|
|
149
|
+
static shouldFail(counter, key, cfg) {
|
|
150
|
+
const n = cfg.simulateFailFirstN ?? 0;
|
|
151
|
+
if (n === 0)
|
|
152
|
+
return false;
|
|
153
|
+
return counter.shouldFail(key, n);
|
|
154
|
+
}
|
|
155
|
+
// ── Webhook ─────────────────────────────────────────────────────
|
|
156
|
+
/**
|
|
157
|
+
* Validate webhook signature against `cfg.simulateWebhookSignature`.
|
|
158
|
+
* Returns true when signatures match (or when incoming equals the default stub value).
|
|
159
|
+
*/
|
|
160
|
+
static validateSignature(incoming, cfg, defaultSignature = 'stub-signature') {
|
|
161
|
+
const expected = cfg.simulateWebhookSignature ?? defaultSignature;
|
|
162
|
+
if (!incoming)
|
|
163
|
+
return false;
|
|
164
|
+
try {
|
|
165
|
+
return timingSafeCompare(expected, incoming);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return incoming === expected;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ── Capabilities ─────────────────────────────────────────────────
|
|
172
|
+
static rateLimitCapabilities(cfg) {
|
|
173
|
+
const out = {};
|
|
174
|
+
if (typeof cfg.simulateRateLimitPerSecond === 'number')
|
|
175
|
+
out.rateLimitPerSecond = cfg.simulateRateLimitPerSecond;
|
|
176
|
+
if (typeof cfg.simulateRateLimitBurst === 'number')
|
|
177
|
+
out.rateLimitBurst = cfg.simulateRateLimitBurst;
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
static healthStatus(cfg) {
|
|
181
|
+
return cfg.simulateHealthStatus ?? 'healthy';
|
|
182
|
+
}
|
|
183
|
+
// ── In-memory store ──────────────────────────────────────────────
|
|
184
|
+
/** Factory — create one store per entity type in the stub constructor. */
|
|
185
|
+
static createStore() {
|
|
186
|
+
return createStore();
|
|
187
|
+
}
|
|
188
|
+
// ── ID generation ────────────────────────────────────────────────
|
|
189
|
+
/** Generate a random stub ID: `stub-<prefix>-<8 hex chars>`. */
|
|
190
|
+
static id(prefix = 'item') {
|
|
191
|
+
return `stub-${prefix}-${TSHash_js_1.TSHash.randomId(4, 'hex')}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create a sequential ID generator: returns a function that yields
|
|
195
|
+
* `stub-<prefix>-001`, `stub-<prefix>-002`, ... (resets per call to this factory).
|
|
196
|
+
*/
|
|
197
|
+
static seqId(prefix = 'item') {
|
|
198
|
+
let n = 0;
|
|
199
|
+
return () => `stub-${prefix}-${String(++n).padStart(3, '0')}`;
|
|
200
|
+
}
|
|
201
|
+
// ── OAuth / token simulation ─────────────────────────────────────
|
|
202
|
+
static fakeToken(cfg = {}) {
|
|
203
|
+
const prefix = cfg.simulateTokenPrefix ?? 'stub-token';
|
|
204
|
+
return {
|
|
205
|
+
accessToken: `${prefix}-access-${TSHash_js_1.TSHash.randomId(8, 'hex')}`,
|
|
206
|
+
refreshToken: `${prefix}-refresh-${TSHash_js_1.TSHash.randomId(8, 'hex')}`,
|
|
207
|
+
expiresAt: Date.now() + 3600_000,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
static fakeAuthUrl(cfg = {}, params = {}) {
|
|
211
|
+
const base = cfg.simulateCallbackBase ?? 'https://stub.local/authorize';
|
|
212
|
+
const qs = new URLSearchParams({ state: TSHash_js_1.TSHash.randomId(8, 'hex'), ...params }).toString();
|
|
213
|
+
return `${base}?${qs}`;
|
|
214
|
+
}
|
|
215
|
+
// ── Exchange rate table ──────────────────────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Look up `from → to` in a rate table, then try inverse, then fall back to `defaultRate`.
|
|
218
|
+
* Same logic as atexchange `Stub` driver.
|
|
219
|
+
*/
|
|
220
|
+
static resolveRate(from, to, table = {}, defaultRate = 1) {
|
|
221
|
+
const f = from.trim().toUpperCase();
|
|
222
|
+
const t = to.trim().toUpperCase();
|
|
223
|
+
if (f === t)
|
|
224
|
+
return 1;
|
|
225
|
+
const direct = table[`${f}:${t}`];
|
|
226
|
+
if (typeof direct === 'number')
|
|
227
|
+
return direct;
|
|
228
|
+
const inverse = table[`${t}:${f}`];
|
|
229
|
+
if (typeof inverse === 'number' && inverse !== 0)
|
|
230
|
+
return 1 / inverse;
|
|
231
|
+
return defaultRate;
|
|
232
|
+
}
|
|
233
|
+
// ── Capability matrix ────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* Build a capability map for packages like atsocial that expose a fixed set of keys.
|
|
236
|
+
* All keys default to `'unsupported'`; `supported` overrides per key.
|
|
237
|
+
*/
|
|
238
|
+
static capabilities(allKeys, supported = {}) {
|
|
239
|
+
const result = {};
|
|
240
|
+
for (const key of allKeys) {
|
|
241
|
+
result[key] = supported[key] ?? 'unsupported';
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
// ── Config extractor ─────────────────────────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Extract all TSStubConfig fields from a raw config object.
|
|
248
|
+
* Used in `normalizeDriverConfig` in each at-package stub.
|
|
249
|
+
*/
|
|
250
|
+
static extractConfig(raw) {
|
|
251
|
+
const out = {};
|
|
252
|
+
const n = (k, type) => {
|
|
253
|
+
const v = raw[k];
|
|
254
|
+
if (typeof v === type)
|
|
255
|
+
out[k] = v;
|
|
256
|
+
};
|
|
257
|
+
n('latencyMs', 'number');
|
|
258
|
+
n('latencyMsMin', 'number');
|
|
259
|
+
n('latencyMsMax', 'number');
|
|
260
|
+
n('simulateTimeoutMs', 'number');
|
|
261
|
+
n('simulateNetworkError', 'boolean');
|
|
262
|
+
n('simulateRateLimitExceeded', 'boolean');
|
|
263
|
+
n('simulateOutcome', 'string');
|
|
264
|
+
n('simulateFailureReason', 'string');
|
|
265
|
+
n('simulateFailFirstN', 'number');
|
|
266
|
+
n('simulateWebhookSignature', 'string');
|
|
267
|
+
n('simulateRateLimitPerSecond', 'number');
|
|
268
|
+
n('simulateRateLimitBurst', 'number');
|
|
269
|
+
n('simulateHealthStatus', 'string');
|
|
270
|
+
n('simulateTokenPrefix', 'string');
|
|
271
|
+
n('simulateCallbackBase', 'string');
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
// ── Links factory ────────────────────────────────────────────────
|
|
275
|
+
/**
|
|
276
|
+
* Noop links object — used as the `protected links` property in stub drivers.
|
|
277
|
+
* scheme defaults to 'stub'.
|
|
278
|
+
*/
|
|
279
|
+
static noopLinks(scheme = 'stub') {
|
|
280
|
+
return {
|
|
281
|
+
default: { BASE: `${scheme}://noop` },
|
|
282
|
+
sandbox: { BASE: `${scheme}://noop` },
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
exports.TSStub = TSStub;
|
|
287
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
288
|
+
// Internal helpers
|
|
289
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
290
|
+
function timingSafeCompare(a, b) {
|
|
291
|
+
const ba = Buffer.from(a, 'utf8');
|
|
292
|
+
const bb = Buffer.from(b, 'utf8');
|
|
293
|
+
if (ba.length !== bb.length)
|
|
294
|
+
return false;
|
|
295
|
+
return (0, node_crypto_1.timingSafeEqual)(ba, bb);
|
|
296
|
+
}
|
package/utils/abort.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared abort helpers for any package that depends on `ts-server-lib` (HTTP via {@link TSRequest},
|
|
3
|
+
* SMTP, perf stubs, etc.). Prefer `signal` on HTTP options where supported; use these for timers
|
|
4
|
+
* and APIs that do not accept `AbortSignal` natively.
|
|
5
|
+
*/
|
|
6
|
+
/** DOM-style abort error (`name === 'AbortError'`). Uses `DOMException` when available. */
|
|
7
|
+
export declare function abortError(): Error;
|
|
8
|
+
export declare function throwIfAborted(signal?: AbortSignal | null): void;
|
|
9
|
+
/**
|
|
10
|
+
* Sleep for `ms` that rejects with `AbortError` if `signal` aborts first.
|
|
11
|
+
* With no `signal`, behaves like `setTimeout` sleep.
|
|
12
|
+
*/
|
|
13
|
+
export declare function abortableDelay(ms: number, signal?: AbortSignal | null): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Await `operation` unless `signal` aborts first. When aborted, runs `onAbort` (e.g. close a socket)
|
|
16
|
+
* then rejects with `AbortError`. With no `signal`, returns `operation` unchanged.
|
|
17
|
+
*/
|
|
18
|
+
export declare function raceWithAbort<T>(operation: Promise<T>, signal: AbortSignal | undefined | null, onAbort?: () => void): Promise<T>;
|
package/utils/abort.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared abort helpers for any package that depends on `ts-server-lib` (HTTP via {@link TSRequest},
|
|
4
|
+
* SMTP, perf stubs, etc.). Prefer `signal` on HTTP options where supported; use these for timers
|
|
5
|
+
* and APIs that do not accept `AbortSignal` natively.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.abortError = abortError;
|
|
9
|
+
exports.throwIfAborted = throwIfAborted;
|
|
10
|
+
exports.abortableDelay = abortableDelay;
|
|
11
|
+
exports.raceWithAbort = raceWithAbort;
|
|
12
|
+
/** DOM-style abort error (`name === 'AbortError'`). Uses `DOMException` when available. */
|
|
13
|
+
function abortError() {
|
|
14
|
+
if (typeof DOMException !== 'undefined') {
|
|
15
|
+
return new DOMException('The operation was aborted', 'AbortError');
|
|
16
|
+
}
|
|
17
|
+
const e = new Error('The operation was aborted');
|
|
18
|
+
e.name = 'AbortError';
|
|
19
|
+
return e;
|
|
20
|
+
}
|
|
21
|
+
function throwIfAborted(signal) {
|
|
22
|
+
if (signal?.aborted) {
|
|
23
|
+
throw abortError();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sleep for `ms` that rejects with `AbortError` if `signal` aborts first.
|
|
28
|
+
* With no `signal`, behaves like `setTimeout` sleep.
|
|
29
|
+
*/
|
|
30
|
+
async function abortableDelay(ms, signal) {
|
|
31
|
+
if (ms <= 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!signal) {
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
throwIfAborted(signal);
|
|
39
|
+
await new Promise((resolve, reject) => {
|
|
40
|
+
const t = setTimeout(() => {
|
|
41
|
+
signal.removeEventListener('abort', onAbort);
|
|
42
|
+
resolve();
|
|
43
|
+
}, ms);
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
signal.removeEventListener('abort', onAbort);
|
|
47
|
+
reject(abortError());
|
|
48
|
+
};
|
|
49
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Await `operation` unless `signal` aborts first. When aborted, runs `onAbort` (e.g. close a socket)
|
|
54
|
+
* then rejects with `AbortError`. With no `signal`, returns `operation` unchanged.
|
|
55
|
+
*/
|
|
56
|
+
async function raceWithAbort(operation, signal, onAbort) {
|
|
57
|
+
if (!signal) {
|
|
58
|
+
return operation;
|
|
59
|
+
}
|
|
60
|
+
throwIfAborted(signal);
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
let settled = false;
|
|
63
|
+
const detachAbort = () => {
|
|
64
|
+
signal.removeEventListener('abort', onAbortHandler);
|
|
65
|
+
};
|
|
66
|
+
const onAbortHandler = () => {
|
|
67
|
+
if (settled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
settled = true;
|
|
71
|
+
detachAbort();
|
|
72
|
+
try {
|
|
73
|
+
onAbort?.();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
reject(abortError());
|
|
79
|
+
};
|
|
80
|
+
signal.addEventListener('abort', onAbortHandler, { once: true });
|
|
81
|
+
operation.then((value) => {
|
|
82
|
+
if (settled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
settled = true;
|
|
86
|
+
detachAbort();
|
|
87
|
+
resolve(value);
|
|
88
|
+
}, (err) => {
|
|
89
|
+
if (settled) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
settled = true;
|
|
93
|
+
detachAbort();
|
|
94
|
+
reject(err);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|