# `Session.beforeCommit`

> 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) · labs, `@experimental` · since 2.0.0 · called as `session.beforeCommit` · optional: reach it through `has()`

```ts
beforeCommit?(name: string, fn: (commit: BeforeCommitContext) => void | Promise<void>, options?: SignalOption): () => void;
```

Registers a hook that runs inside every recorded commit (writes, queued writes, undo, redo,
restore, reload), after loads and before the commit point. An equal write (it commits nothing)
runs no hook: equality is decided before the first hook; a reload still runs them. It is
decided again after the last hook, so a commit the hooks write back to the current state
records nothing (`changed: false`, `seq: null`).
Its writes go through `commit.tx` and join that commit. The display draws nothing, and no other write commits (spec section 6.1, O2b), from the first hook call of a commit to its
commit point. Keep it under 5 ms ('beforeCommit-slow'); never load assets (engine queries such
as [`labs.geometry.read`](Labs.geometry.read.md) are allowed, so it may be async; cache their results); mint ids from
state, never at random. A throw undoes only its own writes, object by object as a savepoint's
(spec section 6.1, O7), and records 'beforeCommit-threw';
the commit proceeds. A hook still pending after 10 s is treated as thrown. If the commit ends
before its commit point (an abort, a supersede, a timeout, [`destroy()`](Session.destroy.md)), `commit.tx` closes and
its `signal` aborts: a later write through it throws HoloTransactionClosedError and records
nothing, nor does the hook's later throw. Check `commit.tx.open` after each `await` before you
change your own state. A collection handle created while the hooks run (`commit.tx.collection`
or anywhere else) lands after the commit point and runs no hook again; a dispose (a lifecycle
change) while they run closes their `commit.tx` with 'other' and runs them again on the state
it left, so a hook may run more than once for one commit. Hooks run in
registration order; the same `name` again replaces the earlier hook. Lifecycle call: immediate.
Returns the unregister function; aborting `signal` unregisters the same way, and nothing
rejects. Registering replays nothing: it runs for later commits, not one made before (the
page's first [`setSceneModel`](Writer.setSceneModel.md)). Lay out the current state once after registering, through one
[`transaction`](Session.transaction.md) (look-only writes record nothing, so no hook runs for it), as the example does.
Cleanup: unregistering stops the hook; rules its writes created stay until you reset them
([`find(q).set({ pose: null })`](PartActions.set.md)) or dispose their owner. Keep a list of what the hook wrote.
Errors: an asset load through `commit.tx` throws HoloScopeError, and so does
[`session.capture()`](Session.capture.md), which would wait for this commit (spec section 6.1, O3).
Not for: reacting after a commit, which is [`on('committed')`](Session.on.md).
Experimental, so optional in this declaration: reach it through [`has(session, 'beforeCommit')`](has.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.beforeCommit.ts`):

```ts
import { has } from '@holostep-configurators/runtime-client';
import type { DeepReadonly, PoseInput, SceneModel, Session, Writer } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
// Install a layout hook; the returned uninstall also resets the rules the hook wrote.
declare function legLayout(
  model: DeepReadonly<SceneModel> | null,
): Array<{ node: string; pose: PoseInput }>;
export function installLegLayout(): () => void {
  const posed = new Set<string>();                         // every node the layout posed
  const layout = async (writer: Writer): Promise<void> => {
    for (const { node, pose } of legLayout(writer.getSceneModel())) {
      await writer.node(node).set({ pose });
      posed.add(node);
    }
  };
  const off = has(session, 'beforeCommit')
    // joins each commit, so it never shows one frame stale
    ? session.beforeCommit('leg-layout', ({ tx }) => layout(tx), { signal })
    // keep this branch: a runtime without the hook gets the same writes, one frame later
    : session.on('committed', ({ recorded }) => { if (recorded) void layout(session); }, { signal });
  void session.transaction((tx) => layout(tx));            // the current state, once: registering replays nothing
  return () => {
    off();                                                 // stops the layout; its rules stay
    for (const node of posed) void session.node(node).set({ pose: null });   // so reset them
    posed.clear();
  };
}
```

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `beforeCommit` names no product |
| 2. A precedent | pass: [Lexical registerNodeTransform](https://lexical.dev/docs/concepts/transforms) (row `G4`); [ProseMirror PluginSpec.appendTransaction](https://prosemirror.net/docs/ref/#state.PluginSpec.appendTransaction) (row `G4`) |
| 3. Two apps, or labs | labs (`@experimental` at a stable path, reached through `has()` or `supports()`): it graduates when a second app uses it; apps today: app-04c2774145, app-0c773a4ee3, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-510d750508, app-59c5b5751e, app-b362a81c85, app-b4ae2fce25, app-bcb7ef21da, app-ceb4eccc72, app-ddfb017681, app-f9a32142f1, app-fc07850558, app-fc306ec3dc, app-fd09090420 |
| 4. The cheaper fix first | decided per need in Appendix B: rows `G4` at tier labs (a member only where no rule, data, asset or app code serves the need) |
