JavaScript API Reference

@lix-js/sdk exports openLix(), createLix(), deleteLix(), the generic JavaScript storage protocol and Value. @lix-js/storage-opfs and @lix-js/storage-filesystem provide concrete storage implementations. openLix() returns a local repository, a thin remote client, or a partial replica with on-demand sync. Plugins are installed by writing a released .lixplugin archive to /.lix/plugins/<plugin-key>.lixplugin in the active branch; see Installing and managing plugins.

import { openLix } from "@lix-js/sdk";

const lix = await openLix();

Hosted repository lifecycle

createLix() provisions a hosted repository and returns { id, url }. Its server.url is the host origin; opening and deleting use the repository URL.

import { createLix, openLix, deleteLix } from "@lix-js/sdk";

const headers = () => ({ Authorization: `Bearer ${token}` });
const repository = await createLix({
  server: { url: "https://example.com", headers },
});
const remote = await openLix({ server: { url: repository.url, headers } });
await remote.close();
await deleteLix({ server: { url: repository.url, headers } });

Supply from: localLix to copy an existing repository instead of creating an empty one. Creation takes one consistent snapshot, including files, branches, history, and untracked rows. It does not attach or change the source handle. Subsequent source edits are not part of that copy. To attach the original durable storage afterward, pause writes during creation, close the local handle, and follow the explicit conversion guide for supported existing replica storage. Opening with server.mode: "partial_replica" does not silently replace unrelated or locally diverged history.

Supply idempotencyKey to recover a creation after a lost response. Retry with the same key and unchanged source snapshot; reusing a key for different content fails. When omitted, the SDK generates a key for that call. Lifecycle requests accept url and headers; custom fetch overrides are not supported.

Browser creation from a local repository requires Fetch request streaming; browsers without it return LIX_UNSUPPORTED_OPERATION. Browser Fetch may also require HTTP/2 or HTTP/3 for these uploads. Lix does not buffer a complete repository as a fallback. Empty creation does not require request streaming.

Opening a missing hosted repository fails; it never provisions one. Deletion removes the hosted resource without deleting local copies. Closing only releases a session. A disconnected replica never recreates a deleted server repository.

openLix()

const lix = await openLix(options?);

Options:

OptionTypeDescription
storageLixStorageLocal storage selected by a provider package. Omit both storage and server for memory.
serverLixServerOptionsDefaults to mode: "remote"; use mode: "partial_replica" with storage for on-demand sync.
telemetryLixTelemetryOptionsOptional onSpan(span) callback that receives telemetry spans. Local and partial-replica modes only.

Connect to a remote server:

const lix = await openLix({
  server: {
    url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
    headers: () => ({ Authorization: `Bearer ${token}` }),
  },
});

Remote file content, SQL rows, and branches live on the server. Use headers for authentication and fetch when you need a custom fetch implementation.

Open a partial replica with on-demand sync by supplying storage and explicitly selecting server.mode: "partial_replica":

import { OpfsStorage } from "@lix-js/storage-opfs";

const lix = await openLix({
  storage: new OpfsStorage({ name: "atelier" }),
  server: {
    mode: "partial_replica",
    url: "https://example.com/lix/01936f4e-7b6c-7c3d-8f9a-123456789abc",
    headers: () => ({ Authorization: `Bearer ${token}` }),
  },
});

Opening installs bounded metadata. SQL fetches missing native inputs on demand and retains them in local storage. Covered reads and writes with resident dependencies execute locally, including offline. Mutations commit locally and upload in the background; success does not wait for server acceptance.

server.mode defaults to "remote", which rejects storage. Partial-replica mode requires storage. "sync" is not an alias and full "replica" mode is not yet supported. See Collaboration for the complete behavior.

Use OpfsStorage to persist a local browser Lix across reloads:

import { openLix } from "@lix-js/sdk";
import { OpfsStorage } from "@lix-js/storage-opfs";

const lix = await openLix({
  storage: new OpfsStorage({ name: "atelier" }),
});

Use FilesystemStorage for a repository directory backed by RocksDB at <repository>/.lix/.internal/rocksdb:

import { openLix } from "@lix-js/sdk";
import { FilesystemStorage } from "@lix-js/storage-filesystem";

const lix = await openLix({
  storage: new FilesystemStorage({ path: "./repository" }),
});

