# Pending

> When a control's action waits on the server, the platform shows a busy spinner and disables the control — but only after a short delay, so a fast action never flashes one. It is automatic: you write nothing. To say something specific while one particular verb runs, read `save.Pending`; for a page-wide affordance (a top progress bar) read the `Pending` ambient. Tune the timing on `app.Ui`.

<!-- id: ui-pending · area: ui · stability: stable · html: https://osysharp.com/reference/ui/pending/ -->

## Summary        {#summary}
When a control's action waits on the server — an `onClick`/`onEnter` that calls a server function and hasn't come back
yet — the platform gives the user feedback automatically: after a short delay the control shows a **busy spinner** and
**disables itself** until the action finishes. You write nothing; every button that hits the server gets it.

The delay is deliberate. Most actions are fast, and a spinner that flashes for 40ms is worse than none — it reads as a
glitch. So the spinner appears only once an action has run longer than the threshold (200ms by default), and once shown
it stays visible for a short minimum so it can't blink off the instant the answer arrives. A fast action shows nothing.

Disabling the control while it is busy also blocks an accidental double-submit — a second click on a half-second "Save"
does nothing rather than firing the action twice.

When you want to say something **specific** while one particular verb runs — a button that reads "Reading…" rather than
just spinning — read that verb's own flag: `Read.Pending` is true while an invocation of `Read` is in flight. It follows
the one action it names, so a page that saves while a read is running does not relabel the read button.

For a **page-wide** busy affordance — a top progress bar, a dimmed overlay — read the `Pending` ambient: `Pending.Any`
is true while any action is in flight, `Pending.Count` is how many. That is the one thing you author; the per-control
spinner needs no code at all.

Three grains, and picking the wrong one is the usual mistake:

| You want | Read | Grain |
|---|---|---|
| A spinner on the control that was pressed | *nothing* — it is automatic | that control |
| A label or field that speaks for ONE verb | `save.Pending` | that action |
| A top bar / overlay for the whole page | `Pending.Any` · `Pending.Count` | the page |

## Signature      {#signature}
```osy syntax
// Automatic — no code. A control whose action suspends on the server shows a delayed spinner + disables itself.

save.Pending     // true while an invocation of THIS action is in flight (a bool) — per-verb
Pending.Any      // true while at least one action is in flight (a bool) — for your own global busy surface
Pending.Count    // how many actions are in flight right now (an int)

// Tune the timing (and, later, supply your own spinner) on app.Ui:
app.Ui = new AppUi {
  PendingDelayMs   = 200,   // show the spinner only after an action runs this long (0 → the 200ms default)
  PendingMinShowMs = 300,   // once shown, keep it at least this long so it can't blink off (0 → the 300ms default)
};
```

## Description    {#description}
The busy affordance is **mechanism, not chrome**. The platform draws a plain spinner (`.osy-spinner`) beside the
control's label and applies the ordinary disabled styling; an app that wants a different look styles it in its own CSS,
the same way it styles any other control state.

It is entirely **client-side**. The first server-rendered paint is never busy (there is nothing pending yet), so a page
reading `Pending.Any` renders its idle state on the server and lights up only once the user starts something.

Which events count: the spinner is for **activation** events — `onClick`, `onEnter`, `onSubmit`, and an `Upload`'s
`onUploaded`. Typing and focus events (`onInput`, `onChange`, `onBlur`) do not spin the field — a keystroke that runs a
check should not make the input look busy. The `Pending` ambient counts every in-flight action regardless.

`Pending.Any`/`Pending.Count` update the **instant** an action begins and settles — the ambient is the raw in-flight
count, not the delayed spinner. So a top bar bound to `Pending.Any` appears immediately; if you want it to respect the
same "don't flash" delay, gate it on your own timer.

`<verb>.Pending` is available on every `action` and `method` a component declares, and reads as an ordinary `bool`
anywhere an expression is legal — a ternary, a prop value, an `if`. Like the ambient it flips the **instant** the action
begins, with no delay: it is what you reach for when you would rather say "Reading…" than show a generic spinner, and
you want that word to appear at once.

It counts **every** invocation of that action, not only the one a control activated — an action called from another
action is still running, and a label claiming otherwise would be wrong in the one case you most want to explain. It also
clears when an action **fails**: the work is over, it just went badly, and a button stuck reading "Reading…" forever is
worse than one that goes back to normal while your failure surface says what happened.

Actions are serialised per page, so an action can sit queued behind another. `.Pending` is true for that wait too — from
the user's side the work has already started, and the label is what tells them so.

A misspelling is a compile error naming what exists (`save.Pendign` → *"a verb has no member 'Pendign' (Pending)"*),
rather than a value that silently reads as nothing.

`PendingDelayMs` / `PendingMinShowMs` are app-wide timing, read from `app.Ui`. Leave them out (or set 0) and the
platform defaults apply. A negative value is a compile error.

## Examples       {#examples}
The common case is **nothing** — the spinner is automatic:
```osy title="no pending code at all — the spinner is automatic" syntax
component InvitePage() {
  action Accept() { var t = AcceptInvite(token, email, password); Session.SignIn(t); }
  render {
    // No pending code. `Accept` calls a server function, so this button spins + disables while it runs.
    Pressable(onClick: Accept) { Text("Accept invitation"); }
  }
}
```

`AcceptInvite` here is a server function reached by a signed-out visitor, so a working version of this also needs it
marked `[AuthMethod]` and wired into `app.AuthBootstrap` — see [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/). The point above is only that
the spinner costs no code either way.

A button that says what it is doing, using the action's own flag:
```osy title="a label that says what this one action is doing" syntax
component ExpenseLineRow(ExpenseLine line) {
  action Read() { ReadReceipt(line); }
  render {
    Pressable(onClick: Read) {
      // Follows `Read` alone — another action finishing elsewhere on the page leaves this label untouched.
      Text(Read.Pending ? "Reading…" : "Read receipt");
    }
  }
}
```

The same flag on a control that takes a `busy` prop, and to keep a form from being resubmitted:
```osy title="the busy prop, and blocking a second submit" syntax
component LoginCard() {
  action SignIn() { Session.SignIn(Authenticate(email, password)); }
  render {
    Button("Sign in", onPress: SignIn, busy: SignIn.Pending, disabled: email == "");
  }
}
```

A page-wide top progress bar, using the `Pending` ambient:
```osy title="a page-wide progress bar" test app=ui-pending
theme App { Colors { Accent = "#0077B6"; } }   // `Accent` is your own token, not a platform one

[Layout]
component Shell() {
  render {
    if (Pending.Any) { Box(h: "2px", bg: Colors.Accent, w: "100%"); }   // a thin bar while anything is in flight
    Outlet();
  }
}
```

Tuning the timing for a whole app:
```osy title="app-wide spinner timing" test app=ui-pending
app.Ui = new AppUi {
  PendingDelayMs   = 120,   // this app's actions are usually instant — show the spinner sooner when they aren't
  PendingMinShowMs = 400,   // …but once it shows, hold it a beat longer
};
```

## See also       {#see-also}
- [UI surfaces (app.Ui)](https://osysharp.com/reference/config/ui/) — the `app.Ui` block that tunes the timing (and nominates the app's system surfaces).
- [Connection](https://osysharp.com/reference/ui/connection/) — the sibling surface for the server-*dropped* case (a full connection-loss overlay).
- [component](https://osysharp.com/reference/ui/component/) — the components and controls the spinner attaches to.
