clutchit 0.0.8 → 0.0.10
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 +17 -10
- package/README.md +74 -51
- package/dist/circuit/circuit.breaker.d.ts +51 -0
- package/dist/circuit/circuit.breaker.js +4 -4
- package/dist/circuit/index.d.ts +10 -0
- package/dist/concurrency/bulkhead.d.ts +28 -0
- package/dist/concurrency/bulkhead.js +3 -3
- package/dist/concurrency/index.d.ts +23 -0
- package/dist/concurrency/rate-limiter.d.ts +29 -0
- package/dist/concurrency/rate-limiter.js +2 -2
- package/dist/concurrency/ref.d.ts +11 -0
- package/dist/concurrency/ref.js +2 -2
- package/dist/concurrency/semaphore.d.ts +15 -0
- package/dist/concurrency/semaphore.js +2 -2
- package/dist/queue/base.queue.d.ts +18 -0
- package/dist/queue/base.queue.js +6 -6
- package/dist/queue/bounded.queue.d.ts +11 -0
- package/dist/queue/bounded.queue.js +10 -10
- package/dist/queue/dropping.queue.d.ts +7 -0
- package/dist/queue/faults.d.ts +24 -0
- package/dist/queue/index.d.ts +15 -0
- package/dist/queue/sliding.queue.d.ts +7 -0
- package/dist/retry/index.d.ts +8 -0
- package/dist/retry/retry.d.ts +21 -0
- package/dist/retry/retry.js +2 -2
- package/dist/schedule/index.d.ts +36 -0
- package/dist/schedule/operators.d.ts +22 -0
- package/dist/schedule/runner.d.ts +31 -0
- package/dist/schedule/runner.js +2 -2
- package/dist/schedule/schedule.d.ts +35 -0
- package/dist/timeout/index.d.ts +7 -0
- package/dist/timeout/timeout.d.ts +18 -0
- package/dist/timeout/timeout.js +2 -2
- package/dist/unthrow/do.d.ts +7 -0
- package/dist/unthrow/do.js +11 -7
- package/dist/unthrow/fault.d.ts +23 -0
- package/dist/unthrow/fault.js +15 -6
- package/dist/unthrow/helpers.d.ts +16 -0
- package/dist/unthrow/helpers.js +32 -9
- package/dist/unthrow/index.d.ts +14 -0
- package/dist/unthrow/index.js +9 -39
- package/dist/unthrow/try.async.d.ts +28 -0
- package/dist/unthrow/try.async.js +114 -0
- package/dist/unthrow/try.d.ts +56 -0
- package/dist/unthrow/{result.js → try.js} +46 -1
- package/package.json +19 -2
- package/dist/unthrow/result.async.js +0 -63
package/CHANGELOG.md
CHANGED
|
@@ -11,22 +11,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
#### `clutchit/unthrow`
|
|
13
13
|
|
|
14
|
-
- `
|
|
15
|
-
- `
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
18
|
-
-
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
14
|
+
- `Try<T, E>` — synchronous typed error container with `Ok` and `Err` variants
|
|
15
|
+
- `TryAsync<T, E>` — async equivalent backed by `Promise<Try<T, E>>`, implements `PromiseLike`
|
|
16
|
+
- `ok()`, `err()`, `okAsync()`, `errAsync()` — standalone constructors
|
|
17
|
+
- `Do` / `DoAsync` — generator-based Do notation with optional context parameter
|
|
18
|
+
- Instance methods: `map`, `mapErr`, `andThen`, `orElse`, `tap`, `tapErr`, `andThrough`, `orThrough`, `catchFault`, `unwrapOr`, `match`
|
|
19
|
+
- `fromPromise` — lift a `Promise` into a `TryAsync` with typed error mapping
|
|
20
|
+
- `fromSafePromise` — lift a never-rejecting promise into `TryAsync<T, never>`
|
|
21
|
+
- `fromThrowable` — wrap a throwing sync function into a `Try`
|
|
22
|
+
- `fromAsyncThrowable` — wrap an async throwing function into a `TryAsync`
|
|
23
|
+
- `combine` / `combineWithAllErrors` — combine arrays of `Try` or `TryAsync`
|
|
24
|
+
- `unwrap` — assert a value is non-null/undefined, or return the fault
|
|
25
|
+
- `flatten` — flatten nested `Try<Try<T, E2>, E1>` or `TryAsync<Try<T, E2>, E1>`
|
|
26
|
+
- `race` — race multiple `TryAsync` values, like `Promise.race`
|
|
21
27
|
- `Fault.Tagged` — factory for structured, tagged error classes with `code`, `_category`, and `_transient`
|
|
22
|
-
- `Fault.
|
|
28
|
+
- `Fault.is` — duck-type guard for the `Fault` interface (accepts `unknown`)
|
|
29
|
+
- `Fault.isDomain` / `Fault.isInfrastructure` / `Fault.isTransient` — type guard helpers (accept `unknown`)
|
|
23
30
|
|
|
24
31
|
#### `clutchit/schedule`
|
|
25
32
|
|
|
26
33
|
- `Schedule` — composable timing primitive with `.next(input, attempt)` and `.pipe(operator)` API
|
|
27
34
|
- Factories: `spaced`, `exponential`, `linear`, `constant`, `fixed`, `windowed`, `fibonacci`, `cron`, `forever`, `once`
|
|
28
35
|
- Operators: `recurs`, `cappedDelay`, `jittered`, `upTo`, `andThen`, `union`, `intersect`, `whileInput`, `whileOutput`, `map`
|
|
29
|
-
- `Schedule.repeat` — run a `
|
|
36
|
+
- `Schedule.repeat` — run a `TryAsync`-returning function repeatedly on a schedule with abort signal support
|
|
30
37
|
- `ScheduleInterruptedFault` — returned when `repeat` is cancelled via `AbortSignal`
|
|
31
38
|
|
|
32
39
|
#### `clutchit/retry`
|
|
@@ -36,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
36
43
|
|
|
37
44
|
#### `clutchit/timeout`
|
|
38
45
|
|
|
39
|
-
- `withTimeout(fn, duration)` — wrap a `
|
|
46
|
+
- `withTimeout(fn, duration)` — wrap a `TryAsync`-returning function with a deadline
|
|
40
47
|
- `createTimeout(duration)` — create a reusable timeout wrapper
|
|
41
48
|
- `TimeoutFault` — transient infrastructure fault emitted on deadline exceeded; aborts the underlying operation
|
|
42
49
|
|
package/README.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# clutchit
|
|
2
2
|
|
|
3
|
-
Resilience primitives for TypeScript
|
|
3
|
+
Resilience primitives for TypeScript.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/clutchit)
|
|
6
|
-
[](
|
|
6
|
+
[](./LICENSE)
|
|
7
7
|
[](https://clutchit.existin.space)
|
|
8
|
-
[](https://bundlephobia.com/package/clutchit)
|
|
9
8
|
[](https://www.npmjs.com/package/clutchit)
|
|
10
9
|
|
|
11
10
|
**[Documentation](https://clutchit.existin.space)** · [npm](https://www.npmjs.com/package/clutchit) · [Changelog](./CHANGELOG.md)
|
|
@@ -20,7 +19,7 @@ npm install clutchit
|
|
|
20
19
|
|
|
21
20
|
| Import | Description |
|
|
22
21
|
|---|---|
|
|
23
|
-
| `clutchit/unthrow` | `
|
|
22
|
+
| `clutchit/unthrow` | `Try<T,E>`, `TryAsync<T,E>`, `Fault`, Do notation |
|
|
24
23
|
| `clutchit/schedule` | Composable timing schedules — exponential, linear, fibonacci, cron, … |
|
|
25
24
|
| `clutchit/retry` | Retry policy with backoff schedules and abort signal support |
|
|
26
25
|
| `clutchit/timeout` | Timeout wrapper with `AbortSignal` propagation |
|
|
@@ -33,10 +32,10 @@ npm install clutchit
|
|
|
33
32
|
## Quick start
|
|
34
33
|
|
|
35
34
|
```ts
|
|
36
|
-
import {
|
|
37
|
-
import { RetryPolicy }
|
|
38
|
-
import { withTimeout }
|
|
39
|
-
import { Schedule }
|
|
35
|
+
import { ok, err, okAsync, errAsync, fromPromise, Fault } from 'clutchit/unthrow';
|
|
36
|
+
import { RetryPolicy } from 'clutchit/retry';
|
|
37
|
+
import { withTimeout } from 'clutchit/timeout';
|
|
38
|
+
import { Schedule } from 'clutchit/schedule';
|
|
40
39
|
|
|
41
40
|
class ApiFault extends Fault.Tagged('API_ERROR', 'infrastructure', true)<{
|
|
42
41
|
status: number;
|
|
@@ -44,8 +43,8 @@ class ApiFault extends Fault.Tagged('API_ERROR', 'infrastructure', true)<{
|
|
|
44
43
|
readonly message = `API returned ${this.status}`;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
function fetchUser(id: string, signal: AbortSignal):
|
|
48
|
-
return
|
|
46
|
+
function fetchUser(id: string, signal: AbortSignal): TryAsync<User, ApiFault> {
|
|
47
|
+
return fromPromise(
|
|
49
48
|
fetch(`/api/users/${id}`, { signal }).then((r) => r.json()),
|
|
50
49
|
() => new ApiFault({ status: 500 }),
|
|
51
50
|
);
|
|
@@ -72,38 +71,38 @@ await result.match({
|
|
|
72
71
|
|
|
73
72
|
Typed error handling without exceptions.
|
|
74
73
|
|
|
75
|
-
###
|
|
74
|
+
### Try
|
|
76
75
|
|
|
77
76
|
```ts
|
|
78
|
-
import {
|
|
77
|
+
import { ok, err } from 'clutchit/unthrow';
|
|
79
78
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
79
|
+
const success = ok(42);
|
|
80
|
+
const failure = err('oops');
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
success.map((n) => n * 2); // Ok(84)
|
|
83
|
+
success.andThen((n) => ok(n)); // Ok(42)
|
|
84
|
+
success.unwrapOr(0); // 42
|
|
85
|
+
failure.unwrapOr(0); // 0
|
|
87
86
|
|
|
88
|
-
|
|
87
|
+
success.match({
|
|
89
88
|
ok: (value) => `got ${value}`,
|
|
90
89
|
err: (error) => `failed: ${error}`,
|
|
91
90
|
});
|
|
92
91
|
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
92
|
+
if (success.isOk()) success.value; // narrowed
|
|
93
|
+
if (failure.isErr()) failure.error; // narrowed
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
Methods: `isOk`, `isErr`, `map`, `mapErr`, `andThen`, `orElse`, `unwrapOr`, `match`
|
|
96
|
+
Methods: `isOk`, `isErr`, `map`, `mapErr`, `andThen`, `orElse`, `tap`, `tapErr`, `andThrough`, `orThrough`, `catchFault`, `unwrapOr`, `match`
|
|
98
97
|
|
|
99
|
-
###
|
|
98
|
+
### TryAsync
|
|
100
99
|
|
|
101
|
-
Wraps `Promise<
|
|
100
|
+
Wraps `Promise<Try<T, E>>`. Awaitable via `PromiseLike`, same chainable API as `Try`.
|
|
102
101
|
|
|
103
102
|
```ts
|
|
104
|
-
import {
|
|
103
|
+
import { fromPromise } from 'clutchit/unthrow';
|
|
105
104
|
|
|
106
|
-
const result =
|
|
105
|
+
const result = fromPromise(
|
|
107
106
|
fetch('/api/data').then((r) => r.json()),
|
|
108
107
|
(err) => new NetworkFault({ cause: String(err) }),
|
|
109
108
|
);
|
|
@@ -113,26 +112,43 @@ const final = await result
|
|
|
113
112
|
.andThen((items) => validate(items));
|
|
114
113
|
```
|
|
115
114
|
|
|
116
|
-
Methods: `map`, `mapErr`, `andThen`, `orElse`, `unwrapOr`, `match` (all return `
|
|
115
|
+
Methods: `map`, `mapErr`, `andThen`, `orElse`, `tap`, `tapErr`, `andThrough`, `orThrough`, `catchFault`, `unwrapOr`, `match` (all return `TryAsync` or `Promise`)
|
|
117
116
|
|
|
118
117
|
### Helpers
|
|
119
118
|
|
|
120
119
|
```ts
|
|
121
|
-
import {
|
|
120
|
+
import { ok, err, okAsync, fromPromise, fromThrowable, fromSafePromise, fromAsyncThrowable, combine, combineWithAllErrors, unwrap, flatten, race } from 'clutchit/unthrow';
|
|
122
121
|
|
|
123
122
|
// Wrap a throwable sync function
|
|
124
|
-
|
|
123
|
+
fromThrowable(
|
|
125
124
|
() => JSON.parse(raw),
|
|
126
125
|
(err) => new ParseFault({ message: String(err) }),
|
|
127
126
|
);
|
|
128
127
|
|
|
128
|
+
// Wrap an async throwable function
|
|
129
|
+
fromAsyncThrowable(
|
|
130
|
+
() => fetchData(),
|
|
131
|
+
(err) => new NetworkFault({ cause: String(err) }),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Lift a safe (never-rejecting) promise
|
|
135
|
+
fromSafePromise(Promise.resolve(42));
|
|
136
|
+
|
|
137
|
+
// Assert non-null values
|
|
138
|
+
unwrap(ok<string | null, E>(value), new NullFault());
|
|
139
|
+
|
|
140
|
+
// Flatten nested results
|
|
141
|
+
flatten(ok(ok(42))); // Ok(42)
|
|
142
|
+
|
|
129
143
|
// Combine — short-circuit on first error
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
combine([ok(1), ok(2)]); // Ok([1, 2])
|
|
145
|
+
combine([okAsync(1), okAsync(2)]); // TryAsync<[1, 2], E>
|
|
132
146
|
|
|
133
147
|
// Combine — collect all errors
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
combineWithAllErrors([ok(1), err('a'), err('b')]); // Err(['a', 'b'])
|
|
149
|
+
|
|
150
|
+
// Race — first to resolve wins
|
|
151
|
+
race([okAsync(1), okAsync(2)]);
|
|
136
152
|
```
|
|
137
153
|
|
|
138
154
|
### Do notation
|
|
@@ -140,22 +156,28 @@ ResultAsync.combineWithAllErrors([a, b, c]); // ResultAsync<[A, B, C], E[]>
|
|
|
140
156
|
Generator-based sequential chaining. `yield*` unwraps `Ok`; any `Err` short-circuits the entire block.
|
|
141
157
|
|
|
142
158
|
```ts
|
|
143
|
-
import {
|
|
159
|
+
import { Do, DoAsync } from 'clutchit/unthrow';
|
|
144
160
|
|
|
145
161
|
// Sync
|
|
146
|
-
const result =
|
|
147
|
-
const user = yield* findUser(id); //
|
|
148
|
-
const perms = yield* getPermissions(user); //
|
|
162
|
+
const result = Do(function* () {
|
|
163
|
+
const user = yield* findUser(id); // Try<User, NotFoundFault>
|
|
164
|
+
const perms = yield* getPermissions(user); // Try<Perms, PermFault>
|
|
149
165
|
return { user, perms };
|
|
150
|
-
//
|
|
166
|
+
// Try<{ user, perms }, NotFoundFault | PermFault>
|
|
151
167
|
});
|
|
152
168
|
|
|
153
169
|
// Async
|
|
154
|
-
const result =
|
|
155
|
-
const user = yield* fetchUser(id); //
|
|
156
|
-
const order = yield* createOrder(user); //
|
|
170
|
+
const result = DoAsync(async function* () {
|
|
171
|
+
const user = yield* fetchUser(id); // TryAsync<User, NetworkFault>
|
|
172
|
+
const order = yield* createOrder(user); // TryAsync<Order, OrderFault>
|
|
157
173
|
return order;
|
|
158
|
-
//
|
|
174
|
+
// TryAsync<Order, NetworkFault | OrderFault>
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// With context
|
|
178
|
+
const result = Do({ db }, function* (ctx) {
|
|
179
|
+
const user = yield* ctx.db.findUser(id);
|
|
180
|
+
return user;
|
|
159
181
|
});
|
|
160
182
|
```
|
|
161
183
|
|
|
@@ -222,7 +244,7 @@ const result = Schedule.repeat(
|
|
|
222
244
|
onExecution: (attempt, delay) => console.log(`#${attempt}, next in ${delay}ms`),
|
|
223
245
|
},
|
|
224
246
|
);
|
|
225
|
-
//
|
|
247
|
+
// TryAsync<number, SyncError | ScheduleInterruptedFault>
|
|
226
248
|
```
|
|
227
249
|
|
|
228
250
|
---
|
|
@@ -241,7 +263,7 @@ const retry = new RetryPolicy({
|
|
|
241
263
|
});
|
|
242
264
|
|
|
243
265
|
const result = retry.execute((signal) => fetchData(signal));
|
|
244
|
-
//
|
|
266
|
+
// TryAsync<Data, FetchFault>
|
|
245
267
|
```
|
|
246
268
|
|
|
247
269
|
Options can be set on the constructor (defaults for all calls) or overridden per `execute` call.
|
|
@@ -265,7 +287,7 @@ const result = withTimeout(
|
|
|
265
287
|
(signal) => fetchData(signal),
|
|
266
288
|
5_000,
|
|
267
289
|
);
|
|
268
|
-
//
|
|
290
|
+
// TryAsync<Data, FetchFault | TimeoutFault>
|
|
269
291
|
|
|
270
292
|
// Reusable wrapper
|
|
271
293
|
const withFiveSeconds = createTimeout(5_000);
|
|
@@ -322,9 +344,9 @@ Atomic mutable reference with internal mutex:
|
|
|
322
344
|
```ts
|
|
323
345
|
const counter = new Concurrency.Ref(0);
|
|
324
346
|
|
|
325
|
-
await counter.update((n) => n + 1); //
|
|
326
|
-
await counter.set(42); //
|
|
327
|
-
const next = await counter.modify( //
|
|
347
|
+
await counter.update((n) => n + 1); // TryAsync<void, never>
|
|
348
|
+
await counter.set(42); // TryAsync<void, never>
|
|
349
|
+
const next = await counter.modify( // TryAsync<number, never>
|
|
328
350
|
(n) => [n + 1, n + 1],
|
|
329
351
|
);
|
|
330
352
|
counter.get(); // current value (sync)
|
|
@@ -347,7 +369,7 @@ const breaker = new Circuit.Breaker({
|
|
|
347
369
|
});
|
|
348
370
|
|
|
349
371
|
const result = breaker.protect(() => callService());
|
|
350
|
-
//
|
|
372
|
+
// TryAsync<T, ServiceFault | CircuitOpenFault>
|
|
351
373
|
|
|
352
374
|
breaker.state; // 'closed' | 'open' | 'half-open'
|
|
353
375
|
breaker.failureCount; // consecutive failures
|
|
@@ -367,13 +389,13 @@ import { Queue, QueueFullFault, QueueEmptyFault } from 'clutchit/queue';
|
|
|
367
389
|
|
|
368
390
|
const q = new Queue.Bounded<Job>({ capacity: 100 });
|
|
369
391
|
|
|
370
|
-
q.offer(job); //
|
|
392
|
+
q.offer(job); // Try<void, QueueFullFault> — non-blocking
|
|
371
393
|
await q.put(job); // blocks until space is available
|
|
372
394
|
await q.put(job, signal); // cancellable
|
|
373
395
|
|
|
374
396
|
const item = await q.take(); // blocks until item available
|
|
375
397
|
const batch = await q.takeBatch(10); // at least 1, up to 10
|
|
376
|
-
const maybe = q.poll(); //
|
|
398
|
+
const maybe = q.poll(); // Try<Job, QueueEmptyFault>
|
|
377
399
|
```
|
|
378
400
|
|
|
379
401
|
### DroppingQueue — drops newest when full
|
|
@@ -422,6 +444,7 @@ class UnauthorizedFault extends Fault.Tagged('UNAUTHORIZED', 'domain')() {
|
|
|
422
444
|
### Type guards
|
|
423
445
|
|
|
424
446
|
```ts
|
|
447
|
+
Fault.is(value); // duck-type check for Fault shape
|
|
425
448
|
Fault.isDomain(fault); // _category === 'domain'
|
|
426
449
|
Fault.isInfrastructure(fault); // _category === 'infrastructure'
|
|
427
450
|
Fault.isTransient(fault); // infrastructure + _transient
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
import { Schedule } from '../schedule/index.js';
|
|
3
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
4
|
+
declare const CircuitOpenFault_base: abstract new (fields: {
|
|
5
|
+
remainingTimeout: number;
|
|
6
|
+
}) => Readonly<{
|
|
7
|
+
remainingTimeout: number;
|
|
8
|
+
}> & {
|
|
9
|
+
readonly code: "CIRCUIT_OPEN";
|
|
10
|
+
readonly _category: "infrastructure";
|
|
11
|
+
readonly _transient: false;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class CircuitOpenFault extends CircuitOpenFault_base {
|
|
15
|
+
readonly message: string;
|
|
16
|
+
}
|
|
17
|
+
export interface CircuitBreakerOptions {
|
|
18
|
+
/** Number of consecutive failures before opening. Default: 5 */
|
|
19
|
+
failureThreshold?: number;
|
|
20
|
+
/** Schedule for reset timing. Default: Schedule.constant(30_000) */
|
|
21
|
+
resetSchedule?: Schedule;
|
|
22
|
+
/** Number of trial requests allowed in half-open state. Default: 1 */
|
|
23
|
+
halfOpenAttempts?: number;
|
|
24
|
+
/** Predicate to determine if an error should count as a failure. Default: all errors count. */
|
|
25
|
+
isFailure?: (error: unknown) => boolean;
|
|
26
|
+
/** Called when circuit state changes (for logging/monitoring). */
|
|
27
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class CircuitBreaker {
|
|
30
|
+
private _state;
|
|
31
|
+
private _failureCount;
|
|
32
|
+
private _resetAttempt;
|
|
33
|
+
private _halfOpenTrials;
|
|
34
|
+
private _nextResetTime;
|
|
35
|
+
private readonly _failureThreshold;
|
|
36
|
+
private readonly _resetSchedule;
|
|
37
|
+
private readonly _maxHalfOpenAttempts;
|
|
38
|
+
private readonly _isFailure;
|
|
39
|
+
private readonly _onStateChange?;
|
|
40
|
+
constructor(options?: CircuitBreakerOptions);
|
|
41
|
+
protect<T, E>(fn: () => TryAsync<T, E>): TryAsync<T, E | CircuitOpenFault>;
|
|
42
|
+
get state(): CircuitState;
|
|
43
|
+
get failureCount(): number;
|
|
44
|
+
reset(): void;
|
|
45
|
+
private onSuccess;
|
|
46
|
+
private onFailure;
|
|
47
|
+
private scheduleReset;
|
|
48
|
+
private transition;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=circuit.breaker.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fault,
|
|
1
|
+
import { Fault, errAsync, fromPromise, } from '../unthrow/index.js';
|
|
2
2
|
import { Schedule } from '../schedule/index.js';
|
|
3
3
|
export class CircuitOpenFault extends Fault.Tagged('CIRCUIT_OPEN', 'infrastructure')() {
|
|
4
4
|
message = `Circuit breaker is open. Retry after ${this.remainingTimeout}ms`;
|
|
@@ -31,16 +31,16 @@ export class CircuitBreaker {
|
|
|
31
31
|
const remainingTimeout = this._nextResetTime
|
|
32
32
|
? Math.max(0, this._nextResetTime - Date.now())
|
|
33
33
|
: 0;
|
|
34
|
-
return
|
|
34
|
+
return errAsync(new CircuitOpenFault({ remainingTimeout }));
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
if (this._state === 'half-open') {
|
|
38
38
|
if (this._halfOpenTrials >= this._maxHalfOpenAttempts) {
|
|
39
|
-
return
|
|
39
|
+
return errAsync(new CircuitOpenFault({ remainingTimeout: 0 }));
|
|
40
40
|
}
|
|
41
41
|
this._halfOpenTrials++;
|
|
42
42
|
}
|
|
43
|
-
return
|
|
43
|
+
return fromPromise((async () => {
|
|
44
44
|
const result = await fn();
|
|
45
45
|
if (result.isOk()) {
|
|
46
46
|
this.onSuccess();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CircuitBreaker } from './circuit.breaker.js';
|
|
2
|
+
import type { CircuitState, CircuitBreakerOptions } from './circuit.breaker.js';
|
|
3
|
+
export { CircuitOpenFault } from './circuit.breaker.js';
|
|
4
|
+
export declare namespace Circuit {
|
|
5
|
+
const Breaker: typeof CircuitBreaker;
|
|
6
|
+
type Breaker = CircuitBreaker;
|
|
7
|
+
type BreakerOptions = CircuitBreakerOptions;
|
|
8
|
+
type State = CircuitState;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
declare const BulkheadRejectedFault_base: abstract new () => Readonly<Record<string, unknown>> & {
|
|
3
|
+
readonly code: "BULKHEAD_REJECTED";
|
|
4
|
+
readonly _category: "infrastructure";
|
|
5
|
+
readonly _transient: false;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class BulkheadRejectedFault extends BulkheadRejectedFault_base {
|
|
9
|
+
readonly message = "Bulkhead capacity exceeded";
|
|
10
|
+
}
|
|
11
|
+
export interface BulkheadOptions {
|
|
12
|
+
concurrency: number;
|
|
13
|
+
queue?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class Bulkhead {
|
|
16
|
+
private _active;
|
|
17
|
+
private readonly _maxConcurrency;
|
|
18
|
+
private readonly _maxQueue;
|
|
19
|
+
private readonly _queue;
|
|
20
|
+
constructor(options: BulkheadOptions);
|
|
21
|
+
execute<T, E>(fn: () => TryAsync<T, E>): TryAsync<T, E | BulkheadRejectedFault>;
|
|
22
|
+
get activeCount(): number;
|
|
23
|
+
get queueSize(): number;
|
|
24
|
+
private acquire;
|
|
25
|
+
private release;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=bulkhead.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fault,
|
|
1
|
+
import { Fault, errAsync, fromPromise, } from '../unthrow/index.js';
|
|
2
2
|
export class BulkheadRejectedFault extends Fault.Tagged('BULKHEAD_REJECTED', 'infrastructure')() {
|
|
3
3
|
message = 'Bulkhead capacity exceeded';
|
|
4
4
|
}
|
|
@@ -14,9 +14,9 @@ export class Bulkhead {
|
|
|
14
14
|
execute(fn) {
|
|
15
15
|
if (this._active >= this._maxConcurrency &&
|
|
16
16
|
this._queue.length >= this._maxQueue) {
|
|
17
|
-
return
|
|
17
|
+
return errAsync(new BulkheadRejectedFault());
|
|
18
18
|
}
|
|
19
|
-
return
|
|
19
|
+
return fromPromise((async () => {
|
|
20
20
|
await this.acquire();
|
|
21
21
|
try {
|
|
22
22
|
const result = await fn();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Semaphore as _Semaphore } from './semaphore.js';
|
|
2
|
+
import type { SemaphoreOptions as _SemaphoreOptions } from './semaphore.js';
|
|
3
|
+
import { RateLimiter as _RateLimiter } from './rate-limiter.js';
|
|
4
|
+
import type { RateLimiterOptions as _RateLimiterOptions } from './rate-limiter.js';
|
|
5
|
+
import { Bulkhead as _Bulkhead } from './bulkhead.js';
|
|
6
|
+
import type { BulkheadOptions as _BulkheadOptions } from './bulkhead.js';
|
|
7
|
+
import { Ref as _Ref } from './ref.js';
|
|
8
|
+
export { RateLimitFault } from './rate-limiter.js';
|
|
9
|
+
export { BulkheadRejectedFault } from './bulkhead.js';
|
|
10
|
+
export declare namespace Concurrency {
|
|
11
|
+
const Semaphore: typeof _Semaphore;
|
|
12
|
+
type Semaphore = _Semaphore;
|
|
13
|
+
type SemaphoreOptions = _SemaphoreOptions;
|
|
14
|
+
const RateLimiter: typeof _RateLimiter;
|
|
15
|
+
type RateLimiter = _RateLimiter;
|
|
16
|
+
type RateLimiterOptions = _RateLimiterOptions;
|
|
17
|
+
const Bulkhead: typeof _Bulkhead;
|
|
18
|
+
type Bulkhead = _Bulkhead;
|
|
19
|
+
type BulkheadOptions = _BulkheadOptions;
|
|
20
|
+
const Ref: typeof _Ref;
|
|
21
|
+
type Ref<T> = _Ref<T>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
declare const RateLimitFault_base: abstract new (fields: {
|
|
3
|
+
retryAfter: number;
|
|
4
|
+
}) => Readonly<{
|
|
5
|
+
retryAfter: number;
|
|
6
|
+
}> & {
|
|
7
|
+
readonly code: "RATE_LIMITED";
|
|
8
|
+
readonly _category: "infrastructure";
|
|
9
|
+
readonly _transient: true;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
};
|
|
12
|
+
export declare class RateLimitFault extends RateLimitFault_base {
|
|
13
|
+
readonly message: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RateLimiterOptions {
|
|
16
|
+
limit: number;
|
|
17
|
+
window: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class RateLimiter {
|
|
20
|
+
private readonly _limit;
|
|
21
|
+
private readonly _window;
|
|
22
|
+
private readonly _timestamps;
|
|
23
|
+
constructor(options: RateLimiterOptions);
|
|
24
|
+
execute<T, E>(fn: () => TryAsync<T, E>): TryAsync<T, E | RateLimitFault>;
|
|
25
|
+
get remaining(): number;
|
|
26
|
+
private cleanup;
|
|
27
|
+
}
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fault,
|
|
1
|
+
import { Fault, errAsync } from '../unthrow/index.js';
|
|
2
2
|
export class RateLimitFault extends Fault.Tagged('RATE_LIMITED', 'infrastructure', true)() {
|
|
3
3
|
message = `Rate limit exceeded. Retry after ${this.retryAfter}ms`;
|
|
4
4
|
}
|
|
@@ -15,7 +15,7 @@ export class RateLimiter {
|
|
|
15
15
|
if (this._timestamps.length >= this._limit) {
|
|
16
16
|
const oldest = this._timestamps[0];
|
|
17
17
|
const retryAfter = Math.max(0, oldest + this._window - Date.now());
|
|
18
|
-
return
|
|
18
|
+
return errAsync(new RateLimitFault({ retryAfter }));
|
|
19
19
|
}
|
|
20
20
|
this._timestamps.push(Date.now());
|
|
21
21
|
return fn();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
export declare class Ref<T> {
|
|
3
|
+
private _value;
|
|
4
|
+
private readonly _mutex;
|
|
5
|
+
constructor(initial: T);
|
|
6
|
+
get(): T;
|
|
7
|
+
modify<R>(fn: (current: T) => [next: T, result: R]): TryAsync<R, never>;
|
|
8
|
+
update(fn: (current: T) => T): TryAsync<void, never>;
|
|
9
|
+
set(value: T): TryAsync<void, never>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=ref.d.ts.map
|
package/dist/concurrency/ref.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { okAsync } from '../unthrow/index.js';
|
|
2
2
|
import { Semaphore } from './semaphore.js';
|
|
3
3
|
export class Ref {
|
|
4
4
|
_value;
|
|
@@ -13,7 +13,7 @@ export class Ref {
|
|
|
13
13
|
return this._mutex.execute(() => {
|
|
14
14
|
const [next, result] = fn(this._value);
|
|
15
15
|
this._value = next;
|
|
16
|
-
return
|
|
16
|
+
return okAsync(result);
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
update(fn) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
export interface SemaphoreOptions {
|
|
3
|
+
permits: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class Semaphore {
|
|
6
|
+
private _available;
|
|
7
|
+
private readonly _queue;
|
|
8
|
+
constructor(options: SemaphoreOptions);
|
|
9
|
+
execute<T, E>(fn: () => TryAsync<T, E>): TryAsync<T, E>;
|
|
10
|
+
get available(): number;
|
|
11
|
+
get waiting(): number;
|
|
12
|
+
private acquire;
|
|
13
|
+
private release;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=semaphore.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fromPromise } from '../unthrow/index.js';
|
|
2
2
|
export class Semaphore {
|
|
3
3
|
_available;
|
|
4
4
|
_queue = [];
|
|
@@ -6,7 +6,7 @@ export class Semaphore {
|
|
|
6
6
|
this._available = options.permits;
|
|
7
7
|
}
|
|
8
8
|
execute(fn) {
|
|
9
|
-
return
|
|
9
|
+
return fromPromise((async () => {
|
|
10
10
|
await this.acquire();
|
|
11
11
|
try {
|
|
12
12
|
const result = await fn();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Try, type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
import { QueueEmptyFault } from './faults.js';
|
|
3
|
+
export interface QueueOptions {
|
|
4
|
+
capacity: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class BaseQueue<T> {
|
|
7
|
+
protected readonly _buffer: T[];
|
|
8
|
+
protected readonly _capacity: number;
|
|
9
|
+
protected readonly _takeWaiters: Array<(item: T) => void>;
|
|
10
|
+
constructor(options: QueueOptions);
|
|
11
|
+
protected _tryTake(): Try<T, QueueEmptyFault>;
|
|
12
|
+
take(signal?: AbortSignal): TryAsync<T, never>;
|
|
13
|
+
takeBatch(n: number, signal?: AbortSignal): TryAsync<T[], never>;
|
|
14
|
+
poll(): Try<T, QueueEmptyFault>;
|
|
15
|
+
get size(): number;
|
|
16
|
+
get capacity(): number;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=base.queue.d.ts.map
|
package/dist/queue/base.queue.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ok, err, okAsync, fromPromise, } from '../unthrow/index.js';
|
|
2
2
|
import { QueueEmptyFault } from './faults.js';
|
|
3
3
|
export class BaseQueue {
|
|
4
4
|
_buffer = [];
|
|
@@ -9,15 +9,15 @@ export class BaseQueue {
|
|
|
9
9
|
}
|
|
10
10
|
_tryTake() {
|
|
11
11
|
if (this._buffer.length > 0) {
|
|
12
|
-
return
|
|
12
|
+
return ok(this._buffer.shift());
|
|
13
13
|
}
|
|
14
|
-
return
|
|
14
|
+
return err(new QueueEmptyFault());
|
|
15
15
|
}
|
|
16
16
|
take(signal) {
|
|
17
17
|
const taken = this._tryTake();
|
|
18
18
|
if (taken.isOk())
|
|
19
|
-
return
|
|
20
|
-
return
|
|
19
|
+
return okAsync(taken.value);
|
|
20
|
+
return fromPromise(new Promise((resolve) => {
|
|
21
21
|
this._takeWaiters.push(resolve);
|
|
22
22
|
signal?.addEventListener('abort', () => {
|
|
23
23
|
const idx = this._takeWaiters.indexOf(resolve);
|
|
@@ -27,7 +27,7 @@ export class BaseQueue {
|
|
|
27
27
|
}), () => undefined);
|
|
28
28
|
}
|
|
29
29
|
takeBatch(n, signal) {
|
|
30
|
-
return
|
|
30
|
+
return fromPromise((async () => {
|
|
31
31
|
const taken = this._tryTake();
|
|
32
32
|
let first;
|
|
33
33
|
if (taken.isOk()) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Try, type TryAsync } from '../unthrow/index.js';
|
|
2
|
+
import { QueueEmptyFault, QueueFullFault } from './faults.js';
|
|
3
|
+
import { BaseQueue, type QueueOptions } from './base.queue.js';
|
|
4
|
+
export declare class BoundedQueue<T> extends BaseQueue<T> {
|
|
5
|
+
private readonly _putWaiters;
|
|
6
|
+
constructor(options: QueueOptions);
|
|
7
|
+
protected _tryTake(): Try<T, QueueEmptyFault>;
|
|
8
|
+
offer(item: T): Try<void, QueueFullFault>;
|
|
9
|
+
put(item: T, signal?: AbortSignal): TryAsync<void, never>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=bounded.queue.d.ts.map
|