API reference › @evolu/common › local‑first/Evolu › DeviceEvolu

Defined in: packages/common/src/local-first/Evolu.ts:2053

The instance of an app's device database, for the app's data that belongs to this device rather than to an AppOwner, such as settings. Its own owner is the device owner, which never syncs, so it has no Evolu.appOwner: local-only tables keep their rows on this device, and a mutation of a synced table must pass, as its owner, an owner the app syncs with Evolu.useOwner, such as one for settings shared by every AppOwner. An AppOwner's data lives only in its own database, which EvoluApp.remove deletes, so neither takes an AppOwner. The app's EvoluConfig.onOwnerDeleted also runs for an owner deleted in it, in any tab.

It has no Evolu.exportDatabase or Evolu.deleteDatabase, because the device database also holds the secret of every AppOwner on this device, which its type hides. An export would write them all in plaintext, even with encrypted local data, and a deletion would lose them all, leaving their databases with nothing to open them. EvoluApp.remove deletes one AppOwner's data. The app disposes it, so it can't be disposed alone. Its SQL isn't traced, because tracing logs values, and those include the secrets.

Extends

  • Omit<Evolu<D>, | "appOwner" | "exportDatabase" | "deleteDatabase" | "useOwner" | "insert" | "update" | "upsert" | typeof Symbol.asyncDispose>

Properties

deleteOwner

readonly deleteOwner: (owner:
  | ShardOwner
  | SharedOwner
| SharedReadonlyOwner) => Promise<Result<void, DeleteOwnerError>>;

Defined in: packages/common/src/local-first/Evolu.ts:1061

Deletes the rows of a ShardOwner, SharedOwner, or SharedReadonlyOwner from the local database of this Evolu instance on this device, stops syncing the owner, and resolves once the rows are gone.

The deletion runs after the mutations and requests made before it. It deletes the owner's rows of every table, local-only tables included, and its history in one transaction. Then it ends every Evolu.useOwner registration of the owner in every instance of the database, identified by Evolu.name, in every tab, so relays don't bring the rows back, and calling the UnuseOwner of such a registration does nothing. Then each instance calls EvoluConfig.onOwnerDeleted, this one before code awaiting this call continues, and loads its subscribed queries again, which return the owner's rows until they load.

It deletes only this device's copy. Changes not yet synced are lost. Relays keep the owner's data, and so do other local databases that use the owner. A later useOwner syncs the data back, and a mutation with the owner stores its rows again, so stop using the owner first, such as by removing it from where the app lists it. Encrypted copies in device backups and in freed storage remain.

It takes no AppOwner, whose ID names the database; Evolu.deleteDatabase deletes the whole database instead. An owner created from the AppOwner's secret has the same ID, so this method throws for it.

Each call deletes on its own, in call order. It fails with an UnknownError when SQLite fails the deletion, which then deletes nothing, and the owner keeps syncing. When the database is refused, it fails with the refusal's error, as EvoluError describes. It fails with EvoluDisposedError when this instance is disposed before the deletion ends, which may or may not then run.

Example

import {
  assertType,
  type DeleteOwnerError,
  type Evolu,
  type Result,
  type ShardOwner,
} from "@evolu/common";

const deleteWallet = (evolu: Evolu, walletOwner: ShardOwner) =>
  evolu.deleteOwner(walletOwner);

assertType<
  Awaited<ReturnType<typeof deleteWallet>>,
  Result<void, DeleteOwnerError>
>();

const _deleteAppOwner = (evolu: Evolu) =>
  // @ts-expect-error deleteOwner takes no AppOwner, which names the database.
  evolu.deleteOwner(evolu.appOwner);

Inherited from

Omit.deleteOwner


devicePersistence

readonly devicePersistence: Promise<DevicePersistence>;

Defined in: packages/common/src/local-first/Evolu.ts:562

Whether this device keeps the database, which createEvolu decides before it creates the instance.

Apps tell the user when it is NotPersisted, because then data that has not synced is lost when the instance ends. On the web this includes its tab entering the back-forward cache. Apps must not tell the user that data is saved on the device when it is Unknown.

Example

import { assertEqual } from "@evolu/common";
import type { DevicePersistence } from "@evolu/common/local-first";

// The notice for `await evolu.devicePersistence`, or null for none.
const persistenceNotice = (
  devicePersistence: DevicePersistence,
): string | null =>
  devicePersistence === "NotPersisted"
    ? "Your data isn't kept on this device. Changes that haven't synced are lost when you close this tab."
    : null;

assertEqual(persistenceNotice("Unknown"), null);
assertEqual(
  persistenceNotice("NotPersisted"),
  "Your data isn't kept on this device. Changes that haven't synced are lost when you close this tab.",
);

Inherited from

Omit.devicePersistence


getMutationSize

readonly getMutationSize: <TableName>(table: TableName, values: Partial<MutationValues<S[TableName], "update">>) => number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative"> & Brand<"Positive">;

