Skip to content

@syncraft-labs/core

The @syncraft-labs/core package provides the underlying engine for Syncraft Labs. It is framework-agnostic and can be used in any JavaScript/TypeScript environment.

API Reference

createSyncStore<T>(config): SyncStore<T>

Create a new store instance. Each store manages one slice of state identified by storageKey.

Config: SyncStoreConfig<T>

| 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 overflow strategy triggers | | overflowStrategy | "reject" \| "dropOldest" \| "forceFlush" | "reject" | Behavior when maxOutboxSize is reached: "reject" throws Error, "dropOldest" drops oldest entry with warning, "forceFlush" invokes onOverflow callback to attempt sync before write | | onOverflow | (info: OutboxOverflowInfo) => void \| Promise<void> | undefined | Callback invoked on outbox overflow events | | storageMode | "document" \| "collection" | "document" | Storage strategy: "document" (single blob) or "collection" (per-entity records) | | idField | string | undefined | Required when storageMode is "collection". Property name of entity unique ID |

Returns: SyncStore<T>

MethodSignatureDescription
get()() => Promise<T | undefined>Async read — memory → IndexedDB fallback
getSnapshot()() => T | undefinedSynchronous read from memory (fast path)
set(updater)(updater: DraftUpdater<T>) => Promise<void>Mutate via draft. Optimistic + durable
subscribe(listener)(listener: SyncListener<T>) => UnsubscribeListen to state changes
hydrate()() => Promise<T | undefined>Load from IndexedDB (call once on init)
getOutbox()() => Promise<readonly OutboxEntry<T>[]>Read pending mutations
compactOutbox()() => Promise<readonly OutboxEntry<T>[]>Return compacted view of outbox entries (last-write-wins)
clearOutbox(ids)(ids: readonly string[]) => Promise<void>Remove synced entries by ID
destroy()() => voidClose IndexedDB connection, clear listeners
isHydratingboolean (getter)true until hydrate() completes

Core 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[]; // Applied JSON patches
readonly inversePatches: Patch[]; // Inverse patches for rollback
}

Utilities

deepFreeze<T>(obj: T): Readonly<T>

Recursively freeze an object and all nested properties using Object.freeze(). Used in development mode (NODE_ENV !== "production") to prevent accidental direct state mutations.

import { deepFreeze } from "@syncraft-labs/core";
const frozen = deepFreeze({ user: { name: "Alice" } });
// frozen.user.name = "Bob"; // Throws TypeError in strict mode

assertNoCycles(obj: unknown, context?: string): void

Recursively traverse an object tree and throw a descriptive Error if a circular reference is detected. Supports DAG (Directed Acyclic Graph) / diamond shared references without false positives.

import { assertNoCycles } from "@syncraft-labs/core";
assertNoCycles(state, "Initial state");
// Throws Error: [Syncraft Labs] Circular reference detected at path "a.b.self" during Initial state. State must be a plain acyclic object tree.

validateStateShape(obj: unknown, context?: string): void

Recursively traverse a state object tree to validate that all values conform to supported state shapes (plain objects, arrays, primitives, and Date leaves). Emits a dev warning for Date instances and throws an Error for unsupported types like Map, Set, RegExp, functions, or custom class instances.

import { validateStateShape } from "@syncraft-labs/core";
validateStateShape(state, "hydrate()");
// Warns: [Syncraft Labs] Date detected at path "user.createdAt" during hydrate() — Dates are allowed as leaf values but must be replaced wholesale rather than having their fields mutated.
// Throws: [Syncraft Labs] Unsupported type "Map" detected at path "cache.entries" during hydrate(). State must only contain plain objects, arrays, and primitives. Use a plain object instead.

isUnsupportedType(value: unknown): boolean

Check if a given value is unsupported for state persistence and draft proxying.

import { isUnsupportedType } from "@syncraft-labs/core";
isUnsupportedType(new Map()); // true
isUnsupportedType(new Date()); // false (allowed as leaf)
isUnsupportedType({ a: 1 }); // false

compactOutbox<T>(entries: readonly OutboxEntry<T>[]): CompactResult<T> | null

Compact an array of outbox entries by merging consecutive mutations to the same path (last-write-wins). Returns { compacted, originalIds } or null if empty.

import { compactOutbox } from "@syncraft-labs/core";
const result = compactOutbox(outboxEntries);
if (result) {
await pusher([result.compacted]);
await store.clearOutbox(result.originalIds);
}

applyPatches<T>(baseState: T, patches: readonly Patch[]): T

Apply an array of Immer-style JSON patches to a base state object. Returns a new state object without mutating the original.

import { applyPatches } from "@syncraft-labs/core";
const nextState = applyPatches(baseState, entry.patches);

Patch

interface Patch {
op: "replace" | "add" | "remove";
path: (string | number)[];
value?: unknown;
}

SyncListener<T>

type SyncListener<T> = (state: T) => void;

Unsubscribe

type Unsubscribe = () => void;