# `Writer.select`

> 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 [`Writer`](Writer.md) · inherited by [`Tx`](Tx.md), [`Session`](Session.md), [`testing.FakeSession`](testing.FakeSession.md) · stable · since 2.0.0 · called as `session.select`

```ts
select(group: string, material: string, options?: RecordedOptions<C> & SignalOption): Promise<Report>;
```

Selects `material` (a MaterialNode `nodeId`) in the global-material GroupNode `group` (its
`nodeId`): recorded, priced and saved. Merging and in the write queue: never superseded, so it
commits as its own undo step even while a sync loads. The parts it changes crossfade over the
document's `materials.fadeMs` as its layers resolve it (420 ms when no layer sets it), after
the commit point.
Outcome: 'committed' at its commit point, after its loads; through a `tx`, 'staged' once the
assets it names have loaded (asset failures: [`problems`](ReportFields.problems.md)). Behind an open transaction
it settles only when that transaction closes (a stalled one at its `timeoutMs` (10 s by
default), one holding the display within 14 s of its hold, later while it still loads): show
the value you wrote.
'rolled-back' with reason 'internal-error' when applying it failed inside the runtime
(through a `tx`, it then rejects AbortError and the transaction resolves 'rolled-back'; spec
section 6.4).
Errors: rejects HoloInputError 'unknown-id' when `group` is not a global-material GroupNode
(a variant GroupNode's `nodeId` included) or `material` is not one of its MaterialNodes at the
call. One that was there at the call and is gone at its turn in the queue (a rollback or an
earlier commit removed it) refuses with reason 'conflict' and changes nothing (spec section
6.4).
A select whose group dresses no mesh still commits: it reports [`matched`](ReportFields.matched.md) 1 and no Problem,
and the price and the recorded choice change while the look does not. The cause is the scene
model's: a `globalMaterialList` `meshName` close to one of its model's mesh names (a typo) was
recorded once, when that model was placed, as 'target-unmatched' in [`info().problems`](InfoReport.problems.md) with
`didYouMean`; a group no placed model names records nothing, as 1.x. A page that must know
checks the meshes itself: `session.find({ node, name: meshName }).list().length`.
Not for: a look-only preview, which is a rule:
[`find(q).set({ material: { materialNodeId } })`](PartActions.set.md); a variant GroupNode's selected child, which is
[`applyPatch`](Writer.applyPatch.md) replacing `/model/node/{id}/defaultNodeId`, or [`setSceneModel`](Writer.setSceneModel.md).

## Options

Through [`Session`](Session.md):

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `actor` | `Actor` | the `expectGroup`'s actor when one is given, else 'app' | Who made the write. |
| `checkpoint` | `JsonValue` | unchanged | Page state stored atomically with this step; returned in [`state().checkpoint`](SessionState.checkpoint.md) and the feed. |
| `dryRun` | `boolean` | false | Runs and reports, then rolls back by design ('dry-run'). |
| `expectGroup` | `HistoryGroup` | undefined | A guard, checked at the call: this write belongs to the undo group you are running. |
| `expectRevision` | `number` | undefined (no check) | If-match on [`session.revision`](Session.revision.md). |
| `origin` | `string` | null | Your tag, echoed unchanged in [`Report.origin`](ReportFields.origin.md) and `cause.origin`. |
| `signal` | `AbortSignal` | undefined | Cancels this call; the member's JSDoc says what an abort does. |
| `supersede` | `boolean` | false (the write is queued behind the transaction and applies after it closes) | While a transaction is open: roll it back (it resolves 'superseded'); the writes queued behind it apply in call order, then this one, which supersedes an… |
| `undo` | `'rebase' \| (string & {})` | a recorded-state change gets one step labelled 'Change'; a look-only write gets none; the session's first model (cause 'load') gets none, whatever the label: it is the floor of history | An undo label makes this write one undo step. |

Through [`Tx`](Tx.md):

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

Refused here (typed `never`, so passing one fails to compile): `actor`, `checkpoint`, `dryRun`, `expectGroup`, `expectRevision`, `origin`, `supersede`, `undo`.

## Abort (`signal`)

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

> A write: before the commit point it rolls back and rejects AbortError; after it, no effect, except that a running camera move stops where the last frame drew it (`CameraMoveOptions`).
>
> Through a `tx` the commit point is the transaction's: aborting a write still loading rejects AbortError and stages nothing (catch it: unobserved, it rolls the transaction back, spec section 6.1, O6); aborting one already staged unstages it, its paths keeping the latest other write to them as after a failed savepoint (O7), and its Promise stays resolved 'staged'.

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

```ts
import type { Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
declare const fabricGroup: string;                         // the GroupNode's nodeId
declare const label: { text: string };
export async function pickFabric(materialNodeId: string, name: string): Promise<void> {
  const before = label.text;
  label.text = name;                                       // show the value you wrote, at once
  try {
    // May be queued behind a transaction: never read it back before this settles.
    const r = await session.select(fabricGroup, materialNodeId, {
      actor: 'shopper', origin: 'swatch',
    });
    if (r.outcome === 'refused') label.text = before;      // gone at its turn: 'conflict'
  } catch (error) {
    label.text = before;                                   // invalid input or abort: nothing ran
    throw error;
  }
}
```

## Related

From [`ViewerDocumentJson.materials.fadeMs`](ViewerDocumentJson.materials.md):

> The crossfade of a selection change (`select`, undo, redo, [`setSceneModel`](Writer.setSceneModel.md)), ms, as 1.x's
> `viewer.transition`: the outgoing and incoming base colours (`baseColor` times its map, or
> `baseColor` alone without one) blend in linear colour space, and so do the roughness maps,
> `metallic`, `roughness`, `normalScale` and the environment intensity, each map sampled at its
> own placement (`uv`, `scale` or `sizeM`, `offset`, `rotationDeg`); other fields switch at the
> swap. Each changed part fades from what it drew, and a part outside the change keeps its look,
> also when it already wears the incoming material. A part's
> first material and a part the same commit loads never fade. `select` still resolves at its
> commit point; [`idle()`](Session.idle.md) waits for the fade, and a capture draws its target (spec section 6.8);
> through a `tx` it starts when the commit is shown. 0 swaps in one frame. Counts as 0 under
> reduced motion (spec section 6.2). A change during the fade takes over as
> [`PartWriteOptions.fadeMs`](PartWriteOptions.fadeMs.md) says (spec section 6.3).

## Generic-API test (spec 3.1)

| Rule | Result |
| --- | --- |
| 1. Neutral name | pass: `select` names no product |
| 2. A precedent | pass: a correctness rule (rows `X1`) |
| 3. Two apps, or labs | pass: 28 apps (ledger: app-4fd23bc869, app-822bb9d6aa, app-8675b3225c, app-b362a81c85, app-bcb7ef21da, app-c1384707fc, app-e3b47169bc, app-e61a0b42ad and 7 more; fleet cross-check: app-04c2774145, app-0c773a4ee3, app-37356bf09f, app-378d190f1b, app-4762caacdc, app-4f16d2f6da, app-4fd23bc869, app-510d750508 and 20 more) |
| 4. The cheaper fix first | decided per need in Appendix B: rows `X1` at tier stable (a member only where no rule, data, asset or app code serves the need) |