Defined in: packages/common/src/local-first/Evolu.ts:733

Returns the size of a mutation of table with values, measured as maxMutationSize describes.

Use it to check values that column Types do not bound before mutating, or to show how much of the limit a mutation uses. It accepts any of the table's columns, so the values of an insert, update, or upsert fit, and it ignores id and isDeleted, which are not columns. Mutations of local-only tables are measured too, although they are exempt from the limit.

Example

import {
  assertType,
  type Evolu,
  maxMutationSize,
  type NonEmptyTrimmedString100,
  type TestEvoluSchema,
} from "@evolu/common";

const fitsTodo = (
  evolu: Evolu<TestEvoluSchema>,
  title: NonEmptyTrimmedString100,
) => evolu.getMutationSize("todo", { title }) <= maxMutationSize;

assertType<ReturnType<typeof fitsTodo>, boolean>();

Inherited from

Omit.getMutationSize


getQueryRows

readonly getQueryRows: <R>(query: Query<D, R>) => QueryRows<R>;

Defined in: packages/common/src/local-first/Evolu.ts:917

Get QueryRows.

Example

import {
  assertType,
  createQueryBuilder,
  testEvoluSchema,
  type TestEvoluSchema,
  type Evolu,
  type QueryRows,
} from "@evolu/common";

const createQuery = createQueryBuilder(testEvoluSchema);
const allTodos = createQuery((db) => db.selectFrom("todo").select("title"));
const getTodos = (evolu: Evolu<TestEvoluSchema>) =>
  evolu.getQueryRows(allTodos);

assertType<ReturnType<typeof getTodos>, QueryRows<typeof allTodos.Row>>();

Inherited from

Omit.getQueryRows


insert

readonly insert: DeviceMutation<D, "insert">;

Defined in: packages/common/src/local-first/Evolu.ts:2073

Evolu.insert, as a DeviceMutation.


loadQueries

readonly loadQueries: <Q>(queries: [...Q[]]) => [...QueriesToQueryRowsPromises<Q>[]];

Defined in: packages/common/src/local-first/Evolu.ts:847

Load an array of Query values and return an array of QueryRows promises. It's like queries.map(loadQuery) but with proper types for returned promises.

Example

import {
  assertType,
  createQueryBuilder,
  testEvoluSchema,
  type TestEvoluSchema,
  testTodoId,
  type Evolu,
  type QueryRows,
} from "@evolu/common";

const createQuery = createQueryBuilder(testEvoluSchema);
const allTodos = createQuery((db) =>
  db.selectFrom("todo").select(["id", "title"]),
);
const firstTodo = createQuery((db) =>
  db.selectFrom("todo").select("title").where("id", "=", testTodoId),
);
const loadTodoQueries = (evolu: Evolu<TestEvoluSchema>) =>
  evolu.loadQueries([allTodos, firstTodo]);

assertType<
  ReturnType<typeof loadTodoQueries>,
  [
    Promise<QueryRows<typeof allTodos.Row>>,
    Promise<QueryRows<typeof firstTodo.Row>>,
  ]
>();

Inherited from

Omit.loadQueries


loadQuery

readonly loadQuery: <R>(query: Query<D, R>, options?: LoadQueryOptions) => Promise<QueryRows<R>>;

Defined in: packages/common/src/local-first/Evolu.ts:805

Load Query and return a promise with QueryRows.

The returned promise rejects with an UnknownError when SQLite fails the query, for example on a syntax error in a raw SQL fragment or on data its SQL functions cannot read, such as json_extract on text that is not JSON. Only that query fails: other queries load, mutations are stored, and EvoluErrorDep.evoluError is not set. React's useQuery throws the error to the nearest error boundary. If the database is refused, because it refused startup or its worker failed, an uncached load rejects with an UnknownError, the refusal itself or one wrapping it, and cached loads stay pending until disposal resolves them with empty rows, so a suspended component stays suspended. Cached loads that a deletion of the database dropped stay pending, because the app replaces the instance; see Evolu.deleteDatabase. An uncached load never stays pending past disposal, also the disposal a deletion of the database causes: it resolves with empty rows, which then say nothing about the database, and the instance's methods throw. Observe EvoluErrorDep.evoluError to display the refusal independently of query loading.

Cached loading is batched. Returned promises are cached while pending and can be reused after they settle, which prevents redundant database queries and supports React Suspense (stable references while pending). A mutation or incoming sync invalidates the cache of an unsubscribed query, so its next load reads again. A subscribed query keeps its cached result and the subscription refreshes it, so a load can return rows that a pending refresh is about to replace. When SQLite fails a refresh, the query keeps its rows, its loads reject until a refresh succeeds, and its subscribers are notified, so React's useQuery throws the error then. To skip the cache, pass cache: false; see LoadQueryOptions.cache.

