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 · inherited by testing.FakeSession · 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 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()), 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). Lay out the current state once after registering, through one transaction (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 })) 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(), which would wait for this commit (spec section 6.1, O3). Not for: reacting after a commit, which is on('committed'). Experimental, so optional in this declaration: reach it through has(session, 'beforeCommit').

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).

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 (row G4); ProseMirror 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)