Use selective synchronization when only explicit paths should be imported:

const storage = new FilesystemStorage({
  path: "./repository",
  syncAllFiles: false,
});
const lix = await openLix({ storage });
await storage.importPaths(["notes/today.md"]);

Call storage.syncDiskToLix() to run one manual sync pass that imports pending disk changes into Lix. It returns Promise<void> and requires an open Lix instance.

await storage.syncDiskToLix();

Lix instance

syncHealth()

const health = await lix.syncHealth();
// { state, appliedCursor, observedCursor, failures, terminalError }

Returns the local partial-replica worker's health without querying SQL or fetching remote data. state is inactive, running, stalled, failed, or stopped. failures holds independent descriptor, publication, upload, and lease errors, each with a code and message. terminalError describes a failed worker. Cursors and terminal errors are null when unavailable.

Successful local reads do not clear sync failures. running means no known worker failure, not guaranteed server freshness. Other modes report inactive. Call this method before closing the JavaScript session.

prepareOfflineEditing()

await lix.prepareOfflineEditing();

For a partial replica, explicitly fetches the bounded immutable graph metadata used to validate and append local writes. Call it while connected before relying on the next local edit offline. It performs no SQL mutation or publication and keeps read-only opens lazy. File contents, plugins, and other operation-specific inputs remain demand-driven, so this does not make arbitrary SQL workloads offline-ready. Other sync modes reject the call.

query()

const rows = await lix.query<TRow>(sql, params?, { maxBytes?, signal? }?);

Reads the latest committed state. query() never writes: statements that write reject with LIX_ERROR_READ_ONLY (use execute()). Reads that call uuidv7() or current_timestamp are reads. Pass a single statement; multi-statement SQL is rejected.

The result is buffered and all-or-nothing. It is capped at 64 MiB of row data, measured in the engine; a larger result rejects early with LIX_ERROR_RESULT_TOO_LARGE instead of being truncated. Raise the cap with { maxBytes } (Infinity opts out), or read large results with stream().

const files = await lix.query<{ path: string }>(
  "SELECT path FROM lix_file WHERE path LIKE $1 ORDER BY path",
  ["/docs/%"],
);

For a partial replica with on-demand sync, prefetch a view by running its SELECT on hover, then run the same SELECT when opening it. Resident inputs stay local.

SqlParam is null, a boolean, a finite number, a string, a Uint8Array, or a Value. Bind JSON with json(value):

import { json } from "@lix-js/sdk";

await lix.execute("INSERT INTO lix_key_value (key, value) VALUES ($1, $2)", [
  "settings",
  json({ theme: "dark" }),
]);

Anything else (plain objects, arrays, Date, undefined, NaN, Infinity, bigint) throws LIX_INVALID_PARAM instead of being coerced.

Rows are plain objects keyed by column name. A result with two columns of the same name rejects with LIX_ERROR_AMBIGUOUS_COLUMN; alias them (SELECT a.id AS a_id, b.id AS b_id).

Reads have no deadline. Aborting signal rejects the call with the signal's reason and cancels the read in the engine: streaming reads (scans and the filters and projections over them) stop within a few milliseconds, so the next call on the same Lix runs at once. Closing a snapshot or the Lix cancels the reads running through it the same way.

Two phases do not yield: an in-memory sort (ORDER BY without a small LIMIT) and window evaluation over its output run as one long synchronous step once their input is read, so an abort during them takes effect only when that step ends. Measured in review as the delay before the next query after an abort: ORDER BY 15 ms natively (40M rows) and about 1.2 s on the wasm worker (6M rows); a window over a sort 11.7 s natively (20M rows) and 4.7 s on the wasm worker (3M rows). A wasm worker is single-threaded, so the whole worker, including other requests and a close, waits for that time. Reads that return rows already in key order (primary-key scans and streams) have no such phase. Bound sorted and windowed reads with LIMIT or a narrower WHERE, or keep them to modest row counts on the worker.

queryOne()

const row = await lix.queryOne<TRow>(sql, params?, { signal? }?);

Reads at most one row: resolves undefined when there is none and rejects LIX_ERROR_NOT_UNIQUE when there are more. Not byte-capped.

stream()

const rows = await lix.stream<TRow>(sql, params?, { maxBytes?, signal? }?);

