node-comfortv2.0.0

nc.func

Function helpers: debounce, throttle, memoize, retry with backoff, timeouts, error handling without try/catch, and typed composition.

const { func } = require("@ix-xs/node-comfort");
import { debounce } from "@ix-xs/node-comfort/func";
const save = func.debounce(persist, 300);
const data = await func.retry(() => fetchJSON(url), { attempts: 5, delay: 200, backoff: 2 });
const [error, user] = await func.to(getUser(id));
GuideExplanations and examples for nc.func.
Read the guide →

Functions

nc.func.debounce()

debounce<F extends (...args: any[]) => any>(fn: F, wait: number, options?: DebounceOptions): DebouncedFunction<F>

Waits for a pause of wait ms between calls, then runs once with the latest arguments. For search boxes, resizing, auto-saving...

Parameters

NameTypeDescription
fnF
waitnumberPause needed, in ms.
optionsoptionalDebounceOptions

Returns DebouncedFunction<F>

Example

const search = func.debounce((query) => api.search(query), 300);
input.on("input", (e) => search(e.target.value));

const save = func.debounce(persist, 1000, { maxWait: 5000 }); // saves at least every 5s

nc.func.throttle()

throttle<F extends (...args: any[]) => any>(fn: F, wait: number, options?: ThrottleOptions): ThrottledFunction<F>

Runs at most once every wait ms, however often it's called. For scroll handlers, progress reports, rate-limited APIs...

Parameters

NameTypeDescription
fnF
waitnumberMinimum gap between calls, in ms.
optionsoptionalThrottleOptions

Returns ThrottledFunction<F>

Example

const report = func.throttle((pct) => console.log(`${pct}%`), 1000);

nc.func.once()

once<F extends (...args: any[]) => any>(fn: F): F

Runs fn the first time only; later calls return the same result.

Parameters

NameType
fnF

Returns F

Example

const connect = func.once(() => createPool(config));

nc.func.memoize()

memoize<F extends (...args: any[]) => any>(fn: F, options?: MemoizeOptions | ((...args: Parameters<F>) => unknown) | undefined): MemoizedFunction<F>

Caches results by arguments. Supports expiry (ttl), a size limit (max) and async functions: a failed promise isn't cached, so the next call tries again.

Parameters

NameTypeDescription
fnF
optionsoptionalMemoizeOptions | ((...args: Parameters<F>) => unknown)Options, or a function that builds the cache key.

Returns MemoizedFunction<F>

Example

const getUser = func.memoize((id) => db.users.find(id), { ttl: 60_000, max: 1000 });
getUser.delete(42); // forget one entry
getUser.clear();    // forget everything

nc.func.retry()

retry<T>(fn: (attempt: number) => T | PromiseLike<T>, options?: RetryOptions): Promise<Awaited<T>>

Calls fn until it succeeds, waiting between attempts. If every attempt fails, the last error is thrown.

Parameters

NameTypeDescription
fn(attempt: number) => T | PromiseLike<T>Receives the attempt number, starting at 1.
optionsoptionalRetryOptions

Returns Promise<Awaited<T>>

Throws The last error, or an AbortError if the signal fires.

Example

const data = await func.retry(() => fetchJSON(url), {
  attempts: 5,
  delay: 200,
  backoff: 2, // 200, 400, 800, 1600 ms
  shouldRetry: (error) => !(error instanceof nc.errors.HttpError && error.status < 500),
});

nc.func.timeout()

timeout<T>(promise: PromiseLike<T> | (() => T | PromiseLike<T>), ms: number, message?: string): Promise<Awaited<T>>

Rejects with a TimeoutError if the promise takes longer than ms.

Parameters

NameTypeDescription
promisePromiseLike<T> | (() => PromiseLike<T> | T)A promise, or a function that returns one.
msnumber
messageoptionalstring

Returns Promise<Awaited<T>>

Throws TimeoutError

Example

const res = await func.timeout(fetch(url), 5000);
const rows = await func.timeout(() => slowQuery(), 2000, "Database too slow");

nc.func.delay()

delay<F extends (...args: any[]) => any>(fn: F, ms: number, ...args: Parameters<F>): Promise<Awaited<ReturnType<F>>>

Waits ms, then calls fn with the arguments and resolves with its result.

Parameters

NameType
fnF
msnumber
...argsParameters<F>

