Skip to main content

Preact

import * as mod from "jsr:@nzip/lofi/preact";

Optional Preact bindings for the lofi runtime: live typed queries (useLiveQuery), table mutations (useTableMutations), the per-write sync lifecycle (useWrite, usePendingWrites, useSyncStatus), first-load progress (useBootProgress), PWA install/update state (usePwaState), device capabilities (useDeviceCapabilities), the schema-compatibility gate (useSchemaCompat), and the storage-container fork guard (useStorageFork), plus package-owned example components (DeviceStatus, PwaActions, RuntimeRecovery, TicketEnrollForm) that application layouts may compose or replace with UI built on the same public runtime APIs.

DeviceStatus

function

function DeviceStatus(): VNode

Renders the Device gate: every subsystem's live status — storage, sync, auth, and PWA — grouped by the module that owns it, so the panel doubles as a map of where to hook in. Optional: it is built entirely on public runtime APIs (device capabilities, PWA state, runtime diagnostics, the session), so an application can replace it with its own status UI over the same sources.

Returns: The device diagnostics panel, grouped by owning subsystem.

Example

import { DeviceStatus } from "@nzip/lofi/preact";

export function SettingsPage() {
return <DeviceStatus />;
}

Notices

function

function Notices({ ..., ... }: NoticesProps): VNode

The built-in durable-notice surface: renders the live s.notice queue as an ARIA live region so a message enqueued when a write settled — success or the "a rejected write still flashed success" case — is announced and dismissable. Optional and entirely public: it is useNotices plus default markup, so an app can drop it and render its own (a toast stack, a banner) over the same hook.

ParameterDescription
propsOptional region label and a custom per-notice renderer.

Returns: The always-mounted live notices region.

Example

import { Notices } from "@nzip/lofi/preact";

export function AppChrome() {
return <Notices />;
}

PwaActions

function

function PwaActions({ ..., ..., ... }: PwaActionsProps): VNode | null

A composable install/update surface that keeps browser event handling package-owned.

ParameterDescription
propsAn optional controller override and heading text.

Returns: The install/update section, or null when no action or status is relevant.

Example

import { PwaActions } from "@nzip/lofi/preact";

<PwaActions title="Install this app" />;

pwaFailureMessage

function

function pwaFailureMessage(code: PwaFailureCode): string

Returns actionable, non-technical recovery guidance for a PWA failure.

RuntimeRecovery

function

function RuntimeRecovery({ ..., ... }: RuntimeRecoveryProps): VNode | null

Renders recovery only when another tab is running an incompatible broker version.

ParameterDescription
propsThe startup failure to inspect and an optional reload override.

Returns: The recovery prompt, or null when no incompatible-broker failure is present.

Example

import { RuntimeRecovery } from "@nzip/lofi/preact";

<RuntimeRecovery failure={startupFailure} />;

TicketEnrollForm

function

function TicketEnrollForm({ ..., ..., ..., ... }: TicketEnrollFormProps): VNode

The app-connect ticket enrollment form, shaped for password managers: the ticket is a current-password field with a label-as-username companion, so the manager offers to save the ticket against this origin on first paste and autofills it later. That custody is the durable copy on devices that cannot seal admin capability behind a passkey — the node stores only hashes, so the manager's copy is the one that survives.

After enrolling a provision-scoped ticket whose capability was split (see provision.ts), the form offers the passkey-sealing ceremony, and states the password-manager fallback when the ceremony reports prf-unavailable or is cancelled.

ParameterDescription
propsOptional callbacks, heading, and injectable implementations.

Returns: The enrollment form and its follow-on custody choices.

Example

import { TicketEnrollForm } from "@nzip/lofi/preact";

<TicketEnrollForm title="Connect to your node" />;

useBootProgress

function

function useBootProgress(): BootProgress

Subscribes a Preact component to first-load progress, so a loading state can distinguish the engine download (with byte progress on a cold first visit) from opening persistent storage.

Returns: The current first-load progress, kept live via subscription.

Example

import { useBootProgress } from "@nzip/lofi/preact";

const boot = useBootProgress();
if (boot.phase === "downloading" && boot.totalBytes) {
const percent = Math.round((boot.loadedBytes / boot.totalBytes) * 100);
return <p>Downloading the app · {percent}%</p>;
}

useDeviceCapabilities

function

function useDeviceCapabilities(): DeviceCapabilitiesState

