# `Session.onFrame`

> Contract 2.0.0 · package sha256 `a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e` · generated from `configurator-runtime.d.ts` by `services/onsite-scripter/scripts/build-contract-docs.mjs`; edit the source, not this file.

Method of [`Session`](Session.md) · inherited by [`testing.FakeSession`](testing.FakeSession.md) · stable · since 2.0.0 · called as `session.onFrame`

```ts
onFrame<R>(callback: (frame: Frame) => NotAsync<R>, options?: FrameOptions): () => void;
```

A per-frame hook. Synchronous: an async callback is a type error. Writes go through the [`Frame`](Frame.md)
writer and show in the frame being drawn; they are transient (never stored, undone, counted or
reported). Budget: all hooks together 3 ms per frame, averaged over the last 60 drawn frames
('frame-budget', at most once per 10 s). A hook that throws on 3
frames in a row is removed ('handler-threw' each time, then 'hook-removed').
Paused while the display is held. Lifecycle call:
immediate; returns the function that removes the hook. A removal (that function, `signal`, or
the hook removing itself in its call) releases the hook's writes from the next frame, which the
runtime draws even when nothing else changes; the frame being drawn keeps them, those of the
call that removed it included. So a hand-over to a resting hook shows no released pose.
A look-only write outside the write queue (a rule) that a hook calls while no
transaction is open, needing nothing loaded, decoded or compiled, commits before this frame is
drawn and shows in it beside the [`Frame`](Frame.md) writes; its events dispatch after the frame. Any
other write called in a hook commits as usual, in a later frame.
Not for: a change that should stay or be undone, which is `set` (a rule) or a scene-model
write.
Errors: a throw records 'handler-threw'; 3 in a row remove the hook and record 'hook-removed'
once (its disposer then does nothing). Over budget records 'frame-budget'.

## Options

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `continuous` | `boolean` | false | Keep drawing frames while this hook lives, even with nothing else changing. |
| `phase` | `'update' \| 'late'` | 'update' | 'update': before animations and the camera. |
| `signal` | `AbortSignal` | undefined | Aborting removes the hook; already aborted, none is added ([`SignalOption`](SignalOption.md)). |

## Abort (`signal`)

This call is a creator (`SignalOption.signal`, [the kinds of call](../read-this-first.md#conventions)).

> A creator (`collection` for a new name, `materials.create`, `on`, `onFrame`, `beforeCommit`, `follow`, `input.drag`, `labs.lightLinking`) given a signal already aborted returns at once, already ended: a `Collection` or `Material` reading `disposed: true` that holds no content and no live name, or a registration that never runs and whose disposer does nothing.
>
> It records no Problem for the abort and throws only what it would throw anyway, as the DOM's `addEventListener` with an aborted signal adds nothing (spec section 6.5).

## Examples

Imports: values (`has`, `validate`, the `Holo*Error` classes, the pure helpers) from `@holostep-configurators/runtime-client`, types from `@holostep-configurators/runtime-client/contract`, `/math` and `/testing` names from those entries; `boot` from the runtime module the root's `loadRuntimeAtFloor` resolves to. A type your page defines is your app's: declare it there, never import it from the package.

Compiled under the contract's strict settings (`examples/declaration/05-Session.onFrame-1.ts`):

```ts
import type { Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
const glow = session.materials.create({ emissive: '#ffaa00', emissiveStrength: 0 }, { signal });
await session.find({ tag: 'indicator' }).set({ material: glow });     // a rule: persistent
session.onFrame((frame) => {
  // transient: no commit per frame
  frame.material(glow, { emissiveStrength: 1 + Math.sin(frame.timeMs / 300) });
}, { continuous: true, signal });
```

Compiled under the contract's strict settings (`examples/declaration/05-Session.onFrame-2.ts`):

```ts
import type { Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// a cut-away reads the camera, so it runs 'late': in 'update' it lags one frame behind a move
const frontWall = session.find({ tag: 'wall-front' });              // the wall on the +Z side
// parts hung under the wall (its casings, `Placement.parent`) hide with it (spec section 6.10, R8)
session.onFrame((frame) => {
  frame.visible(frontWall, Math.abs(session.camera.state.azimuthDeg) >= 90);  // this frame's camera
}, { phase: 'late', signal });
```

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `onFrame` names no product |
| 2. A precedent | pass: [Unity MonoBehaviour.LateUpdate](https://docs.unity3d.com/ScriptReference/MonoBehaviour.LateUpdate.html) (row `G3`); [glTF 2.0 animations](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations) (row `G52`); [Window.requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) (row `K33`); [Unity Transform.RotateAround](https://docs.unity3d.com/ScriptReference/Transform.RotateAround.html) (row `K5`); [Babylon.js TransformNode.setPivotPoint](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/Meshes/transformNode.ts) (row `K5`); [Unity Object.Instantiate](https://docs.unity3d.com/ScriptReference/Object.Instantiate.html) (row `K8`); and 1 more in coverage.json |
| 3. Two apps, or labs | pass: 27 apps (ledger: app-37356bf09f, app-378d190f1b, app-4f16d2f6da, app-4fd23bc869, app-510d750508, app-59c5b5751e, app-822bb9d6aa, app-8675b3225c and 10 more; fleet cross-check: app-04c2774145, app-0c773a4ee3, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85 and 13 more) |
| 4. The cheaper fix first | decided per need in Appendix B: rows `G3`, `G35`, `G52`, `K33`, `K41`, `K5`, `K8` at tier labs, stable (a member only where no rule, data, asset or app code serves the need) |
