arrow-resilience-kit 0.10.0 → 0.10.1
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 +21 -0
- package/README.md +240 -0
- package/kotlin-kotlin-stdlib.js +8 -8
- package/kotlinx-coroutines-core.js +2 -2
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sorin Albu-Irimies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# arrow-resilience-kit
|
|
2
|
+
|
|
3
|
+
Resilience patterns for JavaScript and TypeScript: **retry with backoff** (honoring `Retry-After`), **circuit breakers**, **bulkheads**, **rate limiters**, **time limits**, **hedged requests**, **failover**, **retry budgets**, **adaptive concurrency** and a **TTL/LRU cache**.
|
|
4
|
+
|
|
5
|
+
It is the JS build of the Kotlin Multiplatform library [arrow-resilience-kit](https://github.com/sorinirimies/arrow-resilience-kit), built on [Arrow](https://arrow-kt.io): the same, well-tested implementation that runs on the JVM, Android, iOS and native, exposed through a Promise-based API with generated TypeScript types.
|
|
6
|
+
|
|
7
|
+
- **Promise-based**: every operation takes an `async` function and returns a `Promise`.
|
|
8
|
+
- **Typed**: ships its own `.d.ts`; no `@types` package needed.
|
|
9
|
+
- **ES modules and CommonJS**: `import` and `require()` both work, with named exports.
|
|
10
|
+
- **No runtime dependencies**.
|
|
11
|
+
- **Cancellable**: operations that wait take an `AbortSignal`, like `fetch`.
|
|
12
|
+
- **Your errors, unchanged**: a rejection carries the exact error your function threw.
|
|
13
|
+
- **Silent**: logs nothing unless you ask it to.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install arrow-resilience-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { CircuitBreaker, Retry } from "arrow-resilience-kit";
|
|
23
|
+
|
|
24
|
+
// Retry transient failures, waiting what the server asks for on 429.
|
|
25
|
+
const user = await Retry.withBackoff(() => fetchJson("https://api.example.com/user"), {
|
|
26
|
+
retries: 3,
|
|
27
|
+
shouldRetry: (e) => e.status === 429 || e.status >= 500,
|
|
28
|
+
delayMillis: (e) => e.retryAfterMillis, // undefined = exponential backoff
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Stop calling a service that keeps failing; try again after 30 s.
|
|
32
|
+
const breaker = await CircuitBreaker.create(5, 30_000);
|
|
33
|
+
const orders = await breaker.execute(() => fetchJson("https://orders.example.com/latest"));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Every `create(...)` is `async` and takes positional arguments, all optional (see the signatures below). Durations are plain milliseconds.
|
|
37
|
+
|
|
38
|
+
## Patterns
|
|
39
|
+
|
|
40
|
+
### Retry
|
|
41
|
+
|
|
42
|
+
`Retry.withBackoff(block, options?)` is the general-purpose retry for network calls:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Retry } from "arrow-resilience-kit";
|
|
46
|
+
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const data = await Retry.withBackoff(() => fetchJson(url), {
|
|
49
|
+
retries: 5, // retries after the first attempt (default 3)
|
|
50
|
+
baseMillis: 200, // first backoff (default 100), multiplied by `factor` (default 2)
|
|
51
|
+
maxDelayMillis: 10_000, // cap for the computed backoff
|
|
52
|
+
jitter: 0.1, // ±10 % randomization of the computed backoff
|
|
53
|
+
shouldRetry: (e, retry) => e.status === 429 || e.status >= 500,
|
|
54
|
+
delayMillis: (e, retry) => e.retryAfterMillis, // exact wait for this error, no jitter or cap
|
|
55
|
+
onRetry: (e, retry, delayMillis) => console.debug(`retry ${retry} in ${delayMillis} ms`),
|
|
56
|
+
signal: controller.signal, // abort to stop waiting and reject with signal.reason
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Simpler variants: `Retry.withExponentialBackoff(block, retries?, baseMillis?, factor?, signal?)`, `Retry.withConstantDelay(block, retries?, delayMillis?, signal?)`, `Retry.orDefault(block, fallback, retries?, baseMillis?, factor?)` and `Retry.ifMatches(block, shouldRetry, retries?, delayMillis?)`.
|
|
61
|
+
|
|
62
|
+
### Circuit breaker
|
|
63
|
+
|
|
64
|
+
Fails fast while a dependency is down instead of piling up slow, failing calls.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { CircuitBreaker, SlidingWindowCircuitBreaker } from "arrow-resilience-kit";
|
|
68
|
+
|
|
69
|
+
// Opens after 5 consecutive failures; after 30 s lets 1 trial call through.
|
|
70
|
+
const breaker = await CircuitBreaker.create(5, 30_000, 1, 1);
|
|
71
|
+
breaker.onStateChange((from, to) => console.info(`circuit ${from} -> ${to}`));
|
|
72
|
+
|
|
73
|
+
const result = await breaker.executeOrFallback(
|
|
74
|
+
async () => cachedValue, // used while the circuit is open
|
|
75
|
+
() => fetchJson(url),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Opens when 50 % of the last 20 calls failed (once at least 10 were made).
|
|
79
|
+
const windowed = await SlidingWindowCircuitBreaker.create(20, 10, 0.5, 30_000);
|
|
80
|
+
await windowed.execute(() => fetchJson(url));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Bulkhead
|
|
84
|
+
|
|
85
|
+
Limits how many calls run at once, so one slow dependency can't take every connection.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { Bulkhead } from "arrow-resilience-kit";
|
|
89
|
+
|
|
90
|
+
// 10 concurrent calls, 50 waiting, wait at most 2 s for a slot.
|
|
91
|
+
const bulkhead = await Bulkhead.create(10, 50, 2_000);
|
|
92
|
+
const report = await bulkhead.execute(() => fetchJson(url));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Rate limiter
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { RateLimiter, SlidingWindowRateLimiter } from "arrow-resilience-kit";
|
|
99
|
+
|
|
100
|
+
// Token bucket: 5 calls per second, bursts of up to 10.
|
|
101
|
+
const limiter = await RateLimiter.create(5, 10);
|
|
102
|
+
await limiter.execute(() => fetchJson(url)); // waits for a token
|
|
103
|
+
|
|
104
|
+
// At most 100 calls in any 60 s window.
|
|
105
|
+
const window = await SlidingWindowRateLimiter.create(100, 60_000);
|
|
106
|
+
const maybe = await window.tryExecute(() => fetchJson(url)); // null when over the limit
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Time limiter
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { TimeLimiter } from "arrow-resilience-kit";
|
|
113
|
+
|
|
114
|
+
const limiter = await TimeLimiter.create(5_000); // default timeout
|
|
115
|
+
const fast = await limiter.execute(1_000, () => fetchJson(url)); // per-call timeout
|
|
116
|
+
const orNull = await limiter.executeOrNull(undefined, () => fetchJson(url));
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Hedged requests
|
|
120
|
+
|
|
121
|
+
Starts a backup request when the first one is slow, and takes whichever answers first.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { hedge } from "arrow-resilience-kit";
|
|
125
|
+
|
|
126
|
+
// A second attempt after 200 ms, at most 2 extra attempts.
|
|
127
|
+
const fastest = await hedge(() => fetchJson(url), 200, 2);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Failover
|
|
131
|
+
|
|
132
|
+
Tries providers in order, skipping one that keeps failing.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { Failover, FailoverProviderSpec } from "arrow-resilience-kit";
|
|
136
|
+
|
|
137
|
+
const source = await Failover.create([
|
|
138
|
+
new FailoverProviderSpec("primary", () => fetchJson("https://primary.example.com/data")),
|
|
139
|
+
new FailoverProviderSpec("replica", () => fetchJson("https://replica.example.com/data")),
|
|
140
|
+
]);
|
|
141
|
+
const value = await source.execute();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Retry budget
|
|
145
|
+
|
|
146
|
+
Caps retries across all calls to a dependency (a ratio of successful calls), so retries can't amplify an outage.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { RetryBudget } from "arrow-resilience-kit";
|
|
150
|
+
|
|
151
|
+
const budget = await RetryBudget.create(10, 0.2); // up to 10 tokens, +0.2 per success
|
|
152
|
+
const reply = await budget.retry(() => fetchJson(url), 3, 100, 2_000);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Adaptive limiter
|
|
156
|
+
|
|
157
|
+
Adjusts its concurrency limit to observed latency: grows while calls are fast, shrinks when they slow down.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { AdaptiveLimiter } from "arrow-resilience-kit";
|
|
161
|
+
|
|
162
|
+
const adaptive = await AdaptiveLimiter.create(10, 1, 100, 1, 0.5, 250);
|
|
163
|
+
await adaptive.execute(() => fetchJson(url));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Cache
|
|
167
|
+
|
|
168
|
+
String-keyed cache with LRU (or LFU/FIFO) eviction and an optional TTL.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { Cache } from "arrow-resilience-kit";
|
|
172
|
+
|
|
173
|
+
const cache = await Cache.create<User>(1_000, 60_000, "LRU");
|
|
174
|
+
const cached = await cache.getOrPut("user:42", () => fetchJson("https://api.example.com/users/42"));
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Errors
|
|
178
|
+
|
|
179
|
+
A rejected promise carries **exactly the error your function rejected with**, so `instanceof` and custom fields keep working:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
try {
|
|
183
|
+
await breaker.execute(() => fetchJson(url));
|
|
184
|
+
} catch (e) {
|
|
185
|
+
if (e instanceof HttpError && e.status === 404) {
|
|
186
|
+
// not found, as thrown by fetchJson
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Errors raised by the library itself (circuit open, bulkhead full, timeout, retry budget exhausted, ...) are `Error`s with a descriptive message.
|
|
192
|
+
|
|
193
|
+
## Cancellation
|
|
194
|
+
|
|
195
|
+
`execute(...)` on the circuit breakers, bulkhead, rate limiters, adaptive limiter, time limiter and failover, plus `RetryBudget.retry(...)`, `hedge(...)` and the `Retry` functions accept an `AbortSignal` (the last argument, or the `signal` option of `Retry.withBackoff`). Aborting cancels whatever the library is waiting for (a retry delay, a permit, a timeout) and rejects with `signal.reason`, the same contract as `fetch(url, { signal })`. To also stop the work inside your function, pass the signal into it:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
const pending = breaker.execute(() => fetch(url, { signal: controller.signal }), controller.signal);
|
|
200
|
+
controller.abort();
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Async context
|
|
204
|
+
|
|
205
|
+
On Node.js 20.16+ and Bun, your callbacks run in the async context of the call that passed them, so `AsyncLocalStorage` values (request ids, tracing spans) belong to the right caller even when many calls run concurrently.
|
|
206
|
+
|
|
207
|
+
## Logging
|
|
208
|
+
|
|
209
|
+
Nothing is logged by default. Turn logging on, or route it into your own logger:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import { Logging } from "arrow-resilience-kit";
|
|
213
|
+
|
|
214
|
+
Logging.setLevel("WARN"); // OFF (default), ERROR, WARN, INFO, DEBUG, TRACE
|
|
215
|
+
Logging.setHandler((level, message, error) => console.log(`[${level}] ${message}`, error ?? ""));
|
|
216
|
+
Logging.setHandler(null); // back to the console
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Statistics
|
|
220
|
+
|
|
221
|
+
Most patterns report what they did, for metrics and dashboards:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const stats = await bulkhead.statistics();
|
|
225
|
+
console.log(stats.totalCalls, stats.rejectedCalls);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Size
|
|
229
|
+
|
|
230
|
+
About 1.3 MB of JavaScript before bundling (the Kotlin standard library and coroutines are compiled in), plus source maps; no dependencies. Declared free of side effects, so bundlers can drop it when unused.
|
|
231
|
+
|
|
232
|
+
## Links
|
|
233
|
+
|
|
234
|
+
- [Source, Kotlin/JVM/iOS documentation and design notes](https://github.com/sorinirimies/arrow-resilience-kit)
|
|
235
|
+
- [Changelog](https://github.com/sorinirimies/arrow-resilience-kit/blob/main/CHANGELOG.md)
|
|
236
|
+
- [Issues](https://github.com/sorinirimies/arrow-resilience-kit/issues)
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT
|
package/kotlin-kotlin-stdlib.js
CHANGED
|
@@ -98,23 +98,23 @@ if (typeof Math.clz32 === 'undefined') {
|
|
|
98
98
|
initMetadataForInterface(Comparator, 'Comparator');
|
|
99
99
|
initMetadataForObject(Unit, 'Unit');
|
|
100
100
|
initMetadataForClass(AbstractCollection, 'AbstractCollection', VOID, VOID, [Collection]);
|
|
101
|
-
initMetadataForClass(AbstractMutableCollection, 'AbstractMutableCollection', VOID, AbstractCollection, [
|
|
101
|
+
initMetadataForClass(AbstractMutableCollection, 'AbstractMutableCollection', VOID, AbstractCollection, [Collection, MutableIterable]);
|
|
102
102
|
initMetadataForClass(IteratorImpl, 'IteratorImpl');
|
|
103
103
|
initMetadataForClass(ListIteratorImpl, 'ListIteratorImpl', VOID, IteratorImpl);
|
|
104
|
-
initMetadataForClass(AbstractMutableList, 'AbstractMutableList', VOID, AbstractMutableCollection, [
|
|
104
|
+
initMetadataForClass(AbstractMutableList, 'AbstractMutableList', VOID, AbstractMutableCollection, [Collection, MutableIterable, KtList]);
|
|
105
105
|
initMetadataForClass(AbstractMap, 'AbstractMap', VOID, VOID, [KtMap]);
|
|
106
106
|
initMetadataForClass(AbstractMutableMap, 'AbstractMutableMap', VOID, AbstractMap, [KtMap]);
|
|
107
|
-
initMetadataForClass(AbstractMutableSet, 'AbstractMutableSet', VOID, AbstractMutableCollection, [
|
|
107
|
+
initMetadataForClass(AbstractMutableSet, 'AbstractMutableSet', VOID, AbstractMutableCollection, [Collection, MutableIterable, KtSet]);
|
|
108
108
|
initMetadataForCompanion(Companion_1);
|
|
109
109
|
initMetadataForInterface(RandomAccess, 'RandomAccess');
|
|
110
|
-
initMetadataForClass(ArrayList, 'ArrayList', ArrayList_init_$Create$, AbstractMutableList, [
|
|
110
|
+
initMetadataForClass(ArrayList, 'ArrayList', ArrayList_init_$Create$, AbstractMutableList, [Collection, MutableIterable, KtList, RandomAccess]);
|
|
111
111
|
initMetadataForClass(HashMap, 'HashMap', HashMap_init_$Create$, AbstractMutableMap, [KtMap]);
|
|
112
|
-
initMetadataForClass(HashMapKeys, 'HashMapKeys', VOID, AbstractMutableSet, [
|
|
113
|
-
initMetadataForClass(HashMapEntrySetBase, 'HashMapEntrySetBase', VOID, AbstractMutableSet, [
|
|
112
|
+
initMetadataForClass(HashMapKeys, 'HashMapKeys', VOID, AbstractMutableSet, [Collection, MutableIterable, KtSet]);
|
|
113
|
+
initMetadataForClass(HashMapEntrySetBase, 'HashMapEntrySetBase', VOID, AbstractMutableSet, [Collection, MutableIterable, KtSet]);
|
|
114
114
|
initMetadataForClass(HashMapEntrySet, 'HashMapEntrySet', VOID, HashMapEntrySetBase);
|
|
115
115
|
initMetadataForClass(HashMapKeysDefault$iterator$1);
|
|
116
116
|
initMetadataForClass(HashMapKeysDefault, 'HashMapKeysDefault', VOID, AbstractMutableSet);
|
|
117
|
-
initMetadataForClass(HashSet, 'HashSet', HashSet_init_$Create$, AbstractMutableSet, [
|
|
117
|
+
initMetadataForClass(HashSet, 'HashSet', HashSet_init_$Create$, AbstractMutableSet, [Collection, MutableIterable, KtSet]);
|
|
118
118
|
initMetadataForCompanion(Companion_2);
|
|
119
119
|
initMetadataForClass(Itr, 'Itr');
|
|
120
120
|
initMetadataForClass(KeysItr, 'KeysItr', VOID, Itr);
|
|
@@ -158,7 +158,7 @@ if (typeof Math.clz32 === 'undefined') {
|
|
|
158
158
|
initMetadataForInterface(InternalMap, 'InternalMap');
|
|
159
159
|
initMetadataForClass(InternalHashMap, 'InternalHashMap', InternalHashMap_init_$Create$, VOID, [InternalMap]);
|
|
160
160
|
initMetadataForClass(LinkedHashMap, 'LinkedHashMap', LinkedHashMap_init_$Create$, HashMap, [KtMap]);
|
|
161
|
-
initMetadataForClass(LinkedHashSet, 'LinkedHashSet', LinkedHashSet_init_$Create$, HashSet, [
|
|
161
|
+
initMetadataForClass(LinkedHashSet, 'LinkedHashSet', LinkedHashSet_init_$Create$, HashSet, [Collection, MutableIterable, KtSet]);
|
|
162
162
|
initMetadataForClass(AtomicReference, 'AtomicReference');
|
|
163
163
|
initMetadataForClass(BaseOutput, 'BaseOutput');
|
|
164
164
|
initMetadataForClass(NodeJsOutput, 'NodeJsOutput', VOID, BaseOutput);
|
|
@@ -217,13 +217,13 @@
|
|
|
217
217
|
initMetadataForCoroutine($doSelectSuspendCOROUTINE$, CoroutineImpl);
|
|
218
218
|
initMetadataForCoroutine($completeCOROUTINE$, CoroutineImpl);
|
|
219
219
|
initMetadataForCoroutine($processResultAndInvokeBlockRecoveringExceptionCOROUTINE$, CoroutineImpl);
|
|
220
|
-
initMetadataForClass(SelectImplementation, 'SelectImplementation', VOID, VOID, [CancelHandler,
|
|
220
|
+
initMetadataForClass(SelectImplementation, 'SelectImplementation', VOID, VOID, [CancelHandler, Waiter, SelectInstance], [0, 2]);
|
|
221
221
|
initMetadataForClass(SemaphoreAndMutexImpl, 'SemaphoreAndMutexImpl', VOID, VOID, VOID, [0]);
|
|
222
222
|
initMetadataForClass(SemaphoreImpl, 'SemaphoreImpl', VOID, SemaphoreAndMutexImpl, VOID, [0]);
|
|
223
223
|
initMetadataForClass(SemaphoreSegment, 'SemaphoreSegment', VOID, Segment);
|
|
224
224
|
initMetadataForClass(SetTimeoutBasedDispatcher, 'SetTimeoutBasedDispatcher', VOID, CoroutineDispatcher, [Delay], [1]);
|
|
225
225
|
initMetadataForObject(NodeDispatcher, 'NodeDispatcher', VOID, SetTimeoutBasedDispatcher, VOID, [1]);
|
|
226
|
-
initMetadataForClass(MessageQueue, 'MessageQueue', VOID, VOID, [
|
|
226
|
+
initMetadataForClass(MessageQueue, 'MessageQueue', VOID, VOID, [Collection, MutableIterable, KtList]);
|
|
227
227
|
initMetadataForClass(ScheduledMessageQueue, 'ScheduledMessageQueue', VOID, MessageQueue);
|
|
228
228
|
initMetadataForClass(WindowMessageQueue, 'WindowMessageQueue', VOID, MessageQueue);
|
|
229
229
|
initMetadataForObject(Dispatchers, 'Dispatchers');
|