Reads the browser's device capabilities once on mount and exposes an explicit persistence request that refreshes the report with the browser's verdict.

Returns: The capability report and the persistence request action.

Example

import { useDeviceCapabilities } from "@nzip/lofi/preact";

const { report, requestPersistence } = useDeviceCapabilities();
if (!report) return <p>Checking device capabilities…</p>;

useLiveQuery

function

function useLiveQuery<T extends TableRow>(createQuery: () => QueryBuilder<T>, dependencies: readonly unknown[]): LiveQuerySnapshot<T>

Subscribes a Preact component to any typed Jazz query.

Equivalent mounted queries share one Jazz subscription. The snapshot preserves the exact row type produced by the builder, including select and include projections. An empty rows array with status: "ready" is an empty result, not a loading signal.

ParameterDescription
createQueryBuilds the typed Jazz query; re-invoked when dependencies change.
dependenciesValues that, when changed, release the query and open a replacement.

Returns: The live snapshot: status, exact typed rows, and error.

Example

import { useLiveQuery } from "@nzip/lofi/preact";
import { app } from "../app.ts";

const records = useLiveQuery(
() => app.schema.records.where({ workspaceId, archived: false }),
[workspaceId],
);
// records.status is "loading" | "ready" | "error"; records.rows is typed.

useNotices

function

function useNotices(): NoticesSurface

Subscribes a Preact component to the durable notice queue. The list stays live across enqueues (including effects that fire at a boot re-arm), dismissals, and TTL retirement.

Returns: The live notices and the dismissal actions.

Example

import { useNotices } from "@nzip/lofi/preact";

const { notices, dismiss } = useNotices();
return notices.map((n) => (
<div key={n.id} data-tone={n.tone}>
{n.message}
<button type="button" onClick={() => dismiss(n.id)}>Dismiss</button>
</div>
));

usePendingWrites

function

function usePendingWrites(): PendingWritesSnapshot

The reload-safe set of writes still waiting to sync, for "N changes waiting to sync" indicators. The set is rebuilt from the durable journal at boot, so it survives a reload with writes still pending.

Returns: The current PendingWritesSnapshot: the count and the journaled writes still awaiting their sync fate, oldest first.

Example

const pending = usePendingWrites();
return pending.count > 0 ? <p>{pending.count} change(s) waiting to sync</p> : null;

usePwaState

function

function usePwaState(controller: PwaController): PwaState

Subscribes a Preact component to an isolated or shared PWA controller.

ParameterDescription
controllerThe controller to observe; defaults to the package-wide PWA controller.

Returns: The current install/update state, kept live via subscription.

Example

import { usePwaState } from "@nzip/lofi/preact";

const pwa = usePwaState();
const updateReady = pwa.update === "ready";

useSchemaCompat

function

function useSchemaCompat(): SchemaCompatState

Subscribes a Preact component to the schema-compatibility gate, so apps can render their own read-only banner in place of the framework default (suppress the default with pwa: { updateBanner: "none" } in defineLofiApp).

Returns: The current compatibility state, kept live via subscription.

Example

import { useSchemaCompat } from "@nzip/lofi/preact";

const compat = useSchemaCompat();
if (compat.state === "data-ahead") return <ReadOnlyBanner message={compat.message} />;

useStorageFork

function

function useStorageFork(guard: StorageForkSurface): StorageForkState

Subscribes a Preact component to the storage-container fork guard, so apps can render their own install warning or fork notice in place of the framework default (suppress the default with pwa: { forkNotice: "none" } in defineLofiApp).

ParameterDescription
guardThe guard to observe; defaults to the package-wide guard.

Returns: The current fork state, kept live via subscription.

Example

import { useStorageFork } from "@nzip/lofi/preact";

const fork = useStorageFork();
if (fork.state === "fork-detected") return <ForkNotice message={fork.message} />;

useSyncStatus

function

function useSyncStatus(row: { id: string } | null | undefined): RowSyncStatus

The sync status of one row for per-row badges: waiting while a journaled write touching the row has not settled, rejected when the row's latest settled write was denied, and synced otherwise. waiting is reload-safe — it derives from the durable journal. rejected outlives the pruned journal entry only for the current session: after a reload the badge is gone. For a durable rejection response, declare an onRejected effect on the verb instead of relying on the badge.

Example

const status = useSyncStatus(task);
{status === "waiting" && <span class="badge">waiting to sync</span>}

useTableMutations

function

function useTableMutations<T extends TableRow, Init>(table: TableProxy<T, Init>): TableMutations<T, Init>

