clutchit 0.0.7 → 0.0.9

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 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
- - `Result<T, E>` — synchronous typed error container with `Ok` and `Err` variants
15
- - `ResultAsync<T, E>` — async equivalent backed by `Promise<Result<T, E>>`, implements `PromiseLike`
16
- - `Result.Do` / `ResultAsync.Do` — generator-based Do notation for ergonomic sequential chaining
17
- - `Result.combine` / `ResultAsync.combine` — combine arrays of results, short-circuit on first error
18
- - `Result.combineWithAllErrors` / `ResultAsync.combineWithAllErrors` collect all errors instead of stopping at first
19
- - `Result.fromThrowable` — wrap a throwing sync function into a `Result`
20
- - `ResultAsync.fromPromise` — lift a `Promise` into a `ResultAsync` with typed error mapping
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.isDomain` / `Fault.isInfrastructure` / `Fault.isTransient` type guard helpers
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 `ResultAsync`-returning function repeatedly on a schedule with abort signal support
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 `ResultAsync`-returning function with a deadline
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 — typed errors, retry, timeout, circuit breaking, scheduling, concurrency, and queues.
3
+ Resilience primitives for TypeScript.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/clutchit)](https://www.npmjs.com/package/clutchit)
6
- [![license](https://img.shields.io/npm/l/clutchit)](../LICENSE)
6
+ [![license](https://img.shields.io/npm/l/clutchit)](./LICENSE)
7
7
  [![docs](https://img.shields.io/badge/docs-clutchit.existin.space-blue)](https://clutchit.existin.space)
8
- [![bundle size](https://img.shields.io/bundlephobia/minzip/clutchit)](https://bundlephobia.com/package/clutchit)
9
8
  [![types](https://img.shields.io/npm/types/clutchit)](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)
@@ -14,15 +13,13 @@ Resilience primitives for TypeScript — typed errors, retry, timeout, circuit b
14
13
 
15
14
  ```bash
16
15
  npm install clutchit
17
- # pnpm add clutchit
18
- # bun add clutchit
19
16
  ```
20
17
 
21
18
  ## Modules
22
19
 
23
20
  | Import | Description |
24
21
  |---|---|
25
- | `clutchit/unthrow` | `Result<T,E>`, `ResultAsync<T,E>`, `Fault`, Do notation |
22
+ | `clutchit/unthrow` | `Try<T,E>`, `TryAsync<T,E>`, `Fault`, Do notation |
26
23
  | `clutchit/schedule` | Composable timing schedules — exponential, linear, fibonacci, cron, … |
27
24
  | `clutchit/retry` | Retry policy with backoff schedules and abort signal support |
28
25
  | `clutchit/timeout` | Timeout wrapper with `AbortSignal` propagation |
@@ -35,10 +32,10 @@ npm install clutchit
35
32
  ## Quick start
36
33
 
37
34
  ```ts
38
- import { ResultAsync, Fault } from 'clutchit/unthrow';
39
- import { RetryPolicy } from 'clutchit/retry';
40
- import { withTimeout } from 'clutchit/timeout';
41
- import { Schedule } from 'clutchit/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';
42
39
 