Streams a read with bounded memory. It resolves once the statement is planned and its first page has arrived, so SQL errors reject here; later runtime errors reject a later pull. A stream has no size cap and no deadline, and it never expires while it is being consumed. The statement runs against one state that stays pinned until the stream ends or closes, so writes committed after it opened are not visible in it.

RowStream<TRow> iterates rows; pages() iterates arrays of rows from the same cursor (the first page is small, later pages hold up to 4096 rows or 1 MiB). columns lists the column names.

for await (const row of await lix.stream("SELECT id, pattern FROM inlang_variant")) {
  compile(row);
}

await using variants = await lix.stream("SELECT id FROM inlang_variant");
for await (const page of variants.pages()) render(page);

break, return(), close(), await using, aborting signal, or closing Lix ends the stream and releases its state; close() resolves once the engine has stopped. A pull after an abort rejects with the signal's reason, and after lix.close() with LIX_ERROR_CLOSED, so a truncated read is never mistaken for a complete one. An error is final: later pulls reject with the same error. An open stream does not block writes on the same handle.

Streams never write. A few statements must buffer before their first page: reads of file content (lix_file.content), reads inside a transaction, and reads on partial replicas. Those buffers are capped by maxBytes (64 MiB by default) and reject with LIX_ERROR_RESULT_TOO_LARGE beyond it. Remote handles reject streams with LIX_UNSUPPORTED_REMOTE_OPERATION; page remote reads with keyset queries.

execute()

const { rows, rowsAffected, commit } = await lix.execute(sql, params?, options?);

Runs one PostgreSQL-dialect statement: a write, DDL, or a read. Pass a single statement. To write several statements atomically, use a transaction; to run one statement with many parameter sets, use executeMany(). Do not concatenate statements into one SQL string.

