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));