Implement limit(fn, n), which wraps a callback so it runs at most n times. The first n invocations call fn and return its result. Every invocation after that returns the result of the last real call, without invoking fn again. Think of it as a "burn after reading" wrapper — there are n real shots, then the wrapper just hands back the last receipt.
// Returns a wrapped function. The first `n` calls invoke `fn` and return its
// result. Calls n+1 and beyond return the last cached result without calling
// fn. If n is 0 or negative, fn is never invoked and every call returns
// undefined.
function limit<T extends (...args: any[]) => any>(fn: T, n: number): T;
let calls = 0;
const sayHi = limit((name) => { calls++; return `hi ${name}`; }, 2);
sayHi('Ada'); // 'hi Ada' (call 1: invokes fn)
sayHi('Bea'); // 'hi Bea' (call 2: invokes fn — this is the Nth call)
sayHi('Cara'); // 'hi Bea' (call 3: returns cached, fn NOT invoked)
sayHi('Dee'); // 'hi Bea' (still cached)
// calls === 2 — fn ran exactly twice
// n = 0 means fn never runs. Every call returns undefined.
const blocked = limit(() => 'should never see this', 0);
blocked(); // undefined
blocked(); // undefined
// Each limit() factory call is independent — its own counter and cache.
const a = limit((x) => x * 2, 1);
const b = limit((x) => x * 10, 1);
a(5); // 10
b(7); // 70 (b has its own counter; a's call didn't consume b's quota)
a(99); // 10 (a's cache: the last real result was 10)
b(99); // 70
n = 2, calls 1 and 2 both call fn. Call 3 is the first one that returns the cache. Off-by-one here is the most common bug.fn returned on call n, regardless of the new arguments. The wrapper ignores its arguments once the limit is reached.fn throws on call 2 with n = 3, that throw propagates out of the wrapper and consumes one of the three slots. The counter ticks on entry, not on success.n. Treat any n less than 1 (including 0, negatives, NaN) as 0 — fn never runs. Floor non-integers (n = 2.7 behaves like n = 2).limit(fn, n) returns a fresh wrapper with its own counter and its own cache. Two wrappers around the same fn do not share state.this and all arguments. For the calls that do invoke fn, pass through every argument and respect this so the wrapper works with .call, .apply, and methods on objects.