# @helpers4/promise

> Tree-shakable TypeScript utility functions for the `promise` domain.
> Package: `@helpers4/promise` — Version: 3.0.4
> License: LGPL-3.0-or-later

## Installation

```sh
npm install @helpers4/promise
# or
pnpm add @helpers4/promise
```

## Usage

```typescript
import { consoleLogPromise, createMutex, createSemaphore, ... } from '@helpers4/promise';
```

## Functions

| Function | Description |
|---|---|
| `consoleLogPromise` | Returns a function that logs data to the console and passes it through. |
| `createMutex` | Creates a mutex: a lock allowing at most one holder at a time, queueing excess `acquire()` callers i |
| `createSemaphore` | Creates a semaphore limiting concurrent access to `permits` holders at a time, queueing excess `acqu |
| `defer` | Runs an async function and guarantees that all deferred callbacks are executed afterwards, in LIFO o |
| `delay` | Creates a promise that resolves after specified delay |
| `falsyPromiseOrThrow` | Returns a function that passes through falsy data or throws an error. |
| `guard` | Wraps a function so that if it throws, a default value is returned instead of propagating the error. |
| `meaningPromiseOrThrow` | Returns a function that passes through meaningful data or throws an error. Data is considered meanin |
| `parallel` | Runs an array of async functions with a concurrency limit. At most `limit` functions will be running |
| `parallelSettle` | Runs an array of async functions with a concurrency limit, partitioning the outcomes instead of reje |
| `resolveRecord` | Resolves an array of keys into a record by calling an async mapper for each key. All mapper calls ru |
| `retry` | Retries a promise-returning function up to maxAttempts times |
| `safeFetch` | Wraps `fetch` with built-in error handling: returns `null` when the request fails (network error, no |
| `settle` | Runs an array of promises concurrently and partitions the outcomes instead of rejecting on the first |
| `timeout` | Wraps a promise to reject with a `TimeoutError` if it does not resolve within the specified duration |
| `truthyPromiseOrThrow` | Returns a function that passes through truthy data or throws an error. |
| `tryit` | Wraps a function so it never throws. Instead, it returns a `[error, result]` tuple. Useful for avoid |

---

## API Reference

### `consoleLogPromise`

Returns a function that logs data to the console and passes it through.

```typescript
import { consoleLogPromise } from '@helpers4/promise';

consoleLogPromise<T>(prefix?: string): function
```

**Parameters:**

- `prefix?: string` — Optional prefix for the console log

**Returns:** `function` — A function that logs and returns the data

**Examples:**

*Log and pass-through in a promise chain*

Creates a function that logs data with an optional prefix and returns it unchanged.

```typescript
Promise.resolve(42).then(consoleLogPromise('value:'))
// logs "value: 42" and resolves with 42
```

---

### `createMutex`

Creates a mutex: a lock allowing at most one holder at a time, queueing excess `acquire()`
callers in FIFO order. A createSemaphore with a single permit.

A factory function rather than a class, matching this package's other stateful helpers
(`debounce`, `throttle`): the returned object closes over an internal semaphore.

Typical use: deduplicating concurrent callers of a non-reentrant operation, e.g. making
sure only one in-flight token-refresh call happens at a time while others wait for it.

```typescript
import { createMutex } from '@helpers4/promise';

createMutex(): Mutex
```

**Returns:** `Mutex` — A Mutex

**Examples:**

*Deduplicate concurrent token refreshes*

Only one caller actually refreshes; the rest wait and reuse its result.

```typescript
const mutex = createMutex();
let cached: string | undefined;

async function getToken() {
  return mutex.run(async () => {
    if (cached) return cached;
    cached = await refreshToken(); // only ever runs once per cache miss
    return cached;
  });
}

