API reference › @evolu/common › local‑first/Shared › OwnerSyncStatus

type OwnerSyncStatus = NoRelaysSyncStatus | RelaySyncStatus;

Defined in: packages/common/src/local-first/Shared.ts:865

What an app shows users about syncing one owner of one database, from syncStateToOwnerSyncStatus or the React and Vue useOwnerSyncStatus.

Its variants:

  • NoRelays: nothing syncs the owner here. There is no snapshot yet, the owner's transports are still being set up, the app uses no relays for it, it is registered only as readonly, or the database refused startup, which evoluError reports.
  • Syncing: a relay connects for the first time or reconciles. A first connection to a host that drops packets stays Syncing until the platform gives up on it, which can take a minute.
  • Synced: a relay is up to date, and none syncs.
  • Offline: every relay is disconnected. Evolu keeps reconnecting, up to 30 seconds apart, so Offline can briefly outlast the outage.
  • Error: a relay failed or offers a change this database skipped. error is the newest failure, or without one, the newest skipped change: a failure stops syncing through its relay, while a skipped change leaves out only that change.

Evolu stores changes in the local database before they sync, so sync needs no UI while it works: show nothing for NoRelays, Syncing, and Synced. An indicator that changes with every edit distracts, and screen readers announce each change.

For Offline and Error, show one quiet line that lasts as long as the status, not a dialog, which interrupts, or a toast, which disappears while the problem lasts. Do not say that changes are saved on this device unless Evolu.devicePersistence is Persisted: a browser may keep them only for a private session or delete them later. Evolu reports Offline at once; an app may wait a few seconds before showing it, because brief disconnections, such as waking from sleep, reconnect quickly. For Error, write actionable text for the error types the app can act on, such as a ProtocolQuotaError: the relay stores no more data for the owner, so offer more quota, such as a plan upgrade, then call Evolu.requestSync with the owner's ID. For any other error, show generic text that names the error type, which helps when the user reports it.

Render the line inside one element with role="status" that stays mounted: screen readers announce changes only in a live region that already exists, and a polite announcement fits a status that loses nothing. Do not use role="alert".

Show the app owner's status once for the whole app, such as below the header, and another owner's status where the app shows that owner's data. Each Evolu instance finds its own status by Evolu.name, so an app with several databases shows the status of the one the user works in.

Example

import { assertEqual, Millis } from "@evolu/common";
import { testAppOwner, type OwnerSyncStatus } from "@evolu/common/local-first";

// What to tell the user, or null while sync works or is not used.
const syncStatusToMessage = (status: OwnerSyncStatus): string | null => {
  switch (status.type) {
    case "NoRelays":
    case "Syncing":
    case "Synced":
      return null;
    case "Offline":
      return "Offline. Changes will sync when you're back online.";
    case "Error":
      return status.error.type === "ProtocolQuotaError"
        ? "Sync is paused because the sync server is full."
        : `Sync error: ${status.error.type}.`;
  }
};

assertEqual(syncStatusToMessage({ type: "Synced" }), null);
assertEqual(
  syncStatusToMessage({ type: "Offline" }),
  "Offline. Changes will sync when you're back online.",
);
assertEqual(
  syncStatusToMessage({
    type: "Error",
    error: {
      type: "ProtocolQuotaError",
      ownerId: testAppOwner.id,
      at: Millis.orThrow(1000),
    },
  }),
  "Sync is paused because the sync server is full.",
);
assertEqual(
  syncStatusToMessage({
    type: "Error",
    error: { type: "SyncFailed", at: Millis.orThrow(1000) },
  }),
  "Sync error: SyncFailed.",
);