# Frame-request scheduler lab

An original, dependency-free JavaScript teaching adapter for event-driven frame requests. It has no runtime dependency on Spark or Three.js and makes no network requests.

## Downloads

- [Complete lab ZIP](./frame-request-lab.zip)
- [Scheduler module](./scheduler.mjs)
- [Node tests](./scheduler.test.mjs)
- [Test runner / JSON report generator](./run-tests.mjs)
- [Recorded test report](./test-report.json)
- [Source-mapped event-wiring checklist](./event-wiring-checklist.csv)

Save the three `.mjs` files together, or extract the ZIP. Use Node.js 22 or later; no package installation is needed. From that directory:

```sh
node --test scheduler.test.mjs
node run-tests.mjs
```

The second command reruns the tests and replaces `test-report.json` with the actual result. The published report records the Node version used. Test-runner durations are not rendering benchmarks.

## What this adapter does

`createFrameRequester({ schedule, cancel, draw })` returns `invalidate()` and `dispose()`.

- `schedule(callback)` must queue a future callback, never call it synchronously, and return a handle. Browser `requestAnimationFrame` has that shape.
- `cancel(handle)` cancels a queued callback. Handle zero is valid.
- `draw(timestamp)` must be synchronous. Any thrown error propagates; there is no automatic retry or error swallowing.
- `invalidate()` returns true when it queues work and false when a frame is already pending or the adapter is disposed.
- The pending ticket is cleared before draw. Invalidation during draw therefore queues a later frame instead of disappearing.
- `dispose()` cancels pending work and makes subsequent invalidations harmless. Remove application listeners separately and release renderer resources separately.

Example browser wiring, shown for adaptation and **not browser-tested by this lab**:

```js
import { createFrameRequester } from './scheduler.mjs';

const frames = createFrameRequester({
  schedule: (callback) => window.requestAnimationFrame(callback),
  cancel: (handle) => window.cancelAnimationFrame(handle),
  draw: () => renderer.render(scene, camera),
});

frames.invalidate(); // Initial frame; future events call invalidate again.
// Later: remove event listeners, then frames.dispose().
```

The example assumes you already created `renderer`, `scene`, and `camera`. It is not a standalone viewer. Damping, animation, or other evolving state needs its own update/invalidation wiring.

## Evidence and limits

Ten Node built-in tests exercise a controlled fake frame queue: burst coalescing, a later Promise completion, invalidation during draw, idle flushes, cancellation and stale delivery after disposal, recovery after a thrown draw, a next-frame request surviving a thrown draw, scheduling-error recovery, timestamp forwarding, and invalid callback rejection.

This is **synthetic scheduling-logic validation only**. It does not run Spark, Three.js, a browser, a GPU, or real `requestAnimationFrame`. It does not establish visual correctness, streaming completeness, frame pacing, latency, energy use, battery savings, or performance. A passing result is evidence about this adapter under the specified callback contract, not evidence that a viewer integration works.

## Sources

- [Spark: on-demand rendering](https://sparkjs.dev/docs/on-demand-rendering/), consulted October 12, 2026. Describes connecting `onDirty` to frame requests and requesting frames after application changes. Spark's asynchronous results need subsequent rendering to appear.
- [Three.js: rendering on demand](https://threejs.org/manual/pages/rendering-on-demand.html), consulted October 12, 2026. Explains change-driven rendering and why damped controls need deferred, coalesced requests.

The checklist maps individual integration reminders to these references. The adapter, fake queue, tests, and report generator are original educational code, not copied implementation code or an official library integration.
