# `Session.idle`

> 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.idle`

```ts
idle(options?: SignalOption): Promise<void>;
```

Resolves when nothing is open, queued, loading or animating, and the latest commit is shown:
the frame that draws it has been drawn, so [`info().scene`](InfoReport.scene.md), the canvas or a screenshot read
right after it shows that state. Open-ended motion (`autoRotate`, a clip played with
`loop: true`, a `continuous` hook) doesn't count, nor does refinement that keeps improving a
still frame afterwards (the soft ground shadow settling, a sharper still at rest). The only
call that waits for the screen. Where that frame cannot come it ends without that frame: on a
hidden page (a background tab), and while `el` is out of the viewport (the browser reports it,
or no animation frame comes for about 2 s: a cross-origin iframe scrolled away draws none).
The canvas and [`info().scene`](InfoReport.scene.md) may then hold an older frame: for an image, [`capture()`](Session.capture.md)'s
default source renders committed state off-display there too. Rejects AbortError when
`signal` aborts.
On the fake (`/testing`) time moves only on `tick`, so a bare `await fake.idle()` never
settles: start it, then pump `fake.tick(16)` with an `await` between ticks until it settles,
with a bound (a few hundred ticks; one is not always enough), as `createFakeSession` shows.
What to wait on. Never a timer: a `setTimeout` or a debounce guesses, and a slow phone or a
large GLB outlasts the guess. Each of these settles exactly when its write has landed:
- one write: its own Promise, at its commit point, its loads included; a camera move, a clip
  or a `durationMs` tween resolves when it finishes ('superseded': a newer write won);
- the newest of a burst (a slider, fast swatch clicks): abort the older write's `signal` when
  a new one starts, and await the newest; never debounce to let the runtime catch up;
- several writes all or nothing: the [`transaction`](Session.transaction.md) Promise;
- any commit, once shown, whoever made it (the shopper, an undo, a store sync): its event,
  'committed' or a state event such as 'scene-model:changed' (skip your own by
  `cause.origin`); 'camera:rest' when the camera has settled;
- everything loaded, eased and drawn (before a capture, a "ready" state, a test's assertion):
  `idle()`;
- a value each frame (a spin, a follow, a cut-away): an [`onFrame`](Session.onFrame.md) hook, never `setInterval`.
An open transaction counts as open: `idle()` waits for it to settle, so an app whose own
transaction's `work` waits on that app (a dialog, a click) finishes or aborts that
transaction before it awaits `idle()`, or the transaction rolls back with reason 'timeout'
after its `timeoutMs` without progress and its staged change is lost.
Errors: throws HoloScopeError inside `work` or [`beforeCommit`](Session.beforeCommit.md).
Not for: waiting for one write, which is that write's own Promise.

## Options

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `signal` | `AbortSignal` | undefined | Cancels this call; the member's JSDoc says what an abort does. |

## Abort (`signal`)

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

> A wait (`idle`, `capture`): aborting rejects AbortError and nothing rolls back.

## Example

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.idle.ts`):

```ts
import type { DeepReadonly, SceneModel, Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// What to wait on: the runtime says when a write has landed. No setTimeout, no debounce.
declare function showPrice(model: DeepReadonly<SceneModel>): void;
declare function setSaveEnabled(on: boolean): void;
// any commit, the shopper's, an undo's or yours, once it is shown: its event
session.on('scene-model:changed', ({ model }) => showPrice(model), { signal });
let latest: AbortController | null = null;
export async function pickFinish(material: string): Promise<void> {
  latest?.abort();                                   // newest wins, with no debounce
  const mine = (latest = new AbortController());
  setSaveEnabled(false);
  try {
    // one write: its own Promise, at its commit point, its loads included
    const r = await session.select('finish', material, {
      actor: 'shopper',                               // a shopper's pick
      origin: 'swatch',
      signal: mine.signal,
    });
    // 'refused' or 'rolled-back': the finish didn't change, and Save comes back below
    if (r.outcome === 'committed') await session.idle({ signal: mine.signal }); // drawn
  } catch (error) {
    // a newer pick aborted this one: still queued, it never lands; committed, it stops here
    if ((error as Error).name !== 'AbortError') throw error;
  } finally {
    if (latest === mine) setSaveEnabled(true);       // the newest pick settled, any outcome
  }
}
```

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `idle` names no product |
| 2. A precedent | pass: [model-viewer (ar, camera-orbit, auto-rotate)](https://modelviewer.dev/docs/) (row `G37`); [Unity Material.Lerp](https://docs.unity3d.com/ScriptReference/Material.Lerp.html) (row `K17`); [WebGL KHR_parallel_shader_compile](https://registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile/) (row `K35`); [Babylon.js Scene.whenReadyAsync](https://github.com/BabylonJS/Babylon.js/blob/master/packages/dev/core/src/scene.ts) (row `K35`) |
| 3. Two apps, or labs | pass: 28 apps (ledger: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508 and 20 more; fleet cross-check: app-bcb7ef21da, app-e61a0b42ad, app-f3598fad77, app-fb16dd1985, app-fd09090420, configurator-template) |
| 4. The cheaper fix first | decided per need in Appendix B: rows `G37`, `K17`, `K35` at tier stable (a member only where no rule, data, asset or app code serves the need) |