await Promise.all([getToken(), getToken(), getToken()]);
// refreshToken() was called exactly once
```

*isLocked() reports whether the lock is currently held*

Useful for diagnostics or conditionally skipping work while locked.

```typescript
const mutex = createMutex();
mutex.isLocked() // => false
const release = await mutex.acquire();
mutex.isLocked() // => true
release();
mutex.isLocked() // => false
```

---

### `createSemaphore`

Creates a semaphore limiting concurrent access to `permits` holders at a time, queueing
excess `acquire()` callers in FIFO order (first to call `acquire` is first granted a
permit when one frees up).

A factory function rather than a class, matching this package's other stateful helpers
(`debounce`, `throttle`): the returned object closes over its internal queue/counter.

Use for concurrency limiting, e.g. rate-limiting calls to an external API. For
mutual-exclusion (at most one holder), see createMutex, which is a semaphore
with one permit.

```typescript
import { createSemaphore } from '@helpers4/promise';

createSemaphore(permits: number): Semaphore
```

**Parameters:**

- `permits: number` — Maximum number of concurrent holders. Must be `>= 1` (`Infinity` is
  accepted, for an effectively uncapped semaphore); a non-integer is floored (`2.9`
  behaves as `2`).

**Returns:** `Semaphore` — A Semaphore

**Examples:**

*Limit concurrent calls to an external API*

At most `permits` calls run at once; the rest queue and wait their turn.

```typescript
const semaphore = createSemaphore(2);
const results = await Promise.all(
  urls.map((url) => semaphore.run(() => fetch(url)))
);
// at most 2 fetch() calls in flight at any time
```

*run() releases the permit even on error*

Prefer run() over manual acquire()/release() — it cannot leak a permit.

```typescript
const semaphore = createSemaphore(1);
await semaphore.run(() => {
  throw new Error('oops');
}).catch(() => {});
semaphore.availablePermits() // => 1, the permit was released
```

*Manual acquire() returns a one-shot release function*

Calling the same release function twice throws — always caught, even under contention.

```typescript
const semaphore = createSemaphore(1);
const release = await semaphore.acquire();
release();
release(); // throws RangeError: this permit was already released
```

---

### `defer`

Runs an async function and guarantees that all deferred callbacks are
executed afterwards, in LIFO order (last registered = first executed),
regardless of whether the main work succeeds or throws.

Inspired by Radashi's `defer`. Useful for resource cleanup, temporary file
removal, or any "undo" logic that must run even on failure.

```typescript
import { defer } from '@helpers4/promise';

defer<T>(fn: function): Promise<T>
```

**Parameters:**

- `fn: function` — An async function that receives a `defer` registration function.

**Returns:** `Promise<T>` — The resolved value of `fn`.

**Examples:**

*Cleanup always runs*

Registered callbacks execute after the main function, even on success.

```typescript
const result = await defer(async (d) => {
  d(() => console.log('cleanup'));
  return 42;
});
// logs: 'cleanup' — result is 42
```

*LIFO order*

Multiple callbacks are called in reverse registration order.

```typescript
await defer(async (d) => {
  d(() => console.log('step 1'));
  d(() => console.log('step 2'));
  d(() => console.log('step 3'));
});
// logs: 'step 3', 'step 2', 'step 1'
```

*Cleanup runs even on failure*

Callbacks still execute when the main function throws; the error is re-thrown after.

```typescript
const releaseLock = () => console.log('lock released');
await defer(async (d) => {
  d(releaseLock);
  throw new Error('something failed');
}).catch(() => {});
// logs: 'lock released'
```

---

### `delay`

Creates a promise that resolves after specified delay

```typescript
import { delay } from '@helpers4/promise';

delay<T = void>(ms: number, value?: T): Promise<T>
```

**Parameters:**

- `ms: number` — Milliseconds to delay
- `value?: T` — Optional value to resolve with

**Returns:** `Promise<T>` — Promise that resolves after delay

**Examples:**

*Wait a specified duration*

Creates a promise that resolves after the given milliseconds.

```typescript
await delay(100)
// resolves after 100ms
```

*Resolve with a value*

Optionally resolves with a provided value.

```typescript
const result = await delay(100, 'done')
// => 'done'
```

---

### `falsyPromiseOrThrow`

Returns a function that passes through falsy data or throws an error.

```typescript
import { falsyPromiseOrThrow } from '@helpers4/promise';