43
40
  class ApiFault extends Fault.Tagged('API_ERROR', 'infrastructure', true)<{
44
41
  status: number;
@@ -46,8 +43,8 @@ class ApiFault extends Fault.Tagged('API_ERROR', 'infrastructure', true)<{
46
43
  readonly message = `API returned ${this.status}`;
47
44
  }
48
45
 
49
- function fetchUser(id: string, signal: AbortSignal): ResultAsync<User, ApiFault> {
50
- return ResultAsync.fromPromise(
46
+ function fetchUser(id: string, signal: AbortSignal): TryAsync<User, ApiFault> {
47
+ return fromPromise(
51
48
  fetch(`/api/users/${id}`, { signal }).then((r) => r.json()),
52
49
  () => new ApiFault({ status: 500 }),
53
50
  );
@@ -74,38 +71,38 @@ await result.match({
74
71
 
75
72
  Typed error handling without exceptions.
76
73
 
77
- ### Result
74
+ ### Try
78
75
 
79
76
  ```ts
80
- import { Result } from 'clutchit/unthrow';
77
+ import { ok, err } from 'clutchit/unthrow';
81
78
 
82
- const ok = Result.ok(42);
83
- const fail = Result.err('oops');
79
+ const success = ok(42);
80
+ const failure = err('oops');
84
81
 
85
- ok.map((n) => n * 2); // Ok(84)
86
- ok.andThen((n) => Result.ok(n)); // Ok(42)
87
- ok.unwrapOr(0); // 42
88
- fail.unwrapOr(0); // 0
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
89
86
 
90
- ok.match({
87
+ success.match({
91
88
  ok: (value) => `got ${value}`,
92
89
  err: (error) => `failed: ${error}`,
93
90
  });
94
91
 
95
- if (ok.isOk()) ok.value; // narrowed
96
- if (fail.isErr()) fail.error; // narrowed
92
+ if (success.isOk()) success.value; // narrowed
93
+ if (failure.isErr()) failure.error; // narrowed
97
94
  ```
98
95
 
99
- Methods: `isOk`, `isErr`, `map`, `mapErr`, `andThen`, `orElse`, `unwrapOr`, `match`
96
+ Methods: `isOk`, `isErr`, `map`, `mapErr`, `andThen`, `orElse`, `tap`, `tapErr`, `andThrough`, `orThrough`, `catchFault`, `unwrapOr`, `match`
100
97
 
101
- ### ResultAsync
98
+ ### TryAsync
102
99
 
103
- Wraps `Promise<Result<T, E>>`. Awaitable via `PromiseLike`, same chainable API as `Result`.
100
+ Wraps `Promise<Try<T, E>>`. Awaitable via `PromiseLike`, same chainable API as `Try`.
104
101
 
105
102
  ```ts
106
- import { ResultAsync } from 'clutchit/unthrow';
103
+ import { fromPromise } from 'clutchit/unthrow';
107
104
 
108
- const result = ResultAsync.fromPromise(
105
+ const result = fromPromise(
109
106
  fetch('/api/data').then((r) => r.json()),
110
107
  (err) => new NetworkFault({ cause: String(err) }),
111
108
  );
@@ -115,26 +112,43 @@ const final = await result
115
112
  .andThen((items) => validate(items));
116
113
  ```
117
114
 
118
- Methods: `map`, `mapErr`, `andThen`, `orElse`, `unwrapOr`, `match` (all return `ResultAsync` or `Promise`)
115
+ Methods: `map`, `mapErr`, `andThen`, `orElse`, `tap`, `tapErr`, `andThrough`, `orThrough`, `catchFault`, `unwrapOr`, `match` (all return `TryAsync` or `Promise`)
119
116
 
120
117
  ### Helpers
121
118
 
122
119
  ```ts
123
- import { Result, ResultAsync } from 'clutchit/unthrow';
120
+ import { ok, err, okAsync, fromPromise, fromThrowable, fromSafePromise, fromAsyncThrowable, combine, combineWithAllErrors, unwrap, flatten, race } from 'clutchit/unthrow';
124
121
 
125
122
  // Wrap a throwable sync function
126
- Result.fromThrowable(
123
+ fromThrowable(
127
124
  () => JSON.parse(raw),
128
125
  (err) => new ParseFault({ message: String(err) }),
129
126
  );
130
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
+
131
143
  // Combine — short-circuit on first error
132
- Result.combine([resultA, resultB]); // Result<[A, B], E>
133
- ResultAsync.combine([asyncA, asyncB]); // ResultAsync<[A, B], E>
144
+ combine([ok(1), ok(2)]); // Ok([1, 2])
145
+ combine([okAsync(1), okAsync(2)]); // TryAsync<[1, 2], E>
134
146
 
135
147
  // Combine — collect all errors
136
- Result.combineWithAllErrors([a, b, c]); // Result<[A, B, C], E[]>
137
- ResultAsync.combineWithAllErrors([a, b, c]); // ResultAsync<[A, B, C], E[]>
148
+ combineWithAllErrors([ok(1), err('a'), err('b')]); // Err(['a', 'b'])
149
+
150
+ // Race — first to resolve wins
151
+ race([okAsync(1), okAsync(2)]);
138
152
  ```
139
153
 
140
154
  ### Do notation
@@ -142,22 +156,28 @@ ResultAsync.combineWithAllErrors([a, b, c]); // ResultAsync<[A, B, C], E[]>
142
156
  Generator-based sequential chaining. `yield*` unwraps `Ok`; any `Err` short-circuits the entire block.
143
157
 
144
158
  ```ts
145
- import { Result, ResultAsync } from 'clutchit/unthrow';
159
+ import { Do, DoAsync } from 'clutchit/unthrow';
146
160
 
147
161
  // Sync
148
- const result = Result.Do(function* () {
149
- const user = yield* findUser(id); // Result<User, NotFoundFault>
150
- const perms = yield* getPermissions(user); // Result<Perms, PermFault>
162
+ const result = Do(function* () {
163
+ const user = yield* findUser(id); // Try<User, NotFoundFault>
164
+ const perms = yield* getPermissions(user); // Try<Perms, PermFault>
151
165
  return { user, perms };
152
- // Result<{ user, perms }, NotFoundFault | PermFault>
166
+ // Try<{ user, perms }, NotFoundFault | PermFault>
153
167
  });
154
168
 
155
169
  // Async
156
- const result = ResultAsync.Do(async function* () {
157
- const user = yield* fetchUser(id); // ResultAsync<User, NetworkFault>
158
- const order = yield* createOrder(user); // ResultAsync<Order, OrderFault>
170
+ const result = DoAsync(async function* () {
171
+ const user = yield* fetchUser(id); // TryAsync<User, NetworkFault>
172
+ const order = yield* createOrder(user); // TryAsync<Order, OrderFault>
159
173
  return order;
160
- // ResultAsync<Order, NetworkFault | OrderFault>
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;
161
181
  });
162
182
  ```
163
183
 
@@ -224,7 +244,7 @@ const result = Schedule.repeat(
224
244
  onExecution: (attempt, delay) => console.log(`#${attempt}, next in ${delay}ms`),
225
245
  },
226
246
  );
227
- // ResultAsync<number, SyncError | ScheduleInterruptedFault>
247
+ // TryAsync<number, SyncError | ScheduleInterruptedFault>
228
248
  ```
229
249
 
230
250
  ---
@@ -243,7 +263,7 @@ const retry = new RetryPolicy({
243
263
  });
244
264
 
245
265
  const result = retry.execute((signal) => fetchData(signal));
246
- // ResultAsync<Data, FetchFault>
266
+ // TryAsync<Data, FetchFault>
247
267
  ```
248
268
 
249
269
  Options can be set on the constructor (defaults for all calls) or overridden per `execute` call.
@@ -267,7 +287,7 @@ const result = withTimeout(
267
287
  (signal) => fetchData(signal),
268
288
  5_000,
269
289
  );
270
- // ResultAsync<Data, FetchFault | TimeoutFault>
290
+ // TryAsync<Data, FetchFault | TimeoutFault>
271
291
 
272
292
  // Reusable wrapper
273
293
  const withFiveSeconds = createTimeout(5_000);
@@ -324,9 +344,9 @@ Atomic mutable reference with internal mutex:
324
344
  ```ts
325
345
  const counter = new Concurrency.Ref(0);
326
346
 
327
- await counter.update((n) => n + 1); // ResultAsync<void, never>
328
- await counter.set(42); // ResultAsync<void, never>
329
- const next = await counter.modify( // ResultAsync<number, never>
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>
330
350
  (n) => [n + 1, n + 1],
331
351
  );
332
352
  counter.get(); // current value (sync)
@@ -349,7 +369,7 @@ const breaker = new Circuit.Breaker({
349
369
  });
350
370
 
351
371
  const result = breaker.protect(() => callService());
352
- // ResultAsync<T, ServiceFault | CircuitOpenFault>
372
+ // TryAsync<T, ServiceFault | CircuitOpenFault>
353
373
 
354
374
  breaker.state; // 'closed' | 'open' | 'half-open'
355
375
  breaker.failureCount; // consecutive failures
@@ -369,13 +389,13 @@ import { Queue, QueueFullFault, QueueEmptyFault } from 'clutchit/queue';
369
389
 
370
390
  const q = new Queue.Bounded<Job>({ capacity: 100 });
371
391
 
372
- q.offer(job); // Result<void, QueueFullFault> — non-blocking
392
+ q.offer(job); // Try<void, QueueFullFault> — non-blocking
373
393
  await q.put(job); // blocks until space is available
374
394
  await q.put(job, signal); // cancellable
375
395
 
376
396
  const item = await q.take(); // blocks until item available
377
397
  const batch = await q.takeBatch(10); // at least 1, up to 10
378
- const maybe = q.poll(); // Result<Job, QueueEmptyFault>
398
+ const maybe = q.poll(); // Try<Job, QueueEmptyFault>
379
399
  ```
380
400
 
381
401
  ### DroppingQueue — drops newest when full
@@ -424,6 +444,7 @@ class UnauthorizedFault extends Fault.Tagged('UNAUTHORIZED', 'domain')() {
424
444
  ### Type guards
425
445
 
426
446
  ```ts
447
+ Fault.is(value); // duck-type check for Fault shape
427
448
  Fault.isDomain(fault); // _category === 'domain'
428
449
  Fault.isInfrastructure(fault); // _category === 'infrastructure'
429
450
  Fault.isTransient(fault); // infrastructure + _transient
@@ -1,4 +1,4 @@
1
- import { Fault, ResultAsync } from '../unthrow/index.js';
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 ResultAsync.err(new CircuitOpenFault({ remainingTimeout }));
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 ResultAsync.err(new CircuitOpenFault({ remainingTimeout: 0 }));
39
+ return errAsync(new CircuitOpenFault({ remainingTimeout: 0 }));
40
40
  }
41
41
  this._halfOpenTrials++;
42
42
  }
43
- return ResultAsync.fromPromise((async () => {
43
+ return fromPromise((async () => {
44
44
  const result = await fn();
45
45
  if (result.isOk()) {
46
46
  this.onSuccess();
@@ -1,4 +1,4 @@
1
- import { Fault, ResultAsync } from '../unthrow/index.js';
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 ResultAsync.err(new BulkheadRejectedFault());
17
+ return errAsync(new BulkheadRejectedFault());
18
18
  }
19
- return ResultAsync.fromPromise((async () => {
19
+ return fromPromise((async () => {
20
20
  await this.acquire();
21
21
  try {
22
22
  const result = await fn();
@@ -1,4 +1,4 @@
1
- import { Fault, ResultAsync } from '../unthrow/index.js';
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 ResultAsync.err(new RateLimitFault({ retryAfter }));
18
+ return errAsync(new RateLimitFault({ retryAfter }));
19
19
  }
20
20
  this._timestamps.push(Date.now());
21
21
  return fn();
@@ -1,4 +1,4 @@
1
- import { ResultAsync } from '../unthrow/index.js';
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 ResultAsync.ok(result);
16
+ return okAsync(result);
17
17
  });
18
18
  }
19
19
  update(fn) {
@@ -1,4 +1,4 @@
1
- import { ResultAsync } from '../unthrow/index.js';
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 ResultAsync.fromPromise((async () => {
9
+ return fromPromise((async () => {
10
10
  await this.acquire();
11
11
  try {
12
12
  const result = await fn();
@@ -1,4 +1,4 @@
1
- import { Result, ResultAsync } from '../unthrow/index.js';
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 Result.ok(this._buffer.shift());
12
+ return ok(this._buffer.shift());
13
13
  }
14
- return Result.err(new QueueEmptyFault());
14
+ return err(new QueueEmptyFault());
15
15
  }
16
16
  take(signal) {
17
17
  const taken = this._tryTake();
18
18
  if (taken.isOk())
19
- return ResultAsync.ok(taken.value);
20
- return ResultAsync.fromPromise(new Promise((resolve) => {
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 ResultAsync.fromPromise((async () => {
30
+ return fromPromise((async () => {
31
31
  const taken = this._tryTake();
32
32
  let first;
33
33
  if (taken.isOk()) {
@@ -1,4 +1,4 @@
1
- import { Result, ResultAsync } from '../unthrow/index.js';
1
+ import { ok, err, okAsync, fromPromise, } from '../unthrow/index.js';
2
2
  import { QueueEmptyFault, QueueFullFault } from './faults.js';
3
3
  import { BaseQueue } from './base.queue.js';
4
4
  export class BoundedQueue extends BaseQueue {
@@ -15,42 +15,42 @@ export class BoundedQueue extends BaseQueue {
15
15
  this._buffer.push(pw.item);
16
16
  pw.resolve();
17
17
  }
18
- return Result.ok(item);
18
+ return ok(item);
19
19
  }
20
20
  // Buffer empty — try put waiters directly
21
21
  const pw = this._putWaiters.shift();
22
22
  if (pw) {
23
23
  pw.resolve();
24
- return Result.ok(pw.item);
24
+ return ok(pw.item);
25
25
  }
26
- return Result.err(new QueueEmptyFault());
26
+ return err(new QueueEmptyFault());
27
27
  }
28
28
  offer(item) {
29
29
  // Give directly to a waiting consumer
30
30
  const waiter = this._takeWaiters.shift();
31
31
  if (waiter) {
32
32
  waiter(item);
33
- return Result.ok(undefined);
33
+ return ok(undefined);
34
34
  }
35
35
  if (this._buffer.length >= this._capacity) {
36
- return Result.err(new QueueFullFault({ capacity: this._capacity }));
36
+ return err(new QueueFullFault({ capacity: this._capacity }));
37
37
  }
38
38
  this._buffer.push(item);
39
- return Result.ok(undefined);
39
+ return ok(undefined);
40
40
  }
41
41
  put(item, signal) {
42
42
  // Give directly to a waiting consumer
43
43
  const waiter = this._takeWaiters.shift();
44
44
  if (waiter) {
45
45
  waiter(item);
46
- return ResultAsync.ok(undefined);
46
+ return okAsync(undefined);
47
47
  }
48
48
  if (this._buffer.length < this._capacity) {
49
49
  this._buffer.push(item);
50
- return ResultAsync.ok(undefined);
50
+ return okAsync(undefined);
51
51
  }
52
52
  // Wait for space
53
- return ResultAsync.fromPromise(new Promise((resolve) => {
53
+ return fromPromise(new Promise((resolve) => {
54
54
  const entry = { item, resolve };
55
55
  this._putWaiters.push(entry);
56
56
  signal?.addEventListener('abort', () => {
@@ -1,4 +1,4 @@
1
- import { ResultAsync } from '../unthrow/index.js';
1
+ import { fromPromise } from '../unthrow/index.js';
2
2
  import { Schedule } from '../schedule/index.js';
3
3
  const NEVER_ABORT = new AbortController().signal;
4
4
  const DEFAULT_SCHEDULE = Schedule.exponential(200).pipe(Schedule.recurs(3));
@@ -32,7 +32,7 @@ export class RetryPolicy {
32
32
  const onRetry = options?.onRetry ??
33
33
  this.defaults?.onRetry;
34
34
  const signal = options?.signal ?? this.defaults?.signal;
35
- return ResultAsync.fromPromise((async () => {
35
+ return fromPromise((async () => {
36
36
  let lastError;
37
37
  for (let attempt = 0;; attempt++) {
38
38
  const result = await fn(signal ?? NEVER_ABORT);
@@ -1,4 +1,4 @@
1
- import { Fault, ResultAsync } from '../unthrow/index.js';
1
+ import { Fault, fromPromise } from '../unthrow/index.js';
2
2
  export class ScheduleInterruptedFault extends Fault.Tagged('SCHEDULE_INTERRUPTED', 'infrastructure')() {
3
3
  message = 'Scheduled execution was interrupted';
4
4
  }
@@ -30,7 +30,7 @@ function sleep(ms, signal) {
30
30
  export function repeat(schedule, fn, options) {
31
31
  const signal = options?.signal;
32
32
  const onExecution = options?.onExecution;
33
- return ResultAsync.fromPromise((async () => {
33
+ return fromPromise((async () => {
34
34
  for (let attempt = 0;; attempt++) {
35
35
  // eslint-disable-next-line @typescript-eslint/only-throw-error -- intentional: fromPromise catches and maps E
36
36
  if (signal?.aborted)
@@ -1,10 +1,10 @@
1
- import { Fault, ResultAsync } from '../unthrow/index.js';
1
+ import { Fault, fromPromise } from '../unthrow/index.js';
2
2
  export class TimeoutFault extends Fault.Tagged('TIMEOUT', 'infrastructure', true)() {
3
3
  message = `Operation timed out after ${this.duration}ms`;
4
4
  }
5
5
  export function withTimeout(fn, duration) {
6
6
  const controller = new AbortController();
7
- return ResultAsync.fromPromise(
7
+ return fromPromise(
8
8
  /* eslint-disable @typescript-eslint/prefer-promise-reject-errors -- intentional: rejections are typed E | TimeoutFault, caught by fromPromise */
9
9
  new Promise((resolve, reject) => {
10
10
  const timer = setTimeout(() => {
@@ -1,7 +1,9 @@
1
- import { ok } from './result.js';
2
- import { ResultAsync } from './result.async.js';
3
- export function Do(generator) {
4
- const gen = generator();
1
+ import { ok } from './try.js';
2
+ import { TryAsync } from './try.async.js';
3
+ export function Do(ctxOrGenerator, generator) {
4
+ const gen = typeof ctxOrGenerator === 'function'
5
+ ? ctxOrGenerator()
6
+ : generator(ctxOrGenerator);
5
7
  const next = gen.next();
6
8
  while (!next.done) {
7
9
  // Each yielded value is an Err — short-circuit
@@ -9,9 +11,11 @@ export function Do(generator) {
9
11
  }
10
12
  return ok(next.value);
11
13
  }
12
- export function DoAsync(generator) {
13
- return new ResultAsync((async () => {
14
- const gen = generator();
14
+ export function DoAsync(ctxOrGenerator, generator) {
15
+ return new TryAsync((async () => {
16
+ const gen = typeof ctxOrGenerator === 'function'
17
+ ? ctxOrGenerator()
18
+ : generator(ctxOrGenerator);
15
19
  const next = await gen.next();
16
20
  while (!next.done) {
17
21
  // Each yielded value is an Err — short-circuit
@@ -1,11 +1,20 @@
1
- export function isDomainFault(error) {
2
- return error._category === 'domain';
1
+ export function isFault(value) {
2
+ return (typeof value === 'object' &&
3
+ value !== null &&
4
+ typeof value.code === 'string' &&
5
+ typeof value.message === 'string' &&
6
+ (value._category === 'domain' ||
7
+ value._category === 'infrastructure') &&
8
+ typeof value._transient === 'boolean');
3
9
  }
4
- export function isInfrastructureFault(error) {
5
- return error._category === 'infrastructure';
10
+ export function isDomainFault(value) {
11
+ return isFault(value) && value._category === 'domain';
6
12
  }
7
- export function isTransientFault(error) {
8
- return error._transient && error._category === 'infrastructure';
13
+ export function isInfrastructureFault(value) {
14
+ return isFault(value) && value._category === 'infrastructure';
15
+ }
16
+ export function isTransientFault(value) {
17
+ return (isFault(value) && value._transient && value._category === 'infrastructure');
9
18
  }
10
19
  export function Fault(code, category, transient) {
11
20
  const _transient = (transient ?? false);
@@ -1,12 +1,12 @@
1
- import { ok, err } from './result.js';
2
- import { ResultAsync } from './result.async.js';
1
+ import { ok, err } from './try.js';
2
+ import { TryAsync } from './try.async.js';
3
3
  export function combine(results) {
4
4
  if (results.length === 0) {
5
5
  return ok([]);
6
6
  }
7
- if (results[0] instanceof ResultAsync) {
8
- const asyncResults = results;
9
- return new ResultAsync(Promise.all(asyncResults.map((r) => r.then((v) => v))).then((settled) => combineSync(settled)));
7
+ if (results[0] instanceof TryAsync) {
8
+ const asyncTrys = results;
9
+ return new TryAsync(Promise.all(asyncTrys.map((r) => r.then((v) => v))).then((settled) => combineSync(settled)));
10
10
  }
11
11
  return combineSync(results);
12
12
  }
@@ -23,9 +23,9 @@ export function combineWithAllErrors(results) {
23
23
  if (results.length === 0) {
24
24
  return ok([]);
25
25
  }
26
- if (results[0] instanceof ResultAsync) {
27
- const asyncResults = results;
28
- return new ResultAsync(Promise.all(asyncResults.map((r) => r.then((v) => v))).then((settled) => combineWithAllErrorsSync(settled)));
26
+ if (results[0] instanceof TryAsync) {
27
+ const asyncTrys = results;
28
+ return new TryAsync(Promise.all(asyncTrys.map((r) => r.then((v) => v))).then((settled) => combineWithAllErrorsSync(settled)));
29
29
  }
30
30
  return combineWithAllErrorsSync(results);
31
31
  }
@@ -43,7 +43,10 @@ function combineWithAllErrorsSync(results) {
43
43
  return errors.length > 0 ? err(errors) : ok(values);
44
44
  }
45
45
  export function fromPromise(promise, errorMapper) {
46
- return new ResultAsync(promise.then((value) => ok(value), (error) => err(errorMapper(error))));
46
+ return new TryAsync(promise.then((value) => ok(value), (error) => err(errorMapper(error))));
47
+ }
48
+ export function fromSafePromise(promise) {
49
+ return new TryAsync(promise.then((value) => ok(value)));
47
50
  }
48
51
  export function fromThrowable(fn, errorMapper) {
49
52
  try {
@@ -53,4 +56,24 @@ export function fromThrowable(fn, errorMapper) {
53
56
  return err(errorMapper(error));
54
57
  }
55
58
  }
59
+ export function fromAsyncThrowable(fn, errorMapper) {
60
+ try {
61
+ return fromPromise(fn(), errorMapper);
62
+ }
63
+ catch (error) {
64
+ return new TryAsync(Promise.resolve(err(errorMapper(error))));
65
+ }
66
+ }
67
+ export function unwrap(result, fault) {
68
+ return result.andThen((value) => (value != null ? ok(value) : err(fault)));
69
+ }
70
+ export function flatten(result) {
71
+ return result.andThen((inner) => inner);
72
+ }
73
+ export function race(results) {
74
+ if (results.length === 0) {
75
+ return new TryAsync(Promise.reject(new Error('race requires at least one TryAsync')));
76
+ }
77
+ return new TryAsync(Promise.race(results.map((r) => r.then((v) => v))));
78
+ }
56
79
  //# sourceMappingURL=helpers.js.map
@@ -1,48 +1,18 @@
1
- import { ok as _ok, err as _err } from './result.js';
2
- import { okAsync, errAsync, } from './result.async.js';
3
- import { fromPromise as _fromPromise, fromThrowable as _fromThrowable, combine as _combine, combineWithAllErrors as _combineWithAllErrors, } from './helpers.js';
4
- import { Do as _Do, DoAsync } from './do.js';
5
- import { Fault as _Fault, isDomainFault as _isDomainFault, isInfrastructureFault as _isInfrastructureFault, isTransientFault as _isTransientFault, } from './fault.js';
6
- // Re-export instance types (needed in user signatures)
7
- export { Ok, Err } from './result.js';
8
- // eslint-disable-next-line @typescript-eslint/no-namespace
9
- export var Result;
10
- (function (Result) {
11
- Result.ok = _ok;
12
- Result.err = _err;
13
- Result.fromThrowable = _fromThrowable;
14
- function combine(results) {
15
- return _combine(results);
16
- }
17
- Result.combine = combine;
18
- function combineWithAllErrors(results) {
19
- return _combineWithAllErrors(results);
20
- }
21
- Result.combineWithAllErrors = combineWithAllErrors;
22
- Result.Do = _Do;
23
- })(Result || (Result = {}));
24
- // eslint-disable-next-line @typescript-eslint/no-namespace
25
- export var ResultAsync;
26
- (function (ResultAsync) {
27
- ResultAsync.ok = okAsync;
28
- ResultAsync.err = errAsync;
29
- ResultAsync.fromPromise = _fromPromise;
30
- function combine(results) {
31
- return _combine(results);
32
- }
33
- ResultAsync.combine = combine;
34
- function combineWithAllErrors(results) {
35
- return _combineWithAllErrors(results);
36
- }
37
- ResultAsync.combineWithAllErrors = combineWithAllErrors;
38
- ResultAsync.Do = DoAsync;
39
- })(ResultAsync || (ResultAsync = {}));
1
+ // ── Fault ────────────────────────────────────────────────────────
2
+ import { Fault as _Fault, isFault as _isFault, isDomainFault as _isDomainFault, isInfrastructureFault as _isInfrastructureFault, isTransientFault as _isTransientFault, } from './fault.js';
40
3
  // eslint-disable-next-line @typescript-eslint/no-namespace
41
4
  export var Fault;
42
5
  (function (Fault) {
43
6
  Fault.Tagged = _Fault;
7
+ Fault.is = _isFault;
44
8
  Fault.isDomain = _isDomainFault;
45
9
  Fault.isInfrastructure = _isInfrastructureFault;
46
10
  Fault.isTransient = _isTransientFault;
47
11
  })(Fault || (Fault = {}));
12
+ // ── Try ──────────────────────────────────────────────────────────
13
+ export { Ok, Err, ok, err } from './try.js';
14
+ export { TryAsync, okAsync, errAsync } from './try.async.js';
15
+ export { fromPromise, fromSafePromise, fromThrowable, fromAsyncThrowable, combine, combineWithAllErrors, unwrap, flatten, race, } from './helpers.js';
16
+ // ── Do ──────────────────────────────────────────────────────────
17
+ export { Do, DoAsync } from './do.js';
48
18
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,114 @@
1
+ import { Ok, Err, ok, err } from './try.js';
2
+ export class TryAsync {
3
+ _promise;
4
+ then;
5
+ constructor(promise) {
6
+ this._promise = promise;
7
+ this.then = promise.then.bind(promise);
8
+ }
9
+ map(fn) {
10
+ return new TryAsync(this._promise.then(async (result) => {
11
+ if (result.isErr())
12
+ return new Err(result.error);
13
+ return new Ok(await fn(result.value));
14
+ }));
15
+ }
16
+ mapErr(fn) {
17
+ return new TryAsync(this._promise.then(async (result) => {
18
+ if (result.isOk())
19
+ return new Ok(result.value);
20
+ return new Err(await fn(result.error));
21
+ }));
22
+ }
23
+ andThen(fn) {
24
+ return new TryAsync(this._promise.then((result) => {
25
+ if (result.isErr())
26
+ return new Err(result.error);
27
+ const next = fn(result.value);
28
+ return next instanceof TryAsync ? next._promise : next;
29
+ }));
30
+ }
31
+ orElse(fn) {
32
+ return new TryAsync(this._promise.then((result) => {
33
+ if (result.isOk())
34
+ return new Ok(result.value);
35
+ const next = fn(result.error);
36
+ return next instanceof TryAsync ? next._promise : next;
37
+ }));
38
+ }
39
+ tap(fn) {
40
+ return new TryAsync(this._promise.then(async (result) => {
41
+ if (result.isOk())
42
+ await fn(result.value);
43
+ return result;
44
+ }));
45
+ }
46
+ tapErr(fn) {
47
+ return new TryAsync(this._promise.then(async (result) => {
48
+ if (result.isErr())
49
+ await fn(result.error);
50
+ return result;
51
+ }));
52
+ }
53
+ andThrough(fn) {
54
+ return new TryAsync(this._promise.then(async (result) => {
55
+ if (result.isErr())
56
+ return new Err(result.error);
57
+ const through = fn(result.value);
58
+ const throughResult = through instanceof TryAsync ? await through : through;
59
+ if (throughResult.isErr())
60
+ return new Err(throughResult.error);
61
+ return new Ok(result.value);
62
+ }));
63
+ }
64
+ orThrough(fn) {
65
+ return new TryAsync(this._promise.then(async (result) => {
66
+ if (result.isOk())
67
+ return new Ok(result.value);
68
+ const through = fn(result.error);
69
+ const throughResult = through instanceof TryAsync ? await through : through;
70
+ if (throughResult.isErr())
71
+ return new Err(throughResult.error);
72
+ return new Err(result.error);
73
+ }));
74
+ }
75
+ catchFault(code, fn) {
76
+ return new TryAsync(this._promise.then((result) => {
77
+ if (result.isOk())
78
+ return new Ok(result.value);
79
+ const error = result.error;
80
+ if (typeof error === 'object' &&
81
+ error !== null &&
82
+ 'code' in error &&
83
+ error.code === code) {
84
+ const next = fn(error);
85
+ return next instanceof TryAsync ? next._promise : next;
86
+ }
87
+ return new Err(error);
88
+ }));
89
+ }
90
+ async unwrapOr(defaultValue) {
91
+ const result = await this;
92
+ return result.unwrapOr(defaultValue);
93
+ }
94
+ async match(cases) {
95
+ const result = await this;
96
+ return result.match(cases);
97
+ }
98
+ async *[Symbol.asyncIterator]() {
99
+ const result = await this._promise;
100
+ if (result.isErr()) {
101
+ // @ts-expect-error -- structurally equivalent, safe for DoAsync notation
102
+ yield result;
103
+ throw new Error('Unreachable: DoAsync generator continued after Err yield');
104
+ }
105
+ return result.value;
106
+ }
107
+ }
108
+ export function okAsync(value) {
109
+ return new TryAsync(Promise.resolve(ok(value)));
110
+ }
111
+ export function errAsync(error) {
112
+ return new TryAsync(Promise.resolve(err(error)));
113
+ }
114
+ //# sourceMappingURL=try.async.js.map
@@ -24,6 +24,25 @@ export class Ok {
24
24
  orElse(_fn) {
25
25
  return new Ok(this.value);
26
26
  }
27
+ tap(fn) {
28
+ fn(this.value);
29
+ return this;
30
+ }
31
+ tapErr(_fn) {
32
+ return this;
33
+ }
34
+ andThrough(fn) {
35
+ const result = fn(this.value);
36
+ if (result.isErr())
37
+ return new Err(result.error);
38
+ return this;
39
+ }
40
+ orThrough(_fn) {
41
+ return this;
42
+ }
43
+ catchFault(_code, _fn) {
44
+ return this;
45
+ }
27
46
  unwrapOr(_defaultValue) {
28
47
  return this.value;
29
48
  }
@@ -59,6 +78,32 @@ export class Err {
59
78
  orElse(fn) {
60
79
  return fn(this.error);
61
80
  }
81
+ tap(_fn) {
82
+ return this;
83
+ }
84
+ tapErr(fn) {
85
+ fn(this.error);
86
+ return this;
87
+ }
88
+ andThrough(_fn) {
89
+ return this;
90
+ }
91
+ orThrough(fn) {
92
+ const result = fn(this.error);
93
+ if (result.isErr())
94
+ return new Err(result.error);
95
+ return this;
96
+ }
97
+ catchFault(code, fn) {
98
+ const error = this.error;
99
+ if (typeof error === 'object' &&
100
+ error !== null &&
101
+ 'code' in error &&
102
+ error.code === code) {
103
+ return fn(error);
104
+ }
105
+ return this;
106
+ }
62
107
  unwrapOr(defaultValue) {
63
108
  return defaultValue;
64
109
  }
@@ -77,4 +122,4 @@ export function ok(value) {
77
122
  export function err(error) {
78
123
  return new Err(error);
79
124
  }
80
- //# sourceMappingURL=result.js.map
125
+ //# sourceMappingURL=try.js.map
package/package.json CHANGED
@@ -1,7 +1,24 @@
1
1
  {
2
2
  "name": "clutchit",
3
- "version": "0.0.7",
4
- "description": "",
3
+ "version": "0.0.9",
4
+ "description": "Resilience primitives for TypeScript — typed errors, retry, timeout, circuit breaking, scheduling, concurrency, and queues.",
5
+ "keywords": [
6
+ "typescript",
7
+ "resilience",
8
+ "error-handling",
9
+ "result",
10
+ "typed-errors",
11
+ "retry",
12
+ "timeout",
13
+ "circuit-breaker",
14
+ "schedule",
15
+ "backoff",
16
+ "concurrency",
17
+ "semaphore",
18
+ "bulkhead",
19
+ "rate-limiter",
20
+ "queue"
21
+ ],
5
22
  "author": {
6
23
  "name": "hexac",
7
24
  "url": "https://existin.space",
@@ -1,63 +0,0 @@
1
- import { Ok, Err, ok, err } from './result.js';
2
- export class ResultAsync {
3
- _promise;
4
- then;
5
- constructor(promise) {
6
- this._promise = promise;
7
- this.then = promise.then.bind(promise);
8
- }
9
- map(fn) {
10
- return new ResultAsync(this._promise.then(async (result) => {
11
- if (result.isErr())
12
- return new Err(result.error);
13
- return new Ok(await fn(result.value));
14
- }));
15
- }
16
- mapErr(fn) {
17
- return new ResultAsync(this._promise.then(async (result) => {
18
- if (result.isOk())
19
- return new Ok(result.value);
20
- return new Err(await fn(result.error));
21
- }));
22
- }
23
- andThen(fn) {
24
- return new ResultAsync(this._promise.then((result) => {
25
- if (result.isErr())
26
- return new Err(result.error);
27
- const next = fn(result.value);
28
- return next instanceof ResultAsync ? next._promise : next;
29
- }));
30
- }
31
- orElse(fn) {
32
- return new ResultAsync(this._promise.then((result) => {
33
- if (result.isOk())
34
- return new Ok(result.value);
35
- const next = fn(result.error);
36
- return next instanceof ResultAsync ? next._promise : next;
37
- }));
38
- }
39
- async unwrapOr(defaultValue) {
40
- const result = await this;
41
- return result.unwrapOr(defaultValue);
42
- }
43
- async match(cases) {
44
- const result = await this;
45
- return result.match(cases);
46
- }
47
- async *[Symbol.asyncIterator]() {
48
- const result = await this._promise;
49
- if (result.isErr()) {
50
- // @ts-expect-error -- structurally equivalent, safe for DoAsync notation
51
- yield result;
52
- throw new Error('Unreachable: DoAsync generator continued after Err yield');
53
- }
54
- return result.value;
55
- }
56
- }
57
- export function okAsync(value) {
58
- return new ResultAsync(Promise.resolve(ok(value)));
59
- }
60
- export function errAsync(error) {
61
- return new ResultAsync(Promise.resolve(err(error)));
62
- }
63
- //# sourceMappingURL=result.async.js.map