Binds one typed Jazz table to stable insert, update, and remove methods.

Each method returns a thenable WriteHandle: awaiting it resolves after local durability, and the handle's saved and synced promises track the write's sync fate. When managed sync is active, global confirmation continues in the background and updates the shared table-scoped pending, durability, and error state.

ParameterDescription
tableThe typed schema table to mutate, e.g. app.schema.records.

Returns: Stable insert, update, and remove methods plus the shared mutation state. To attach declared effects to a write, wrap the same table in a verb (s.mutation(name, s.insert(table), { … })) and observe its WriteHandle with useWrite.

Example

import { settleUiMutation } from "@nzip/lofi";
import { useTableMutations } from "@nzip/lofi/preact";
import { app } from "../app.ts";

function ArchiveButton({ id }: { id: string }) {
const records = useTableMutations(app.schema.records);
// The handle is thenable: settled at saved, while the sync fate and the
// shared pending/durability state continue in the background.
return (
<button onClick={() => void settleUiMutation(records.update(id, { archived: true }))}>
Archive {records.pending > 0 && "…"}
</button>
);
}

useWrite

function

function useWrite<T>(write: WriteHandle<T> | null | undefined): WriteState

Observes one write's lifecycle. Pass the WriteHandle returned by a verb or a table mutation; the component re-renders on each WriteStage change, and because handles are level-triggered a component that mounts after a transition sees the current stage immediately — no missed events. reason carries the WriteRejection once a write settles as rejected.

Example

const [write, setWrite] = useState<WriteHandle<Order> | null>(null);
const { stage, reason } = useWrite(write);
// stage: "saving" | "saved" | "syncing" | "synced" | "rejected" | null

WriteHandle

class

class WriteHandle<T> implements PromiseLike<T> {
constructor(writeId: string);
get writeId(): string;
get batchId(): string | null;
get stage(): WriteStage;
get reason(): WriteRejection | null;
get saved(): Promise<T>;
get synced(): Promise<T>;
then<Fulfilled = T, Rejected = never>(onfulfilled?: ((value: T) => Fulfilled | PromiseLike<Fulfilled>) | null, onrejected?: ((reason: unknown) => Rejected | PromiseLike<Rejected>) | null): Promise<Fulfilled | Rejected>;
subscribe(listener: () => void): () => void;
}

A single write observed through the author-facing lifecycle.

await write (the thenable) resolves at saved with the write's value — for inserts, the created row. write.synced resolves when the store confirms the write and rejects with WriteRejectedError when the store denies it. stage and reason are current-state properties; subscribe notifies immediately and on every later change.

On a device without managed sync there is no store to confirm anything: local durability is settlement, and the handle reaches synced as soon as it is saved. In Preact components, render a handle with useWrite and the app-wide pending set with usePendingWrites.

Handles are issued by the runtime and are observe-only: the lifecycle mutators live on a controller the ledger keeps at construction, so no consumer of a handle can advance or settle it.

Example

const write = placeOrder({ sku, quantity }); // a verb returns a WriteHandle
const order = await write; // resolves at saved — safe to navigate
write.synced.catch((error) => {
if (error instanceof WriteRejectedError) showDenied(error.message);
});

BootProgress

type

type BootProgress = {
phase: BootProgressPhase;
loadedBytes: number;
totalBytes: number | null;
};

Live first-load progress for application status UI.

BootProgressPhase

type

type BootProgressPhase =
| "pending"
| "downloading"
| "opening"
| "ready"
| "failed";

Phases between a painted shell and an open runtime.

  • pending — the runtime has not been requested yet.
  • downloading — the engine binary is downloading; on a cold first visit this is the long phase, with byte progress in BootProgress.
  • opening — the engine is instantiating and persistent storage is opening.
  • ready — the runtime is open; live queries answer from local data.
  • failed — the runtime could not open; the cause is in runtime diagnostics (startupFailure).

DeviceCapabilitiesState

type

type DeviceCapabilitiesState = {
report: DeviceCapabilityReport | null;
requestPersistence(): Promise<void>;
};

State returned by useDeviceCapabilities.

DeviceCapabilityReport

type

type DeviceCapabilityReport = {
secureContext: boolean;
opfs: boolean;
sharedWorker: boolean;
webLocks: boolean;
messageChannel: boolean;
durableDriverSupported: boolean;
webAuthn: boolean;
prf: PrfSupport;
persistentPermission: "granted" | "not-granted" | "unavailable" | "error";
displayMode: "standalone" | "browser";
};

