livestorejs/livestore

XState integration as a state target (rebase‑aware)

Open

#842 opened on Nov 20, 2025

View on GitHub
 (0 comments) (0 reactions) (0 assignees)TypeScript (128 forks)github user discovery
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.XState alongside State.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 replay flag (and metadata like rebaseGeneration, 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 rebaseGeneration and 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?
  • Effects & replay
    • Policy for effectful actions/invocations during rebase/replay; is context.replay enough, or do we require idempotency contracts and a “confirmed‑only” effects bus?
  • 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?

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.

Contributor guide