flowshield 1.0.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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/dist/cjs/compose.js +53 -0
- package/dist/cjs/compose.js.map +1 -0
- package/dist/cjs/index.js +32 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/policies/bulkhead.js +75 -0
- package/dist/cjs/policies/bulkhead.js.map +1 -0
- package/dist/cjs/policies/cache.js +99 -0
- package/dist/cjs/policies/cache.js.map +1 -0
- package/dist/cjs/policies/circuit-breaker.js +110 -0
- package/dist/cjs/policies/circuit-breaker.js.map +1 -0
- package/dist/cjs/policies/fallback.js +33 -0
- package/dist/cjs/policies/fallback.js.map +1 -0
- package/dist/cjs/policies/hedge.js +74 -0
- package/dist/cjs/policies/hedge.js.map +1 -0
- package/dist/cjs/policies/rate-limiter.js +92 -0
- package/dist/cjs/policies/rate-limiter.js.map +1 -0
- package/dist/cjs/policies/retry.js +61 -0
- package/dist/cjs/policies/retry.js.map +1 -0
- package/dist/cjs/policies/timeout.js +45 -0
- package/dist/cjs/policies/timeout.js.map +1 -0
- package/dist/cjs/types.js +55 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils.js +84 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/compose.js +49 -0
- package/dist/esm/compose.js.map +1 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/policies/bulkhead.js +72 -0
- package/dist/esm/policies/bulkhead.js.map +1 -0
- package/dist/esm/policies/cache.js +96 -0
- package/dist/esm/policies/cache.js.map +1 -0
- package/dist/esm/policies/circuit-breaker.js +107 -0
- package/dist/esm/policies/circuit-breaker.js.map +1 -0
- package/dist/esm/policies/fallback.js +30 -0
- package/dist/esm/policies/fallback.js.map +1 -0
- package/dist/esm/policies/hedge.js +71 -0
- package/dist/esm/policies/hedge.js.map +1 -0
- package/dist/esm/policies/rate-limiter.js +89 -0
- package/dist/esm/policies/rate-limiter.js.map +1 -0
- package/dist/esm/policies/retry.js +58 -0
- package/dist/esm/policies/retry.js.map +1 -0
- package/dist/esm/policies/timeout.js +42 -0
- package/dist/esm/policies/timeout.js.map +1 -0
- package/dist/esm/types.js +46 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.js +77 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/types/compose.d.ts +32 -0
- package/dist/types/compose.d.ts.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/policies/bulkhead.d.ts +20 -0
- package/dist/types/policies/bulkhead.d.ts.map +1 -0
- package/dist/types/policies/cache.d.ts +19 -0
- package/dist/types/policies/cache.d.ts.map +1 -0
- package/dist/types/policies/circuit-breaker.d.ts +19 -0
- package/dist/types/policies/circuit-breaker.d.ts.map +1 -0
- package/dist/types/policies/fallback.d.ts +14 -0
- package/dist/types/policies/fallback.d.ts.map +1 -0
- package/dist/types/policies/hedge.d.ts +15 -0
- package/dist/types/policies/hedge.d.ts.map +1 -0
- package/dist/types/policies/rate-limiter.d.ts +19 -0
- package/dist/types/policies/rate-limiter.d.ts.map +1 -0
- package/dist/types/policies/retry.d.ts +14 -0
- package/dist/types/policies/retry.d.ts.map +1 -0
- package/dist/types/policies/timeout.d.ts +15 -0
- package/dist/types/policies/timeout.d.ts.map +1 -0
- package/dist/types/types.d.ts +133 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils.d.ts +21 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ─── Policies ───────────────────────────────────────────────────────────────
|
|
2
|
+
export { retry } from './policies/retry.js';
|
|
3
|
+
export { circuitBreaker } from './policies/circuit-breaker.js';
|
|
4
|
+
export { timeout } from './policies/timeout.js';
|
|
5
|
+
export { bulkhead } from './policies/bulkhead.js';
|
|
6
|
+
export { fallback } from './policies/fallback.js';
|
|
7
|
+
export { rateLimiter } from './policies/rate-limiter.js';
|
|
8
|
+
export { hedge } from './policies/hedge.js';
|
|
9
|
+
export { cache } from './policies/cache.js';
|
|
10
|
+
// ─── Composition ────────────────────────────────────────────────────────────
|
|
11
|
+
export { pipe, wrap } from './compose.js';
|
|
12
|
+
export { FlowShieldError, TimeoutError, CircuitOpenError, BulkheadRejectedError, RateLimitExceededError, RetryExhaustedError, } from './types.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAE5C,+EAA+E;AAC/E,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAqB1C,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BulkheadRejectedError } from '../types.js';
|
|
2
|
+
import { assertNonNegative, assertPositiveInteger } from '../utils.js';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
maxConcurrent: 10,
|
|
5
|
+
maxQueue: 0,
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Creates a bulkhead (concurrency limiter) policy that limits the number
|
|
9
|
+
* of concurrent executions of the wrapped operation.
|
|
10
|
+
*
|
|
11
|
+
* Returns an object with `execute` (the policy function) and `handle`
|
|
12
|
+
* (an interface to inspect the bulkhead's running / queued counts).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const bh = bulkhead({ maxConcurrent: 5, maxQueue: 10 });
|
|
17
|
+
* const result = await bh.execute(() => fetch('/api/data'));
|
|
18
|
+
* console.log(bh.handle.running); // number of active calls
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function bulkhead(options = {}) {
|
|
22
|
+
const maxConcurrent = options.maxConcurrent ?? DEFAULTS.maxConcurrent;
|
|
23
|
+
const maxQueue = options.maxQueue ?? DEFAULTS.maxQueue;
|
|
24
|
+
assertPositiveInteger(maxConcurrent, 'maxConcurrent');
|
|
25
|
+
assertNonNegative(maxQueue, 'maxQueue');
|
|
26
|
+
let running = 0;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const queue = [];
|
|
29
|
+
function dequeue() {
|
|
30
|
+
while (running < maxConcurrent && queue.length > 0) {
|
|
31
|
+
const entry = queue.shift();
|
|
32
|
+
run(entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function run(entry) {
|
|
36
|
+
running++;
|
|
37
|
+
entry
|
|
38
|
+
.fn()
|
|
39
|
+
.then((value) => {
|
|
40
|
+
entry.resolve(value);
|
|
41
|
+
}, (error) => {
|
|
42
|
+
entry.reject(error);
|
|
43
|
+
})
|
|
44
|
+
.finally(() => {
|
|
45
|
+
running--;
|
|
46
|
+
dequeue();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const handle = {
|
|
50
|
+
get running() {
|
|
51
|
+
return running;
|
|
52
|
+
},
|
|
53
|
+
get queued() {
|
|
54
|
+
return queue.length;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
function execute(fn) {
|
|
58
|
+
if (running < maxConcurrent) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
run({ fn, resolve, reject });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (queue.length >= maxQueue) {
|
|
64
|
+
return Promise.reject(new BulkheadRejectedError());
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
queue.push({ fn, resolve, reject });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { execute, handle };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=bulkhead.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bulkhead.js","sourceRoot":"","sources":["../../../src/policies/bulkhead.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,QAAQ,GAAG;IACf,aAAa,EAAE,EAAE;IACjB,QAAQ,EAAE,CAAC;CACZ,CAAC;AAQF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,QAAQ,CAAC,UAA2B,EAAE;IAIpD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,QAAQ,CAAC,aAAa,CAAC;IACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC;IAEvD,qBAAqB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;IACtD,iBAAiB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAExC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,8DAA8D;IAC9D,MAAM,KAAK,GAAsB,EAAE,CAAC;IAEpC,SAAS,OAAO;QACd,OAAO,OAAO,GAAG,aAAa,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACnD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;YAC7B,GAAG,CAAC,KAAK,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IAED,SAAS,GAAG,CAAI,KAAoB;QAClC,OAAO,EAAE,CAAC;QACV,KAAK;aACF,EAAE,EAAE;aACJ,IAAI,CACH,CAAC,KAAK,EAAE,EAAE;YACR,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;YACR,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC,CACF;aACA,OAAO,CAAC,GAAG,EAAE;YACZ,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACP,CAAC;IAED,MAAM,MAAM,GAAmB;QAC7B,IAAI,OAAO;YACT,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,IAAI,MAAM;YACR,OAAO,KAAK,CAAC,MAAM,CAAC;QACtB,CAAC;KACF,CAAC;IAEF,SAAS,OAAO,CAAI,EAAoB;QACtC,IAAI,OAAO,GAAG,aAAa,EAAE,CAAC;YAC5B,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACxC,GAAG,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ,EAAE,CAAC;YAC7B,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,qBAAqB,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { assertPositive } from '../utils.js';
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
ttl: 60_000,
|
|
4
|
+
staleWhileRevalidate: false,
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Creates a TTL-based cache policy that memoizes the result of the wrapped
|
|
8
|
+
* async operation. Supports stale-while-revalidate for background refresh.
|
|
9
|
+
*
|
|
10
|
+
* Returns an object with `execute` (the policy function) and `handle`
|
|
11
|
+
* (an interface to inspect / invalidate the cache).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const c = cache({ ttl: 10_000 });
|
|
16
|
+
* const result = await c.execute(() => fetch('/api/data').then(r => r.json()));
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function cache(options = {}) {
|
|
20
|
+
const ttl = options.ttl ?? DEFAULTS.ttl;
|
|
21
|
+
const staleWhileRevalidate = options.staleWhileRevalidate ?? DEFAULTS.staleWhileRevalidate;
|
|
22
|
+
const keyFn = options.keyFn ?? (() => 'default');
|
|
23
|
+
const onEvict = options.onEvict;
|
|
24
|
+
assertPositive(ttl, 'ttl');
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const store = new Map();
|
|
27
|
+
function evict(key) {
|
|
28
|
+
const entry = store.get(key);
|
|
29
|
+
if (entry) {
|
|
30
|
+
store.delete(key);
|
|
31
|
+
onEvict?.(key, entry.value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const handle = {
|
|
35
|
+
get size() {
|
|
36
|
+
return store.size;
|
|
37
|
+
},
|
|
38
|
+
invalidate(key) {
|
|
39
|
+
if (key !== undefined) {
|
|
40
|
+
evict(key);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Invalidate default key
|
|
44
|
+
evict(keyFn());
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
clear() {
|
|
48
|
+
if (onEvict) {
|
|
49
|
+
for (const [k, v] of store) {
|
|
50
|
+
onEvict(k, v.value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
store.clear();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
async function execute(fn) {
|
|
57
|
+
const key = keyFn();
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const entry = store.get(key);
|
|
60
|
+
if (entry) {
|
|
61
|
+
if (now < entry.expiresAt) {
|
|
62
|
+
// Fresh cache hit
|
|
63
|
+
return entry.value;
|
|
64
|
+
}
|
|
65
|
+
if (staleWhileRevalidate && !entry.stale) {
|
|
66
|
+
// Return stale value and revalidate in background
|
|
67
|
+
entry.stale = true;
|
|
68
|
+
fn()
|
|
69
|
+
.then((value) => {
|
|
70
|
+
store.set(key, {
|
|
71
|
+
value,
|
|
72
|
+
expiresAt: Date.now() + ttl,
|
|
73
|
+
stale: false,
|
|
74
|
+
});
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
// Revalidation failed — keep stale value, lift stale flag
|
|
78
|
+
// so next request tries again
|
|
79
|
+
entry.stale = false;
|
|
80
|
+
});
|
|
81
|
+
return entry.value;
|
|
82
|
+
}
|
|
83
|
+
// Expired and not SWR — evict and re-fetch
|
|
84
|
+
evict(key);
|
|
85
|
+
}
|
|
86
|
+
const value = await fn();
|
|
87
|
+
store.set(key, {
|
|
88
|
+
value,
|
|
89
|
+
expiresAt: now + ttl,
|
|
90
|
+
stale: false,
|
|
91
|
+
});
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
return { execute, handle };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../src/policies/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CircuitOpenError } from '../types.js';
|
|
2
|
+
import { assertPositiveInteger, assertPositive } from '../utils.js';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
failureThreshold: 5,
|
|
5
|
+
successThreshold: 1,
|
|
6
|
+
resetTimeout: 30_000,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates a circuit breaker policy that prevents calls to a failing service.
|
|
10
|
+
*
|
|
11
|
+
* Returns an object with `execute` (the policy function) and `handle`
|
|
12
|
+
* (an interface to inspect / reset the breaker state).
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const cb = circuitBreaker({ failureThreshold: 3, resetTimeout: 10_000 });
|
|
17
|
+
* const result = await cb.execute(() => fetch('/api/health'));
|
|
18
|
+
* console.log(cb.handle.state); // 'closed'
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function circuitBreaker(options = {}) {
|
|
22
|
+
const failureThreshold = options.failureThreshold ?? DEFAULTS.failureThreshold;
|
|
23
|
+
const successThreshold = options.successThreshold ?? DEFAULTS.successThreshold;
|
|
24
|
+
const resetTimeout = options.resetTimeout ?? DEFAULTS.resetTimeout;
|
|
25
|
+
assertPositiveInteger(failureThreshold, 'failureThreshold');
|
|
26
|
+
assertPositiveInteger(successThreshold, 'successThreshold');
|
|
27
|
+
assertPositive(resetTimeout, 'resetTimeout');
|
|
28
|
+
let state = 'closed';
|
|
29
|
+
let failureCount = 0;
|
|
30
|
+
let successCount = 0;
|
|
31
|
+
let nextAttemptTime = 0;
|
|
32
|
+
function trip() {
|
|
33
|
+
state = 'open';
|
|
34
|
+
nextAttemptTime = Date.now() + resetTimeout;
|
|
35
|
+
options.onOpen?.();
|
|
36
|
+
}
|
|
37
|
+
function resetState() {
|
|
38
|
+
state = 'closed';
|
|
39
|
+
failureCount = 0;
|
|
40
|
+
successCount = 0;
|
|
41
|
+
options.onClose?.();
|
|
42
|
+
}
|
|
43
|
+
function tryTransitionToHalfOpen() {
|
|
44
|
+
if (state === 'open' && Date.now() >= nextAttemptTime) {
|
|
45
|
+
state = 'half-open';
|
|
46
|
+
successCount = 0;
|
|
47
|
+
options.onHalfOpen?.();
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const handle = {
|
|
53
|
+
get state() {
|
|
54
|
+
// Auto-transition if timer has elapsed
|
|
55
|
+
if (state === 'open' && Date.now() >= nextAttemptTime) {
|
|
56
|
+
state = 'half-open';
|
|
57
|
+
successCount = 0;
|
|
58
|
+
options.onHalfOpen?.();
|
|
59
|
+
}
|
|
60
|
+
return state;
|
|
61
|
+
},
|
|
62
|
+
get failureCount() {
|
|
63
|
+
return failureCount;
|
|
64
|
+
},
|
|
65
|
+
get successCount() {
|
|
66
|
+
return successCount;
|
|
67
|
+
},
|
|
68
|
+
reset() {
|
|
69
|
+
resetState();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
async function execute(fn) {
|
|
73
|
+
if (state === 'open') {
|
|
74
|
+
if (!tryTransitionToHalfOpen()) {
|
|
75
|
+
throw new CircuitOpenError();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const result = await fn();
|
|
80
|
+
// Success handling
|
|
81
|
+
if (state === 'half-open') {
|
|
82
|
+
successCount++;
|
|
83
|
+
if (successCount >= successThreshold) {
|
|
84
|
+
resetState();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// In closed state, reset failure count on success
|
|
89
|
+
failureCount = 0;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
failureCount++;
|
|
95
|
+
if (state === 'half-open') {
|
|
96
|
+
// Any failure in half-open trips back to open
|
|
97
|
+
trip();
|
|
98
|
+
}
|
|
99
|
+
else if (failureCount >= failureThreshold) {
|
|
100
|
+
trip();
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { execute, handle };
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=circuit-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"circuit-breaker.js","sourceRoot":"","sources":["../../../src/policies/circuit-breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a fallback policy that returns a fallback value (or the result
|
|
3
|
+
* of a fallback function) when the wrapped operation fails.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const result = await fallback({ fallback: [] })(
|
|
8
|
+
* () => fetch('/api/items').then(r => r.json()),
|
|
9
|
+
* );
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export function fallback(options) {
|
|
13
|
+
const shouldFallback = options.shouldFallback ?? (() => true);
|
|
14
|
+
return async (fn) => {
|
|
15
|
+
try {
|
|
16
|
+
return await fn();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (!shouldFallback(error)) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
const fb = options.fallback;
|
|
23
|
+
if (typeof fb === 'function') {
|
|
24
|
+
return await fb(error);
|
|
25
|
+
}
|
|
26
|
+
return fb;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=fallback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback.js","sourceRoot":"","sources":["../../../src/policies/fallback.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,QAAQ,CAAI,OAA2B;IACrD,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAE9D,OAAO,KAAK,EAAK,EAAoB,EAAkB,EAAE;QACvD,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3B,MAAM,KAAK,CAAC;YACd,CAAC;YAED,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;YAC5B,IAAI,OAAO,EAAE,KAAK,UAAU,EAAE,CAAC;gBAC7B,OAAO,MAAO,EAAyC,CAAC,KAAK,CAAC,CAAC;YACjE,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { assertPositive } from '../utils.js';
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
hedgeDelay: 2000,
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Creates a hedge policy that sends a parallel (hedged) request if the
|
|
7
|
+
* primary request doesn't resolve within the configured delay.
|
|
8
|
+
* The first request to resolve wins; the other's result is discarded.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const result = await hedge({ hedgeDelay: 1000 })(
|
|
13
|
+
* () => fetch('/api/data').then(r => r.json()),
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function hedge(options = {}) {
|
|
18
|
+
const hedgeDelay = options.hedgeDelay ?? DEFAULTS.hedgeDelay;
|
|
19
|
+
assertPositive(hedgeDelay, 'hedgeDelay');
|
|
20
|
+
return async (fn) => {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
let settled = false;
|
|
23
|
+
let primaryDone = false;
|
|
24
|
+
let hedgeDone = false;
|
|
25
|
+
let primaryError;
|
|
26
|
+
let hedgeLaunched = false;
|
|
27
|
+
const trySettle = (value) => {
|
|
28
|
+
if (!settled) {
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(hedgeTimer);
|
|
31
|
+
resolve(value);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const tryRejectBoth = () => {
|
|
35
|
+
if (primaryDone && hedgeDone && !settled) {
|
|
36
|
+
settled = true;
|
|
37
|
+
reject(primaryError);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const doLaunchHedge = () => {
|
|
41
|
+
hedgeLaunched = true;
|
|
42
|
+
const hedgeRequest = fn();
|
|
43
|
+
hedgeRequest.then(trySettle, (_hedgeErr) => {
|
|
44
|
+
hedgeDone = true;
|
|
45
|
+
if (!primaryDone && !settled) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
tryRejectBoth();
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
// Primary request
|
|
52
|
+
const primary = fn();
|
|
53
|
+
primary.then(trySettle, (err) => {
|
|
54
|
+
primaryDone = true;
|
|
55
|
+
primaryError = err;
|
|
56
|
+
if (!hedgeLaunched) {
|
|
57
|
+
clearTimeout(hedgeTimer);
|
|
58
|
+
doLaunchHedge();
|
|
59
|
+
}
|
|
60
|
+
tryRejectBoth();
|
|
61
|
+
});
|
|
62
|
+
// Schedule hedged request
|
|
63
|
+
const hedgeTimer = setTimeout(() => {
|
|
64
|
+
if (!settled) {
|
|
65
|
+
doLaunchHedge();
|
|
66
|
+
}
|
|
67
|
+
}, hedgeDelay);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=hedge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hedge.js","sourceRoot":"","sources":["../../../src/policies/hedge.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,QAAQ,GAAG;IACf,UAAU,EAAE,IAAI;CACjB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,KAAK,CAAC,UAAwB,EAAE;IAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,QAAQ,CAAC,UAAU,CAAC;IAE7D,cAAc,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;IAEzC,OAAO,KAAK,EAAK,EAAoB,EAAc,EAAE;QACnD,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,IAAI,WAAW,GAAG,KAAK,CAAC;YACxB,IAAI,SAAS,GAAG,KAAK,CAAC;YACtB,IAAI,YAAqB,CAAC;YAC1B,IAAI,aAAa,GAAG,KAAK,CAAC;YAE1B,MAAM,SAAS,GAAG,CAAC,KAAQ,EAAE,EAAE;gBAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,GAAG,IAAI,CAAC;oBACf,YAAY,CAAC,UAAU,CAAC,CAAC;oBACzB,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC;YACH,CAAC,CAAC;YAEF,MAAM,aAAa,GAAG,GAAG,EAAE;gBACzB,IAAI,WAAW,IAAI,SAAS,IAAI,CAAC,OAAO,EAAE,CAAC;oBACzC,OAAO,GAAG,IAAI,CAAC;oBACf,MAAM,CAAC,YAAY,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC,CAAC;YAEF,MAAM,aAAa,GAAG,GAAG,EAAE;gBACzB,aAAa,GAAG,IAAI,CAAC;gBACrB,MAAM,YAAY,GAAG,EAAE,EAAE,CAAC;gBAC1B,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,SAAkB,EAAE,EAAE;oBAClD,SAAS,GAAG,IAAI,CAAC;oBACjB,IAAI,CAAC,WAAW,IAAI,CAAC,OAAO,EAAE,CAAC;wBAC7B,OAAO;oBACT,CAAC;oBACD,aAAa,EAAE,CAAC;gBAClB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;YAEF,kBAAkB;YAClB,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC;YACrB,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,GAAY,EAAE,EAAE;gBACvC,WAAW,GAAG,IAAI,CAAC;gBACnB,YAAY,GAAG,GAAG,CAAC;gBAEnB,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,YAAY,CAAC,UAAU,CAAC,CAAC;oBACzB,aAAa,EAAE,CAAC;gBAClB,CAAC;gBAED,aAAa,EAAE,CAAC;YAClB,CAAC,CAAC,CAAC;YAEH,0BAA0B;YAC1B,MAAM,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE;gBACjC,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,aAAa,EAAE,CAAC;gBAClB,CAAC;YACH,CAAC,EAAE,UAAU,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { RateLimitExceededError } from '../types.js';
|
|
2
|
+
import { assertPositiveInteger, assertPositive } from '../utils.js';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
tokensPerInterval: 10,
|
|
5
|
+
interval: 1000,
|
|
6
|
+
rejectOnLimit: false,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates a token-bucket rate limiter policy that limits how many times
|
|
10
|
+
* the wrapped operation can be executed within a time interval.
|
|
11
|
+
*
|
|
12
|
+
* Returns an object with `execute` (the policy function) and `handle`
|
|
13
|
+
* (an interface to inspect / reset the bucket).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const rl = rateLimiter({ tokensPerInterval: 5, interval: 1000 });
|
|
18
|
+
* const result = await rl.execute(() => fetch('/api/data'));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function rateLimiter(options = {}) {
|
|
22
|
+
const tokensPerInterval = options.tokensPerInterval ?? DEFAULTS.tokensPerInterval;
|
|
23
|
+
const interval = options.interval ?? DEFAULTS.interval;
|
|
24
|
+
const rejectOnLimit = options.rejectOnLimit ?? DEFAULTS.rejectOnLimit;
|
|
25
|
+
assertPositiveInteger(tokensPerInterval, 'tokensPerInterval');
|
|
26
|
+
assertPositive(interval, 'interval');
|
|
27
|
+
let tokens = tokensPerInterval;
|
|
28
|
+
let lastRefill = Date.now();
|
|
29
|
+
const waiters = [];
|
|
30
|
+
function refill() {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const elapsed = now - lastRefill;
|
|
33
|
+
const newTokens = Math.floor((elapsed / interval) * tokensPerInterval);
|
|
34
|
+
if (newTokens > 0) {
|
|
35
|
+
tokens = Math.min(tokensPerInterval, tokens + newTokens);
|
|
36
|
+
lastRefill = now;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function releaseWaiters() {
|
|
40
|
+
while (tokens > 0 && waiters.length > 0) {
|
|
41
|
+
tokens--;
|
|
42
|
+
const waiter = waiters.shift();
|
|
43
|
+
waiter.resolve();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Refill timer — only active when there are waiters
|
|
47
|
+
let refillTimer = null;
|
|
48
|
+
function startRefillTimer() {
|
|
49
|
+
if (refillTimer !== null)
|
|
50
|
+
return;
|
|
51
|
+
refillTimer = setInterval(() => {
|
|
52
|
+
refill();
|
|
53
|
+
releaseWaiters();
|
|
54
|
+
if (waiters.length === 0 && refillTimer !== null) {
|
|
55
|
+
clearInterval(refillTimer);
|
|
56
|
+
refillTimer = null;
|
|
57
|
+
}
|
|
58
|
+
}, Math.min(interval, 100));
|
|
59
|
+
}
|
|
60
|
+
const handle = {
|
|
61
|
+
get availableTokens() {
|
|
62
|
+
refill();
|
|
63
|
+
return tokens;
|
|
64
|
+
},
|
|
65
|
+
reset() {
|
|
66
|
+
tokens = tokensPerInterval;
|
|
67
|
+
lastRefill = Date.now();
|
|
68
|
+
releaseWaiters();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
async function execute(fn) {
|
|
72
|
+
refill();
|
|
73
|
+
if (tokens > 0) {
|
|
74
|
+
tokens--;
|
|
75
|
+
return fn();
|
|
76
|
+
}
|
|
77
|
+
if (rejectOnLimit) {
|
|
78
|
+
throw new RateLimitExceededError();
|
|
79
|
+
}
|
|
80
|
+
// Queue and wait for a token
|
|
81
|
+
await new Promise((resolve) => {
|
|
82
|
+
waiters.push({ resolve });
|
|
83
|
+
startRefillTimer();
|
|
84
|
+
});
|
|
85
|
+
return fn();
|
|
86
|
+
}
|
|
87
|
+
return { execute, handle };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../../src/policies/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { RetryExhaustedError } from '../types.js';
|
|
2
|
+
import { sleep, calculateDelay, assertPositiveInteger, assertPositive } from '../utils.js';
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
maxAttempts: 3,
|
|
5
|
+
delay: 200,
|
|
6
|
+
backoff: 'exponential',
|
|
7
|
+
maxDelay: 30_000,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a retry policy that re-executes the wrapped operation on failure
|
|
11
|
+
* using the configured backoff strategy.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const result = await retry({ maxAttempts: 5, backoff: 'exponential' })(
|
|
16
|
+
* () => fetch('/api/data'),
|
|
17
|
+
* );
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function retry(options = {}) {
|
|
21
|
+
const maxAttempts = options.maxAttempts ?? DEFAULTS.maxAttempts;
|
|
22
|
+
const baseDelay = options.delay ?? DEFAULTS.delay;
|
|
23
|
+
const backoff = options.backoff ?? DEFAULTS.backoff;
|
|
24
|
+
const maxDelay = options.maxDelay ?? DEFAULTS.maxDelay;
|
|
25
|
+
const shouldRetry = options.shouldRetry ?? (() => true);
|
|
26
|
+
const onRetry = options.onRetry;
|
|
27
|
+
const signal = options.signal;
|
|
28
|
+
assertPositiveInteger(maxAttempts, 'maxAttempts');
|
|
29
|
+
assertPositive(baseDelay, 'delay');
|
|
30
|
+
assertPositive(maxDelay, 'maxDelay');
|
|
31
|
+
return async (fn) => {
|
|
32
|
+
let lastError;
|
|
33
|
+
let previousDelay = baseDelay;
|
|
34
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
35
|
+
if (signal?.aborted) {
|
|
36
|
+
throw signal.reason;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return await fn();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
lastError = error;
|
|
43
|
+
if (attempt === maxAttempts) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (!shouldRetry(error, attempt)) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const delayMs = calculateDelay(attempt, baseDelay, backoff, maxDelay, previousDelay);
|
|
50
|
+
previousDelay = delayMs;
|
|
51
|
+
onRetry?.(error, attempt, delayMs);
|
|
52
|
+
await sleep(delayMs, signal);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new RetryExhaustedError(maxAttempts, lastError);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../../src/policies/retry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3F,MAAM,QAAQ,GAAG;IACf,WAAW,EAAE,CAAC;IACd,KAAK,EAAE,GAAG;IACV,OAAO,EAAE,aAAsB;IAC/B,QAAQ,EAAE,MAAM;CACjB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,KAAK,CAAC,UAAwB,EAAE;IAC9C,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,QAAQ,CAAC,WAAW,CAAC;IAChE,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAI,QAAQ,CAAC,KAAK,CAAC;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC;IACpD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC;IACvD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,qBAAqB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAClD,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACnC,cAAc,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAErC,OAAO,KAAK,EAAK,EAAoB,EAAc,EAAE;QACnD,IAAI,SAAkB,CAAC;QACvB,IAAI,aAAa,GAAG,SAAS,CAAC;QAE9B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;YACxD,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACpB,MAAM,MAAM,CAAC,MAAM,CAAC;YACtB,CAAC;YAED,IAAI,CAAC;gBACH,OAAO,MAAM,EAAE,EAAE,CAAC;YACpB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,SAAS,GAAG,KAAK,CAAC;gBAElB,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;oBAC5B,MAAM;gBACR,CAAC;gBAED,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;oBACjC,MAAM;gBACR,CAAC;gBAED,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC;gBACrF,aAAa,GAAG,OAAO,CAAC;gBAExB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;gBAEnC,MAAM,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,MAAM,IAAI,mBAAmB,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IACxD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TimeoutError } from '../types.js';
|
|
2
|
+
import { assertPositive } from '../utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a timeout policy that rejects if the wrapped operation
|
|
5
|
+
* does not resolve within the specified deadline.
|
|
6
|
+
*
|
|
7
|
+
* Uses AbortController for cooperative cancellation when the
|
|
8
|
+
* wrapped function supports it.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const result = await timeout({ ms: 5000 })(() => fetch('/api/data'));
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function timeout(options) {
|
|
16
|
+
assertPositive(options.ms, 'ms');
|
|
17
|
+
return async (fn) => {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
const timer = setTimeout(() => {
|
|
21
|
+
if (!settled) {
|
|
22
|
+
settled = true;
|
|
23
|
+
reject(new TimeoutError(options.ms));
|
|
24
|
+
}
|
|
25
|
+
}, options.ms);
|
|
26
|
+
fn().then((value) => {
|
|
27
|
+
if (!settled) {
|
|
28
|
+
settled = true;
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
resolve(value);
|
|
31
|
+
}
|
|
32
|
+
}, (error) => {
|
|
33
|
+
if (!settled) {
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
reject(error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../../src/policies/timeout.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,OAAuB;IAC7C,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAEjC,OAAO,KAAK,EAAK,EAAoB,EAAc,EAAE;QACnD,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,IAAI,OAAO,GAAG,KAAK,CAAC;YAEpB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,GAAG,IAAI,CAAC;oBACf,MAAM,CAAC,IAAI,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;gBACvC,CAAC;YACH,CAAC,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;YAEf,EAAE,EAAE,CAAC,IAAI,CACP,CAAC,KAAK,EAAE,EAAE;gBACR,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,GAAG,IAAI,CAAC;oBACf,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC;YACH,CAAC,EACD,CAAC,KAAK,EAAE,EAAE;gBACR,IAAI,CAAC,OAAO,EAAE,CAAC;oBACb,OAAO,GAAG,IAAI,CAAC;oBACf,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,MAAM,CAAC,KAAK,CAAC,CAAC;gBAChB,CAAC;YACH,CAAC,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
|