# Loading and connection state

In a `hibiki_rails` app the state lives on the server, so every user gesture is
a round trip: the click travels up the socket, the server runs the action,
and any re-rendered HTML travels back down. While that trip is in flight,
there's no indicator on the page to say so — and if the socket drops, the page
keeps looking live while quietly answering nothing. Both issues are
covered by the same part of the stack: the gem's JS client, the packaged
JavaScript that runs in the browser and drives each island (see
[The JS client](/hibiki/the-js-client/)). It records what
it knows on the island fragment — the region of the page bound to one
channel subscription — as HTML attributes. Turning those attributes into something the user can see, like a
spinner or a dimmed panel or an offline notice, is just a few lines of CSS the
app manages.

Mid-trip, an island root looks like this:

```html
<!-- rendered by tag.div(**hibiki_island(TodosChannel, cid: @cid))
     second line: set by the JS client at runtime -->
<div data-controller="hibiki" data-hibiki-channel-value="TodosChannel"
     data-hibiki-state="ready" data-hibiki-busy aria-busy="true">
  ...
</div>
```

And one CSS rule is enough to make the server round trip visible to the user:

```css
[data-hibiki-busy] { opacity: 0.6 }
```

The rest of this page walks through each attribute, the states it can
hold, and the timings behind them.

## What the JS client sets

| Where          | Attribute           | What it says                                          |
| -------------- | ------------------- | ----------------------------------------------------- |
| island root    | `data-hibiki-busy`  | present while an action is in flight                  |
| island root    | `aria-busy="true"`  | the same fact, for assistive tech                     |
| island root    | `data-hibiki-state` | one of `connecting`, `ready`, `offline`, `stalled`    |
| firing control | `data-hibiki-busy`  | on the control that fired the action                  |

Every other `data-hibiki-*` attribute (like the `channel-value` in the
code example above) is generated by a Ruby helper and is a private
contract you should never write by hand. The four in this table run in
the other direction: no helper emits them — the JS client writes them at
runtime. To your app they are **read-only**, and CSS is the only place
to read them.

## Making the loading state visible

The pattern is always the same two pieces: put the element you want the
user to see in the island's own markup, hidden by default, and write a
descendant selector that reveals it while an ancestor carries the flag.
The JS client renders no UI of its own — no spinner element, no toast — the indicator can be any
element you like, styled however you like, revealed by the same flags.

```erb
<!-- in the view -->
<%= tag.div(**hibiki_island(TodosChannel, cid: @cid)) do %>
  <span class="spinner" aria-hidden="true"></span>
  <span class="offline-note">Connection lost — reconnecting…</span>
  ...
<% end %>
```

```css
/* inside your CSS file */
.spinner, .offline-note                     { display: none }
[data-hibiki-busy] .spinner                 { display: inline-block }
[data-hibiki-state="offline"] .offline-note { display: inline }
```

With these minimal code, the spinner now shows for any server trip the island is running, and the note shows whenever the socket is down. You can style the note as a fixed-position banner and it becomes a toast.

Two common tips to get the selectors right:

- The island root is the only element that ever carries
  `data-hibiki-state`. So `[data-hibiki-state][data-hibiki-busy]` means
  "this island is busy", while `[data-hibiki-busy]:not([data-hibiki-state])`
  means "this control is busy".
- The element that fires a `submit` is the **form**, not the button inside
  it, so the form is what turns busy. A spinner usually sits inside the
  button (not directly under the form), so you should reach for it with a
  descendant selector instead of a child selector:

  ```css
  [data-hibiki-busy] .spinner   { }  /* matches */
  [data-hibiki-busy] > .spinner { }  /* silently matches nothing */
  ```

## The four connection states

`data-hibiki-state` tracks the subscription that keeps the island live:

| State        | When                                            | What the user is looking at                            |
| ------------ | ----------------------------------------------- | ------------------------------------------------------ |
| `connecting` | The page has loaded; the server has not yet confirmed the subscription | Real content, but **inert** — clicks are queued, not answered |
| `ready`      | The subscription is confirmed                   | Normal operation                                       |
| `offline`    | The socket dropped; ActionCable is retrying     | Content that is **frozen** — and nothing else on the page would say so |
| `stalled`    | An action outlived `busyCeiling` with no answer | A trip we lost — said plainly rather than cleared silently |

`connecting` is set **synchronously**, as the controller connects and
before the subscription is even opened, so CSS can dim the island for the
whole window rather than from the middle of it.

