Core Concepts
Syncraft Labs operates heavily on the concept of Optimistic Updates and Eventual Consistency. To use it effectively, you must understand the mental model behind its data flow.
Data Flow
Every time you mutate state via update(), the data flows through several stages instantly:
┌─────────────────────────────────────────────────────────────┐│ User Action: update(draft => { draft.count++ }) │└──────────────────────────────┬──────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Proxy Draft Generator (produceWithPatches) ││ Generates: nextState, patches, inversePatches │└──────┬───────────────────────┬───────────────────────┬──────┘ │ │ │ ▼ ▼ ▼┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐│ Memory Cache │ │ IndexedDB State │ │ IndexedDB Outbox ││ (Optimistic)│ │ (Durable State) │ │ (Mutations Log) │└──────┬───────┘ └─────────┬────────┘ └────────┬─────────┘ │ │ │ ▼ ▼ ▼┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐│ Subscribers │ │ Revert on Failure│ │ Background Sync ││ Notified │ │ (Auto-Rollback)│ │ (Pusher Loop) │└──────────────┘ └──────────────────┘ └──────────────────┘- User Action: You call
update(draft => { draft.count++ }). - Draft processing: Our zero-dependency, proxy-based
produceWithPatchesgenerator intercepts draft mutations and produces thenextState, as well aspatchesandinversePatches. - Optimistic UI: The in-memory cache is updated instantly. The UI re-renders synchronously.
- Persistence: The
nextStateis written to IndexedDB. A newOutboxEntryis appended to the outbox queue in IndexedDB. - Synchronization: The background loop wakes up, reads the outbox, and attempts to push pending entries to the server.
Optimistic Updates & Rollback
Because Syncraft Labs updates the memory instantly (before IndexedDB confirms the write), the UI never hangs on slow storage.
However, if the IndexedDB write fails (e.g., due to strict storage quotas in private browsing), Syncraft Labs performs an automatic rollback:
- The memory is reverted to the previous state using captured
inversePatches. - All subscribers are re-notified with the reverted state.
- The UI gracefully snaps back, and the
errorstate is populated for you to render a warning.
Architecture
Syncraft Labs is structured into three distinct layers to provide flexibility and SSR safety:
┌─────────────────────────────────────────────────────────────┐│ Component Layer (React / Vue) ││ ││ useSync("todos", { fetcher, pusher }) ││ │ ││ ├──▶ useSyncExternalStore / shallowRef ││ ├──▶ Auto-hydration (onMount) ││ ├──▶ Background sync loop (pusher) ││ └──▶ Network tracking (online/offline) │└─────────────────┬───────────────────────────────────────────┘ │┌─────────────────▼───────────────────────────────────────────┐│ Core Layer (@syncraft-labs/core) ││ ││ createSyncStore<T>({ storageKey, initialState }) ││ │ ││ ├──▶ In-memory cache (instant reads) ││ ├──▶ Custom produceWithPatches (immutable mutations) ││ ├──▶ Subscriber notifications (sync, immediate) ││ └──▶ Optimistic update + rollback on failure │└─────────────────┬───────────────────────────────────────────┘ │┌─────────────────▼───────────────────────────────────────────┐│ Storage Layer (IndexedDB via idb) ││ ││ Database: "syncraft-labs_{key}" ││ ┌──────────────────┐ ┌────────────────────────┐ ││ │ state store │ │ outbox store │ ││ │ key: "current" │ │ key: entry.id (UUID) │ ││ │ value: T │ │ value: OutboxEntry<T> │ ││ └──────────────────┘ └────────────────────────┘ │└─────────────────────────────────────────────────────────────┘Storage Schema
Each storageKey you provide creates its own, isolated IndexedDB database prefixed with syncraft-labs_.
For example, useSync("my-cart") will create a database named syncraft-labs_my-cart.
Inside this database, there are object stores configured based on your storageMode:
statestore (storageMode: "document"): Holds exactly one row (key: "current"). This contains the full snapshot blob of your data.state_entitiesstore (storageMode: "collection"): Holds individual per-entity records keyed by entity ID. Updating one entity writes only its record rather than rewriting the entire state blob.outboxstore: Holds pending mutations. Each row is anOutboxEntrycontaining the exact patches that were applied, a timestamp, and a UUID.