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
  • viewId identifies one mounted view instance.
  • view identifies the stable view definition.
  • version advances monotonically and guards concurrent saves.
  • sessionId optionally binds the snapshot to a browser session.
  • data contains server-owned signal values.
  • regions contains render hashes keyed by region element id.
  • applied records recent action nonces for replay protection.
  • createdAt, updatedAt, and expiresAt are 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 or null.
  • 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.