One state you might expect is missing from the table: a "loading" state
for when the page first loads and there is nothing to show yet. In a
hibiki app that moment never exists. The page arrives from your Rails
controller as an ordinary server-rendered page, already full of real
content, and every update after that replaces valid content with newer
valid content. So during a round trip, what is on screen is **stale, not
absent** — either dim it or add a badge near it, but don't swap the content for a skeleton.

## Clicks before the connection is ready

A freshly loaded island is not ready to answer clicks. Opening its
subscription takes a few trips between browser and server — tens of
milliseconds on localhost, about a second on a real network — and until
the server confirms it, ActionCable's `Subscription#perform` silently
does nothing. A click in that window would simply be lost. So the JS
client **queues** it, and sends the whole queue the moment the
confirmation arrives.

The queue exists for this first window only. When the socket drops later,
clicks are dropped, not queued. The reason lives on the server: a
reconnect is not a resume — the channel builds a *fresh* signal graph,
with default state, and a click aimed at the old page could mean
something else entirely to the new graph. Dropping it is safer than
replaying it. The user is not left guessing, either: the island reads
`offline` for the incident, and the CSS we added can make this state visible.

## How the busy flag clears

To clear the busy flag, the JS client has to know when a trip is over.
Every action it performs carries a sequence number under the reserved
payload key **`hbk`**, added last so a form field named `hbk` can never
overwrite it. (It is the second reserved key — ActionCable's own
`Subscription#perform` already writes `action`.) When the action has run
on the server, the server sends that sequence number back as an
acknowledgement — an *ack*, from here on — and the ack is what clears
the flag.

You might expect the returning HTML to be the signal instead, but a
successful action can legitimately send **zero bytes** back. Hibiki
re-renders through effects, and an effect re-runs only when a value it
read actually changed — so a gesture that writes nothing new (a save with
no edits, a filter set to the value it already holds) correctly renders
nothing at all. Waiting for HTML would leave the spinner up forever; the
ack arrives either way.

For apps that customize the JS side (for example, to tune the busy
timings introduced in the next section): if you subclass
`ChannelController` and override its `received` method, the busy flag
still clears. The base class handles the ack itself, before `received`
is ever called, so even an override that forgets `super` cannot break it.

## Tuning the timings

Three class properties on the JS client's controller are provided:

| Property      | Default  | What it is                                                   |
| ------------- | -------- | ------------------------------------------------------------ |
| `busyDelay`   | 150 ms   | How long a trip must last before the busy flag shows at all. A trip that finishes sooner shows nothing, so a fast round trip doesn't flash a spinner for a split second. |
| `busyGrace`   | 60 ms    | How long to keep the flag up after the ack arrives. Re-rendered HTML can trail the ack by a moment, and the grace lets it land while the island still reads busy, instead of the spinner vanishing just before the content changes. |
| `busyCeiling` | 10000 ms | How long a trip can run before the JS client gives it up and marks the island `stalled` — an honest "we lost it" instead of a spinner that never ends. |

To change one, subclass the controller and re-register the subclass
through `hibiki_controller.js`:

```js
import { HibikiController } from "hibiki-rails"

class SlowLink extends HibikiController {
  static busyDelay = 400
}

export default SlowLink
```

There is deliberately no per-island option. The timings compensate for
the network and the server, and every island on the page shares those,
so the knob lives in one place: the controller class, set once for the
whole app.

## If you're used to client-side frameworks

Two habits from client-side frameworks are worth leaving at the door
here.

The first is optimistic UI. Svelte can update the page the
moment the user clicks, because the state lives in the browser and the
framework can compute the result itself. In hibiki the state lives on
the server, so the page cannot change until the server has spoken. The
busy flag is the honest substitute: it says "working on it" instead of
guessing the result — and since `busyDelay` hides the fast trips, most
of the time the answer arrives before there was anything to show.

The second is request/response thinking — expecting each action to come
back with its result, the way a `fetch` resolves. Here the ack promises
only that your action ran. What reaches the screen is owned by effects,
which re-render when the values they read change: sometimes right away,
sometimes much later, sometimes never (when nothing changed). So lean on
the flags for waiting and on the fragments for content, and don't look
for a return value.

Once those two habits are set aside, the loading story is small on
purpose: the JS client raises a few flags, your CSS decides what they
look like, and the server stays the one place where state changes.

For a worked example of all of this — five sites, three CSS variants, in a
file you own — see the loading section of
[CRUD notes](/hibiki/crud-notes/#loading-and-connection-state).
