Guide · @staticflux/data · @staticflux/storage

Data & storage

Transport-agnostic data with exactly one retry layer, caches keyed by epoch, stores whose patches replicate to workers — on top of a single async key-value interface for every backend.

Endpoints and the client

TypeScript
export const DocGetEndpoint = defineEndpoint({
  id: 'docs/get',
  path: (input: { readonly id: string }) => `/docs/${input.id}`,
  response: DocumentSchema,                    // s.object({ … }) — decoded and validated
});

const doc = await client.call(DocGetEndpoint, { id: '42' }, { signal });   // Promise<Result<Doc>>

Per call: a data/request span with traceparent → per-endpoint circuit breaker → retry (full jitter; idempotent endpoints by default) → per attempt: pause gate, timeout, transport, schema decode, epoch check. Transports never retry and resources never retry — the client is the only retry layer. Transports: fetch (JSON + NDJSON), SSE, WebSocket, host bridge, worker port and a mock with latency, failures and offline mode.

Resources: SWR, pagination, batching, streams

TypeScript
export const DocResource = defineResource({ id: 'docs/get-resource', endpoint: DocGetEndpoint, key: (input) => [input.id] });
export const ProfilesResource = defineBatchedResource<string, readonly Profile[], Profile>({
  id: 'users/profiles-resource', endpoint: UserBatchProfilesEndpoint,
  pick: (response, key) => response.find((p) => p.id === key), keyId: (key) => key, staleTimeMs: 60_000,
});

const handle = resources.query(DocResource, { id: '42' });  // reactive: status, data, error
// status: idle | loading | ready | refreshing | error — refreshing keeps the last data (SWR)

Paginated (cursorPagination / offsetPagination), batched (DataLoader-style coalescing), streamed and subscription resources share the cache. After an app refresh the epoch bumps: active resources revalidate and responses tagged with an old epoch are discarded (I-10). mutate + optimistic(…) rolls back on failure.

Schema-first stores with patches

TypeScript
export const ChatStore = defineStore({
  id: 'agent/chat',
  version: 1,
  schema: ChatStateSchema,
  initial: (): ChatState => ({ messages: [], draft: '', status: 'idle' }),
  actions: {
    append(state: ChatState, message: ChatMessage) { state.messages.push(message); },
    setDraft(state: ChatState, text: string) { state.draft = text; },
  },
});
const owner = ChatStore.create();      // the owner; .replica({ sink }) on other threads

Actions are synchronous, touch only a draft and roll back entirely on throw; state is frozen plain data. Each outermost action emits one versioned PatchMessage — the format @staticflux/worker replicates and hydration snapshots.

User-data slices

TypeScript
export const ThemeSlice = defineSlice({
  key: 'settings/theme', scope: 'user', version: 1,
  schema: s.object({ mode: s.enum(['system', 'light', 'dark', 'high-contrast']) }),
  default: () => ({ mode: 'system' as const }),
});
const theme = userData.slice(ThemeSlice);     // reactive value, batched flushes, migrations
theme.set({ mode: 'dark' });

Slices are scoped device | user | workspace | session, versioned with migrations on read (a failed migration yields defaults and a report — never a crash), conflict-resolved per slice, budgeted, and wiped on logout by default.

One async storage interface

TypeScript
const store = createStore<Prefs>({
  backend: [createIndexedDbStore({ namespace: 'prefs' }), () => createMemoryStore()],
  middleware: [withBudget({ maxBytes: 64_000 })],
});
// AsyncStore: get · set · delete · has · clear · seek · batch · watch — all Promise<Result>

Adapters: memory, localStorage / sessionStorage, cookies, IndexedDB, OPFS, remote. Middleware returns the same interface: namespaced, withTtl, withCodec (JSON, versioned envelope, binary), encrypted (AES-GCM, key rotation), withBudget, withTelemetry, tiered (write-through / write-back), fallback. Slices, handoff, feature archives and advisor history all accept an AsyncStore through their *FromStore adapters. Large immutable content — images, video, document parts — goes to the content-addressed @staticflux/blob store instead (see the editors guide).

AGENTS.md on GitHub: @staticflux/data · @staticflux/storage · @staticflux/handoff