@syncraft-labs/core
v0.2.0
Published
Local-first state synchronization engine — core library.
Downloads
511
Maintainers
Readme
@syncraft-labs/core
Local-first state synchronization engine — framework-agnostic core library.
@syncraft-labs/core is the engine behind Syncraft Labs. It provides a type-safe, framework-agnostic store that combines in-memory caching for instant reads, IndexedDB for offline persistence, and an outbox queue for eventual synchronization.
Install
npm install @syncraft-labs/coreQuick Start
import { createSyncStore } from "@syncraft-labs/core";
interface AppState {
count: number;
}
const store = createSyncStore<AppState>({
storageKey: "my-counter",
initialState: { count: 0 },
});
// 1. Hydrate from IndexedDB
await store.hydrate();
// 2. Read state (synchronous after hydration)
const state = store.getSnapshot(); // { count: 0 }
// 3. Mutate with Immer drafts — instant + durable
await store.set((draft) => {
draft.count += 1;
});
// 4. Subscribe to changes
const unsubscribe = store.subscribe((newState) => {
console.log("State:", newState.count);
});
// 5. Clean up when done
unsubscribe();
store.destroy();API
createSyncStore<T>(config): SyncStore<T>
Create a new store instance. Each store manages one slice of state identified by storageKey.
Config: SyncStoreConfig<T>
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| storageKey | string | required | Unique key for IndexedDB database name |
| initialState | T | undefined | Default state when no persisted data exists |
| maxOutboxSize | number | 1000 | Maximum outbox entries before set() throws |
Returns: SyncStore<T>
| Method | Signature | Description |
|--------|-----------|-------------|
| get() | () => Promise<T \| undefined> | Async read — memory → IndexedDB fallback |
| getSnapshot() | () => T \| undefined | Synchronous read from memory (fast path) |
| set(updater) | (updater: DraftUpdater<T>) => Promise<void> | Mutate via Immer draft. Optimistic + durable |
| subscribe(listener) | (listener: SyncListener<T>) => Unsubscribe | Listen to state changes |
| hydrate() | () => Promise<T \| undefined> | Load from IndexedDB (call once on init) |
| getOutbox() | () => Promise<readonly OutboxEntry<T>[]> | Read pending mutations |
| clearOutbox(ids) | (ids: readonly string[]) => Promise<void> | Remove synced entries by ID |
| destroy() | () => void | Close IndexedDB connection, clear listeners |
| isHydrating | boolean (getter) | true until hydrate() completes |
Types
DraftUpdater<T>
type DraftUpdater<T> = (draft: T) => void | T;Two patterns:
- Mutate the draft (most common):
(draft) => { draft.count += 1; } - Replace entirely:
() => freshDataFromServer
OutboxEntry<T>
interface OutboxEntry<T> {
readonly id: string; // UUID v4
readonly timestamp: number; // Unix ms
readonly patches: Patch[]; // Immer patches (what changed)
readonly inversePatches: Patch[]; // Undo patches (for rollback)
readonly snapshot: T; // Full state after mutation
}SyncListener<T>
type SyncListener<T> = (state: T) => void;Unsubscribe
type Unsubscribe = () => void;Data Flow
store.set(draft => { draft.count++ })
│
▼
┌─────────────────────────────┐
│ Immer produceWithPatches │ → nextState, patches, inversePatches
└─────────────────────────────┘
│
├──▶ Update in-memory cache (instant, optimistic)
├──▶ Notify all subscribers (triggers UI re-render)
├──▶ Write to IndexedDB (durable)
└──▶ Append OutboxEntry to IndexedDB (for eventual sync)
If IndexedDB write fails:
└──▶ Rollback memory + re-notify subscribersStorage Schema
Each storageKey maps to its own IndexedDB database:
Database: "syncraft-labs_{storageKey}"
├── state store (key: "current", value: T)
└── outbox store (key: entry.id, value: OutboxEntry<T>)Optimistic Updates & Rollback
When you call store.set():
- Memory updates instantly — the UI sees the change immediately
- IndexedDB writes async — persists for durability
- If persistence fails — memory is rolled back to the previous state, subscribers are re-notified
This ensures the UI is never stuck showing data that isn't actually persisted.
Framework Integrations
- React:
@syncraft-labs/react—useSynchook withuseSyncExternalStore - Vue:
@syncraft-labs/vue—useSynccomposable withshallowRef
License
MIT © Denis Listiadi
