API reference › @evolu/common › local‑first/Evolu › AppOwnerEvolu
Defined in: packages/common/src/local-first/Evolu.ts:2135
An Evolu instance without disposal, for code that uses an instance it doesn't own, such as one createEvoluApp opened, which the app disposes.
Extends
Properties
appOwner
readonly appOwner: AppOwner;
Defined in: packages/common/src/local-first/Evolu.ts:530
The instance's AppOwner: the one createEvoluApp opened, or
the one passed to createEvolu, available synchronously. Its
mnemonic is what the user backs up, such as in a recovery UI.
Inherited from
Omit.appOwner
deleteDatabase
readonly deleteDatabase: () => Promise<Result<void, DeleteDatabaseError>>;
Defined in: packages/common/src/local-first/Evolu.ts:1000
Deletes the local database of this Evolu instance on this device, and resolves once its files are gone.
The deletion runs after the mutations and requests made before it. Then every instance of the database, identified by Evolu.name, disposes itself in every tab, and the requests they made after the deletion are dropped, so their cached loads stay pending and a suspended component stays suspended until the app replaces the instance, and their uncached loads resolve with empty rows. Before it disposes itself, each instance, this one included, calls EvoluConfig.onDatabaseDeleted, or reloads the app without it. An AppOwner's instance of createEvoluApp does neither: a tab that has the AppOwner open opens its database again, empty, with a new instance; see EvoluApp.current.
It deletes only this device's copy. Changes not yet synced are lost. An instance created later with the same EvoluConfig.appOwner opens a new, empty database, which its transports fill again from relays, so forget the owner first if the data must not come back. Encrypted copies in device backups and in freed storage remain. A deletion is as durable as a commit: a power loss right after it can undo it.
A database kept in memory, as with EvoluConfig.memoryOnly, has a name of its own and no files, so deleting it leaves a stored database of the same owner alone.
It deletes the files by name, so it also deletes a database that doesn't open, such as a corrupt one or one of a newer version. Concurrent calls, of any instance of the database in any tab, share one deletion. It deletes no file another context holds, because that context could still write to it, and fails with DatabaseHeldError when such a file stays held, or when the deletion doesn't finish in time, such as while the tab that starts it shows a modal dialog. Evolu's instances release the files before the deletion, so only a context outside Evolu's coordination can hold them, such as a React Native runtime that a reload replaced. Its next DbWorker then waits for the files, as every startup does, and the database is refused with that error when they stay held. It fails with an UnknownError when the platform fails to delete them. After a failure, the database may be partly or wholly deleted, its instances keep working, and the next call deletes again. It fails with EvoluDisposedError when this instance is disposed before the deletion ends, which may or may not then run, so await it before disposing.
Example
import {
assertType,
type DeleteDatabaseError,
type Evolu,
type Result,
} from "@evolu/common";
const deleteLocalData = (evolu: Evolu) => evolu.deleteDatabase();
assertType<
Awaited<ReturnType<typeof deleteLocalData>>,
Result<void, DeleteDatabaseError>
>();
Inherited from
Omit.deleteDatabase
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
exportDatabase
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
Defined in: packages/common/src/local-first/Evolu.ts:934
Exports the SQLite database file.
The exported file is not encrypted, even when the local database is. Any SQLite tool can read all local data from it, so treat it as plaintext.
Exports are sequential: concurrent calls share one pending export instead of starting parallel exports.
The pending promise rejects with an EvoluDisposedError if this Evolu instance is disposed before export completion, and with an UnknownError when SQLite fails the export, after which the next call exports again. If the database is refused, export stays pending until disposal; see Evolu.loadQuery.
Inherited from
Omit.exportDatabase
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<S, 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: Mutation<S, "insert">;
Defined in: packages/common/src/local-first/Evolu.ts:616
Inserts a row and returns the generated Id.
All non-nullable columns are required, nullable columns are optional, and
id is omitted because Evolu generates it automatically. This ensures
every row has a globally unique, conflict-free identifier without
coordination.
Pass onSuccess when follow-up work must wait until the mutation is stored
and subscribed queries reflect it. It never runs when the database is
unavailable; see Evolu.loadQuery. When the mutation could not be
stored, MutationOptions.onError runs instead, and
EvoluErrorDep.evoluError reports the error. A stored change is not
always visible; see MutationOptions.onSuccess.
Example
import {
assertType,
type TestEvoluSchema,
NonEmptyTrimmedString100,
type Evolu,
type TestTodoId,
} from "@evolu/common";
const insertTodo = (evolu: Evolu<TestEvoluSchema>) =>
evolu.insert("todo", {
title: NonEmptyTrimmedString100.orThrow("Learn Evolu"),
}).id;
const insertTodoAndNotify = (
evolu: Evolu<TestEvoluSchema>,
onSuccess: () => void,
) =>
evolu.insert(
"todo",
{ title: NonEmptyTrimmedString100.orThrow("Another todo") },
{ onSuccess },
).id;
assertType<
[ReturnType<typeof insertTodo>, ReturnType<typeof insertTodoAndNotify>],
[TestTodoId, TestTodoId]
>();
See
Inherited from
Omit.insert
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<S, 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<S>) => (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: Mutation<S, "update">;
Defined in: packages/common/src/local-first/Evolu.ts:663
Updates a row and returns the Id.
Only id is required, all other columns are optional.
In a local-only table, whose deletes remove rows,
it changes only an existing row, so it doesn't bring back a deleted one;
Evolu.upsert creates a row. There, an update of a missing row
changes nothing and isn't an error, so MutationOptions.onSuccess
runs, as after a SQL UPDATE that matches no row.
Example
import {
assertType,
type TestEvoluSchema,
NonEmptyTrimmedString100,
sqliteTrue,
type Evolu,
type TestTodoId,
} from "@evolu/common";
const renameTodo = (evolu: Evolu<TestEvoluSchema>, todoId: TestTodoId) =>
evolu.update("todo", {
id: todoId,
title: NonEmptyTrimmedString100.orThrow("Updated title"),
}).id;
const softDeleteTodo = (evolu: Evolu<TestEvoluSchema>, todoId: TestTodoId) =>
evolu.update("todo", { id: todoId, isDeleted: sqliteTrue }).id;
assertType<
[ReturnType<typeof renameTodo>, ReturnType<typeof softDeleteTodo>],
[TestTodoId, TestTodoId]
>();
See
Inherited from
Omit.update
upsert
readonly upsert: Mutation<S, "upsert">;
Defined in: packages/common/src/local-first/Evolu.ts:701
Upserts a row and returns the Id.
Like insert, but requires an id. Useful for rows with external ID via
createIdFromString. All other non-nullable columns are required,
nullable columns are optional.
Upserting a row that already exists on this device sets its updatedAt and
does not change its createdAt. An upsert made on another device before it
synchronized the row can move createdAt earlier when it arrives. See
SystemColumns.
Example
import {
assertType,
createIdFromString,
type TestEvoluSchema,
NonEmptyTrimmedString100,
type Evolu,
TestTodoId,
} from "@evolu/common";
const stableId = TestTodoId.orThrow(createIdFromString("my-todo-1"));
const upsertTodo = (evolu: Evolu<TestEvoluSchema>) =>
evolu.upsert("todo", {
id: stableId,
title: NonEmptyTrimmedString100.orThrow("Learn Evolu"),
}).id;
assertType<ReturnType<typeof upsertTodo>, TestTodoId>();
See
Inherited from
Omit.upsert
useOwner
readonly useOwner: (owner:
| Owner
| ReadonlyOwner, transports?: readonly [OwnerWebSocketTransport, OwnerWebSocketTransport]) => UnuseOwner;
Defined in: packages/common/src/local-first/Evolu.ts:1132
Use an Owner for sync. Returns a UnuseOwner.
Using an Owner means syncing it with the provided transports, or with the default transports defined in EvoluConfig when transports are omitted. An AppOwner's data lives only in its own database, which its deletion deletes, so an instance syncs its own AppOwner but throws for another one. An instance of createEvoluApp recognizes another AppOwner by the IDs the app lists; one of createEvolu only by its type, so it misses an owner with another AppOwner's ID but another type, such as one created from that AppOwner's secret.
If EvoluConfig.transports is an empty array, this method must be called with explicit non-empty transports.
Owners that use equal transports share one reference-counted connection. The transports in one array must be distinct, so this method throws for equal ones, such as two spellings that RelayUrlFromString converts to one URL.
Example
import {
assertEqual,
createOwnerWebSocketTransport,
deriveShardOwner,
RelayUrl,
testAppOwner,
type Evolu,
type ReadonlyOwner,
type UnuseOwner,
} from "@evolu/common";
const relayUrl = RelayUrl.orThrow("wss://relay.example.com/");
const shardOwner = deriveShardOwner(testAppOwner, ["todos", 1]);
const shardTransport = createOwnerWebSocketTransport({
url: relayUrl,
ownerId: shardOwner.id,
});
const _useShardOwner = (evolu: Evolu): UnuseOwner =>
evolu.useOwner(shardOwner, [shardTransport]);
const _useOwners = (
evolu: Evolu,
owners: ReadonlyArray<ReadonlyOwner>,
): UnuseOwner => {
const unuseOwners = owners.map((owner) =>
evolu.useOwner(owner, [
createOwnerWebSocketTransport({
url: relayUrl,
ownerId: owner.id,
}),
]),
);
return () => {
for (const unuse of unuseOwners) unuse();
};
};
// Call each returned UnuseOwner once to stop syncing.
assertEqual(shardTransport, {
type: "WebSocket",
url: `wss://relay.example.com/?ownerId=${shardOwner.id}`,
});
Inherited from
Omit.useOwner