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

function syncStateToOwnerSyncStatus(
  state: SyncState | null,
  name: string & Brand<"UrlSafeString"> & Brand<"Name">,
  ownerId: string & Brand<"Id"> & Brand<"OwnerId">,
): OwnerSyncStatus;

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

Tells what an app shows about syncing an owner in a database: Error when a relay has one, holding the newest failure of any relay or, without one, the newest skipped change; otherwise the first of Syncing, Synced, and Offline that a relay has, or NoRelays. Accepts null, the store's value before the first snapshot. See OwnerSyncStatus for what to show.

The same status is the same object: statuses other than Error are shared constants, and an Error status is the same object for the same error object, which SyncStateDep.syncState keeps between snapshots while it is unchanged. So compare statuses with ===.

Example

import {
  assertEqual,
  assertSame,
  createId,
  createUnknownError,
  Millis,
  testCreateDeps,
  testName,
} from "@evolu/common";
import {
  syncStateToOwnerSyncStatus,
  testAppOwner,
  type PendingSyncRoute,
  type SyncConnection,
  type SyncRoute,
  type SyncState,
  type SyncTransportId,
} from "@evolu/common/local-first";

const deps = testCreateDeps();
const primaryId = createId<"SyncTransport">(deps);
const backupId = createId<"SyncTransport">(deps);

const stateOf = (
  connections: ReadonlyArray<SyncConnection>,
  routes: ReadonlyArray<SyncRoute>,
): SyncState => ({
  transports: connections.map((connection, index) => ({
    type: "WebSocket",
    id: index === 0 ? primaryId : backupId,
    label: "wss://relay.example",
    connection,
  })),
  tenants: [
    {
      type: "Active",
      name: testName,
      owners: [{ type: "Writable", ownerId: testAppOwner.id, routes }],
    },
  ],
});
const pending = (transportId: SyncTransportId): PendingSyncRoute => ({
  type: "Pending",
  transportId,
  failure: null,
  skippedError: null,
  completeAt: null,
  lastSentAt: null,
  lastReceivedAt: null,
});
const statusOf = (state: SyncState | null) =>
  syncStateToOwnerSyncStatus(state, testName, testAppOwner.id);

// Before the first snapshot, nothing syncs the owner.
assertEqual(statusOf(null), { type: "NoRelays" });

// A first connection is syncing; a lost one is offline.
const connecting: SyncConnection = { type: "Connecting" };
assertEqual(statusOf(stateOf([connecting], [pending(primaryId)])), {
  type: "Syncing",
});
const disconnected: SyncConnection = {
  type: "Disconnected",
  disconnectedAt: Millis.orThrow(2000),
  openedAt: Millis.orThrow(1000),
  error: null,
};
assertEqual(statusOf(stateOf([disconnected], [pending(primaryId)])), {
  type: "Offline",
});

// A quota failure on one relay shows before a newer skipped change on
// another, because the app can act on it.
const quotaError = {
  type: "ProtocolQuotaError",
  ownerId: testAppOwner.id,
  at: Millis.orThrow(3000),
} as const;
const open: SyncConnection = {
  type: "Open",
  openedAt: Millis.orThrow(3400),
  error: null,
};
assertEqual(
  statusOf(
    stateOf(
      [disconnected, open],
      [
        { ...pending(primaryId), failure: quotaError },
        {
          type: "Settled",
          transportId: backupId,
          skippedError: {
            type: "DecryptWithXChaCha20Poly1305Error",
            error: createUnknownError(new Error("invalid tag")),
            at: Millis.orThrow(4000),
          },
          completeAt: null,
          lastSentAt: Millis.orThrow(3500),
          lastReceivedAt: Millis.orThrow(4000),
        },
      ],
    ),
  ),
  { type: "Error", error: quotaError },
);

// The same error gives the same status object.
const quotaState = stateOf(
  [disconnected],
  [{ ...pending(primaryId), failure: quotaError }],
);
assertSame(statusOf(quotaState), statusOf(quotaState));