Skip to content

Networks drop. Laptops sleep. Phones switch towers. Statewire is built for it.

Every command goes into an outbox before it leaves the client. If the connection drops, the outbox keeps it and the client redials on its own. On reconnect it sends what's still pending, under the same numbers as before. The host remembers what it already admitted, so while it keeps the client's record, a resend never runs the handler twice. While the host still holds the answer, it sends it again; an older one comes back as expired.

A reconnect in four steps. One: the client sends a command, then the connection drops before the answer arrives. Two: the command waits in the client's outbox. Three: the client reconnects, gets a fresh snapshot and resends the command under the same sequence number. Four: the host recognizes the number and does not run the handler again; it replays the verdict while it still holds it.Clientredials on its ownOutboxpending commandsHostclient record1seq 72keep4kept verdict3resend seq 7

Survive a reload

Give the client a place to keep its session, and pending sends outlive the tab. After a reload the client picks up its identity and its outbox, and carries on.

import { StatewireHttp, useStatewire } from "statewire";
import { KeyValueSessionStorage } from "statewire/storage";

const session = KeyValueSessionStorage({
  namespace: "my-app:",
  principal: userId,
  key: "main",
  backend: localStorage,
});

const counter = useStatewire({
  transport: StatewireHttp({ url: "/counters/main" }),
  storage: { session },
});

principal is the signed-in account, so two accounts on one browser keep separate sessions. IndexedDB storage is built in too.

What the screen shows

  • The last state it received, with your pending predictions on top.
  • connection.status: connected, retrying with the next attempt time, or stopped with a reason.
  • Fresh state the moment it reconnects. Changes made by others while you were away arrive in that snapshot.

When a send fails

A command that the host rejects, or that can't be confirmed, leaves the outbox and rejects its promise. On a stored session the failure is kept until your code has seen it, even across a reload. Put it back with one call, held until the user says retry:

const counter = useStatewire({
  transport: StatewireHttp({ url: "/counters/main" }),
  storage: { session },
  onCommandChange: (update) => {
    if (update.status !== "settled" || update.failure === undefined) return;
    counter.outbox.insert({
      from: update.id,
      protocol: update.protocol!,
      command: { method: update.method, params: update.params },
      held: true,
    });
  },
});

If the host has lost the client's record, say after a long time away, pending commands settle as lost and the fresh snapshot shows what landed. A retry is a new command, so the host may run it again. For payments and other side effects, send an operation id and have the host check it first.

Hand this to your agent