falsyPromiseOrThrow<T>(error: string): function
```

**Parameters:**

- `error: string` — The error message to throw if data is truthy

**Returns:** `function` — A function that returns the data if falsy, or throws

**Examples:**

*Pass through falsy values*

Returns the value if falsy, throws otherwise.

```typescript
Promise.resolve(null).then(falsyPromiseOrThrow('Expected falsy'))
// => null
```

*Throw on truthy values*

Throws an error when the value is truthy.

```typescript
Promise.resolve('oops').then(falsyPromiseOrThrow('Should be empty'))
// throws Error('Should be empty')
```

---

### `guard`

Wraps a function so that if it throws, a default value is returned instead of propagating the error.
Works with both synchronous and asynchronous functions.

```typescript
import { guard } from '@helpers4/promise';

guard<T>(fn: function, defaultValue: T): Promise<T>
```

**Parameters:**

- `fn: function` — The function to guard
- `defaultValue: T` — The value to return if the function throws

**Returns:** `Promise<T>` — The result of the function, or the default value on error

```typescript
import { guard } from '@helpers4/promise';

guard<T>(fn: function, defaultValue: T): T
```

**Parameters:**

- `fn: function` — The function to guard
- `defaultValue: T` — The value to return if the function throws

**Returns:** `T` — The result of the function, or the default value on error

**Examples:**

*Fallback on parse error*

Returns a default value when the function throws.

```typescript
const result = guard(() => JSON.parse('invalid'), {})
// => {}
```

*Pass-through on success*

Returns the function result when it does not throw.

```typescript
const result = guard(() => JSON.parse('{"a":1}'), {})
// => { a: 1 }
```

---

### `meaningPromiseOrThrow`

Returns a function that passes through meaningful data or throws an error.
Data is considered meaningless if it is null, undefined, empty string, empty object, or empty array.

```typescript
import { meaningPromiseOrThrow } from '@helpers4/promise';

meaningPromiseOrThrow<T>(error: string): function
```

**Parameters:**

- `error: string` — The error message to throw if data is meaningless

**Returns:** `function` — A function that returns the data if meaningful, or throws

**Examples:**

*Pass through meaningful values*

Returns the value if it is not empty (null, undefined, empty string, empty object, empty array).

```typescript
Promise.resolve({ key: 'value' }).then(meaningPromiseOrThrow('No data'))
// => { key: 'value' }
```

*Throw on empty values*

Throws when the value is null, undefined, empty string, empty object, or empty array.

```typescript
Promise.resolve({}).then(meaningPromiseOrThrow('Empty!'))
// throws Error('Empty!')
```

---

### `parallel`

Runs an array of async functions with a concurrency limit.
At most `limit` functions will be running at any time. `Infinity` means no cap (every
function starts immediately); any other non-finite or non-positive value (`NaN`, `0`,
negative) is clamped to `1` (fully sequential) rather than rejected.

```typescript
import { parallel } from '@helpers4/promise';

parallel<T>(functions: readonly function[], limit: number): Promise<T[]>
```

**Parameters:**

- `functions: readonly function[]` — Array of functions that return promises
- `limit: number` — Maximum number of concurrent executions

**Returns:** `Promise<T[]>` — Promise that resolves with an array of results in the same order as the input

**Examples:**

*Run tasks with concurrency limit*

Executes async functions with at most N running concurrently.

```typescript
const results = await parallel(
  [() => fetch('/a'), () => fetch('/b'), () => fetch('/c')],
  2
)
// At most 2 requests run at a time; results are in order
```

*Sequential execution with limit of 1*

Setting limit to 1 runs functions one at a time.

```typescript
await parallel([fnA, fnB, fnC], 1)
// Runs fnA, then fnB, then fnC
```

---

### `parallelSettle`

Runs an array of async functions with a concurrency limit, partitioning the outcomes
instead of rejecting on the first failure — parallel with settle's
fulfilled/rejected split.

Takes functions (not already-created promises, unlike settle) so it controls
*when* each one starts: that's what makes the concurrency limit meaningful. At most
`concurrency` functions run at any time; as soon as one settles, the next queued
function starts.

```typescript
import { parallelSettle } from '@helpers4/promise';