Returns Promise<Awaited<ReturnType<F>>>

Example

await func.delay(() => console.log("1s later"), 1000);

nc.func.to()

to<T, E = Error>(promise: PromiseLike<T> | (() => T | PromiseLike<T>)): Promise<[E, undefined] | [null, Awaited<T>]>

Awaits a promise and gives you [error, value] instead of throwing, so you don't need a try/catch.

Parameters

NameTypeDescription
promisePromiseLike<T> | (() => PromiseLike<T> | T)A promise, or a function (sync errors are caught too).

Returns Promise<[E, undefined] | [null, Awaited<T>]>

Example

const [error, user] = await func.to(db.users.find(id));
if (error) return res.status(500).send(error.message);

nc.func.attempt()

attempt<F extends (...args: any[]) => any>(fn: F): (...args: Parameters<F>) => Promise<[unknown, undefined] | [null, Awaited<ReturnType<F>>]>

Wraps a function so it resolves to [error, value] instead of throwing.

Parameters

NameType
fnF

Returns (...args: Parameters<F>) => Promise<[unknown, undefined] | [null, Awaited<ReturnType<F>>]>

Example

const safeParse = func.attempt(JSON.parse);
const [error, data] = await safeParse(input);

nc.func.promisify()

promisify(fn: (...args: any[]) => void): (...args: any[]) => Promise<any>

Turns a callback-style function into one that returns a promise.

Parameters

NameType
fn(...args: any[]) => void

Returns (...args: any[]) => Promise<any>

Example

const lookup = func.promisify(dns.lookup);
const address = await lookup("nodejs.org");

nc.func.pipe()7 overloads

pipe<A extends any[], B>(f1: (...args: A) => B): (...args: A) => B
pipe<A extends any[], B, C>(f1: (...args: A) => B, f2: (b: B) => C): (...args: A) => C
pipe<A extends any[], B, C, D>(f1: (...args: A) => B, f2: (b: B) => C, f3: (c: C) => D): (...args: A) => D
pipe<A extends any[], B, C, D, E>(f1: (...args: A) => B, f2: (b: B) => C, f3: (c: C) => D, f4: (d: D) => E): (...args: A) => E
pipe<A extends any[], B, C, D, E, F>(f1: (...args: A) => B, f2: (b: B) => C, f3: (c: C) => D, f4: (d: D) => E, f5: (e: E) => F): (...args: A) => F
pipe<A extends any[], B, C, D, E, F, G>(f1: (...args: A) => B, f2: (b: B) => C, f3: (c: C) => D, f4: (d: D) => E, f5: (e: E) => F, f6: (f: F) => G): (...args: A) => G
pipe(...fns: ((arg: any) => any)[]): (...args: any[]) => any

Chains functions from left to right: pipe(f, g)(x) is g(f(x)). Types flow through up to 6 functions.

Parameters

NameType
f1(...args: A) => B
f2(b: B) => C
f3(c: C) => D
f4(d: D) => E
f5(e: E) => F
f6(f: F) => G

Returns (...args: A) => G

Example

const toSlug = func.pipe(nc.str.deburr, nc.str.kebabCase);
toSlug("Crème Brûlée"); // "creme-brulee"

nc.func.compose()

compose(...fns: ((...args: any[]) => any)[]): (...args: any[]) => any

Chains functions from right to left: compose(f, g)(x) is f(g(x)). pipe() reads in execution order and is better typed.

Parameters

NameType
...fns...((...args: any[]) => any)

Returns (...args: any[]) => any

Example

const shout = func.compose((s) => s + "!", (s) => s.toUpperCase());
shout("hi"); // "HI!"

nc.func.curry()

curry(fn: (...args: any[]) => any, arity?: number): (...args: any[]) => any

Lets you pass a function's arguments a few at a time.

Parameters

NameTypeDescription
fn(...args: any[]) => any
arityoptionalnumberHow many arguments to collect before calling.Default: fn.length

Returns (...args: any[]) => any

Example

const add = func.curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6

nc.func.partial()

partial<P extends any[], R extends any[], T>(fn: (...args: [...P, ...R]) => T, ...preset: P): (...args: R) => T

Fixes the first arguments of a function.

Parameters

NameType
fn(...args: [...P, ...R]) => T
...presetP

Returns (...args: R) => T

Example

