livestorejs/livestore
View on GitHubXState integration as a state target (rebase‑aware)
Open
#842 opened on Nov 20, 2025
design decisionenhancementevent-sourcingexperimenthelp wantedneeds-researchstate:needs-researchtype:feature
Repository metrics
- Stars
- (3,599 stars)
- PR merge metrics
- (PR metrics pending)
Description
XState integration as a state target (rebase‑aware)
Motivation
- Allow apps to model complex workflows/constraints with XState while keeping LiveStore’s event log the source of truth.
- Treat XState as an additional (derived) state target next to SQLite materialization, enabling statecharts for orchestration, validation, and UI coordination.
- Preserve LiveStore guarantees (deterministic materialization, rebase handling, sync) while unlocking statechart-level expressiveness.
High‑Level Design
- Introduce
State.XStatealongsideState.SQLite. - Map LiveStore events to XState events; drive machines from the event log.
- Provide a rebase‑aware interpreter lifecycle: on rebase, reinitialize from a safe snapshot and replay the rebased event sequence deterministically.
- Expose machine snapshots as queryables so apps can subscribe to machine state (state value + context) similarly to tables/live queries.
Current Decisions
- Additive to SQLite: Yes — XState runs alongside existing SQLite state materialization (not a replacement).
- Machine granularity: Open — needs careful thought (global vs per‑entity instances).
- Effect policy during replay: Open — promising direction is to expose a
replayflag (and metadata likerebaseGeneration,lastSeq) in a callback context to suppress or gate effects. - Snapshotting: Open — cadence, retention, and storage location need exploration.
- Ordering/consistency across targets: Open — define guarantees between SQLite materializers and XState updates.
- React API: Open — evaluate
useActor/useSelector‑style hooks vs. LiveStore‑native queryables.
Rebase Semantics
- Canonical source remains the event log. Machines are derived, not primary writers.
- On upstream rebase: compute new sequence, reset interpreter to last valid snapshot baseline, replay events in order to compute current machine state.
- Side effects during replay must be suppressed or idempotent:
- Prefer pure actions for state transitions during materialization.
- For effectful actions, require idempotency keys and at‑least‑once semantics, or opt into “no‑effects‑during‑replay” via
context.replay === true.
- Track
rebaseGenerationand last applied sequence number per machine instance to coordinate snapshots and avoid double‑applying transitions.
API Sketch
Note: This API sketch is intentionally very rough and subject to change based on design discussions and spike learnings.
import { State, Schema } from '@livestore/livestore'
import { createMachine } from 'xstate'
const OrderMachine = createMachine(/* typed context/events */)
const machines = State.XState.machines({
order: State.XState.machine({
name: 'order',
machine: OrderMachine,
// map LiveStore events → XState events
onEvent: ({ event, replay, rebaseGeneration, lastSeq }) => {
// return array of XState events or undefined
},
snapshot: {
strategy: 'periodic', // 'none' | 'every-n' | 'periodic'
every: { events: 500 }, // or time‑based
},
effects: { policy: 'suppress' }, // 'suppress' | 'idempotent'
}),
})
const state = State.SQLite.makeState({ tables, materializers })
const actors = State.XState.machines({ /* ... */ })
// createStore({ state: State.compose([state, actors]) })
// Querying
queryDb(State.XState.select('order').byId(orderId)) // → { value, context }
Implementation Notes
- Persistence: Store per‑machine snapshots in an internal table (or expose helpers to project tables) with fields
{ machine, instanceId, value, context, lastSeq, rebaseGeneration }. - Startup: Hydrate interpreters from latest snapshot, then stream deltas from
lastSeq+1. - Ordering: Apply machine updates after SQL materializers or make ordering explicit; ensure deterministic reads across targets.
- Performance: Bounded snapshotting and lazy instantiation for many machine instances; dispose inactive actors.
Open Questions
- Machine granularity
- Single global machine vs per‑entity instance machines; should we provide first‑class helpers for sharded/instance machines?
- Event mapping
- Where should event→machine mapping live? Inline in machine def, or a shared mapper derived from
EventDefRecord? Do we want codegen?
- Where should event→machine mapping live? Inline in machine def, or a shared mapper derived from
- Effects & replay
- Policy for effectful actions/invocations during rebase/replay; is
context.replayenough, or do we require idempotency contracts and a “confirmed‑only” effects bus?
- Policy for effectful actions/invocations during rebase/replay; is
- Snapshots
- Default cadence, limits, and retention; internal system tables vs user‑owned tables.
- Ordering & consistency
- Do we need strict ordering guarantees between SQLite materializers and machine updates, or is eventual consistency acceptable for cross‑target invariants?
- React integration
- Should we expose
useActor/useSelector‑style hooks piggybacking on LiveStore subscriptions, or keep machine snapshots as queryables only?
- Should we expose
Related
- #352 More materialization targets (JavaScript objects, DuckDB, ...) — umbrella for supporting various state materializations.
- (Possibly related) #730 Automatic re‑materialization when materializers or event schema change.
If this direction sounds good, we can spike an MVP API and internal tables, then iterate on effects and devtools.
Footnote: created by assistant on behalf of @schickling to capture design and open questions.