# `Session.on`

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

```ts
on<K extends keyof Events, R>(event: K, handler: (payload: Events[K]) => NotAsync<R>, options?: SignalOption): () => void;
```

Subscribes. Lifecycle call: immediate; unsubscribe is terminal. Handlers run synchronously,
never nested: a state event's after the presented frame, in [`seq`](Session.seq.md) order; 'problem' at once,
when the Problem is recorded (spec section 6.11). An async handler is a type error. A
handler removed during dispatch is skipped for the rest of that round. A write in a handler is
a new commit whose events follow this round. Returns the unsubscribe function; aborting
`signal` unsubscribes the same way, and nothing rejects.
Errors: an unknown event records 'unknown-event' and returns a no-op unsubscribe; a handler
that throws records 'handler-threw', one that returns a Promise 'handler-async', and that
Promise's rejection 'handler-rejected'. A 'problem' handler's own failure is recorded with
event 'problem' but fires no further 'problem' event.
Not for: layout that must show in the same frame as the commit, which is [`beforeCommit`](Session.beforeCommit.md).

## 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 registration (`SignalOption.signal`, [the kinds of call](../read-this-first.md#conventions)).

> A registration (`on`, `beforeCommit`): aborting removes it and nothing rejects.
>
> 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).

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

```ts
import type { Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
declare const analytics: { track(name: string, props: Record<string, string | number>): void };
session.on('committed', ({ cause, label, recorded, patch }) => {
  // skip our own sync and look-only commits
  if (cause.origin === 'store-sync' || !recorded) return;
  analytics.track('configuration-change', {
    actor: cause.actor, label: label ?? '', ops: patch.length,
  });
}, { signal });
```

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `on` names no product |
| 2. A precedent | not recorded: no ledger row (coverage.json) names this member or its owner |
| 3. Two apps, or labs | pass: 21 apps (fleet cross-check: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4fd23bc869, app-822bb9d6aa, app-8675b3225c and 13 more) |
| 4. The cheaper fix first | not recorded per member (Appendix B decides it per need) |
