Serialization
Functions for encoding and decoding machine snapshots using Effect Schema. Enables type-safe persistence and cross-tab synchronization.
Why Schema-Based Context?
When your context contains complex types like Date, you need proper serialization:
const ContextSchema = Schema.Struct({ count: Schema.Number, lastUpdated: Schema.DateFromString, // Date <-> ISO string});- Encoding:
Dateβ"2024-01-15T10:30:00.000Z" - Decoding:
"2024-01-15T10:30:00.000Z"βDate
encodeSnapshot
Encodes a snapshot to a JSON-safe format. Returns an Effect.
function encodeSnapshot<TStateValue, TContext, TContextEncoded>( machine: MachineDefinition<..., TContextEncoded>, snapshot: MachineSnapshot<TStateValue, TContext>,): Effect.Effect<EncodedSnapshot<TStateValue, TContextEncoded>, ParseResult.ParseError>Example:
import { encodeSnapshot } from "effstate";
const program = Effect.gen(function* () { const encoded = yield* encodeSnapshot(machine, actor.getSnapshot()); localStorage.setItem("state", JSON.stringify(encoded));});encodeSnapshotSync
Synchronous version of encodeSnapshot. Throws on error.
function encodeSnapshotSync<TStateValue, TContext, TContextEncoded>( machine: MachineDefinition<..., TContextEncoded>, snapshot: MachineSnapshot<TStateValue, TContext>,): EncodedSnapshot<TStateValue, TContextEncoded>Example:
import { encodeSnapshotSync } from "effstate";
const encoded = encodeSnapshotSync(machine, actor.getSnapshot());localStorage.setItem("state", JSON.stringify(encoded));decodeSnapshot
Decodes a snapshot from JSON-safe format. Returns an Effect.
function decodeSnapshot<TStateValue, TContext, TContextEncoded>( machine: MachineDefinition<..., TContextEncoded>, encoded: EncodedSnapshot<TStateValue, TContextEncoded>,): Effect.Effect<MachineSnapshot<TStateValue, TContext>, ParseResult.ParseError>Example:
import { decodeSnapshot, interpret } from "effstate";
const program = Effect.gen(function* () { const stored = JSON.parse(localStorage.getItem("state")!); const snapshot = yield* decodeSnapshot(machine, stored); const actor = yield* interpret(machine, { snapshot }); return actor;});decodeSnapshotSync
Synchronous version of decodeSnapshot. Throws on error.
function decodeSnapshotSync<TStateValue, TContext, TContextEncoded>( machine: MachineDefinition<..., TContextEncoded>, encoded: EncodedSnapshot<TStateValue, TContextEncoded>,): MachineSnapshot<TStateValue, TContext>Example:
import { decodeSnapshotSync, interpretSync } from "effstate";
const stored = JSON.parse(localStorage.getItem("state")!);const snapshot = decodeSnapshotSync(machine, stored);const actor = interpretSync(machine, { snapshot });createSnapshotSchema
Creates an Effect Schema for validating encoded snapshots. Useful when loading from untrusted sources.
function createSnapshotSchema<TStateValue, TContext, TContextEncoded>( machine: MachineDefinition<..., TContextEncoded>,): Schema.Schema< MachineSnapshot<TStateValue, TContext>, EncodedSnapshot<TStateValue, TContextEncoded>>Example:
import { createSnapshotSchema } from "effstate";import { Schema } from "effect";
const snapshotSchema = createSnapshotSchema(machine);
// Validate untrusted dataconst untrustedData = JSON.parse(userInput);const validatedSnapshot = Schema.decodeUnknownSync(snapshotSchema)(untrustedData);getContextSchema
Extracts the context schema from a machine definition.
function getContextSchema<TContext, TContextEncoded>( machine: MachineDefinition<..., TContextEncoded>,): Schema.Schema<TContext, TContextEncoded>Example:
import { getContextSchema } from "effstate";
const contextSchema = getContextSchema(machine);
// Use for partial context validationconst partialUpdate = Schema.decodeSync(contextSchema)(userInput);EncodedSnapshot Type
The JSON-safe encoded format:
interface EncodedSnapshot<TStateValue extends string, TContextEncoded> { readonly value: TStateValue; readonly context: TContextEncoded;}Note: The event field is not persisted; itβs set to null on decode.
Full Persistence Example
import { createMachine, interpret, encodeSnapshotSync, decodeSnapshotSync,} from "effstate";import { Schema } from "effect";
// Context with Date fieldconst ContextSchema = Schema.Struct({ count: Schema.Number, lastUpdated: Schema.DateFromString,});
const machine = createMachine({ id: "counter", initial: "idle", context: ContextSchema, initialContext: { count: 0, lastUpdated: new Date() }, states: { /* ... */ },});
// Save stateconst save = (actor: MachineActor) => { const encoded = encodeSnapshotSync(machine, actor.getSnapshot()); localStorage.setItem("counter", JSON.stringify(encoded));};
// Load stateconst load = () => { const stored = localStorage.getItem("counter"); if (!stored) return null; return decodeSnapshotSync(machine, JSON.parse(stored));};
// Usageconst persisted = load();const actor = persisted ? interpretSync(machine, { snapshot: persisted }) : interpretSync(machine);
actor.subscribe(() => save(actor));Parent-Child Serialization
When persisting machines with children, handle each level:
const saveWithChildren = (actor: MachineActor) => { const parentSnapshot = encodeSnapshotSync(parentMachine, actor.getSnapshot());
const childSnapshots: Record<string, EncodedSnapshot> = {}; actor.children.forEach((child, id) => { childSnapshots[id] = encodeSnapshotSync(childMachine, child.getSnapshot()); });
localStorage.setItem("state", JSON.stringify({ parent: parentSnapshot, children: childSnapshots, }));};See Also
- Cross-Tab Sync Guide - Using serialization for tab sync
- createMachine - Defining context schemas