const hello = func.partial((greeting, name) => `${greeting}, ${name}!`, "Hello");
hello("Ada"); // "Hello, Ada!"

nc.func.negate()

negate<A extends any[]>(predicate: (...args: A) => unknown): (...args: A) => boolean

The opposite of a predicate.

Parameters

NameType
predicate(...args: A) => unknown

Returns (...args: A) => boolean

Example

files.filter(func.negate((name) => name.startsWith(".")));

nc.func.after()

after<F extends (...args: any[]) => any>(n: number, fn: F): (...args: Parameters<F>) => ReturnType<F> | undefined

Does nothing for the first n - 1 calls, then calls fn every time.

Parameters

NameType
nnumber
fnF

Returns (...args: Parameters<F>) => ReturnType<F> | undefined

Example

const done = func.after(3, () => console.log("All 3 uploads finished"));
uploads.forEach((upload) => upload.on("end", done));

nc.func.before()

before<F extends (...args: any[]) => any>(n: number, fn: F): (...args: Parameters<F>) => ReturnType<F> | undefined

Calls fn for the first n - 1 calls, then keeps returning the last result.

Parameters

NameType
nnumber
fnF

Returns (...args: Parameters<F>) => ReturnType<F> | undefined

Example

const tryLogin = func.before(4, login); // 3 real attempts at most

nc.func.noop()

noop(..._args: unknown[]): void

Does nothing. Useful as a default callback.

nc.func.identity()

identity<T>(value: T): T

Returns its argument.

Parameters

NameType
valueT

Returns T

Example

[0, 1, 2].filter(func.identity); // [1, 2]

Types

Import any of them in TypeScript with import type { DebouncedFunction } from "@ix-xs/node-comfort", or in JavaScript with import("@ix-xs/node-comfort").DebouncedFunction.

DebouncedFunction

A debounced function. flush() runs the pending call now, cancel() drops it, pending() tells you if there's one.

type DebouncedFunction = ((...args: Parameters<F>) => void) & { cancel(): void; flush(): void; pending(): boolean; }

DebounceOptions

Options for debounce().

PropertyTypeDescription
leadingoptionalbooleanCall right away at the start of a burst.
trailingoptionalbooleanCall once the burst is over. Defaults to true.
maxWaitoptionalnumberNever postpone a call longer than this, in ms, even if calls keep coming.

MemoizedFunction

A memoized function, with its cache, clear() and delete(...args).

type MemoizedFunction = F & { cache: Map<unknown, { value: ReturnType<F>; expires: number; }>; clear(): void; delete(...args: Parameters<F>): boolean; }

MemoizeOptions

Options for memoize().

PropertyTypeDescription
resolveroptional((...args: any[]) => unknown)Builds the cache key from the arguments. Defaults to the arguments as JSON.
ttloptionalnumberHow long a result stays cached, in ms.
maxoptionalnumberMaximum number of results; the least recently used goes first.
cacheRejectionsoptionalbooleanKeep rejected promises. By default they're dropped so the next call tries again.

RetryOptions

Options for retry().

PropertyTypeDescription
attemptsoptionalnumberTotal attempts, the first one included. Defaults to 3.
delayoptionalnumber | ((attempt: number, error: unknown) => number)Wait before the next attempt, in ms, or a function that computes it.
backoffoptionalnumberMultiplies the delay after each failure: 2 gives 200, 400, 800... Defaults to 1.
maxDelayoptionalnumberCap for the computed delay, in ms.
jitteroptionalnumberRandomizes the delay, from 0 (none) to 1 (anywhere between 0 and 2x). Stops many clients from retrying in sync.
shouldRetryoptional((error: unknown, attempt: number) => boolean | Promise<boolean>)Return false to give up early, for example on a 4xx error.
onRetryoptional((error: unknown, attempt: number, delay: number) => void)Called before each wait, for logs or metrics.
signaloptionalAbortSignalStops retrying. The attempt in progress isn't interrupted.

ThrottledFunction

A throttled function, with cancel().

type ThrottledFunction = ((...args: Parameters<F>) => void) & { cancel(): void; }

ThrottleOptions

Options for throttle().

PropertyTypeDescription
leadingoptionalbooleanCall right away on the first call. Defaults to true.
trailingoptionalbooleanMake a last call with the latest arguments at the end of the interval. Defaults to true.
node-comfort v2.0.0View the source