Appearance
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 cleanupWriting Data
mutate — Optimistic mutations
typescript
const { id, confirmed } = await store.mutate.setTodo({ id: "abc", text: "Buy milk" });
await confirmed; // optionally wait for serverUse 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 / Method | Returns |
|---|---|
state | 'initializing' | 'ended' |
connectionStatus | 'connecting' | 'connected' | 'disconnected' |
isOnline | boolean |
isBootstrapped | boolean (authoritative data ready to render) |
hasServerData | boolean (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.
| Method | Resolves 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.