clutchit 0.0.8 → 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)
@@ -20,7 +19,7 @@ npm install clutchit
20
19
 
21
20
  | Import | Description |
22
21
  |---|---|
23
- | `clutchit/unthrow` | `Result<T,E>`, `ResultAsync<T,E>`, `Fault`, Do notation |
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 { ResultAsync, Fault } from 'clutchit/unthrow';
37
- import { RetryPolicy } from 'clutchit/retry';
38
- import { withTimeout } from 'clutchit/timeout';
39
- 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';
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): ResultAsync<User, ApiFault> {
48
- return ResultAsync.fromPromise(
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
- ### Result
74
+ ### Try
76
75
 
77
76
  ```ts
78
- import { Result } from 'clutchit/unthrow';
77
+ import { ok, err } from 'clutchit/unthrow';
79
78
 
80
- const ok = Result.ok(42);
81
- const fail = Result.err('oops');
79
+ const success = ok(42);
80
+ const failure = err('oops');
82
81
 
83
- ok.map((n) => n * 2); // Ok(84)
84
- ok.andThen((n) => Result.ok(n)); // Ok(42)
85
- ok.unwrapOr(0); // 42
86
- 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
87
86
 
88
- ok.match({
87
+ success.match({
89
88
  ok: (value) => `got ${value}`,
90
89
  err: (error) => `failed: ${error}`,
91
90
  });
92
91
 
93
- if (ok.isOk()) ok.value; // narrowed
94
- if (fail.isErr()) fail.error; // narrowed
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
- ### ResultAsync
98
+ ### TryAsync
100
99
 
101
- 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`.
102
101
 
103
102
  ```ts
104
- import { ResultAsync } from 'clutchit/unthrow';
103
+ import { fromPromise } from 'clutchit/unthrow';
105
104
 
106
- const result = ResultAsync.fromPromise(
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 `ResultAsync` or `Promise`)
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 { Result, ResultAsync } from 'clutchit/unthrow';
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
- Result.fromThrowable(
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
- Result.combine([resultA, resultB]); // Result<[A, B], E>
131
- 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>
132
146
 
133
147
  // Combine — collect all errors
134
- Result.combineWithAllErrors([a, b, c]); // Result<[A, B, C], E[]>
135
- 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)]);
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 { Result, ResultAsync } from 'clutchit/unthrow';
159
+ import { Do, DoAsync } from 'clutchit/unthrow';
144
160
 
145
161
  // Sync
146
- const result = Result.Do(function* () {
147
- const user = yield* findUser(id); // Result<User, NotFoundFault>
148
- 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>
149
165
  return { user, perms };
150
- // Result<{ user, perms }, NotFoundFault | PermFault>
166
+ // Try<{ user, perms }, NotFoundFault | PermFault>
151
167
  });
152
168
 
153
169
  // Async
154
- const result = ResultAsync.Do(async function* () {
155
- const user = yield* fetchUser(id); // ResultAsync<User, NetworkFault>
156
- 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>
157
173
  return order;
158
- // 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;
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
- // ResultAsync<number, SyncError | ScheduleInterruptedFault>
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
- // ResultAsync<Data, FetchFault>
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
- // ResultAsync<Data, FetchFault | TimeoutFault>
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); // ResultAsync<void, never>
326
- await counter.set(42); // ResultAsync<void, never>
327
- 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>
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
- // ResultAsync<T, ServiceFault | CircuitOpenFault>
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); // Result<void, QueueFullFault> — non-blocking
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(); // Result<Job, QueueEmptyFault>
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
@@ -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.8",
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