Skip to content

SyncedStoreClient API Reference

Full reference for the store client returned by Poe.setupStore(...). For the typed wrapper, see InferSyncedStoreClient<Schema> in api-patterns.md.

Typed Client

typescript
import type { InferSyncedStoreClient } from "poe-tiles-sdk/v1/client.js";
import type { TodoSchema } from "./schema";

export type TodoStoreClient = InferSyncedStoreClient<TodoSchema>;
// Gives you typed: store.mutate.setTodo, store.subscribe, store.action.generateWithAI, etc.

Reading Data

query(fn) — One-time read

typescript
const todo = await store.query((ctx) => ctx.table("todos").get("todo-1"));

subscribe(queryFn, callback) — Reactive updates

The callback fires once with the current result, then whenever an exact key or scan prefix read through ctx changes. Reads from values captured outside ctx are not storage dependencies. After changing a captured value that affects the query, call subscription.refresh() to re-run the query and replace its tracked dependencies.

The return value is a SubscriptionControl: call it as a function to unsubscribe, or call refresh(): Promise<void> to refresh the result and its dependencies.

typescript
let selectedListId = "inbox";
const subscription = store.subscribe(
  (ctx) => ctx.table("todos").scan({ prefix: { itemKey: `${selectedListId}/` } }).values().toArray(),
  (todos) => renderTodoList(todos),
);

selectedListId = "today";
await subscription.refresh();
subscription(); // unsubscribe during cleanup

Writing Data

mutate — Optimistic mutations

typescript
const { id, confirmed } = await store.mutate.setTodo({ id: "abc", text: "Buy milk" });
await confirmed; // optionally wait for server

Use pendingMutationDedupeKey for high-frequency, last-value-wins setters where intermediate offline values do not matter:

typescript
await store.mutate.setPosition(
  { playerId, x, y },
  { pendingMutationDedupeKey: `player-position:${playerId}` },
);

A live client keeps all optimistic pending mutations. If the app is restored from local storage or the offline push queue drains after reconnect, intermediate pending mutations with the same key may be skipped so only the first and latest queued/restored mutations are sent or restored. Use this only for idempotent/state-setting mutations such as cursor or player position updates. Do not use it when later pending mutations depend on skipped intermediate values, or when every mutation represents a distinct event that the server must observe.

action — Server-side actions

Waits for pending mutations to flush first, then calls the server.

typescript
const result = await store.action.generateWithAI({ id: "abc", prompt: "dinner ideas" });

Connection & State

Property / MethodReturns
state'initializing' | 'ended'
connectionStatus'connecting' | 'connected' | 'disconnected'
isOnlineboolean
isBootstrappedboolean (authoritative data ready to render)
hasServerDataboolean (first server-origin data event arrived this session)
isEnded()boolean
endReason'kicked' | 'auth_failed' | 'application' | null

Waiting for Data

None of these are required — queries and mutations work immediately.

MethodResolves when
waitForLocalData()Device storage loaded
waitForServerData()First server-origin data event arrives (which may precede pull completion)
waitForInitialPull()Initial server pull is fully applied
waitForBootstrap()Authoritative data is ready to render (either source)

IDs

typescript
const ordinal = await store.getClientOrdinal(); // Sequential: 0, 1, 2...
const id = await store.makeUniqueId();          // Sortable: "5-61"

Pending Mutations

typescript
const pending = await store.getPendingMutations();
const count = store.getPendingCount();
await store.waitForSync(); // Wait for all pending to confirm

store.onPendingMutationsChanged((mutations) => {
  showSavingIndicator(mutations.length > 0);
});

Subscriptions

typescript
store.subscribeToConnectionStatus((status) => { /* ... */ });
store.onOnlineChanged((isOnline) => { /* ... */ });
store.subscribeToTable("todos", (entries, changes, ctx) => {
  // ctx.userId — current user ID
  // First callback: all entries appear in changes.added (compared against empty)
  // Subsequent: changes.added, changes.modified, changes.removed are deltas
});
store.subscribeToScanEntries("user:", (entries, changes) => { /* ... */ });

subscribe() and subscribeToTable() return a SubscriptionControl. Calling it unsubscribes; await subscription.refresh() re-runs the query and replaces its tracked storage dependencies. Lifecycle and event subscription methods return plain unsubscribe functions.

Error & Lifecycle Events

typescript
store.onFailedMutation((info) => {
  console.error(`${info.mutation.name} failed:`, info.error.message);
});
store.onBackgroundError((error) => {
  const message =
    error.kind === "failed_mutation" ? error.info.error.message : error.message;
  showToast(message);
});
store.onKicked((reason) => { /* duplicate clientId or admin action */ });
store.onAuthFailed((reason) => { /* expired token */ });
store.onDisposed(() => { /* cleanup */ });

Store diagnostics

getDiagnostics() returns the authoritative live-data usage after the caller passes the store's normal authorization policy:

typescript
const diagnostics = await store.getDiagnostics();
// {
//   liveDataSize: number,
//   maxLiveDataSize: number,
//   unit: "logical_size_units"
// }

The unit follows the logical formula documented in limitations.md; it is not SQLite file size or a UTF-8 byte count. A legacy store awaiting the platform backfill rejects with live_data_size_not_initialized rather than returning an inexact number; retry after the backfill reaches that store.

Cleanup

typescript
store.dispose(); // Closes WebSocket, clears timers. Not reversible.