Browser capabilities that determine whether lofi can provide its runtime guarantees.

LiveQuerySnapshot

type

type LiveQuerySnapshot<T extends TableRow> = {
status: "loading" | "ready" | "error";
rows: T[];
error: string | null;
};

Honest read state for an arbitrary typed Jazz query.

NoticeEntry

type

type NoticeEntry = {
id: string;
message: string;
tone: NoticeTone;
createdAt: number;
expiresAt: number | null;
};

One durable notice entry.

NoticesProps

type

type NoticesProps = {
label?: string;
children?: (notice: NoticeEntry, dismiss: () => void) => VNode;
};

Props for the built-in Notices surface.

NoticesSurface

type

type NoticesSurface = {
notices: readonly NoticeEntry[];
dismiss: (id: string) => void;
dismissAll: () => void;
};

The live notices and the actions to retire them.

NoticeTone

type

type NoticeTone =
| "info"
| "success"
| "warning"
| "error";

How a notice is classified for rendering.

PendingWritesSnapshot

type

type PendingWritesSnapshot = {
count: number;
writes: readonly PendingWriteSummary[];
};

The reload-safe pending set powering "N changes waiting to sync".

PendingWriteSummary

type

type PendingWriteSummary = {
writeId: string;
verb: string | null;
table: string;
rowId: string;
op: "insert" | "update" | "remove";
createdAt: number;
expired: boolean;
};

One write not yet settled, as shown by pending-writes surfaces.

PwaActionsProps

interface

interface PwaActionsProps {
readonly controller?: PwaController;
readonly fork?: StorageForkSurface;
readonly title?: string;
}

Optional controller, fork guard, and heading text for PwaActions.

PwaController

type

type PwaController = {
getState(): PwaState;
subscribe(subscriber: (state: PwaState) => void): () => void;
requestInstall(): Promise<PwaInstallState>;
checkForUpdate(): Promise<boolean>;
applyUpdate(): boolean;
initialize(): void;
};

Stateful controller for browser installation and service-worker updates. Most apps use the shared pwaController through the wrapper functions (getPwaState, applyPwaUpdate) or the Preact bindings.

PwaFailureCode

type

type PwaFailureCode =
| "registration"
| "installation"
| "install-prompt"
| "update-check"
| "precache"
| "runtime-cache";

Stable categories for recoverable offline/PWA failures.

PwaInstallState

type

type PwaInstallState =
| "installed"
| "available"
| "prompting"
| "accepted"
| "dismissed"
| "manual-ios"
| "manual-browser"
| "unsupported";

Browser installation states exposed to application UI.

  • installed — running standalone or the browser reports the app installed.
  • available — the browser offered an install prompt; requestPwaInstall opens it.
  • prompting — the deferred prompt is open.
  • accepted / dismissed — the prompt's outcome.
  • manual-ios — install via Share → Add to Home Screen; iOS exposes no prompt API.
  • manual-browser — install is available only through the browser menu.
  • unsupported — no secure context or no service-worker support.

PwaState

type

type PwaState = {
worker: PwaWorkerState;
install: PwaInstallState;
update: PwaUpdateState;
failure?: { code: PwaFailureCode; message: string };
};

Current install, service-worker, and offline-cache state.

PwaUpdateState

type

type PwaUpdateState =
| "idle"
| "checking"
| "installing"
| "ready"
| "applying"
| "failed";

Foreground update-check and waiting-worker states exposed to application UI.

  • idle — no check running and nothing staged.
  • checking — a bounded update check is in flight.
  • installing — a newly discovered worker is downloading and installing.
  • ready — a new worker is staged and waiting: show the update affordance and call applyPwaUpdate.
  • applying — covers the swap until controlled tabs reload.
  • failed — the check or installation failed; the cause is in PwaState.failure.

PwaWorkerState

type

type PwaWorkerState =
| "development-disabled"
| "unsupported"
| "registering"
| "ready"
| "failed";

Service-worker lifecycle states exposed to application UI.

  • development-disabled — registration is skipped outside production builds.
  • unsupported — the environment offers no service-worker container.
  • registering — registration and first activation are in progress.
  • ready — an active worker controls the app and serves the offline shell.
  • failed — registration, installation, or precaching failed; PwaState.failure carries the cause.

RowSyncStatus

type

