@lunora/replica 0.0.0 → 1.0.0-alpha.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE.md +445 -0
  2. package/README.md +239 -29
  3. package/dist/adapters/better-sqlite3.d.mts +29 -0
  4. package/dist/adapters/better-sqlite3.d.ts +29 -0
  5. package/dist/adapters/better-sqlite3.mjs +1 -0
  6. package/dist/adapters/sqlite-wasm.d.mts +30 -0
  7. package/dist/adapters/sqlite-wasm.d.ts +30 -0
  8. package/dist/adapters/sqlite-wasm.mjs +1 -0
  9. package/dist/adapters/sqljs.d.mts +22 -0
  10. package/dist/adapters/sqljs.d.ts +22 -0
  11. package/dist/adapters/sqljs.mjs +1 -0
  12. package/dist/index.d.mts +1074 -0
  13. package/dist/index.d.ts +1074 -0
  14. package/dist/index.mjs +1 -0
  15. package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
  16. package/dist/packem_shared/EventLog-SuC_BKwj.mjs +1 -0
  17. package/dist/packem_shared/EventLogDO-BgUx2GGL.mjs +1 -0
  18. package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
  19. package/dist/packem_shared/EventSource-Bg6zNmRn.mjs +1 -0
  20. package/dist/packem_shared/EventsSync-B3wzXm-b.mjs +1 -0
  21. package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
  22. package/dist/packem_shared/LocalMirror-Bn22hyEB.mjs +4 -0
  23. package/dist/packem_shared/MaterializerRuntime-DFXi-aqd.mjs +1 -0
  24. package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
  25. package/dist/packem_shared/applyDiff-CZAqAC8Y.mjs +1 -0
  26. package/dist/packem_shared/applyDiffToDb-CK-Dcy17.mjs +1 -0
  27. package/dist/packem_shared/classifyChanges-BBc0-770.mjs +1 -0
  28. package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
  29. package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
  30. package/dist/packem_shared/fnv1a-BNN96GYb.mjs +1 -0
  31. package/dist/packem_shared/int64-CCVxepl4.mjs +1 -0
  32. package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
  33. package/dist/packem_shared/local-mirror.d-DzREvGNM.d.mts +562 -0
  34. package/dist/packem_shared/local-mirror.d-wbGXkXaE.d.ts +562 -0
  35. package/dist/packem_shared/subscribeToMirror-DSn8n1IR.mjs +1 -0
  36. package/dist/packem_shared/types.d-BuLTPLaQ.d.mts +22 -0
  37. package/dist/packem_shared/types.d-BuLTPLaQ.d.ts +22 -0
  38. package/dist/packem_shared/wire-key-DfMHAtqH.mjs +1 -0
  39. package/dist/react.d.mts +77 -0
  40. package/dist/react.d.ts +77 -0
  41. package/dist/react.mjs +1 -0
  42. package/package.json +88 -7
