js-dojo

4 apply trap timer

GOAL

Time a hot callback (the hud's score formatter) without touching its body and without hand-writing a wrapper FUNCTION — use a Proxy `apply` trap instead, so whatever you wrap keeps its .name, .length and everything else it had.

CONCEPT

A function is an object, so a Proxy can wrap one. The `apply` trap runs when the proxy is CALLED: apply(target, thisArg, args). Reflect.apply(target, thisArg, args) is how you pass the call on. Anything you leave untrapped (reading .name, .length, typeof) reaches the target on its own — that pass-through is the whole lesson of this kata.

HINT

- performance.now() before/after; add up the delta in a try/finally so a formatter that throws still gets counted. - Dropping thisArg is the classic mistake: `target(...args)` runs the formatter with this === undefined, and method-style callbacks (`hit.describe()`) blow up. Reflect.apply carries it for free.

MIRRORS

a perf probe that wraps the formatters an engine calls on your behalf — hud labels, damage numbers, score readouts. Engines fire them hundreds of times per frame, and they invoke each one with a meaningful `this` (the hit / entity context). Lose `this` and the very next frame crashes.

Run

node 04-apply-trap-timer.cjs   (fails until you fix the TODO)

Source

/*
 * proto-dojo 05-proxy-reflect / 04-apply-trap-timer
 * -------------------------------------------------
 * GOAL: Time a hot callback (the hud's score formatter) without
 *       touching its body and without hand-writing a wrapper FUNCTION —
 *       use a Proxy `apply` trap instead, so whatever you wrap keeps
 *       its .name, .length and everything else it had.
 *
 * CONCEPT: A function is an object, so a Proxy can wrap one. The
 *          `apply` trap runs when the proxy is CALLED: apply(target,
 *          thisArg, args). Reflect.apply(target, thisArg, args) is how
 *          you pass the call on. Anything you leave untrapped (reading
 *          .name, .length, typeof) reaches the target on its own — that
 *          pass-through is the whole lesson of this kata.
 *
 * HINT:   - performance.now() before/after; add up the delta in a
 *           try/finally so a formatter that throws still gets counted.
 *         - Dropping thisArg is the classic mistake:
 *           `target(...args)` runs the formatter with this ===
 *           undefined, and method-style callbacks (`hit.describe()`)
 *           blow up. Reflect.apply carries it for free.
 *
 * MIRRORS: a perf probe that wraps the formatters an engine calls on
 *          your behalf — hud labels, damage numbers, score readouts.
 *          Engines fire them hundreds of times per frame, and they
 *          invoke each one with a meaningful `this` (the hit / entity
 *          context). Lose `this` and the very next frame crashes.
 *
 * Run: node 04-apply-trap-timer.cjs   (fails until you fix the TODO)
 */
'use strict';
const assert = require('node:assert');

function formatScore(value, digits) {
    // simulate a slightly-expensive formatter
    let noise = 0;
    for (let i = 0; i < 1000; i++) noise += Math.sin(i);
    void noise;
    return value.toFixed(digits);
}

/* ------------------------------------------------------------------ */
/* TODO 1: return new Proxy(fn, { apply(target, thisArg, args) {...} }) */
/*         that times each call with performance.now(), accumulates     */
/*         stats.calls / stats.totalMs, and forwards via Reflect.apply. */
/* ------------------------------------------------------------------ */
function timed(fn, stats) {
    // TODO 1: replace this passthrough with `new Proxy(fn, { apply ... })`
    return fn;
}

/* ------------------------- checks --------------------------------- */
const stats = { calls: 0, totalMs: 0 };
const fmt = timed(formatScore, stats);

assert.strictEqual(typeof fmt, 'function',
    'a Proxy around a function is still typeof "function"');
assert.strictEqual(fmt(1234.56789, 2), '1234.57',
    'the timed formatter must return exactly what the original returns');
assert.strictEqual(stats.calls, 1,
    'TODO 1: stats.calls is still 0 after one call — the apply trap is where ' +
    'you count');

for (let i = 0; i < 4; i++) fmt(1234.5 + i / 10, 2);
assert.strictEqual(stats.calls, 5, 'every call must be counted');
assert.ok(Number.isFinite(stats.totalMs) && stats.totalMs >= 0,
    'TODO 1: stats.totalMs must accumulate performance.now() deltas');

// Transparency: untrapped operations fall through to the target.
// (A hand-written `function wrapper(){}` would FAIL both of these.)
assert.strictEqual(fmt.name, 'formatScore',
    'untrapped .name must shine through the proxy');
assert.strictEqual(fmt.length, 2,
    'untrapped .length must shine through the proxy');

// this-forwarding: the engine calls formatters with a context object as
// `this`.
const thisStats = { calls: 0, totalMs: 0 };
const hit = {
    damage: 1.5,
    describe() { return 'damage=' + this.damage.toFixed(2); },
};
hit.describe = timed(hit.describe, thisStats);
assert.strictEqual(hit.describe(), 'damage=1.50',
    'TODO 1 (thisArg): the method lost its `this` — forward with ' +
    'Reflect.apply(target, thisArg, args), not target(...args)');
assert.strictEqual(thisStats.calls, 1, 'method-style calls count too');

console.log('OK 04-apply-trap-timer — %d calls, %s ms total',
    stats.calls, stats.totalMs.toFixed(3));

Solution