type RowSyncStatus = "synced" | "waiting" | "rejected";

Per-row sync state for badges: settled, still waiting, or denied.

RuntimeRecoveryProps

interface

interface RuntimeRecoveryProps {
readonly failure: RuntimeStartupFailure | null;
readonly reload?: () => void;
}

Inputs for Lofi's explicit persistent-runtime recovery action.

RuntimeStartupFailure

type

type RuntimeStartupFailure = {
code: RuntimeStartupFailureCode;
runtimeMode: "local" | "managed";
message: string;
};

Non-sensitive runtime context retained for diagnostics and recovery UI.

RuntimeStartupFailureCode

type

type RuntimeStartupFailureCode =
| "broker-incompatible"
| "configuration-error"
| "reload-loop"
| "storage-startup-failed"
| "unsupported-capabilities";

Stable categories for failures that prevent Lofi's persistent runtime from opening.

SchemaCompatReason

type

type SchemaCompatReason = "schema" | "stale-tab";

Why writes are refused: newer-schema data, or a stale tab after a swap.

SchemaCompatState

type

type SchemaCompatState =
| { state: "unchecked"; reason: "inactive" | "development" | "no-manifest" | "pending" }
| { state: "compatible"; classification: "first-boot" | "equal" | "code-ahead" }
| { state: "data-ahead"; reason: SchemaCompatReason; message: string }
| { state: "updating"; message: string };

The compatibility state exposed through diagnostics and the Preact hook.

SealOutcome

type

type SealOutcome = {
portable: boolean;
};

What a completed sealing ceremony reports about the sealed record.

Session

type

type Session = {
user_id: string | null;
syncAvailable: boolean;
sink: SessionSink | null;
backedUp: boolean;
syncing: boolean;
syncOwnerMismatch: boolean;
phraseGuarded: boolean;
passkeyRecoverable: boolean;
};

A snapshot of the account: what is possible and what the user has chosen.

SessionSink

type

type SessionSink = {
source: "declared" | "default";
host: string;
label?: string;
};

A non-secret description of the sync location in effect.

StorageForkGuard

type

type StorageForkGuard = {
start(): void;
getState(): StorageForkState;
subscribe(listener: (state: StorageForkState) => void): () => void;
dismissFork(): void;
};

The storage-container fork guard for one browsing context.

StorageForkState

type

type StorageForkState =
| { state: "unarmed"; reason: "inactive" | "development" }
| { state: "idle" }
| { state: "browser-data-at-risk" }
| { state: "fork-detected"; message: string };

The storage-container fork state exposed through the guard and the Preact hook.

StorageForkSurface

type

type StorageForkSurface = Pick<StorageForkGuard, "getState" | "subscribe">;

The guard surface a component observes; defaults to the shared guard.

TableMutations

type

type TableMutations<T extends TableRow, Init> = TableMutationSnapshot & { insert(values: Init): WriteHandle<T>; update(id: string, patch: Partial<Init>): WriteHandle<void>; remove(id: string): WriteHandle<void> };

Typed mutation methods plus their shared table-scoped observable state.

TableMutationSnapshot

type

type TableMutationSnapshot = {
pending: number;
durability: WriteDurability;
error: string | null;
};

Observable state shared by every mutation consumer for one table.

TableRow

type

type TableRow = {
id: string;
};

The minimum shape every persisted row exposes to the framework.

TicketEnrollFormProps

interface

interface TicketEnrollFormProps {
readonly onEnrolled?: (session: Session) => void;
readonly title?: string;
readonly enroll?: (ticket: string) => Promise<Session>;
readonly seal?: () => Promise<SealOutcome>;
}

Dependencies TicketEnrollForm accepts for testing and composition.

WriteRejection

type

type WriteRejection = {
cause: "denied" | "expired";
code: string | null;
reason: string;
};

Why a write settled as rejected: the structured cause, code, and reason.

WriteStage

type

type WriteStage =
| "saving"
| "saved"
| "syncing"
| "synced"
| "rejected";

The closed, framework-owned write lifecycle. Stages are monotonic: saving → saved → syncing → synced | rejected. syncing is reserved for a runtime that can observe the transport; the current storage engine exposes no such signal, so today writes move from saved directly to synced or rejected and no handle ever reports syncing. Branch on saved vs settled, not on seeing syncing.

WriteState

type

type WriteState = {
stage: WriteStage | null;
reason: WriteRejection | null;
};

The observable state of one write, re-rendered on every stage change.