@@ -0,0 +1,1074 @@
1
+ export {
2
+ /**
3
+ * \@lunora/replica — Event sourcing runtime + local SQLite mirror for Lunora
4
+ *
5
+ * ## Modules
6
+ *
7
+ * - **EventEmitter** — type-safe event emitter with typed + wildcard listeners.
8
+ * - **EventSource** — event-sourcing runtime that derives state from an
9
+ * append-only log via a reducer.
10
+ * - **SubscriptionManager** — manages state and event-type subscriptions.
11
+ * - **SnapshotStore** — interface + in-memory store for persisting
12
+ * event-sourced state snapshots.
13
+ * - **LocalMirror** — local SQLite mirror that applies typed table diffs.
14
+ * - **EventLog** — append-only log for events and catch-up replication.
15
+ */
16
+ createBetterSqlite3Adapter } from "./adapters/better-sqlite3.js";
17
+ export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.js";
18
+ export { createSqlJsAdapter } from "./adapters/sqljs.js";
19
+ import { S as SqliteAdapter } from "./packem_shared/types.d-BuLTPLaQ.js";
20
+ import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-wbGXkXaE.js";
21
+ export { type C as ClientSeq, type b as EventLogOptions, type c as EventLogSnapshot, type G as GlobalSeq, type d as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, e as classifyChanges, f as createTableDiff, g as diffSize, i as isClientSeq, h as isDiffEmpty, j as isGlobalSeq, k as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-wbGXkXaE.js";
22
+ /**
23
+ * Apply a single {@link TableDiff} to an in-memory row map and return
24
+ * the updated map.
25
+ *
26
+ * The function creates a **shallow copy** of the input map so the caller's
27
+ * reference stays untouched unless they choose to replace it.
28
+ * @example
29
+ * ```ts
30
+ * const rows = new Map<string, Record<string, unknown>>();
31
+ * rows.set("id-1", { name: "alice" });
32
+ *
33
+ * const diff = createTableDiff("users", [
34
+ * { type: "insert", data: { id: "id-2", name: "bob" } },
35
+ * { type: "update", id: "id-1", data: { name: "alice-updated" } },
36
+ * ]);
37
+ *
38
+ * const updated = applyDiff(rows, diff);
39
+ * updated.get("id-1")?.name // "alice-updated"
40
+ * updated.get("id-2")?.name // "bob"
41
+ * ```
42
+ * @experimental
43
+ */
44
+ declare const applyDiff: (current: ReadonlyMap<string, Record<string, unknown>>, diff: TableDiff) => Map<string, Record<string, unknown>>;
45
+ /**
46
+ * Apply an array of diffs **in order**, returning the final row map.
47
+ *
48
+ * This is equivalent to calling {@link applyDiff} repeatedly but copies the
49
+ * input map exactly once rather than once per diff — catch-up replay of an
50
+ * N-diff backlog is a single copy, not N+1.
51
+ * @experimental
52
+ */
53
+ declare const applyDiffs: (current: ReadonlyMap<string, Record<string, unknown>>, diffs: ReadonlyArray<TableDiff>) => Map<string, Record<string, unknown>>;
54
+ /**
55
+ * Merge the row-level effect of a {@link TableDiff} into plain JSON
56
+ * state keyed by table name, returning a new snapshot.
57
+ * @param snapshot Current snapshot, e.g. `{ users: Map<id, row>, posts: Map<id, row> }`.
58
+ * @param diff Contains the target table name and the row-level changes to merge.
59
+ * @returns A shallow copy of `snapshot` with `diff.table`'s map updated.
60
+ * @experimental
61
+ */
62
+ declare const applyDiffToSnapshot: (snapshot: ReadonlyMap<string, ReadonlyMap<string, Record<string, unknown>>>, diff: TableDiff) => Map<string, Map<string, Record<string, unknown>>>;
63
+ /** Map a namespace-and-name pair to a qualified event type string. */
64
+ type QualifiedType<Ns extends string, Name extends string> = `${Ns}.${Name}`;
65
+ /**
66
+ * Extract the payload type from an event schema.
67
+ *
68
+ * A `@lunora/values` validator (e.g. `v.object(...)`) carries its output type on
69
+ * the phantom `__type` field (the same hook `Infer` reads), so match that FIRST
70
+ * — otherwise a validator, being object-shaped, would fall through to the
71
+ * `Record` branch and resolve to the validator instance itself rather than its
72
+ * validated `{ … }` output. A bare factory function or plain descriptor object
73
+ * is still supported as a fallback.
74
+ */
75
+ type PayloadOf<T> = T extends {
76
+ readonly __type: infer P;
77
+ } ? P : T extends ((payload: infer P) => unknown) ? P : T extends Record<string, unknown> ? T : never;
78
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
79
+ /**
80
+ * Produce `{ "ns.name": Payload }` for every event, merged.
81
+ */
82
+ type EventTypeMap<TDefinition extends Record<string, Record<string, unknown>>> = UnionToIntersection<{ [Ns in keyof TDefinition & string]: { [Name in keyof TDefinition[Ns] & string]: { [K in QualifiedType<Ns, Name>]: PayloadOf<TDefinition[Ns][Name]>; }; }[keyof TDefinition[Ns] & string]; }[keyof TDefinition & string]>;
83
+ /**
84
+ * A factory function that creates an {@link InputEvent} for a specific event type.
85
+ *
86
+ * The returned event has no `seq` — it is an optimistic / command payload
87
+ * that the event log will assign a sequence number to on append.
88
+ * @experimental
89
+ */
90
+ interface EventFactory<Type extends string, Payload> {
91
+ (payload: Payload): InputEvent<Type, Payload>;
92
+ /** The fully qualified event type string (e.g. `"chat.messageSent"`). */
93
+ readonly type: Type;
94
+ }
95
+ /**
96
+ * The namespace object returned for each group of events.
97
+ * @experimental
98
+ */
99
+ type EventNamespace<Ns extends string, TDefinition extends Record<string, unknown>> = { [Name in keyof TDefinition & string]: EventFactory<QualifiedType<Ns, Name>, PayloadOf<TDefinition[Name]>>; };
100
+ /**
101
+ * The full result of {@link defineEvents}.
102
+ * @experimental
103
+ */
104
+ type EventsDefinition<TDefinition extends Record<string, Record<string, unknown>>> = { [Ns in keyof TDefinition & string]: EventNamespace<Ns, TDefinition[Ns]>; } & {
105
+ /** Type-level map of event type → payload shape. Useful for generic code. */
106
+ readonly _types: EventTypeMap<TDefinition>;
107
+ };
108
+ /**
109
+ * Declare typed event types for event sourcing.
110
+ *
111
+ * Each key under a namespace becomes a factory function that produces
112
+ * an {@link InputEvent} — an optimistic / command event that the event
113
+ * log will assign a sequence number to on append.
114
+ * @param definition A nested object where the outer keys are namespaces
115
+ * and the inner keys are event names mapped to their
116
+ * payload schemas (or simple type-descriptor objects).
117
+ * @returns An object with the same nesting structure, where each leaf is
118
+ * a factory function plus a `.type` property.
119
+ */
120
+ interface DefineEventsOptions {
121
+ /**
122
+ * Optional version prefix for all event types.
123
+ *
124
+ * When set, every qualified event type is prefixed with `"v<N>."`, enabling
125
+ * versioned event naming like `"v1.chat.messageSent"` or `"v2.chat.messageSent"`.
126
+ * This allows materializers to evolve their handling logic based on the event
127
+ * version without breaking backward compatibility.
128
+ * @example "v1" → event type becomes "v1.chat.messageSent"
129
+ */
130
+ readonly version?: string;
131
+ }
132
+ /**
133
+ * `defineEvents` is part of the experimental `@lunora/replica` API and may change without a major version bump.
134
+ * @experimental
135
+ */
136
+ declare const defineEvents: <TDefinition extends Record<string, Record<string, unknown>>>(definition: TDefinition, options?: DefineEventsOptions) => EventsDefinition<TDefinition>;
137
+ /**
138
+ * Shape of the `events[]` items sent in a POST `/append` body.
139
+ *
140
+ * Like {@link InputEvent} but with `timestamp` optional — omit it to
141
+ * let the server assign the timestamp.
142
+ * @experimental
143
+ */
144
+ interface AppendEventInput {
145
+ /** Globally-unique client identifier (for offline/optimistic support). */
146
+ readonly clientId?: string;
147
+ /** Causal parent sequence number (ClientSeq for optimistic, GlobalSeq for confirmed). */
148
+ readonly parentSeqNum?: Seq;
149
+ /** Arbitrary JSON-serialisable payload. */
150
+ readonly payload: unknown;
151
+ /** Session identifier within the client. */
152
+ readonly sessionId?: string;
153
+ /** Millisecond timestamp (epoch) — omit to let the server assign it. */
154
+ readonly timestamp?: number;
155
+ /** Event type discriminator. */
156
+ readonly type: string;
157
+ }
158
+ /**
159
+ * Options for constructing an {@link EventLogDOClient}.
160
+ * @experimental
161
+ */
162
+ interface EventLogDOClientOptions {
163
+ /**
164
+ * A function that dispatches an HTTP request to the target EventLogDO
165
+ * instance. In a Cloudflare Worker this is:
166
+ *
167
+ * ```ts
168
+ * (req) => env.MY_DO_NAMESPACE.get(id).fetch(req)
169
+ * ```
170
+ */
171
+ fetch: (request: Request) => Promise<Response>;
172
+ }
173
+ /**
174
+ * Lightweight HTTP client for EventLogDO's RPC surface.
175
+ *
176
+ * Each method maps to one of the DO's endpoints, throws on non-OK status,
177
+ * and returns the parsed response body.
178
+ * @experimental
179
+ */
180
+ declare class EventLogDOClient {
181
+ #private;
182
+ constructor(options: EventLogDOClientOptions);
183
+ /**
184
+ * Append one or more events to the log.
185
+ * @param events The events to append.
186
+ * @param options Idempotency controls for the batch.
187
+ * @param options.batchId Optional idempotency key for the whole batch — a
188
+ * retried `append` call with the same `batchId` (e.g. after a network
189
+ * timeout that hid a successful response) returns the originally-persisted
190
+ * entries instead of inserting duplicates.
191
+ * @returns The persisted entries with their assigned `seq` numbers.
192
+ */
193
+ append(events: AppendEventInput[], options?: {
194
+ batchId?: string;
195
+ }): Promise<EventLogEntry[]>;
196
+ /**
197
+ * Fetch ONE page of entries with `seq >= sinceSeq`.
198
+ *
199
+ * The DO bounds every page (500 entries unless `limit` says otherwise, 1000
200
+ * max), so `getSince(0)` is the START of the log, never all of it — a
201
+ * catch-up walks pages until `truncated` is `false`:
202
+ *
203
+ * ```ts
204
+ * let seq = 0;
205
+ * for (;;) {
206
+ * const page = await client.getSince(seq);
207
+ * apply(page.entries);
208
+ * if (!page.truncated || page.cursor === undefined) break;
209
+ * seq = page.cursor;
210
+ * }
211
+ * ```
212
+ * @returns `{ entries, truncated, cursor }` — `cursor` is the `sinceSeq`
213
+ * for the next page and is present exactly when `truncated` is `true`.
214
+ */
215
+ getSince(sinceSeq: number, limit?: number): Promise<{
216
+ cursor?: number;
217
+ entries: EventLogEntry[];
218
+ truncated: boolean;
219
+ }>;
220
+ /**
221
+ * Return the total number of entries currently in the log.
222
+ */
223
+ getSize(): Promise<number>;
224
+ /**
225
+ * Return the full log state — all entries plus the next seq number.
226
+ *
227
+ * Only for a log small enough to answer as one body: the DO refuses with a
228
+ * 413 past its page ceiling, since serialising an unbounded log into one
229
+ * response is what {@link EventLogDOClient.getSince} was bounded to avoid.
230
+ * A catch-up walks `getSince` instead.
231
+ * @throws Error when the log is too large to return in one body
232
+ */
233
+ getState(): Promise<{
234
+ entries: EventLogEntry[];
235
+ nextSeq: number;
236
+ }>;
237
+ }
238
+ /**
239
+ * Type-safe event emitter that powers the event-sourcing runtime.
240
+ * @example
241
+ * ```ts
242
+ * type MyEvents = { userCreated: { id: string; name: string }; error: { message: string } };
243
+ *
244
+ * const emitter = new EventEmitter<MyEvents>();
245
+ * emitter.on("userCreated", (payload) => console.log(payload.name));
246
+ * emitter.emit("userCreated", { id: "1", name: "alice" });
247
+ * ```
248
+ * @experimental
249
+ */
250
+ declare class EventEmitter<EventMap extends Record<string, unknown>> {
251
+ #private;
252
+ /**
253
+ * Register a handler for a specific event type.
254
+ * @returns An unsubscribe function (equivalent to calling {@link off}).
255
+ */
256
+ on<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): () => void;
257
+ /**
258
+ * Remove a previously registered handler.
259
+ */
260
+ off<K extends keyof EventMap>(event: K, handler: (payload: EventMap[K]) => void): void;
261
+ /**
262
+ * Register a wildcard handler that fires for **every** event type.
263
+ * @returns An unsubscribe function.
264
+ */
265
+ onAny(handler: (event: keyof EventMap, payload: unknown) => void): () => void;
266
+ /**
267
+ * Remove a wildcard handler.
268
+ */
269
+ offAny(handler: (event: keyof EventMap, payload: unknown) => void): void;
270
+ /**
271
+ * Emit an event. All registered handlers (typed + wildcard) are invoked
272
+ * synchronously. Exceptions from handlers are caught and silently
273
+ * swallowed — they **must not** break the emitter loop.
274
+ * @returns `true` if at least one handler was called.
275
+ */
276
+ emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): boolean;
277
+ /**
278
+ * Return `true` when at least one listener is registered for `event`.
279
+ */
280
+ hasListeners(event: keyof EventMap): boolean;
281
+ /**
282
+ * Return the number of typed listeners for a specific event.
283
+ */
284
+ listenerCount(event: keyof EventMap): number;
285
+ /**
286
+ * Remove all listeners.
287
+ */
288
+ clear(): void;
289
+ }
290
+ /**
291
+ * Strategy for handling events whose `type` the reducer does not recognise.
292
+ *
293
+ * - `"warn"` _(default)_ — log a warning and skip the event (state unchanged).
294
+ * - `"ignore"` — skip silently (no warning, no error).
295
+ * - `"fail"` — throw an error, halting the apply / replay cycle.
296
+ * - A **callback** — invoked with the entry; return truthy to mark it as
297
+ * handled (no warning), falsy to fall through to the configured fallback.
298
+ * @experimental
299
+ */
300
+ type UnknownEventHandling = "warn" | "ignore" | "fail" | ((entry: EventLogEntry) => boolean);
301
+ /**
302
+ * Events emitted by the {@link EventSource} runtime.
303
+ *
304
+ * A `type` (not `interface`) so it satisfies `EventEmitter`'s
305
+ * `Record<string, unknown>` constraint — interfaces have no implicit index
306
+ * signature and aren't assignable to `Record<string, unknown>`.
307
+ * @experimental
308
+ */
309
+ type EventSourceEvents = {
310
+ /** Fired (once) after the initial replay completes. */
311
+ ready: {
312
+ entryCount: number;
313
+ };
314
+ /** Fired when a replay error occurs — the runtime will skip the bad entry. */
315
+ "replay-error": {
316
+ entry: EventLogEntry;
317
+ error: Error;
318
+ };
319
+ /** Fired after an event has been applied and the state updated. */
320
+ "state-changed": {
321
+ entry: EventLogEntry;
322
+ state: Record<string, unknown>;
323
+ };
324
+ };
325
+ /**
326
+ * Sentinel a reducer can return to EXPLICITLY signal it does not handle a
327
+ * given event's `type` — as opposed to returning the current `state`
328
+ * reference unchanged to represent a legitimate, idempotent no-op for a type
329
+ * it DOES recognise.
330
+ *
331
+ * Reference equality alone can't tell these two cases apart (REPLICA-07): a
332
+ * reducer that intentionally returns `state` for a type it fully understands
333
+ * (e.g. "already applied this event, nothing to do") would otherwise be
334
+ * misclassified as "unhandled" and trigger {@link UnknownEventHandling} — a
335
+ * spurious warning, or worse, a thrown error under `"fail"`. Return `UNHANDLED`
336
+ * only for a `type` your reducer truly does not recognise; every other return
337
+ * (including a `state` returned by reference) is treated as handled.
338
+ *
339
+ * Reducers that always recognise every event they're given (a single
340
+ * always-matching type, or a catch-all) can ignore this entirely.
341
+ * @experimental
342
+ */
343
+ declare const UNHANDLED: unique symbol;
344
+ /**
345
+ * A function that reduces an event into a state mutation.
346
+ *
347
+ * Pure functions are strongly encouraged: given the same event payload
348
+ * and state, they must produce the same next state. Return {@link UNHANDLED}
349
+ * to explicitly mark an event `type` this reducer does not process — see
350
+ * {@link UNHANDLED} for why reference equality against the input `state`
351
+ * cannot be used for this instead.
352
+ * @experimental
353
+ */
354
+ type EventReducer<S> = (state: S, entry: EventLogEntry) => S | typeof UNHANDLED;
355
+ /**
356
+ * Options for constructing an {@link EventSource}.
357
+ * @experimental
358
+ */
359
+ interface EventSourceOptions {
360
+ /**
361
+ * Cap this runtime's internal `log` to this many entries (REPLICA-06).
362
+ * `replayFromLog` copies every entry it replays from the source log into
363
+ * `this.log` too — a second, uncapped copy of the same history — so a
364
+ * long-lived `EventSource` fed by repeated replay accumulates entries in
365
+ * both places forever without a cap.
366
+ *
367
+ * `undefined` (the default) preserves unbounded retention.
368
+ */
369
+ maxLogEntries?: number;
370
+ /**
371
+ * How to handle events whose `type` is not recognised by the reducer.
372
+ * @default "warn"
373
+ */
374
+ unknownEventHandling?: UnknownEventHandling;
375
+ }
376
+ /**
377
+ * Event-sourcing runtime that maintains a derived state by replaying an
378
+ * append-only {@link EventLog}.
379
+ *
380
+ * Usage:
381
+ * ```ts
382
+ * const source = new EventSource(initialState, myReducer);
383
+ * await source.replayFromLog(existingLog);
384
+ *
385
+ * // Later, when a new event arrives:
386
+ * const entry = source.applyEvent("user-created", { id: "1", name: "alice" });
387
+ * console.log(source.state); // updated state
388
+ * ```
389
+ * @experimental
390
+ */
391
+ declare class EventSource<S extends Record<string, unknown> = Record<string, unknown>> {
392
+ #private;
393
+ readonly emitter: EventEmitter<EventSourceEvents>;
394
+ readonly log: EventLog;
395
+ constructor(initialState: S, reducer: EventReducer<S>, options?: EventSourceOptions);
396
+ /**
397
+ * The current derived state. Read-only snapshot; mutate through events.
398
+ */
399
+ get state(): Readonly<S>;
400
+ /**
401
+ * Whether the initial replay from an existing log has completed.
402
+ */
403
+ get replayed(): boolean;
404
+ /**
405
+ * Append a new event to the log and apply it to the current state.
406
+ *
407
+ * Accepts either an {@link InputEvent} (e.g. from a `defineEvents` factory)
408
+ * or the traditional `(type, payload)` pair.
409
+ * @returns The newly created log entry (with its assigned `seq`).
410
+ */
411
+ applyEvent(event: InputEvent, options?: AppendOptions): EventLogEntry;
412
+ applyEvent(type: string, payload: unknown, options?: AppendOptions): EventLogEntry;
413
+ /**
414
+ * Replay all entries from an existing {@link EventLog} to bootstrap
415
+ * the current state.
416
+ *
417
+ * Idempotent across calls: only source entries past the `#lastAppliedSeq`
418
+ * watermark are applied, so re-invoking picks up just the new entries.
419
+ * @param log The external log to replay from.
420
+ */
421
+ replayFromLog(log: EventLog): void;
422
+ /**
423
+ * Reset the runtime to a base state, optionally resuming from a watermark.
424
+ *
425
+ * Useful after loading a snapshot from the DO: pass the snapshot's state as
426
+ * `initialState` and its highest applied source `seq` as `resumeFromSeq`, so
427
+ * the next {@link replayFromLog} applies ONLY the events after the snapshot
428
+ * (`getSince(resumeFromSeq + 1)`) rather than replaying the whole log on top
429
+ * of the snapshot — which would double-apply non-idempotent reducers.
430
+ *
431
+ * Omit `resumeFromSeq` (default `-1`) for a full reset that replays from the
432
+ * beginning.
433
+ * @param initialState The base state to reset to (e.g. a loaded snapshot).
434
+ * @param resumeFromSeq Highest source `seq` already baked into `initialState`, or `-1` to replay all.
435
+ */
436
+ reset(initialState: S, resumeFromSeq?: number): void;
437
+ /**
438
+ * Return an async generator that yields every event as it is applied,
439
+ * starting from the events currently in the log and continuing with
440
+ * every future `applyEvent` / `replayFromLog` call.
441
+ *
442
+ * The generator runs indefinitely unless given a `signal` — callers
443
+ * should break out of the `for await` loop or pass an `AbortSignal` to
444
+ * stop it (an abort settles the generator, `done: true`, on its next
445
+ * iteration step; it does not throw).
446
+ * @example
447
+ * ```ts
448
+ * for await (const entry of source.events()) {
449
+ * console.log("event applied:", entry);
450
+ * }
451
+ * ```
452
+ */
453
+ events(signal?: AbortSignal): AsyncGenerator<EventLogEntry>;
454
+ }
455
+ /**
456
+ * Interface for persisting event-sourced state snapshots.
457
+ *
458
+ * In a Lunora app the primary implementation is backed by the
459
+ * SnapshotDO (a Durable Object) on the server side. On the client
460
+ * the {@link InMemorySnapshotStore} is used for the offline-first
461
+ * local mirror, while a production client would implement this
462
+ * over IndexedDB or OPFS.
463
+ * @experimental
464
+ */
465
+ interface SnapshotStore {
466
+ /** Delete all snapshots. */
467
+ clear: () => Promise<void>;
468
+ /** Delete a single snapshot. */
469
+ delete: (key: string) => Promise<void>;
470
+ /** List all snapshot keys. */
471
+ list: () => Promise<string[]>;
472
+ /** Load a previously saved snapshot, or `null` when not found. */
473
+ load: (key: string) => Promise<unknown>;
474
+ /** Persist a snapshot under `key`. */
475
+ save: (key: string, snapshot: unknown) => Promise<void>;
476
+ }
477
+ /**
478
+ * In-memory snapshot store. Useful for testing and for the local
479
+ * offline-first mirror where persistence is handled at a higher
480
+ * layer (IndexedDB adapter).
481
+ * @experimental
482
+ */
483
+ declare class InMemorySnapshotStore implements SnapshotStore {
484
+ #private;
485
+ save(key: string, snapshot: unknown): Promise<void>;
486
+ load(key: string): Promise<unknown>;
487
+ list(): Promise<string[]>;
488
+ delete(key: string): Promise<void>;
489
+ clear(): Promise<void>;
490
+ }
491
+ /**
492
+ * A function that reduces an event entry into a state mutation.
493
+ *
494
+ * Pure functions are strongly encouraged: given the same event and state,
495
+ * they must produce the same next state for deterministic replay.
496
+ *
497
+ * Return {@link UNHANDLED} for an event `type` the reducer does not recognise —
498
+ * that, and only that, is what {@link MaterializerRuntimeOptions.unknownEventHandling}
499
+ * reacts to. Returning the current `state` is a legitimate, idempotent no-op for
500
+ * a type the reducer DOES handle; reference equality cannot tell the two apart
501
+ * (REPLICA-07), and reading it as "unhandled" warned about — or, under `"fail"`,
502
+ * threw on — an event type the reducer explicitly recognised.
503
+ * @experimental
504
+ */
505
+ type MaterializerReducer<S> = (state: S, entry: EventLogEntry) => S | typeof UNHANDLED;
506
+ /**
507
+ * Options for defining a single materializer.
508
+ * @experimental
509
+ */
510
+ interface MaterializerDef<S> {
511
+ /**
512
+ * Reducer invoked for every event in the log.
513
+ *
514
+ * Return the current state unchanged for a recognised event with nothing to
515
+ * do; return {@link UNHANDLED} for a `type` this reducer does not process.
516
+ */
517
+ handle: MaterializerReducer<S>;
518
+ /** Factory for the initial (empty) state. */
519
+ initial: () => S;
520
+ /** Unique name (used as the snapshot storage key). */
521
+ readonly name: string;
522
+ }
523
+ /**
524
+ * A constructed materializer ready to be used with a {@link MaterializerRuntime}.
525
+ * @experimental
526
+ */
527
+ interface Materializer<S> {
528
+ /**
529
+ * Apply a single event entry through the reducer.
530
+ * @returns `false` when the reducer returned {@link UNHANDLED} (state left
531
+ * untouched), `true` otherwise.
532
+ */
533
+ apply: (entry: EventLogEntry) => boolean;
534
+ readonly def: MaterializerDef<S>;
535
+ /** Reset to the initial state. */
536
+ reset: () => void;
537
+ /** Replace the runtime state (used on snapshot restore / replay). */
538
+ setState: (state: S) => void;
539
+ /** Current (runtime) derived state. */
540
+ readonly state: Readonly<S>;
541
+ }
542
+ /**
543
+ * Declare a materializer — a named reducer that derives state from events.
544
+ *
545
+ * The returned {@link Materializer} object can be used standalone or passed
546
+ * to a {@link MaterializerRuntime} for automatic log subscription.
547
+ * @experimental
548
+ */
549
+ declare const defineMaterializer: <S>(definition: MaterializerDef<S>) => Materializer<S>;
550
+ /**
551
+ * A materializer of any state shape. The {@link MaterializerRuntime} holds a
552
+ * heterogeneous collection and only ever calls `apply(entry)` / `setState(...)`
553
+ * (with cast values) / reads `def.name` — it never needs the concrete state
554
+ * type. `Materializer<unknown>` won't do: `setState(state: S)` makes
555
+ * `Materializer<S>` invariant in `S`, so `Materializer<number>` isn't assignable
556
+ * to `Materializer<unknown>`. Erasing the type param is the idiomatic fix.
557
+ */
558
+ type AnyMaterializer = Materializer<any>;
559
+ /**
560
+ * Options for constructing a {@link MaterializerRuntime}.
561
+ * @experimental
562
+ */
563
+ interface MaterializerRuntimeOptions {
564
+ /**
565
+ * Optional EventLogDO client for persistent event log integration.
566
+ *
567
+ * When provided, the runtime can bootstrap from the DO on startup
568
+ * (recover from snapshots → catch up via `getSince`) and append
569
+ * new events through the DO automatically.
570
+ */
571
+ doClient?: EventLogDOClient;
572
+ /** Optional snapshot store for persisting/recovering materialized state. */
573
+ snapshotStore?: SnapshotStore;
574
+ /**
575
+ * How to handle an event that every materializer explicitly DECLINED — one
576
+ * for which each reducer returned {@link UNHANDLED}.
577
+ *
578
+ * A reducer that instead falls through to `return state` for a type it does
579
+ * not recognise has, as far as the runtime can tell, handled the event: it
580
+ * changed nothing, but it did not decline. `"fail"` and `"warn"` are inert
581
+ * for such a reducer, and no option here can make them otherwise — write the
582
+ * reducer's default branch as `return UNHANDLED` if you want to hear about
583
+ * unknown types.
584
+ *
585
+ * A materializer whose own watermark is already past the entry does not run
586
+ * for it, and does not count as declining it: an entry that was already
587
+ * applied has already been classified, so a catch-up replaying it for a
588
+ * LAGGING materializer alone never re-reports it. Without that, `"fail"`
589
+ * aborted a catch-up on events a snapshot-recovered sibling had processed.
590
+ * @default "warn"
591
+ */
592
+ unknownEventHandling?: UnknownEventHandling;
593
+ }
594
+ /**
595
+ * Runtime that drives one or more materializers from an event log.
596
+ *
597
+ * Handles:
598
+ * - Replaying the full log on startup
599
+ * - Applying new events as they arrive
600
+ * - Periodic snapshot persistence
601
+ * - Recovery from snapshots (replay only what's missing)
602
+ * @experimental
603
+ */
604
+ declare class MaterializerRuntime {
605
+ #private;
606
+ constructor(materializers: AnyMaterializer[], options?: MaterializerRuntimeOptions);
607
+ /**
608
+ * The lowest per-materializer watermark — the seq of the next event that
609
+ * at least one materializer has not yet applied. `0` when there are no
610
+ * materializers.
611
+ */
612
+ get appliedSeq(): number;
613
+ /**
614
+ * Replay a batch of entries, applying each entry only to the
615
+ * materializers whose own watermark is behind it — a materializer at or
616
+ * past an entry's seq (e.g. recovered from a snapshot, or already caught
617
+ * up) skips it, so no materializer ever double-applies an event.
618
+ * @returns The number of entries applied to at least one materializer.
619
+ */
620
+ applyEntries(entries: ReadonlyArray<EventLogEntry>): number;
621
+ /**
622
+ * Attempt to recover materialized state from a snapshot store.
623
+ *
624
+ * When a snapshot is found for a materializer, its state AND its own
625
+ * watermark are restored from that snapshot. A materializer with no
626
+ * snapshot keeps its current watermark (`0` for a fresh runtime) — it
627
+ * does NOT inherit another materializer's watermark, so it still catches
628
+ * up from the very beginning (REPLICA-04: previously a shared watermark
629
+ * was bumped to the MAX across snapshots, permanently skipping events 0..N
630
+ * for any un-snapshotted or lagging materializer).
631
+ * @returns The highest snapshot `appliedSeq` across all materializers, or
632
+ * `0` — kept for backward compatibility; callers that need the fetch
633
+ * watermark for catch-up should use the per-materializer minimum instead
634
+ * (see `initialize`).
635
+ */
636
+ recoverFromSnapshots(): Promise<number>;
637
+ /**
638
+ * Persist the current state of all materializers as snapshots, each
639
+ * tagged with ITS OWN watermark (not a shared one).
640
+ */
641
+ persistSnapshots(): Promise<void>;
642
+ /**
643
+ * Bootstrap the runtime from the EventLogDO.
644
+ *
645
+ * 1. Recover materialized state from snapshots (if a snapshotStore is
646
+ * configured).
647
+ * 2. Fetch entries since the MINIMUM per-materializer watermark from
648
+ * the DO — not the maximum — so a materializer with no snapshot (or a
649
+ * lower one) still receives every event it hasn't seen (REPLICA-04).
650
+ * 3. Apply them through the materializers; `applyEntries` skips each
651
+ * entry for any materializer already past it, so nothing is double-applied.
652
+ *
653
+ * The DO answers one BOUNDED page per request, so step 2/3 walk pages until
654
+ * the log is exhausted — applying each page as it arrives, rather than
655
+ * holding the whole backlog in memory. Taking only the first page (and
656
+ * dropping `truncated`) would silently leave every materializer short of
657
+ * the log's head whenever the backlog exceeds a page.
658
+ *
659
+ * The walk is bounded by {@link MAX_CATCHUP_PAGES}: against a log written
660
+ * faster than it is read, "until the log is exhausted" never arrives and
661
+ * startup would never finish. Hitting the budget returns what was applied
662
+ * with every materializer's watermark advanced, so a later `initialize()`
663
+ * (or the ordinary append path) picks up exactly where this left off.
664
+ *
665
+ * Call this once on startup / after the DO binding is available.
666
+ * @returns The number of entries applied during catch-up.
667
+ */
668
+ initialize(): Promise<number>;
669
+ /**
670
+ * Append an event to the EventLogDO and apply it through all
671
+ * materializers.
672
+ *
673
+ * This is a convenience over calling `doClient.append(...)` +
674
+ * `runtime.applyEntries(...)` yourself — it persists the event
675
+ * **then** applies the returned entry (with its assigned seq).
676
+ * @returns The persisted entry with its DO-assigned `seq` — always, whether
677
+ * or not the entry could be applied to the materializers (see below).
678
+ */
679
+ appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
680
+ /**
681
+ * Reset all materializers to their initial state and clear snapshots.
682
+ */
683
+ reset(): void;
684
+ /**
685
+ * The list of registered materializers.
686
+ */
687
+ get materializers(): ReadonlyArray<Materializer<unknown>>;
688
+ }
689
+ /**
690
+ * Apply a {@link TableDiff} to the given SQLite database by translating
691
+ * each row change into an INSERT, UPDATE, or DELETE statement.
692
+ *
693
+ * All statements are wrapped in a single transaction.
694
+ * @param database SQLite adapter the statements run against.
695
+ * @param diff The table diff to apply.
696
+ * @param pkColumn Primary key column for DELETE/UPDATE (default `"id"`).
697
+ * @experimental
698
+ */
699
+ declare const applyDiffToDatabase: (database: SqliteAdapter, diff: TableDiff, pkColumn?: string) => void;
700
+ /**
701
+ * Apply multiple diffs **in order** within a single transaction.
702
+ *
703
+ * Each diff uses `"id"` as the primary key column. For tables with a custom
704
+ * PK, use {@link applyDiffToDatabase} per-diff and pass the PK explicitly.
705
+ * @experimental
706
+ */
707
+ declare const applyDiffsToDatabase: (database: SqliteAdapter, diffs: ReadonlyArray<TableDiff>) => void;
708
+ interface EventLogDOState {
709
+ storage: {
710
+ sql: {
711
+ exec: (query: string, ...params: unknown[]) => unknown;
712
+ };
713
+ /**
714
+ * The DO platform's native atomic-transaction primitive (async;
715
+ * commits on resolve, rolls back on throw/reject). Test doubles that
716
+ * omit it fall back to a bare (non-transactional) call — see
717
+ * `#handleAppend`.
718
+ */
719
+ transaction?: <T>(closure: () => Promise<T> | T) => Promise<T>;
720
+ };
721
+ }
722
+ /**
723
+ * `EventLogDO` is part of the experimental `@lunora/replica` API and may change without a major version bump.
724
+ * @experimental
725
+ */
726
+ declare class EventLogDO {
727
+ #private;
728
+ protected state: EventLogDOState;
729
+ protected env: unknown;
730
+ constructor(state: EventLogDOState, env: unknown);
731
+ fetch(request: Request): Promise<Response>;
732
+ }
733
+ /**
734
+ * `next()` advances the middleware chain. Called with no argument it forwards
735
+ * the current context unchanged; called with `{ ctx }` it shallow-merges the
736
+ * extension, and the result type reflects the widened context.
737
+ */
738
+ interface MiddlewareNext<ContextIn> {
739
+ (): Promise<ContextIn>;
740
+ <Extension extends Record<string, unknown>>(options: {
741
+ ctx: Extension;
742
+ }): Promise<ContextIn & Extension>;
743
+ }
744
+ /**
745
+ * A middleware receives the current context and a `next` continuation. Its
746
+ * return type becomes the builder's new context, so `return next({ ctx })`
747
+ * propagates the extension into every downstream `.use()` and the handler.
748
+ */
749
+ type Middleware<ContextIn, ContextOut> = (options: {
750
+ ctx: ContextIn;
751
+ next: MiddlewareNext<ContextIn>;
752
+ }) => ContextOut | Promise<ContextOut>;
753
+ /**
754
+ * The per-request `ctx.events` facade that {@link eventsContext} attaches.
755
+ *
756
+ * Each method delegates to the corresponding {@link EventLogDOClient} method,
757
+ * so handlers never need to import or reference the DO client directly.
758
+ * @experimental
759
+ */
760
+ interface EventsFacade {
761
+ /**
762
+ * Append one or more events to the log.
763
+ * @returns The persisted entries with their assigned `seq` numbers.
764
+ */
765
+ append: (events: {
766
+ payload: unknown;
767
+ timestamp?: number;
768
+ type: string;
769
+ }[]) => Promise<EventLogEntry[]>;
770
+ /**
771
+ * Fetch ONE bounded page of entries with `seq >= sinceSeq`.
772
+ * @returns `{ entries, truncated, cursor }` — pass `cursor` back as
773
+ * `sinceSeq` while `truncated` is `true` to walk the whole log.
774
+ */
775
+ getSince: (sinceSeq: number, limit?: number) => Promise<{
776
+ cursor?: number;
777
+ entries: EventLogEntry[];
778
+ truncated: boolean;
779
+ }>;
780
+ /** Return the total number of entries currently in the log. */
781
+ getSize: () => Promise<number>;
782
+ /** Return the full log state — all entries plus the next seq number. */
783
+ getState: () => Promise<{
784
+ entries: EventLogEntry[];
785
+ nextSeq: number;
786
+ }>;
787
+ }
788
+ /**
789
+ * The context shape produced by {@link eventsContext}.
790
+ * @experimental
791
+ */
792
+ interface EventsContextOutput {
793
+ /** Typed event log facade backed by an {@link EventLogDOClient}. */
794
+ readonly events: EventsFacade;
795
+ }
796
+ /**
797
+ * Create a middleware that attaches a typed `ctx.events` facade backed by
798
+ * the given {@link EventLogDOClient}.
799
+ *
800
+ * The facade surfaces `append`, `getSince`, `getSize`, and
801
+ * `getState` — every method the DO client exposes — so handlers can read
802
+ * and write the event log without reaching for the DO stub directly.
803
+ *
804
+ * The middleware is unopinionated about which context it extends — it works
805
+ * with `MutationCtx`, `ActionCtx`, or `QueryCtx` equally.
806
+ * @param client A configured {@link EventLogDOClient} instance.
807
+ * @returns A Lunora middleware that injects `ctx.events`.
808
+ *
809
+ * ```ts
810
+ * const client = new EventLogDOClient({
811
+ * fetch: (req) => env.EVENTS.get(id).fetch(req),
812
+ * });
813
+ *
814
+ * export const logEvent = mutation
815
+ * .use(eventsContext(client))
816
+ * .mutation(async ({ ctx, args }) => {
817
+ * const [entry] = await ctx.events.append([{ type: "order.placed", payload: args }]);
818
+ * return entry;
819
+ * });
820
+ * ```
821
+ * @experimental
822
+ */
823
+ declare const eventsContext: <Context>(client: EventLogDOClient) => Middleware<Context, Context & EventsContextOutput>;
824
+ /**
825
+ * A dependency-light subscription sink interface that mirrors what
826
+ * `LunoraClient.subscribe` expects, so the mirror helper doesn't
827
+ * need to import `@lunora/client`.
828
+ * @experimental
829
+ */
830
+ interface SubscriptionClient {
831
+ subscribe: (functionRef: {
832
+ __lunoraRef: string;
833
+ }, args: Record<string, unknown>, callback: (data: unknown) => void, options?: {
834
+ shardKey?: string;
835
+ }) => () => void;
836
+ }
837
+ /**
838
+ * Subscribe a Lunora-query to the local mirror so every server push
839
+ * is applied to the local SQLite store.
840
+ *
841
+ * Each frame from a Lunora live query is the FULL current result set, so the
842
+ * callback diffs it against the previous frame: a row that is new or whose
843
+ * content changed is upserted, a row that dropped out is deleted, and an
844
+ * unchanged row produces nothing. A frame identical to the last one therefore
845
+ * applies no diff — no event-log entry, no `version` bump, no re-query for the
846
+ * hooks subscribed to the mirror.
847
+ *
848
+ * Rows are keyed by the table's primary key (`id` unless the table was
849
+ * registered with another `primaryKey`). A row without one still lands — the
850
+ * apply path derives a key from the ROW's own content — but it cannot be
851
+ * diffed: it is re-emitted on every frame, and it is never reconciled on
852
+ * removal because only keyed rows are recorded for the delete pass. Repeating an
853
+ * identical frame is therefore a no-op upsert, but each distinct content the
854
+ * un-keyed row ever holds leaves a row behind for the life of the mirror.
855
+ * **Mirror a query that selects the primary key.** An un-keyed shape (an
856
+ * aggregate, or a projection that drops `id`) is supported only so that one such
857
+ * row cannot take the rest of the frame down with it.
858
+ *
859
+ * The mirror table name is derived from the function ref alone (not `args`), so
860
+ * do NOT mirror two subscriptions to the same function with different `args`
861
+ * into the same mirror: they'd share one table and the snapshot-delete pass of
862
+ * one could remove rows still live in the other.
863
+ *
864
+ * `mirror.clearData()` resets the remembered frame. Without that reset the next
865
+ * identical frame diffed clean against a mirror that no longer held the rows —
866
+ * no changes, no `applyDiff` — and the table stayed empty until some row's
867
+ * content happened to change or the page reloaded.
868
+ *
869
+ * Call the returned unsubscribe function to tear down both the client
870
+ * subscription and future mirror writes.
871
+ * @example
872
+ * ```ts
873
+ * const unsub = subscribeToMirror(client, mirror, api.todos.list, { userId });
874
+ * // Later:
875
+ * unsub();
876
+ * ```
877
+ * @experimental
878
+ */
879
+ declare const subscribeToMirror: (client: SubscriptionClient, mirror: LocalMirror, functionRef: {
880
+ __lunoraRef: string;
881
+ }, args: Record<string, unknown>, shardKey?: string) => (() => void);
882
+ /**
883
+ * Callback signature for state-change subscriptions.
884
+ * @experimental
885
+ */
886
+ type StateChangeCallback = (state: Readonly<Record<string, unknown>>) => void;
887
+ /**
888
+ * Callback signature for event-type subscriptions.
889
+ * @experimental
890
+ */
891
+ type EventCallback = (entry: EventLogEntry) => void;
892
+ /**
893
+ * Manages subscriptions to state changes and individual event types
894
+ * for the event-sourcing runtime.
895
+ *
896
+ * Each subscription returns an unsubscribe function — the caller is
897
+ * expected to call it during cleanup (e.g. in a React `useEffect`
898
+ * return or a Svelte `onDestroy`).
899
+ * @example
900
+ * ```ts
901
+ * const subs = new SubscriptionManager();
902
+ *
903
+ * // Subscribe to every state change
904
+ * const unsub1 = subs.onStateChange((state) => console.log("new state", state));
905
+ *
906
+ * // Subscribe to a specific event type
907
+ * const unsub2 = subs.onEvent("user-created", (entry) => console.log("user created", entry.payload));
908
+ *
909
+ * // Later, when state or events arrive:
910
+ * subs.notifyState({ users: [] });
911
+ * subs.notifyEvent({ seq: 1, type: "user-created", payload: { id: "1" }, timestamp: 100 });
912
+ *
913
+ * // Cleanup
914
+ * unsub1();
915
+ * unsub2();
916
+ * ```
917
+ * @experimental
918
+ */
919
+ declare class SubscriptionManager {
920
+ #private;
921
+ /**
922
+ * Subscribe to every state change emitted by the event source.
923
+ * @returns Unsubscribe function.
924
+ */
925
+ onStateChange(callback: StateChangeCallback): () => void;
926
+ /**
927
+ * Subscribe to a specific event type.
928
+ * @param eventType The event type to listen for (matches `entry.type`).
929
+ * @param callback Invoked with each matching entry.
930
+ * @returns Unsubscribe function.
931
+ */
932
+ onEvent(eventType: string, callback: EventCallback): () => void;
933
+ /**
934
+ * Notify all state-change subscribers with the current state.
935
+ */
936
+ notifyState(state: Readonly<Record<string, unknown>>): void;
937
+ /**
938
+ * Notify event-type subscribers whose `eventType` matches.
939
+ */
940
+ notifyEvent(entry: EventLogEntry): void;
941
+ /**
942
+ * Return the total number of active subscriptions.
943
+ */
944
+ get size(): number;
945
+ /**
946
+ * Remove all subscriptions.
947
+ */
948
+ clear(): void;
949
+ }
950
+ /**
951
+ * Options for constructing an {@link EventsSync}.
952
+ * @experimental
953
+ */
954
+ interface EventsSyncOptions {
955
+ /**
956
+ * Replay a batch of events through the derived-state machine.
957
+ *
958
+ * Called with every batch of new events fetched from the log. The
959
+ * consumer should feed these events into their state machine
960
+ * (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
961
+ * so that the machine's state reflects the latest log position.
962
+ *
963
+ * **Must be atomic across the batch: apply every event, or none.** There is
964
+ * no rollback here and none is possible — the state machine is the
965
+ * consumer's. A call that mutates derived state and then throws partway is
966
+ * re-delivered WHOLE on the next poll (the watermark only advances past a
967
+ * batch that fully succeeded, and a replay that threw is not recorded), so a
968
+ * non-atomic implementation applies the events before the throw twice. A
969
+ * call that RETURNS is never re-delivered: {@link EventsSync} tracks the
970
+ * highest applied `seq` separately from the watermark, so a batch whose
971
+ * replay succeeded and whose mirror fan-out then failed is not replayed.
972
+ */
973
+ applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
974
+ /**
975
+ * Fetch the next batch of events whose `seq >= sinceSeq`.
976
+ *
977
+ * It does NOT have to return the whole backlog: a bounded batch is
978
+ * preferred and is what the DO-backed transport gives you
979
+ * ({@link import("@lunora/replica").EventLogDOClient.getSince |
980
+ * EventLogDOClient.getSince()} answers one page). {@link EventsSync} keeps
981
+ * calling with the advanced watermark until a call returns nothing, so the
982
+ * whole log is applied either way — one bounded atom at a time.
983
+ * In a client context this could call a Lunora action that proxies to the
984
+ * event log, or read from an IndexedDB cache.
985
+ *
986
+ * Return an empty array when there are no new events.
987
+ */
988
+ fetchEventsSince: (sinceSeq: number) => Promise<ReadonlyArray<EventLogEntry>>;
989
+ /**
990
+ * Produce {@link TableDiff | TableDiffs} from the current derived state.
991
+ *
992
+ * Called after every batch of events has been applied. The consumer
993
+ * **recomputes a full diff from the current mirror-vs-source state** and
994
+ * returns the diffs needed to bring the LocalMirror up to date.
995
+ *
996
+ * **MUST be idempotent** — it must NOT advance a one-shot cursor as a side
997
+ * effect. A batch that fails partway (a `mirror.applyDiff` throws) is
998
+ * retried on the next poll from the same watermark; if this call consumed a
999
+ * cursor on the first attempt it would return `[]` on the retry and the
1000
+ * un-mirrored diffs would be lost forever. Recompute-from-current-state has
1001
+ * no such hazard: calling it again with no new events returns the same
1002
+ * diffs, and calling it after a partial mirror write returns exactly the
1003
+ * diffs still missing from the mirror.
1004
+ *
1005
+ * Return an empty array when there are no changes to push to the mirror.
1006
+ */
1007
+ getTableDiffs: () => TableDiff[];
1008
+ /**
1009
+ * The local SQLite mirror to apply diffs to.
1010
+ */
1011
+ mirror: LocalMirror;
1012
+ /**
1013
+ * Called when an error occurs during a poll cycle.
1014
+ *
1015
+ * Defaults to `console.error`. Set to a no-op to suppress error logging.
1016
+ */
1017
+ onError?: (error: unknown) => void;
1018
+ /**
1019
+ * How often to poll for new events (in milliseconds).
1020
+ * @default 5000
1021
+ */
1022
+ pollInterval?: number;
1023
+ }
1024
+ /**
1025
+ * Periodically polls an event log, replays events through a state machine,
1026
+ * converts the resulting state into {@link TableDiff | TableDiffs}, and
1027
+ * applies them to a {@link LocalMirror}.
1028
+ *
1029
+ * The class is **transport-agnostic** — it accepts a generic
1030
+ * `fetchEventsSince` function rather than coupling to a specific source
1031
+ * (EventLogDO, WebSocket push, IndexedDB, etc.).
1032
+ *
1033
+ * ## Lifecycle
1034
+ *
1035
+ * 1. Call `start()` to begin periodic polling.
1036
+ * 2. Call `sync()` to perform an immediate one-shot sync.
1037
+ * 3. Call `stop()` to halt polling.
1038
+ *
1039
+ * The current watermark is exposed via `watermark` and advances
1040
+ * monotonically as events are applied.
1041
+ * @experimental
1042
+ */
1043
+ declare class EventsSync {
1044
+ #private;
1045
+ constructor(options: EventsSyncOptions);
1046
+ /**
1047
+ * The current watermark — the next `seq` the sync will fetch from.
1048
+ *
1049
+ * Starts at `0` (fetch everything). Advances to `max(seq) + 1` after
1050
+ * each successful poll cycle.
1051
+ */
1052
+ get watermark(): number;
1053
+ /**
1054
+ * Start polling for new events on the configured interval.
1055
+ *
1056
+ * Does nothing if polling is already active.
1057
+ * Does **not** perform an initial sync — call {@link sync} once if you
1058
+ * need to catch up immediately.
1059
+ */
1060
+ start(): void;
1061
+ /**
1062
+ * Stop polling for new events.
1063
+ *
1064
+ * Safe to call when not started.
1065
+ */
1066
+ stop(): void;
1067
+ /**
1068
+ * Perform a one-shot sync: fetch events since the current watermark,
1069
+ * apply them through the state machine, and push diffs to the mirror.
1070
+ * @returns The number of events that were fetched and applied.
1071
+ */
1072
+ sync(): Promise<number>;
1073
+ }
1074
+ export { type AppendEventInput, type AppendOptions, type EventCallback, EventEmitter, type EventFactory, EventLog, EventLogDO, EventLogDOClient, type EventLogDOClientOptions, type EventLogEntry, type EventNamespace, type EventReducer, EventSource, type EventSourceEvents, type EventSourceOptions, type EventsContextOutput, type EventsDefinition, type EventsFacade, EventsSync, type EventsSyncOptions, InMemorySnapshotStore, type InputEvent, LocalMirror, type Materializer, type MaterializerDef, type MaterializerReducer, MaterializerRuntime, type MaterializerRuntimeOptions, type Seq, type SnapshotStore, type SqliteAdapter, type StateChangeCallback, type SubscriptionClient, SubscriptionManager, type TableDiff, UNHANDLED, type UnknownEventHandling, applyDiff, applyDiffToDatabase as applyDiffToDb, applyDiffToSnapshot, applyDiffs, applyDiffsToDatabase as applyDiffsToDb, defineEvents, defineMaterializer, eventsContext, subscribeToMirror };