parallelSettle<T>(functions: readonly function[], concurrency: number): Promise<object>
```

**Parameters:**

- `functions: readonly function[]` — Array of functions that return promises
- `concurrency: number` — Maximum number of concurrent executions. `Infinity` means no cap; any
  other non-finite or non-positive value (`NaN`, `0`, negative) is clamped to `1` (fully
  sequential) rather than rejected.

**Returns:** `Promise<object>` — An object with `fulfilled` values and `rejected` reasons, each in input order

**Examples:**

*Limit concurrency without one failure stopping the rest*

Runs at most `concurrency` functions at a time and partitions outcomes instead of rejecting on the first failure.

```typescript
const { fulfilled, rejected } = await parallelSettle(
  [() => fetch('/a'), () => fetch('/b'), () => fetch('/c')],
  2
)
// At most 2 requests run at a time; a failing request doesn't stop the others
```

*All functions succeed*

Returns an empty rejected array when nothing fails.

```typescript
const { fulfilled, rejected } = await parallelSettle(
  [() => Promise.resolve('a'), () => Promise.resolve('b')],
  2
)
// => { fulfilled: ['a', 'b'], rejected: [] }
```

---

### `resolveRecord`

Resolves an array of keys into a record by calling an async mapper for each key.
All mapper calls run concurrently via `Promise.all`.

Unlike parallel, which returns an array, `resolveRecord` preserves the
key-to-value relationship in the result.

```typescript
import { resolveRecord } from '@helpers4/promise';

resolveRecord<K extends PropertyKey, V>(keys: readonly K[], mapper: function): Promise<Record<K, V>>
```

**Parameters:**

- `keys: readonly K[]` — The keys to resolve
- `mapper: function` — Async function called for each key, returning the associated value

**Returns:** `Promise<Record<K, V>>` — A record mapping each key to its resolved value

**Examples:**

*Fetch data for multiple keys concurrently*

All mapper calls run in parallel via Promise.all.

```typescript
const stars = await resolveRecord(
  ['helpers4/typescript', 'helpers4/devcontainer'],
  async (repo) => fetchRepoStars(repo)
);
// => { 'helpers4/typescript': 42, 'helpers4/devcontainer': 17 }
```

---

### `retry`

Retries a promise-returning function up to maxAttempts times

```typescript
import { retry } from '@helpers4/promise';

retry<T>(fn: function, maxAttempts: number, delayMs: number): Promise<T>
```

**Parameters:**

- `fn: function` — The function to retry
- `maxAttempts: number` (default: `3`) — Maximum number of attempts
- `delayMs: number` (default: `1000`) — Delay between attempts in milliseconds

**Returns:** `Promise<T>` — Promise that resolves with the result or rejects with the last error

**Examples:**

*Retry a failing function*

Retries the function up to maxAttempts times before giving up.

```typescript
let attempt = 0;
await retry(() => {
  attempt++;
  if (attempt < 3) throw new Error('not yet');
  return Promise.resolve('success');
}, 3, 10)
// => 'success' (after 2 failures)
```

---

### `safeFetch`

Wraps `fetch` with built-in error handling: returns `null` when the
request fails (network error, non-OK status, or parse error) instead
of throwing.

```typescript
import { safeFetch } from '@helpers4/promise';

safeFetch<T>(input: URL | RequestInfo, init?: RequestInit, options: SafeFetchOptions): Promise<T | null>
```

**Parameters:**

- `input: URL | RequestInfo` — URL or `Request` object passed to `fetch`
- `init?: RequestInit` — Optional `RequestInit` options passed to `fetch`
- `options: SafeFetchOptions` (default: `{}`) — Parsing options (default: `{ parse: 'json' }`)

**Returns:** `Promise<T | null>` — The parsed response body, or `null` on any failure

**Examples:**

*Fetch JSON safely*

Returns `null` on network error or non-OK status instead of throwing.

```typescript
const repo = await safeFetch<{ stars: number }>(
  'https://api.github.com/repos/helpers4/typescript'
);
if (repo === null) {
  console.warn('Failed to fetch repo data');
} else {
  console.log(repo.stars);
}
```

*Fetch plain text*

Pass { parse: "text" } to get the raw response body as a string.

```typescript
const content = await safeFetch<string>(
  'https://example.com/data.txt',
  undefined,
  { parse: 'text' }
);
```

---

### `settle`

Runs an array of promises concurrently and partitions the outcomes instead of
rejecting on the first failure, unlike `Promise.all`.
Built on top of `Promise.allSettled`, but returns fulfilled values and rejection
reasons already split apart so callers don't need to inspect `status` themselves.

No concurrency limit, and none is possible here: `promises` are already-constructed
`Promise` objects, and a promise starts running the moment it's created — by the time
`settle` receives the array, every promise in it is already in flight. There is
nothing left to throttle. If you need to cap how many run at once (e.g. many file
reads or requests), use parallelSettle instead, which takes functions
(`() => Promise<T>`) so it controls *when* each one starts.

```typescript
import { settle } from '@helpers4/promise';