To subscribe a query for automatic updates, use Evolu.subscribeQuery.

Example

import {
  assertType,
  createQueryBuilder,
  testEvoluSchema,
  type TestEvoluSchema,
  type Evolu,
  type QueryRows,
} from "@evolu/common";

const createQuery = createQueryBuilder(testEvoluSchema);
const allTodos = createQuery((db) => db.selectFrom("todo").selectAll());
const loadTodos = async (evolu: Evolu<TestEvoluSchema>) => {
  const rows = await evolu.loadQuery(allTodos);
  return rows;
};
const loadUncachedTodos = (evolu: Evolu<TestEvoluSchema>) =>
  evolu.loadQuery(allTodos, { cache: false });

assertType<
  ReturnType<typeof loadTodos>,
  Promise<QueryRows<typeof allTodos.Row>>
>();
assertType<
  ReturnType<typeof loadUncachedTodos>,
  Promise<QueryRows<typeof allTodos.Row>>
>();

Inherited from

Omit.loadQuery


name

readonly name: string & Brand<"UrlSafeString"> & Brand<"Name">;

Defined in: packages/common/src/local-first/Evolu.ts:523

Evolu instance name is derived from EvoluConfig.appName, AppOwner's hash, and, for a memory database, a random instance ID; see AppName.

Inherited from

Omit.name


requestSync

readonly requestSync: (ownerId: string & Brand<"Id"> & Brand<"OwnerId">) => void;

Defined in: packages/common/src/local-first/Evolu.ts:1179

Requests a new synchronization round for an active OwnerId.

Reconciles locally stored changes, including writes previously rejected with ProtocolQuotaError, through the owner's active transports. Call this after the relay provider confirms additional quota is available. It also checks again the changes this database skipped from the owner's relays, and sends such a relay the changes received from other relays, which otherwise wait for its next sync, such as after a reconnect. On a relay that sends no heartbeats, a request is also how the shared worker notices a connection that died without closing: an unanswered request replaces it after the request timeout, ninety seconds unless it has grown, as Liveness in the Shared module describes. Existing connections and Evolu.useOwner registrations are retained, including registrations shared by multiple instances or tabs.

The owner must have an active writable registration in this database. Unregistered or read-only owners are ignored. Requests are skipped while all of the owner's transports are closed; they synchronize when they reopen. Disposal drops locally buffered requests. Calling a disposed instance throws, like other Evolu operations.

Returns immediately, without waiting for synchronization to complete. The owner's routes in sync state show a failure, such as a quota rejection, as failure, and a skipped change as skippedError.

Example

import { assertType, type Evolu, type OwnerId } from "@evolu/common";

// Call after successfully increasing the affected owner's relay quota.
const onQuotaIncreased = (evolu: Evolu, ownerId: OwnerId) => {
  evolu.requestSync(ownerId);
};

assertType<typeof onQuotaIncreased, (evolu: Evolu, ownerId: OwnerId) => void>();

Inherited from

Omit.requestSync


subscribeQuery

readonly subscribeQuery: (query: Query<D>) => (listener: Listener) => Unsubscribe;

Defined in: packages/common/src/local-first/Evolu.ts:885

Subscribe to Query QueryRows changes.

Cached rows invalidated before subscription are refreshed. Subscribing alone does not load a query that has no cached rows.

Example

import {
  assertType,
  createQueryBuilder,
  testEvoluSchema,
  type TestEvoluSchema,
  type Evolu,
  type QueryRows,
  type Unsubscribe,
} from "@evolu/common";

const createQuery = createQueryBuilder(testEvoluSchema);
const allTodos = createQuery((db) => db.selectFrom("todo").select("title"));
const subscribeToTodos = (
  evolu: Evolu<TestEvoluSchema>,
  onRows: (rows: QueryRows<typeof allTodos.Row>) => void,
) =>
  evolu.subscribeQuery(allTodos)(() => {
    onRows(evolu.getQueryRows(allTodos));
  });

assertType<ReturnType<typeof subscribeToTodos>, Unsubscribe>();

Inherited from

Omit.subscribeQuery


update

readonly update: DeviceMutation<D, "update">;

Defined in: packages/common/src/local-first/Evolu.ts:2075

Evolu.update, as a DeviceMutation.


upsert

readonly upsert: DeviceMutation<D, "upsert">;

Defined in: packages/common/src/local-first/Evolu.ts:2077

Evolu.upsert, as a DeviceMutation.


useOwner

readonly useOwner: (owner:
  | ShardOwner
  | SharedOwner
  | SharedReadonlyOwner, transports: readonly [OwnerWebSocketTransport, OwnerWebSocketTransport]) => UnuseOwner;

Defined in: packages/common/src/local-first/Evolu.ts:2068

Evolu.useOwner, for an owner that isn't an AppOwner. The device instance has no default transports, so it takes them.