Skip to content

Mini app synchronization

Mini apps use the installation's existing multiplexed /rpc/ws socket. A change notification names an owned resource and scope; the resource's HTTP read supplies the authoritative value. Repeated notifications are harmless. Navigation, selections and unsaved drafts remain local to each window.

Choose the source

ResourceSDK entry pointHealthy behavior
Project files, including agent file editsui/file-resource-sync: useFileResourceSyncRead on matching file notifications; no periodic polling.
Session or user JSON storesbackend: createStoreChangeFeed; ui/store-resource: useStoreChangesCommit and native file notifications share one scoped feed.
A board whose routes return snapshotsui/store-snapshot: useStoreSnapshotValidated reads on notifications; mutation answers supersede older reads.
Service-owned statebackend: observeMutations and an owned wsFrames contributionExplicit successful mutation maps; read methods stay silent.
High-frequency external dataui/realtime-feed: createRealtimeFeed, useRealtimeFeedScoped typed deltas; source-specific recovery.

For a project file read model:

TypeScript
const sync = useFileResourceSync({
  sessionId,
  prefix: '.localix/my-app',
  enabled: visible,
  read: (signal) => api.list(sessionId, signal),
  apply: (snapshot) => setSaved(snapshot),
  onError: (error) => setReadError(error),
})

Pass cancellation through the client to the request. refresh() waits for registered reads to settle, including a follow-up owed after an overlapping invalidation. It returns Promise<void> rather than a snapshot or success verdict; disconnect, teardown, and exhausted retries also release waiters. Keep failure state in onError and treat only apply as a new accepted snapshot. supersede() expires an older read when a direct mutation answer already supplies the newer value. Keep optimistic/draft state out of an unconditional apply: merge independent changes or surface a conflict.

read must settle and honor its abort signal; the scheduler supplies no request deadline of its own. Give network reads an appropriate timeout as well as cancellation. For a failing read, onError runs after retries are exhausted, rather than on every attempt. Deactivating or disposing the reader releases refresh waiters even if a read ignores cancellation; that read's result cannot be applied, but its underlying work can continue.

Route snapshot options

useStoreSnapshot keeps a schema-validated last-good snapshot. Its intervalMs is the fallback cadence while the store feed is not live; backstopMs is the cadence with live coverage and defaults to null. Setting intervalMs: null disables the disconnected fallback too. Choose a non-null fallback when external edits must remain visible without native coverage.

This hook's refresh() returns void: it starts a read, unlike the awaitable file-resource refresh above. Observe snapshot and error for its result. send(body, options) sends a mutation and validates its returned snapshot with the same schema; it resolves to that response or null on failure. Scope and request ordering determine whether the response is applied to the visible state. A failed read retains the previous valid snapshot.

Reads and sends use an eight-second abort timeout by default, configurable with timeoutMs. A timeout or invalid mutation response does not roll back a server write that already committed. Reconcile before retrying a mutation whose outcome is unknown, using the owning domain's revision or idempotency contract.

Publish after commit

A JSON-store backend registers createStoreChangeFeed with its mini app name, an ownership-checking resolveDir(userId, sessionId), a committed-write subscription, and ctx.watchPath when available. Register its wsFrames and release it through onShutdown. Never accept an absolute subscription path or trust a user ID from the command payload. acceptPath can narrow a directory to the owning user's file; coarse filesystem events invalidate conservatively.

The shared protocol uses commands <app>:store.subscribe and <app>:store.unsubscribe, frame tag c, and { plugin, sessionId } invalidations. A status frame additionally supplies live. sessionId: null names installation-owned mini app state, not an arbitrary user's state. The socket's host-supplied user ID still governs resolution and reads. The local API uses self-asserted identity; this scope contract does not authenticate a network caller. Frames never contain credentials or documents.

live: true means the native source is covered, not merely that the host accepted a command. Without a native watcher, committed API changes still arrive promptly but the reader keeps its fallback for external file edits. Watcher failure and socket disconnect set liveness false.

Service-backed state uses an explicit mutation map with observeMutations. HTTP, agent tools and internal callers must share that service instance or a shared publisher. Do not infer mutations from HTTP verbs: a POST may generate a preview or perform a read. Notification failure cannot turn a successful committed write into a failed mutation.

Two change feeds

The store change feed and host.changes do different jobs, so a mini app may use both:

QuestionStore change feed (createStoreChangeFeed)host.changes
Who listensOpen views over /rpc/wsBackend consumers in the mini app
What it coversCommitted writes and external file edits (watcher)Notices the mini app records; the core's deletions
Missed while awayNothing kept: the view reads again on reconnectKept in SQLite; a consumer catches up at its cursor
Cost per writeAn in-memory nudge, coalesced per 50 msOne database row

A view needs the newest state, not a history: a reconnect read already covers what it missed, and a file edited outside the app has no transaction to record. Recording every store write durably would add a database row per write and still not cover an edit made while the watcher was down. A backend consumer that builds derived data (an index, a digest, a cleanup) needs the opposite: every notice, across restarts. Keep store refreshes on the store feed and durable processing on host.changes.

Read and recover

createResourceSync from ui/resource-sync is shared with core. It coalesces invalidations, serializes each registered reader, rejects superseded answers, aborts cancellable reads on teardown, and retries failures with bounded delays (1, 2, and 4 seconds by default, then waits for a new invalidation or refresh). New invalidation bypasses a pending retry delay. Shared resources use this scheduler too, including invalidations during their first load and cache reset while a request is pending.

Subscribe and then reconcile to close the initial-read gap. Reconcile again after reconnect and returning to visibility. Follow the owning work item's visibility; hidden store feeds unsubscribe and resume with a fresh read. Do not create a steady timer when all change sources are covered. Use intervalMs: null for event-driven snapshots. useVisiblePolling still supports an optional focus read without a timer.

A server cache must expire before the subsequent read: use UserEntityStore.invalidate(userId) or an equivalent domain-owned policy. Otherwise the socket can work perfectly while HTTP keeps returning the cached old value. Native watchers complement successful mutation notifications; they do not replace revision checks, write serialization or draft conflict handling.

Retain polling only for a concrete coverage gap: unsupported watchers, a lost connection, metrics without an event API, or a Git directory outside the watched project. Local clocks and batched analytics writes are not state polling. Event delivery is not distributed exactly-once execution; reconnect reads and existing revision/idempotency guards remain necessary.

Validate a mini app

Exercise initial subscription, edits from another window, agent/API writes, atomic file replacement, bursts during an in-flight read, reconnect, scope changes, watcher failure and unmount. Assert that an older answer cannot undo a newer mutation, a dirty draft survives, and a healthy idle resource makes no periodic requests. Test user isolation and release both write and filesystem listeners.

Source of truth