settle<T>(promises: readonly Promise<T>[]): Promise<object>
```

**Parameters:**

- `promises: readonly Promise<T>[]` — Promises to run concurrently

**Returns:** `Promise<object>` — An object with `fulfilled` values and `rejected` reasons, each in input order

**Examples:**

*Partition fulfilled and rejected outcomes*

Runs promises concurrently and splits results instead of rejecting on the first failure.

```typescript
const { fulfilled, rejected } = await settle([
  Promise.resolve(1),
  Promise.reject(new Error('boom')),
  Promise.resolve(3),
])
// => { fulfilled: [1, 3], rejected: [Error('boom')] }
```

*All promises succeed*

Returns an empty rejected array when nothing fails.

```typescript
const { fulfilled, rejected } = await settle([Promise.resolve('a'), Promise.resolve('b')])
// => { fulfilled: ['a', 'b'], rejected: [] }
```

---

### `timeout`

Wraps a promise to reject with a `TimeoutError` if it does not resolve within the specified duration.

```typescript
import { timeout } from '@helpers4/promise';

timeout<T>(promise: Promise<T>, ms: number): Promise<T>
```

**Parameters:**

- `promise: Promise<T>` — The promise to wrap
- `ms: number` — Timeout duration in milliseconds

**Returns:** `Promise<T>` — A promise that rejects with `TimeoutError` if the timeout is exceeded

**Examples:**

*Reject a slow promise*

Throws a TimeoutError if the promise does not resolve in time.

```typescript
await timeout(fetch('/api/data'), 5000)
// Rejects with TimeoutError if fetch takes longer than 5s
```

*Resolve fast promise normally*

Returns the value if the promise resolves before the timeout.

```typescript
const result = await timeout(Promise.resolve('fast'), 1000)
// => 'fast'
```

---

### `truthyPromiseOrThrow`

Returns a function that passes through truthy data or throws an error.

```typescript
import { truthyPromiseOrThrow } from '@helpers4/promise';

truthyPromiseOrThrow<T>(error: string): function
```

**Parameters:**

- `error: string` — The error message to throw if data is falsy

**Returns:** `function` — A function that returns the data if truthy, or throws

**Examples:**

*Pass through truthy values*

Returns the value if truthy, throws otherwise.

```typescript
Promise.resolve('data').then(truthyPromiseOrThrow('No data'))
// => 'data'
```

*Throw on falsy values*

Throws an error when the value is falsy.

```typescript
Promise.resolve('').then(truthyPromiseOrThrow('Empty!'))
// throws Error('Empty!')
```

---

### `tryit`

Wraps a function so it never throws. Instead, it returns a `[error, result]` tuple.
Useful for avoiding try/catch blocks and handling errors in a functional style.

```typescript
import { tryit } from '@helpers4/promise';

tryit<TArgs extends readonly unknown[], TReturn>(fn: function): function
```

**Parameters:**

- `fn: function` — The function to wrap (sync or async)

**Returns:** `function` — A new function that returns a `Result` tuple

**Examples:**

*Safe JSON parsing*

Wraps JSON.parse to return a tuple instead of throwing.

```typescript
const safeParse = tryit(JSON.parse);
const [error, data] = safeParse('{"a":1}');
// error === undefined, data === { a: 1 }
```

*Catching errors without try/catch*

On error, the first element of the tuple is the Error.

```typescript
const safeParse = tryit(JSON.parse);
const [error, data] = safeParse('invalid');
// error instanceof SyntaxError, data === undefined
```

---
