Function utilities
Rate-limiting, memoization, composition, currying, tiny FP primitives.
import { debounce, throttle, memoize, once, after, before, pipe, compose, tap, curry, partial, partialRight, noop, identity, constant, negate, escapeRegex,} from "@mongez/reinforcements";Rate-limiting
debounce
debounce<T>(fn: T, wait: number, options?: { leading?: boolean; // default false trailing?: boolean; // default true maxWait?: number; // force invocation at this cap, even if input keeps coming}): Debounced<T>
type Debounced<T> = T & { cancel(): void; // drop any pending invocation flush(): void; // invoke the pending call immediately pending(): boolean;};const save = debounce(payload => api.save(payload), 500, { maxWait: 3000 });
input.addEventListener("input", e => save(e.target.value));button.addEventListener("click", () => save.flush());window.addEventListener("beforeunload", () => save.cancel());throttle
throttle<T>(fn: T, wait: number, options?: { leading?: boolean; // default true trailing?: boolean; // default true}): Throttled<T>
type Throttled<T> = T & { cancel(): void; flush(): void; pending(): boolean };const onScroll = throttle(() => layout(), 100);window.addEventListener("scroll", onScroll);Caching
memoize
memoize<T>(fn: T, options?: { resolver?: (...args: Parameters<T>) => string; // default JSON.stringify(args) ttl?: number; // ms; default Infinity}): Memoized<T>
type Memoized<T> = T & { clear(): void; forget(key: string): void };const lookup = memoize((id: string) => db.users.find(id), { ttl: 60_000 });
lookup("u1"); // hits DBlookup("u1"); // cachedlookup.forget(JSON.stringify(["u1"])); // surgical invalidation (default key = JSON.stringify(args))lookup.clear(); // nuke everythingCustom key:
const cached = memoize( (a: User, b: User) => similarity(a, b), { resolver: (a, b) => `${a.id}-${b.id}` },);Call-count gating
once<T>(fn: T): T // run once, cache result foreverafter<T>(n: number, fn: T): (...args) => R | undefined // only invokes after N-th callbefore<T>(n: number, fn: T): (...args) => R | undefined // up to N times; later calls return last resultconst init = once(() => expensiveSetup());init(); init(); init(); // expensiveSetup runs once
const onAllDone = after(3, () => render());onAllDone(); onAllDone(); onAllDone(); // render() on the third call
const tryConnect = before(3, () => connect());// up to 3 actual connect() calls; further calls return the last resultComposition
pipe / compose
pipe<A>(value: A): Apipe<A, B>(value: A, fn1: (a: A) => B): Bpipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C// …up to 4 typed steps; variadic fallback after that
compose<A, B, C>(fn2: (b: B) => C, fn1: (a: A) => B): (a: A) => C// right-to-leftpipe(2, n => n + 1, n => n * 10); // 30
const shout = compose( (s: string) => s + "!", (s: string) => s.toUpperCase(),);shout("hi"); // "HI!"tap / tap.with
tap<T>(value: T, sideEffect: (value: T) => void): Ttap.with<T>(sideEffect: (value: T) => void): (value: T) => TSide-effect probe. tap.with returns a pipeline-friendly identity-with-side-effect.
pipe(value, trim, tap.with(console.log), toSnakeCase);Currying & partial application
curry(fn: (...args) => R): anypartial<T>(fn: T, ...preset): (...rest) => ReturnType<T>partialRight<T>(fn: T, ...preset): (...rest) => ReturnType<T>const add = curry((a: number, b: number, c: number) => a + b + c);add(1)(2)(3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
const greet = (greeting: string, name: string) => `${greeting}, ${name}`;const hello = partial(greet, "Hello");hello("Ada"); // "Hello, Ada"
const divide = (a: number, b: number) => a / b;const halve = partialRight(divide, 2);halve(10); // 5Tiny FP primitives
noop(): void // does nothingidentity<T>(value: T): T // returns argumentconstant<T>(value: T): () => T // returns a function that always yields valuenegate<T>(predicate: T): (...args) => boolean // inverts a predicateitems.filter(identity); // drop falsy valuesevents.on("data", noop); // explicitly ignoreconst always42 = constant(42);const isOdd = negate((n: number) => n % 2 === 0);escapeRegex
escapeRegex(string: string): stringEscape regex meta-characters so a literal string can be used in a RegExp.
new RegExp(escapeRegex("a.b+c")); // matches the literal "a.b+c"