state
js/ui/web/state.ts
fino:ui/web/state — durable server-driven UI view snapshots.
View snapshots are per-view-instance state records. They hold server-owned signal values, the latest rendered-region hashes, idempotency nonces, and a monotonic version used for compare-and-swap saves and SSE reconnects. Stores clone values on input and output so request-local mutation cannot leak across events.
Storage model
A store keeps one current head snapshot per viewId plus retained history.
save() can be guarded with expectVersion so concurrent action requests do
not silently overwrite each other. A mismatch throws
ViewVersionConflictError; callers should reload the latest snapshot before
retrying.
Snapshot data, regions, and applied must be JSON-serializable. Values
such as undefined, functions, symbols, and cyclic objects are rejected or
cannot round-trip through the database store. sweep() removes expired
heads, and implementations also delete their retained history for those
views.
InMemoryViewStore is process-local and intended for tests or prototypes.
DatabaseViewStore persists through fino:database and creates its tables
on open().
import { DatabaseViewStore, type ViewSnapshot } from 'fino:ui/web/state';
const store = await DatabaseViewStore.open('sqlite://ui.db');
const now = Date.now();
const snapshot: ViewSnapshot = {
viewId: 'todos-1',
view: 'todos',
version: 0,
data: { items: [] },
regions: {},
applied: [],
createdAt: now,
updatedAt: now,
expiresAt: now + 30 * 60_000,
};
await store.save(snapshot);
const head = await store.load('todos-1');
Interfaces
interface ViewSnapshot {
Durable state for one mounted view instance.
version is the compare-and-swap token and SSE event id. data contains
server-owned L2 values, while regions stores hashes for the latest rendered
HTML per patch target.
Fields
viewIdidentifies one mounted view instance.viewidentifies the stable view definition.versionadvances monotonically and guards concurrent saves.sessionIdoptionally binds the snapshot to a browser session.datacontains server-owned signal values.regionscontains render hashes keyed by region element id.appliedrecords recent action nonces for replay protection.createdAt,updatedAt, andexpiresAtare Unix timestamps in milliseconds.
Properties
viewId: string
Random or keyed view instance id embedded in forms and live channels.
view: string
Stable view definition id.
version: number
Monotonic snapshot version used for compare-and-swap saves and SSE event ids.
sessionId?: string
Optional owning browser session id.
data: Record<string, unknown>
JSON-serializable server-owned signal values.
regions: Record<string, string>
Last-rendered HTML hashes keyed by region element id.
applied: Array<{
rid: string;
action: string;
}>
Recent action nonces used to avoid double-submit replays.
createdAt: number
Creation time in milliseconds since the Unix epoch.
updatedAt: number
Last update time in milliseconds since the Unix epoch.
expiresAt: number
Expiration time in milliseconds since the Unix epoch.
interface ViewStateStore {
Storage contract for durable view snapshots.
Stores clone snapshots on load and save. save() honors expectVersion as a
compare-and-swap guard and throws ViewVersionConflictError on mismatch.
Methods
load()reads the current head snapshot ornull.save()replaces the head and appends the same snapshot to history.history()returns retained snapshots newest first.delete()removes the head and retained history.sweep()removes expired heads and returns the number deleted.
Methods
load(viewId: string): Promise<ViewSnapshot | null>
Load the current head snapshot for viewId.
Returns null when no snapshot exists. The returned snapshot is detached
from store state, so mutating it does not mutate the persisted head.
save(snapshot: ViewSnapshot, opts?: {
expectVersion?: number;
}): Promise<void>
Save a new head snapshot and append it to history.
When opts.expectVersion is provided, the current head must have exactly
that version. Mismatches, including a missing current head, throw
ViewVersionConflictError.
history(viewId: string, opts?: {
limit?: number;
}): Promise<ViewSnapshot[]>
Return retained history for viewId, newest first.
opts.limit caps the number of returned snapshots when provided.
delete(viewId: string): Promise<void>
Delete a snapshot head and its retained history.
Unknown view ids are treated as a successful no-op.
sweep(now?: number): Promise<number>
Delete expired snapshots and return the number of heads removed.
now defaults to Date.now(). Snapshots with expiresAt <= now are
expired.
Classes
class ViewVersionConflictError extends Error {
Error thrown when a compare-and-swap snapshot save sees a different version.
actual is null when the store has no current head for the view id.
Constructors
constructor(viewId: string, expected: number, actual: number | null)
Create a conflict error for a failed save(..., { expectVersion }).
class InMemoryViewStore implements ViewStateStore {
In-memory view snapshot store for tests and single-process prototypes.
Snapshots live only in this JavaScript process. The store still clones on
load/save, appends history, validates JSON-serializable snapshot fields, and
enforces expectVersion the same way as DatabaseViewStore.
Methods
async load(viewId: string): Promise<ViewSnapshot | null>
Load the current in-memory head snapshot for viewId, or null.
async save(snapshot: ViewSnapshot, opts: {
expectVersion?: number;
} = {}): Promise<void>
Save an in-memory head snapshot and prepend it to retained history.
opts.expectVersion, when provided, must match the current head version.
The snapshot is validated for JSON-compatible data, regions, and
applied values before storage.
async history(viewId: string, opts: {
limit?: number;
} = {}): Promise<ViewSnapshot[]>
Return retained in-memory history for viewId, newest first.
async delete(viewId: string): Promise<void>
Delete an in-memory head snapshot and all retained history for viewId.
async sweep(now: number = Date.now()): Promise<number>
Delete expired in-memory heads and return the number removed.
class DatabaseViewStore implements ViewStateStore {
Database-backed view snapshot store.
open() accepts the same target strings as Database.open(), including
SQLite paths and future generic database targets.
The store writes the current head to ui_view_snapshots and appends each
saved version to ui_view_snapshot_history. save() runs in a transaction
so the head and history stay consistent.
Static Methods
static async open(target: string, opts?: {
fs?: object;
}): Promise<DatabaseViewStore>
Open a database-backed view store and create required tables.
target is passed to Database.open(), so values such as :memory:,
sqlite://path/to/ui.db, or other supported database targets are accepted.
opts.fs is forwarded for filesystem-backed database providers.
Methods
async load(viewId: string): Promise<ViewSnapshot | null>
Load the current database head snapshot for viewId, or null.
async save(snapshot: ViewSnapshot, opts: {
expectVersion?: number;
} = {}): Promise<void>
Save a database head snapshot and append it to history in one transaction.
opts.expectVersion, when provided, must match the currently loaded head
version. The snapshot is validated before any database writes occur.
async history(viewId: string, opts: {
limit?: number;
} = {}): Promise<ViewSnapshot[]>
Return retained database history for viewId, newest first.
async delete(viewId: string): Promise<void>
Delete a database head snapshot and all retained history for viewId.
async sweep(now: number = Date.now()): Promise<number>
Delete expired database heads and return the number removed.
Each expired view is deleted through delete(), so retained history is
removed with the head snapshot.
async close(): Promise<void>
Close the underlying database connection.