PackagesReplica@lunora/replica

@lunora/replica

Local-first replica runtime for Lunora — EventEmitter, subscriptions, snapshot DO, and replay.

@lunora/replica provides a local-first replica runtime and local SQLite mirror for Lunora. It combines an append-only event log, a type-safe reducer-based state derivation engine (EventSource), a typed EventEmitter, a Durable Object-backed event log for persistence in the Cloudflare environment, and a local SQLite mirror (LocalMirror) that applies typed row-level diffs in browser/Node.js.

import { EventSource } from "@lunora/replica";

interface TodoState {
    items: Array<{ id: string; title: string; done: boolean }>;
}

const reducer = (state: TodoState, entry): TodoState => {
    switch (entry.type) {
        case "todo-added":
            return { items: [...state.items, { ...entry.payload, done: false }] };
        case "todo-toggled":
            return {
                items: state.items.map((t) => (t.id === entry.payload.id ? { ...t, done: !t.done } : t)),
            };
        default:
            return state;
    }
};

const source = new EventSource<TodoState>({ items: [] }, reducer);

// Apply an event — updates state and records it in the log:
source.applyEvent("todo-added", { id: "1", title: "Write docs" });
console.log(source.state.items.length); // 1

// Replay from an existing log to restore state:
source.replayFromLog(existingLog);
console.log("replayed:", source.replayed); // true

Install

pnpm add @lunora/replica
# @lunora/server is an optional peer — only needed when using EventLogDO:
pnpm add @lunora/server
# sql.js is an optional peer — only needed when using LocalMirror in the browser:
pnpm add sql.js

Key exports

ExportDescription
EventSourceReducer-based state machine that derives state from an append-only EventLog.
EventEmitterTyped event emitter with on/off/emit and wildcard listeners.
defineEventsType-safe event factory — declare event types and their payload shapes.
defineMaterializer / MaterializerRuntimeSubscribe an EventSource to a Durable Object's event feed.
EventLogDODurable Object that persists an event log to DO storage with SQLite.
EventLogDOClientClient for appending / querying events on an EventLogDO.
SubscriptionManagerManage event-type and state-change subscriptions with typed callbacks.
InMemorySnapshotStoreIn-memory store for persisting event-sourced state snapshots.
eventsContextIntegrates the event source into a Lunora action context.

EventSource

The core event-sourcing runtime. Construct it with an initial state and a reducer function; events are appended to an in-memory EventLog and the reducer derives the next state.

const source = new EventSource(initialState, reducer);

// Listen for state changes:
source.emitter.on("state-changed", ({ entry, state }) => {
    console.log(`After ${entry.type}:`, state);
});

// Apply a new event:
const entry = source.applyEvent("item-created", { id: "abc", name: "foo" });

// Replay from a persisted log on startup:
await source.replayFromLog(persistedLog);

// Reset and re-replay from a snapshot:
source.reset(snapshotState);
source.replayFromLog(afterSnapshotLog);

Events emitted by EventSource:

EventPayloadWhen
ready{ entryCount: number }After initial replay completes.
replay-error{ entry: EventLogEntry, error: Error }On a reducer error during replay.
state-changed{ entry: EventLogEntry, state: Record<string, unknown> }After every applied entry.

EventEmitter

A typed, framework-agnostic event emitter. Replaces the Node.js EventEmitter in environments where it isn't available (browsers, service workers, workerd).

import { EventEmitter } from "@lunora/replica";

interface MyEvents {
    data: { id: string; value: number };
    error: { message: string };
}

const emitter = new EventEmitter<MyEvents>();

emitter.on("data", (payload) => console.log(payload.id));
emitter.emit("data", { id: "abc", value: 42 });

// Wildcard listener — receives every event:
emitter.on("*", (event, payload) => console.log(event, payload));

// One-shot:
emitter.once("error", (payload) => console.error(payload.message));

defineEvents

Declare a family of typed events and their payloads:

import { defineEvents } from "@lunora/replica";

const todoEvents = defineEvents({
    "todo:created": (id: string, title: string) => ({ id, title, done: false }),
    "todo:toggled": (id: string) => ({ id }),
    "todo:deleted": (id: string) => ({ id }),
});

const entry = source.applyEvent("todo:created", todoEvents["todo:created"]("1", "Write docs"));

EventLogDO (Durable Object)

A Durable Object that persists events to DO SQLite storage:

// In wrangler.jsonc, export this class:
export { EventLogDO } from "@lunora/replica";

// From a mutation or action:
const client = new EventLogDOClient({ namespace: "my-app" });
const entry = await client.append({ type: "order-placed", payload: { orderId: "123" } });

// Query historical events:
const events = await client.query({ type: "order-placed", limit: 50 });

SubscriptionManager

Manage typed subscriptions to event types and state changes:

import { SubscriptionManager } from "@lunora/replica";

const subs = new SubscriptionManager();

// Subscribe to a specific event type:
const unsub = subs.onEvent("order-placed", (entry) => {
    console.log("Order placed:", entry.payload);
});

// Subscribe to any state change:
subs.onStateChange((entry) => {
    // Called after every applied event
});

// Clean up:
unsub();

eventsContext

Wires the event-sourcing runtime into a Lunora action context — use it to make events accessible in your actions:

import { eventsContext } from "@lunora/replica";

// In your schema or action definition:
export const myAction = action({
    args: {/* ... */},
    handler: eventsContext(async (ctx, args) => {
        // ctx.events.source — the EventSource
        // ctx.events.append(type, payload) — shorthand
        await ctx.events.append("user-action", { action: args.type });
    }),
});

See also