The result holds the rows the statement returned (a read's result or RETURNING rows), buffered under the same 64 MiB cap as query() (maxBytes). A write whose RETURNING rows exceed the cap fails without committing. For reads, query() is the idiomatic form and can never write.

ExecuteOptions:

OptionTypeDescription
maxAutoCommitRetriesnumberMaximum automatic transaction replays after the initial attempt. 0 fails fast; omit to retain default recovery budgets.
originKeystringOptional origin label for the mutation.
idempotencyKeystringStable identity for one logical remote SQL mutation. Supply the same key when retrying after a lost response, and the server applies the mutation only once. Remote Lix generates one per call when omitted. Sent as Idempotency-Key, not SQL options.
maxBytesnumberByte cap of the returned rows (default 64 MiB); Infinity opts out. Not supported on remote handles.
signalAbortSignalAborting before the statement is sent prevents it from running. Once it runs, the call settles with its real outcome.

Result:

type ExecuteResult<TRow = Row> = {
  columns: { name: string; type: "null" | "boolean" | "integer" | "real" | "text" | "jsonb" | "row_ref" | "timestamptz" | "blob" }[];
  rows: TRow[];
  rowsAffected: number;
  notices: { code: string; message: string; hint?: string }[];
  commit: { before: string; after: string } | null;
};
FieldDescription
columnsColumn names and SQL value types in result order. Empty for statements that return no rows.
rowsPlain objects keyed by column name.
rowsAffectedNumber of rows affected by a write.
noticesNon-fatal engine notices with { code, message, hint? }.
commitExact active-branch transition { before, after } for an auto-committed write; null for reads. Equal endpoints mean no active-branch movement. Transaction statements omit this field; commit() returns it.

executeMany()

const { rowsAffected } = await lix.executeMany(sql, paramSets, options?);

Runs one statement once per parameter set, atomically and in one round trip: every set applies or none does. It reports only the total rowsAffected, so a statement that returns rows (a read or RETURNING) rejects with LIX_INVALID_PARAM; use execute() per set inside a transaction when you need the rows. Options are the same as execute() without maxBytes.

await lix.executeMany("INSERT INTO lix_key_value (key, value) VALUES ($1, $2)", [
  ["theme", "dark"],
  ["locale", "de"],
]);

observe()

const events = lix.observe(sql, params?, { signal }?);

Observes a SQL query as a standard async iterator. Each value is { sequence, mutationSequence, result }, beginning with the initial result. Consume it with for await...of; manual next() returns { value, done }.

const controller = new AbortController();
try {
  for await (const event of lix.observe("SELECT path FROM lix_file", [], {
    signal: controller.signal,
  })) {
    console.log(event.result.rows.length);
    // break exits iteration and releases the observation.
  }
} catch (error) {
  console.error("Observation failed", error);
}
// A component or task owner can abort from outside the loop:
controller.abort();

Aborting, reaching EOF, or closing Lix ends iteration and settles pending reads. Breaking or returning from the loop releases the observation through the standard iterator return() method. Errors reject and terminate the observation. Cancellation does not cancel work already started by your loop body; check the signal before publishing the result of additional asynchronous work. Result coalescing is unchanged: observations represent current query results, not a lossless mutation log.

The custom ObserveEvents export and observation close() method are removed.

beginTransaction()

const tx = await lix.beginTransaction();

Starts an independent transaction context on this handle's current branch and account. Run statements that belong to the transaction through tx.execute(), tx.executeMany() and tx.query(); its reads see its staged writes. Ordinary lix.query() reads and lix.observe() remain available on the original handle and see committed data. Observers publish relevant updates after commit; rolled-back writes are never published. Changing the original handle's branch does not retarget the transaction. Local transactions retain the caller's previously acknowledged plugin-file view, so plugins can merge stale content against the correct base.

Each handle permits one opening or active explicit transaction at a time. Use openAnotherSession() for another independent handle when needed.

Commit or roll back the transaction before closing the original handle. Closing with an opening or active transaction still fails with LIX_INVALID_TRANSACTION_STATE.

SQL UPDATE and DELETE decisions, and successful explicit SQL reads used to decide later writes, are protected until commit. When another commit lands on the branch after this transaction opens, Lix re-checks exactly what those decisions depended on: every row an explicit read or UPDATE/DELETE predicate returned or could have matched, and every row the transaction writes. Commits that touched none of them are rebased onto the new head and the transaction commits normally, so concurrent writes to unrelated rows, other files, or other tables do not interfere. If a concurrent commit changed, inserted, or deleted one of those rows, committing fails with LIX_TRANSACTION_CONFLICT and nothing is published; this prevents lost updates and write skew on rows the transaction read. lix_file statements that select files by id are checked per file; a path predicate is resolved through the branch's whole path index and still conflicts with concurrent file writes. Plugin-backed file content is checked per file: a plugin re-derives all of a file's rows from its content, so any concurrent change to the same file conflicts. Reads of branch, history, change, or checkpoint state (lix_branch, lix_change, lix_history, lix_diff, lix_as_of, lix_commit_ancestry, lix_active_branch_commit_id(), lix_root_commit_id(), lix_working_diff_checkpoint_commit_id(), checkpoint functions) cannot be validated row by row; a transaction that read them conflicts with any concurrent change to its branch or to shared/global state. A concurrent schema registration or change also conflicts, because the transaction's statements were planned against the previous schema catalog. Constraint checks (uniqueness, row references) and ON DELETE actions are re-evaluated against the latest state when a transaction is rebased. Read-only transactions always commit successfully and return { commit: null }. A successfully planned update or delete keeps its rows protected even if it matches no rows or subsequently fails and the transaction continues with other writes.

A conflict error's details say what overlapped: reason is readSetChanged or writeSetChanged with overlaps listing up to eight { branchId, schemaKey, fileId, rowPk } identities (and overlapCount the total), unvalidatedReadChanged with source naming the read that has no row-level validation (including a concurrent schema catalog change), staleSnapshotNotRebased when the transaction's writes (for example untracked or global rows) cannot be rebased onto a newer head, or commitRaced when another commit won the final atomic publication. retryable is true in every case. Start a new transaction and rerun its statements against current state, or use lix.transaction() to do that automatically.

Rows returned by RETURNING inside an explicit transaction are provisional until commit() succeeds. Auto-committed execute() returns canonical lixcol_change_id values for direct new-image projections after the commit; OLD projections retain the prior ID. Automatic calls (execute(), executeMany()) can rerun the whole statement or parameter-set batch after a known failed transaction. Explicit transactions remain caller-controlled.

Set maxAutoCommitRetries to cap these replays across both transaction contention and expired transaction snapshots. The initial attempt does not count; 0 returns the first failure without re-executing the transaction. When omitted, Lix permits up to 16 contention retries and separately bounds expired-snapshot recovery by its existing time budget. An explicit cap cannot extend that expiry budget. This option does not control pure-read recovery, idempotency receipt lookups, or network retries.

await lix.execute(sql, params, { maxAutoCommitRetries: 0 });
await lix.executeMany(sql, paramSets, { maxAutoCommitRetries: 2 });

Rust callers use .with_max_auto_commit_retries(0) on lix.execute(...) or lix.execute_many(...), including remote handles.

Re-execution reevaluates predicates against current state. An update guarded by an expected revision may therefore succeed with zero affected rows after a retry; check rowsAffected as well as commit success. Retries do not provide general serializable isolation for arbitrary reads or cross-branch dependencies.

Lix never automatically re-executes an unknown commit outcome or an operation marked as having completed execution/publication. Remote mutation recovery first looks up its idempotency receipt: a durable matching receipt replays the saved result; a known transaction conflict with no receipt may retry with the same identity. Absence after an unknown outcome is not proof that the write failed.

Errors exiting the automatic transaction retry loop retain their code and details and add autoCommitRetryCount and autoCommitRetryStopReason; an explicit cap is included as maxAutoCommitRetries. Debug tracing records each replay and its error code. The count covers re-executions, not receipt lookups or transport work.

Unconditional INSERT ... ON CONFLICT DO UPDATE file saves and explicit branch merges retain their collaboration semantics. Use UPDATE ... WHERE with an expected revision and require commit success when publication depends on that revision remaining current.

const tx = await lix.beginTransaction();
try {
  await tx.execute("INSERT INTO lix_file (path, content) VALUES ($1, $2)", [
    "/hello.txt",
    new TextEncoder().encode("hello"),
  ]);
  await tx.commit();
} catch (error) {
  await tx.rollback();
  throw error;
}

transaction()

const { value, commit, retries } = await lix.transaction(
  async (tx) => {
    const { rows } = await tx.execute(
      "SELECT value FROM lix_key_value WHERE key = 'counter'",
    );
    const next = (rows[0]?.value as number) + 1;
    await tx.execute(
      "UPDATE lix_key_value SET value = $1 WHERE key = 'counter'",
      [next],
    );
    return next;
  },
  { maxRetries: 3 },
);

Runs the callback in an explicit transaction and commits it. If the commit (or a statement) fails with LIX_TRANSACTION_CONFLICT, the transaction is rolled back and the callback runs again on a fresh transaction, up to maxRetries times (default 3; 0 never reruns). Any other error rolls the transaction back and rejects immediately. After the last allowed rerun, the conflict rejects with details.transactionRetryCount and details.maxTransactionRetries added.

Returns { value, commit, retries }: the callback's return value from the attempt that committed, that attempt's CommitSpan (or null for a read-only transaction), and how many reruns were needed. The callback must not commit or roll back tx itself, and side effects outside the transaction must tolerate being repeated. Pass { signal } to cancel: aborting before the commit starts rolls the transaction back (reads in flight stop in the engine), and the call rejects with the signal's reason. An abort that lands while the commit is in flight does not stop it: the commit completes and the call resolves with its outcome.

activeBranchId()

const branchId = await lix.activeBranchId();

Returns the id of the branch the Lix instance is currently reading and writing.

activeAccountId()

const accountId = await lix.activeAccountId();

Returns the id of the active account.

subscribeActiveBranch()

const unsubscribe = lix.subscribeActiveBranch(listener);

Subscribes to successful branch switches made through this Lix handle. The listener is a function with no arguments. Returns an unsubscribe function.

Checkpoints

Checkpointing uses the canonical SQL surface rather than a separate typed SDK method:

const result = await lix.execute(
  "SELECT commit_id FROM lix_create_checkpoint($1, $2)",
  ["Validate imports", { _type: "zettel_doc", blocks: [] }],
);
const commitId = result.rows[0].commit_id;

Pass null for either argument when no title or comment is needed. If both are null, lix_log().conversation_id is also NULL.

See Checkpoints for scoped row-reference selections.

Undo and redo

Undo and redo are SQL commands. Execute SELECT commit_id FROM lix_undo() or SELECT commit_id FROM lix_redo() and read the returned receipt row. See Undo and redo for explicit targets and row-reference selections.

createBranch()

const branch = await lix.createBranch({
  name: "Explore",
});

Creates a branch.

Options:

OptionTypeDescription
namestringBranch name.
idstringOptional explicit branch id.
fromCommitIdstringOptional commit id to start from.

Result:

type CreateBranchReceipt = {
  id: string;
  name: string;
  hidden: boolean;
  commitId: string;
};

switchBranch()

await lix.switchBranch({ branchId });

Switches the Lix instance to another branch. Plain SQL tables read and write the active branch.

type SwitchBranchReceipt = { branchId: string };

mergeBranchPreview()

const preview = await lix.mergeBranchPreview({
  sourceBranchId: draft.id,
});

Computes the merge result from sourceBranchId into the active branch without applying it.

Result:

type MergeBranchPreview = {
  outcome: "alreadyUpToDate" | "fastForward" | "mergeCommitted";
  targetBranchId: string;
  sourceBranchId: string;
  baseCommitId: string;
  targetHeadCommitId: string;
  sourceHeadCommitId: string;
  changeStats: MergeChangeStats;
};

mergeBranch()

const merge = await lix.mergeBranch({
  sourceBranchId: draft.id,
});

Merges sourceBranchId into the active branch.

Result:

type MergeBranchReceipt = {
  outcome: "alreadyUpToDate" | "fastForward" | "mergeCommitted";
  targetBranchId: string;
  sourceBranchId: string;
  baseCommitId: string;
  targetHeadBeforeCommitId: string;
  sourceHeadBeforeCommitId: string;
  targetHeadAfterCommitId: string;
  createdMergeCommitId: string | null;
  changeStats: MergeChangeStats;
};

MergeChangeStats:

type MergeChangeStats = {
  total: number;
  added: number;
  modified: number;
  removed: number;
};

close()

await lix.close();

Closes the Lix handle and its storage resources.

Transaction

A transaction is a reader too: query(), queryOne() and stream() see its own staged writes. Transaction execute() returns StatementResult<TRow>, defined as Omit<ExecuteResult<TRow>, "commit">. The durable receipt arrives only from commit(), which returns CommitReceipt = { commit: CommitSpan | null }.

Transactions expose:

MethodDescription
query(sql, params?, options?)Read the transaction's state, including its staged writes.
queryOne(sql, params?, options?)At most one row of the transaction's state.
stream(sql, params?, options?)Stream the transaction's state when the stream opens. The rows are buffered before the first page, under maxBytes.
execute(sql, params?, options?)Run any statement inside the transaction. Same options as lix.execute(), except maxAutoCommitRetries (transactions never replay).
executeMany(sql, paramSets, options?)Stage one statement per parameter set; all sets stage or none do.
commit()Commit and close the transaction, returning { commit } with its durable span or null for a read-only transaction.
rollback()Roll back the transaction and close the transaction handle.

Result rows

execute() returns ordinary JavaScript objects.

const row = result.rows[0]!;

Use row.column_name, row[dynamicColumn], destructuring, spread, or JSON.stringify(row) directly. A read with duplicate column names rejects with LIX_ERROR_AMBIGUOUS_COLUMN, since an object holds one value per name; alias the columns. execute() never fails after its statement ran: for such a result it resolves, and reading rows throws LIX_ERROR_AMBIGUOUS_COLUMN while columns, rowsAffected and commit stay readable.

Value

Value constructs explicitly typed SQL parameters. Returned values are native JavaScript values and their SQL types are described by result.columns.

Accessors:

MethodReturn typeDescription
toJS()unknownReturns a defensive copy of the native JS value.
asBytes()Uint8Array | undefinedReturns a defensive copy for blob values.

Constructors:

MethodDescription
Value.null()Create a SQL null value.
Value.integer(value)Create an integer value.
Value.boolean(value)Create a boolean value.
Value.real(value)Create a real number value.
Value.text(value)Create a text value.
Value.jsonb(value)Create a JSONB value.
Value.timestamptz(value)Create a timestamptz value from an RFC 3339 string.
Value.blob(value)Create a blob value from Uint8Array.
Value.from(raw)Convert a SqlParam (a scalar, Uint8Array, or Value) into a Value.

json(value) is shorthand for Value.jsonb(value): the way to bind a JSON object or array as a parameter.