@lunora/replica 1.0.0-alpha.8 → 1.0.0-alpha.80

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 (57) hide show
  1. package/LICENSE.md +214 -0
  2. package/README.md +55 -10
  3. package/dist/adapters/better-sqlite3.d.mts +1 -1
  4. package/dist/adapters/better-sqlite3.d.ts +1 -1
  5. package/dist/adapters/better-sqlite3.mjs +1 -29
  6. package/dist/adapters/sqlite-wasm.d.mts +3 -9
  7. package/dist/adapters/sqlite-wasm.d.ts +3 -9
  8. package/dist/adapters/sqlite-wasm.mjs +1 -44
  9. package/dist/adapters/sqljs.d.mts +1 -1
  10. package/dist/adapters/sqljs.d.ts +1 -1
  11. package/dist/adapters/sqljs.mjs +1 -55
  12. package/dist/index.d.mts +113 -52
  13. package/dist/index.d.ts +113 -52
  14. package/dist/index.mjs +1 -20
  15. package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
  16. package/dist/packem_shared/EventLog-B1-yhArT.mjs +1 -0
  17. package/dist/packem_shared/EventLogDO-BF9ZWc6C.mjs +1 -0
  18. package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
  19. package/dist/packem_shared/EventSource-BC0hKJSA.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-ZmB8SJFe.mjs +4 -0
  23. package/dist/packem_shared/MaterializerRuntime-S-Knx6BM.mjs +1 -0
  24. package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
  25. package/dist/packem_shared/applyDiff-DRJ1gap3.mjs +1 -0
  26. package/dist/packem_shared/applyDiffToDb-C6ek5Elp.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/isClientSeq-D2Xm0_lj.mjs +1 -0
  32. package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-CErKffFW.d.ts} +20 -12
  33. package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-CtQovAv_.d.mts} +20 -12
  34. package/dist/packem_shared/subscribeToMirror-BT4oOBng.mjs +1 -0
  35. package/dist/packem_shared/{types.d-CkMkSwLJ.d.mts → types.d-BuLTPLaQ.d.mts} +0 -2
  36. package/dist/packem_shared/{types.d-CkMkSwLJ.d.ts → types.d-BuLTPLaQ.d.ts} +0 -2
  37. package/dist/react.d.mts +39 -29
  38. package/dist/react.d.ts +39 -29
  39. package/dist/react.mjs +1 -15
  40. package/package.json +1 -1
  41. package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
  42. package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
  43. package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
  44. package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
  45. package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
  46. package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
  47. package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
  48. package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
  49. package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
  50. package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
  51. package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
  52. package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
  53. package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
  54. package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
  55. package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
  56. package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
  57. package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
package/dist/index.d.mts CHANGED
@@ -1,9 +1,24 @@
1
- export { createBetterSqlite3Adapter } from "./adapters/better-sqlite3.mjs";
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.mjs";
2
17
  export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.mjs";
3
18
  export { createSqlJsAdapter } from "./adapters/sqljs.mjs";
4
- import { S as SqliteAdapter } from "./packem_shared/types.d-CkMkSwLJ.mjs";
5
- 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-DTavX_y0.mjs";
6
- 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-DTavX_y0.mjs";
19
+ import { S as SqliteAdapter } from "./packem_shared/types.d-BuLTPLaQ.mjs";
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-CtQovAv_.mjs";
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-CtQovAv_.mjs";
7
22
  /**
8
23
  * Apply a single {@link TableDiff} to an in-memory row map and return
9
24
  * the updated map.
@@ -30,15 +45,16 @@ declare const applyDiff: (current: ReadonlyMap<string, Record<string, unknown>>,
30
45
  /**
31
46
  * Apply an array of diffs **in order**, returning the final row map.
32
47
  *
33
- * This is equivalent to calling {@link applyDiff} repeatedly but avoids
34
- * intermediate map copies.
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.
35
51
  * @experimental
36
52
  */
37
53
  declare const applyDiffs: (current: ReadonlyMap<string, Record<string, unknown>>, diffs: ReadonlyArray<TableDiff>) => Map<string, Record<string, unknown>>;
38
54
  /**
39
55
  * Merge the row-level effect of a {@link TableDiff} into plain JSON
40
56
  * state keyed by table name, returning a new snapshot.
41
- * @param snapshot Current snapshot, e.g. `{ users: Map&lt;id, row>, posts: Map&lt;id, row> }`.
57
+ * @param snapshot Current snapshot, e.g. `{ users: Map<id, row>, posts: Map<id, row> }`.
42
58
  * @param diff Contains the target table name and the row-level changes to merge.
43
59
  * @returns A shallow copy of `snapshot` with `diff.table`'s map updated.
44
60
  * @experimental
@@ -105,7 +121,7 @@ interface DefineEventsOptions {
105
121
  /**
106
122
  * Optional version prefix for all event types.
107
123
  *
108
- * When set, every qualified event type is prefixed with `"v&lt;N>."`, enabling
124
+ * When set, every qualified event type is prefixed with `"v<N>."`, enabling
109
125
  * versioned event naming like `"v1.chat.messageSent"` or `"v2.chat.messageSent"`.
110
126
  * This allows materializers to evolve their handling logic based on the event
111
127
  * version without breaking backward compatibility.
@@ -178,19 +194,28 @@ declare class EventLogDOClient {
178
194
  batchId?: string;
179
195
  }): Promise<EventLogEntry[]>;
180
196
  /**
181
- * Fetch all entries with `seq >= sinceSeq`.
197
+ * Fetch ONE page of entries with `seq >= sinceSeq`.
182
198
  *
183
- * Pass `sinceSeq = 0` to fetch the entire log.
184
- */
185
- getSince(sinceSeq: number): Promise<EventLogEntry[]>;
186
- /**
187
- * Fetch a paginated range of entries.
188
- * @returns `{ entries, hasMore }` — `hasMore` is `true` when another
189
- * page exists (i.e. the DO returned `limit + 1` rows).
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`.
190
214
  */
191
- getRange(fromSeq: number, limit?: number): Promise<{
215
+ getSince(sinceSeq: number, limit?: number): Promise<{
216
+ cursor?: number;
192
217
  entries: EventLogEntry[];
193
- hasMore: boolean;
218
+ truncated: boolean;
194
219
  }>;
195
220
  /**
196
221
  * Return the total number of entries currently in the log.
@@ -271,8 +296,8 @@ type UnknownEventHandling = "warn" | "ignore" | "fail" | ((entry: EventLogEntry)
271
296
  * Events emitted by the {@link EventSource} runtime.
272
297
  *
273
298
  * A `type` (not `interface`) so it satisfies `EventEmitter`'s
274
- * `Record&lt;string, unknown>` constraint — interfaces have no implicit index
275
- * signature and aren't assignable to `Record&lt;string, unknown>`.
299
+ * `Record<string, unknown>` constraint — interfaces have no implicit index
300
+ * signature and aren't assignable to `Record<string, unknown>`.
276
301
  * @experimental
277
302
  */
278
303
  type EventSourceEvents = {
@@ -408,8 +433,10 @@ declare class EventSource<S extends Record<string, unknown> = Record<string, unk
408
433
  * starting from the events currently in the log and continuing with
409
434
  * every future `applyEvent` / `replayFromLog` call.
410
435
  *
411
- * The generator runs indefinitely it never returns. Callers should
412
- * break out of the `for await` loop or use an `AbortSignal` to stop.
436
+ * The generator runs indefinitely unless given a `signal` callers
437
+ * should break out of the `for await` loop or pass an `AbortSignal` to
438
+ * stop it (an abort settles the generator, `done: true`, on its next
439
+ * iteration step; it does not throw).
413
440
  * @example
414
441
  * ```ts
415
442
  * for await (const entry of source.events()) {
@@ -506,9 +533,9 @@ declare const defineMaterializer: <S>(definition: MaterializerDef<S>) => Materia
506
533
  * A materializer of any state shape. The {@link MaterializerRuntime} holds a
507
534
  * heterogeneous collection and only ever calls `apply(entry)` / `setState(...)`
508
535
  * (with cast values) / reads `def.name` — it never needs the concrete state
509
- * type. `Materializer&lt;unknown>` won't do: `setState(state: S)` makes
510
- * `Materializer&lt;S>` invariant in `S`, so `Materializer&lt;number>` isn't assignable
511
- * to `Materializer&lt;unknown>`. Erasing the type param is the idiomatic fix.
536
+ * type. `Materializer<unknown>` won't do: `setState(state: S)` makes
537
+ * `Materializer<S>` invariant in `S`, so `Materializer<number>` isn't assignable
538
+ * to `Materializer<unknown>`. Erasing the type param is the idiomatic fix.
512
539
  */
513
540
  type AnyMaterializer = Materializer<any>;
514
541
  /**
@@ -585,12 +612,24 @@ declare class MaterializerRuntime {
585
612
  *
586
613
  * 1. Recover materialized state from snapshots (if a snapshotStore is
587
614
  * configured).
588
- * 2. Fetch all entries since the MINIMUM per-materializer watermark from
615
+ * 2. Fetch entries since the MINIMUM per-materializer watermark from
589
616
  * the DO — not the maximum — so a materializer with no snapshot (or a
590
617
  * lower one) still receives every event it hasn't seen (REPLICA-04).
591
618
  * 3. Apply them through the materializers; `applyEntries` skips each
592
619
  * entry for any materializer already past it, so nothing is double-applied.
593
620
  *
621
+ * The DO answers one BOUNDED page per request, so step 2/3 walk pages until
622
+ * the log is exhausted — applying each page as it arrives, rather than
623
+ * holding the whole backlog in memory. Taking only the first page (and
624
+ * dropping `truncated`) would silently leave every materializer short of
625
+ * the log's head whenever the backlog exceeds a page.
626
+ *
627
+ * The walk is bounded by {@link MAX_CATCHUP_PAGES}: against a log written
628
+ * faster than it is read, "until the log is exhausted" never arrives and
629
+ * startup would never finish. Hitting the budget returns what was applied
630
+ * with every materializer's watermark advanced, so a later `initialize()`
631
+ * (or the ordinary append path) picks up exactly where this left off.
632
+ *
594
633
  * Call this once on startup / after the DO binding is available.
595
634
  * @returns The number of entries applied during catch-up.
596
635
  */
@@ -602,7 +641,8 @@ declare class MaterializerRuntime {
602
641
  * This is a convenience over calling `doClient.append(...)` +
603
642
  * `runtime.applyEntries(...)` yourself — it persists the event
604
643
  * **then** applies the returned entry (with its assigned seq).
605
- * @returns The persisted entry with its DO-assigned `seq`.
644
+ * @returns The persisted entry with its DO-assigned `seq` — always, whether
645
+ * or not the entry could be applied to the materializers (see below).
606
646
  */
607
647
  appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
608
648
  /**
@@ -696,20 +736,15 @@ interface EventsFacade {
696
736
  type: string;
697
737
  }[]) => Promise<EventLogEntry[]>;
698
738
  /**
699
- * Fetch a paginated range of entries.
700
- * @returns `{ entries, hasMore }` — `hasMore` is `true` when another
701
- * page exists.
739
+ * Fetch ONE bounded page of entries with `seq >= sinceSeq`.
740
+ * @returns `{ entries, truncated, cursor }` — pass `cursor` back as
741
+ * `sinceSeq` while `truncated` is `true` to walk the whole log.
702
742
  */
703
- getRange: (fromSeq: number, limit?: number) => Promise<{
743
+ getSince: (sinceSeq: number, limit?: number) => Promise<{
744
+ cursor?: number;
704
745
  entries: EventLogEntry[];
705
- hasMore: boolean;
746
+ truncated: boolean;
706
747
  }>;
707
- /**
708
- * Fetch all entries with `seq >= sinceSeq`.
709
- *
710
- * Pass `sinceSeq = 0` to fetch the entire log.
711
- */
712
- getSince: (sinceSeq: number) => Promise<EventLogEntry[]>;
713
748
  /** Return the total number of entries currently in the log. */
714
749
  getSize: () => Promise<number>;
715
750
  /** Return the full log state — all entries plus the next seq number. */
@@ -730,7 +765,7 @@ interface EventsContextOutput {
730
765
  * Create a middleware that attaches a typed `ctx.events` facade backed by
731
766
  * the given {@link EventLogDOClient}.
732
767
  *
733
- * The facade surfaces `append`, `getSince`, `getRange`, `getSize`, and
768
+ * The facade surfaces `append`, `getSince`, `getSize`, and
734
769
  * `getState` — every method the DO client exposes — so handlers can read
735
770
  * and write the event log without reaching for the DO stub directly.
736
771
  *
@@ -772,12 +807,16 @@ interface SubscriptionClient {
772
807
  * is applied to the local SQLite store.
773
808
  *
774
809
  * Each frame from a Lunora live query is the FULL current result set, so the
775
- * callback treats it as a snapshot: it upserts every row present and emits a
776
- * `delete` for any id that was mirrored on a previous frame but is absent now
777
- * otherwise rows that drop out of the server result would linger stale in the
778
- * local mirror. Rows are keyed by their `id` field (the mirror's default primary
779
- * key); a row without an `id` can't be reconciled on removal, and — because the
780
- * mirror table's `id` column is `NOT NULL` — will fail the insert.
810
+ * callback diffs it against the previous frame: a row that is new or whose
811
+ * content changed is upserted, a row that dropped out is deleted, and an
812
+ * unchanged row produces nothing. A frame identical to the last one therefore
813
+ * applies no diff no event-log entry, no `version` bump, no re-query for the
814
+ * hooks subscribed to the mirror.
815
+ *
816
+ * Rows are keyed by the table's primary key (`id` unless the table was
817
+ * registered with another `primaryKey`); a row without one can't be diffed or
818
+ * reconciled on removal, and — because the mirror's key column is `NOT NULL` —
819
+ * will fail the insert.
781
820
  *
782
821
  * The mirror table name is derived from the function ref alone (not `args`), so
783
822
  * do NOT mirror two subscriptions to the same function with different `args`
@@ -877,15 +916,28 @@ interface EventsSyncOptions {
877
916
  * consumer should feed these events into their state machine
878
917
  * (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
879
918
  * so that the machine's state reflects the latest log position.
919
+ *
920
+ * **Must be atomic across the batch: apply every event, or none.** There is
921
+ * no rollback here and none is possible — the state machine is the
922
+ * consumer's. A call that mutates derived state and then throws partway is
923
+ * re-delivered WHOLE on the next poll (the watermark only advances past a
924
+ * batch that fully succeeded, and a replay that threw is not recorded), so a
925
+ * non-atomic implementation applies the events before the throw twice. A
926
+ * call that RETURNS is never re-delivered: {@link EventsSync} tracks the
927
+ * highest applied `seq` separately from the watermark, so a batch whose
928
+ * replay succeeded and whose mirror fan-out then failed is not replayed.
880
929
  */
881
930
  applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
882
931
  /**
883
- * Fetch all events whose `seq >= sinceSeq`.
932
+ * Fetch the next batch of events whose `seq >= sinceSeq`.
884
933
  *
885
- * In a server-side context, this typically wraps
886
- * {@link import("@lunora/replica").EventLogDOClient.getSince |
887
- * EventLogDOClient.getSince()}.
888
- * In a client context it could call a Lunora action that proxies to the
934
+ * It does NOT have to return the whole backlog: a bounded batch is
935
+ * preferred and is what the DO-backed transport gives you
936
+ * ({@link import("@lunora/replica").EventLogDOClient.getSince |
937
+ * EventLogDOClient.getSince()} answers one page). {@link EventsSync} keeps
938
+ * calling with the advanced watermark until a call returns nothing, so the
939
+ * whole log is applied either way — one bounded atom at a time.
940
+ * In a client context this could call a Lunora action that proxies to the
889
941
  * event log, or read from an IndexedDB cache.
890
942
  *
891
943
  * Return an empty array when there are no new events.
@@ -895,8 +947,17 @@ interface EventsSyncOptions {
895
947
  * Produce {@link TableDiff | TableDiffs} from the current derived state.
896
948
  *
897
949
  * Called after every batch of events has been applied. The consumer
898
- * compares the state _before_ and _after_ the batch and returns the
899
- * diffs needed to bring the LocalMirror up to date.
950
+ * **recomputes a full diff from the current mirror-vs-source state** and
951
+ * returns the diffs needed to bring the LocalMirror up to date.
952
+ *
953
+ * **MUST be idempotent** — it must NOT advance a one-shot cursor as a side
954
+ * effect. A batch that fails partway (a `mirror.applyDiff` throws) is
955
+ * retried on the next poll from the same watermark; if this call consumed a
956
+ * cursor on the first attempt it would return `[]` on the retry and the
957
+ * un-mirrored diffs would be lost forever. Recompute-from-current-state has
958
+ * no such hazard: calling it again with no new events returns the same
959
+ * diffs, and calling it after a partial mirror write returns exactly the
960
+ * diffs still missing from the mirror.
900
961
  *
901
962
  * Return an empty array when there are no changes to push to the mirror.
902
963
  */
package/dist/index.d.ts CHANGED
@@ -1,9 +1,24 @@
1
- export { createBetterSqlite3Adapter } from "./adapters/better-sqlite3.js";
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";
2
17
  export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.js";
3
18
  export { createSqlJsAdapter } from "./adapters/sqljs.js";
4
- import { S as SqliteAdapter } from "./packem_shared/types.d-CkMkSwLJ.js";
5
- 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-CyGOpUES.js";
6
- 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-CyGOpUES.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-CErKffFW.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-CErKffFW.js";
7
22
  /**
8
23
  * Apply a single {@link TableDiff} to an in-memory row map and return
9
24
  * the updated map.
@@ -30,15 +45,16 @@ declare const applyDiff: (current: ReadonlyMap<string, Record<string, unknown>>,
30
45
  /**
31
46
  * Apply an array of diffs **in order**, returning the final row map.
32
47
  *
33
- * This is equivalent to calling {@link applyDiff} repeatedly but avoids
34
- * intermediate map copies.
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.
35
51
  * @experimental
36
52
  */
37
53
  declare const applyDiffs: (current: ReadonlyMap<string, Record<string, unknown>>, diffs: ReadonlyArray<TableDiff>) => Map<string, Record<string, unknown>>;
38
54
  /**
39
55
  * Merge the row-level effect of a {@link TableDiff} into plain JSON
40
56
  * state keyed by table name, returning a new snapshot.
41
- * @param snapshot Current snapshot, e.g. `{ users: Map&lt;id, row>, posts: Map&lt;id, row> }`.
57
+ * @param snapshot Current snapshot, e.g. `{ users: Map<id, row>, posts: Map<id, row> }`.
42
58
  * @param diff Contains the target table name and the row-level changes to merge.
43
59
  * @returns A shallow copy of `snapshot` with `diff.table`'s map updated.
44
60
  * @experimental
@@ -105,7 +121,7 @@ interface DefineEventsOptions {
105
121
  /**
106
122
  * Optional version prefix for all event types.
107
123
  *
108
- * When set, every qualified event type is prefixed with `"v&lt;N>."`, enabling
124
+ * When set, every qualified event type is prefixed with `"v<N>."`, enabling
109
125
  * versioned event naming like `"v1.chat.messageSent"` or `"v2.chat.messageSent"`.
110
126
  * This allows materializers to evolve their handling logic based on the event
111
127
  * version without breaking backward compatibility.
@@ -178,19 +194,28 @@ declare class EventLogDOClient {
178
194
  batchId?: string;
179
195
  }): Promise<EventLogEntry[]>;
180
196
  /**
181
- * Fetch all entries with `seq >= sinceSeq`.
197
+ * Fetch ONE page of entries with `seq >= sinceSeq`.
182
198
  *
183
- * Pass `sinceSeq = 0` to fetch the entire log.
184
- */
185
- getSince(sinceSeq: number): Promise<EventLogEntry[]>;
186
- /**
187
- * Fetch a paginated range of entries.
188
- * @returns `{ entries, hasMore }` — `hasMore` is `true` when another
189
- * page exists (i.e. the DO returned `limit + 1` rows).
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`.
190
214
  */
191
- getRange(fromSeq: number, limit?: number): Promise<{
215
+ getSince(sinceSeq: number, limit?: number): Promise<{
216
+ cursor?: number;
192
217
  entries: EventLogEntry[];
193
- hasMore: boolean;
218
+ truncated: boolean;
194
219
  }>;
195
220
  /**
196
221
  * Return the total number of entries currently in the log.
@@ -271,8 +296,8 @@ type UnknownEventHandling = "warn" | "ignore" | "fail" | ((entry: EventLogEntry)
271
296
  * Events emitted by the {@link EventSource} runtime.
272
297
  *
273
298
  * A `type` (not `interface`) so it satisfies `EventEmitter`'s
274
- * `Record&lt;string, unknown>` constraint — interfaces have no implicit index
275
- * signature and aren't assignable to `Record&lt;string, unknown>`.
299
+ * `Record<string, unknown>` constraint — interfaces have no implicit index
300
+ * signature and aren't assignable to `Record<string, unknown>`.
276
301
  * @experimental
277
302
  */
278
303
  type EventSourceEvents = {
@@ -408,8 +433,10 @@ declare class EventSource<S extends Record<string, unknown> = Record<string, unk
408
433
  * starting from the events currently in the log and continuing with
409
434
  * every future `applyEvent` / `replayFromLog` call.
410
435
  *
411
- * The generator runs indefinitely it never returns. Callers should
412
- * break out of the `for await` loop or use an `AbortSignal` to stop.
436
+ * The generator runs indefinitely unless given a `signal` callers
437
+ * should break out of the `for await` loop or pass an `AbortSignal` to
438
+ * stop it (an abort settles the generator, `done: true`, on its next
439
+ * iteration step; it does not throw).
413
440
  * @example
414
441
  * ```ts
415
442
  * for await (const entry of source.events()) {
@@ -506,9 +533,9 @@ declare const defineMaterializer: <S>(definition: MaterializerDef<S>) => Materia
506
533
  * A materializer of any state shape. The {@link MaterializerRuntime} holds a
507
534
  * heterogeneous collection and only ever calls `apply(entry)` / `setState(...)`
508
535
  * (with cast values) / reads `def.name` — it never needs the concrete state
509
- * type. `Materializer&lt;unknown>` won't do: `setState(state: S)` makes
510
- * `Materializer&lt;S>` invariant in `S`, so `Materializer&lt;number>` isn't assignable
511
- * to `Materializer&lt;unknown>`. Erasing the type param is the idiomatic fix.
536
+ * type. `Materializer<unknown>` won't do: `setState(state: S)` makes
537
+ * `Materializer<S>` invariant in `S`, so `Materializer<number>` isn't assignable
538
+ * to `Materializer<unknown>`. Erasing the type param is the idiomatic fix.
512
539
  */
513
540
  type AnyMaterializer = Materializer<any>;
514
541
  /**
@@ -585,12 +612,24 @@ declare class MaterializerRuntime {
585
612
  *
586
613
  * 1. Recover materialized state from snapshots (if a snapshotStore is
587
614
  * configured).
588
- * 2. Fetch all entries since the MINIMUM per-materializer watermark from
615
+ * 2. Fetch entries since the MINIMUM per-materializer watermark from
589
616
  * the DO — not the maximum — so a materializer with no snapshot (or a
590
617
  * lower one) still receives every event it hasn't seen (REPLICA-04).
591
618
  * 3. Apply them through the materializers; `applyEntries` skips each
592
619
  * entry for any materializer already past it, so nothing is double-applied.
593
620
  *
621
+ * The DO answers one BOUNDED page per request, so step 2/3 walk pages until
622
+ * the log is exhausted — applying each page as it arrives, rather than
623
+ * holding the whole backlog in memory. Taking only the first page (and
624
+ * dropping `truncated`) would silently leave every materializer short of
625
+ * the log's head whenever the backlog exceeds a page.
626
+ *
627
+ * The walk is bounded by {@link MAX_CATCHUP_PAGES}: against a log written
628
+ * faster than it is read, "until the log is exhausted" never arrives and
629
+ * startup would never finish. Hitting the budget returns what was applied
630
+ * with every materializer's watermark advanced, so a later `initialize()`
631
+ * (or the ordinary append path) picks up exactly where this left off.
632
+ *
594
633
  * Call this once on startup / after the DO binding is available.
595
634
  * @returns The number of entries applied during catch-up.
596
635
  */
@@ -602,7 +641,8 @@ declare class MaterializerRuntime {
602
641
  * This is a convenience over calling `doClient.append(...)` +
603
642
  * `runtime.applyEntries(...)` yourself — it persists the event
604
643
  * **then** applies the returned entry (with its assigned seq).
605
- * @returns The persisted entry with its DO-assigned `seq`.
644
+ * @returns The persisted entry with its DO-assigned `seq` — always, whether
645
+ * or not the entry could be applied to the materializers (see below).
606
646
  */
607
647
  appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
608
648
  /**
@@ -696,20 +736,15 @@ interface EventsFacade {
696
736
  type: string;
697
737
  }[]) => Promise<EventLogEntry[]>;
698
738
  /**
699
- * Fetch a paginated range of entries.
700
- * @returns `{ entries, hasMore }` — `hasMore` is `true` when another
701
- * page exists.
739
+ * Fetch ONE bounded page of entries with `seq >= sinceSeq`.
740
+ * @returns `{ entries, truncated, cursor }` — pass `cursor` back as
741
+ * `sinceSeq` while `truncated` is `true` to walk the whole log.
702
742
  */
703
- getRange: (fromSeq: number, limit?: number) => Promise<{
743
+ getSince: (sinceSeq: number, limit?: number) => Promise<{
744
+ cursor?: number;
704
745
  entries: EventLogEntry[];
705
- hasMore: boolean;
746
+ truncated: boolean;
706
747
  }>;
707
- /**
708
- * Fetch all entries with `seq >= sinceSeq`.
709
- *
710
- * Pass `sinceSeq = 0` to fetch the entire log.
711
- */
712
- getSince: (sinceSeq: number) => Promise<EventLogEntry[]>;
713
748
  /** Return the total number of entries currently in the log. */
714
749
  getSize: () => Promise<number>;
715
750
  /** Return the full log state — all entries plus the next seq number. */
@@ -730,7 +765,7 @@ interface EventsContextOutput {
730
765
  * Create a middleware that attaches a typed `ctx.events` facade backed by
731
766
  * the given {@link EventLogDOClient}.
732
767
  *
733
- * The facade surfaces `append`, `getSince`, `getRange`, `getSize`, and
768
+ * The facade surfaces `append`, `getSince`, `getSize`, and
734
769
  * `getState` — every method the DO client exposes — so handlers can read
735
770
  * and write the event log without reaching for the DO stub directly.
736
771
  *
@@ -772,12 +807,16 @@ interface SubscriptionClient {
772
807
  * is applied to the local SQLite store.
773
808
  *
774
809
  * Each frame from a Lunora live query is the FULL current result set, so the
775
- * callback treats it as a snapshot: it upserts every row present and emits a
776
- * `delete` for any id that was mirrored on a previous frame but is absent now
777
- * otherwise rows that drop out of the server result would linger stale in the
778
- * local mirror. Rows are keyed by their `id` field (the mirror's default primary
779
- * key); a row without an `id` can't be reconciled on removal, and — because the
780
- * mirror table's `id` column is `NOT NULL` — will fail the insert.
810
+ * callback diffs it against the previous frame: a row that is new or whose
811
+ * content changed is upserted, a row that dropped out is deleted, and an
812
+ * unchanged row produces nothing. A frame identical to the last one therefore
813
+ * applies no diff no event-log entry, no `version` bump, no re-query for the
814
+ * hooks subscribed to the mirror.
815
+ *
816
+ * Rows are keyed by the table's primary key (`id` unless the table was
817
+ * registered with another `primaryKey`); a row without one can't be diffed or
818
+ * reconciled on removal, and — because the mirror's key column is `NOT NULL` —
819
+ * will fail the insert.
781
820
  *
782
821
  * The mirror table name is derived from the function ref alone (not `args`), so
783
822
  * do NOT mirror two subscriptions to the same function with different `args`
@@ -877,15 +916,28 @@ interface EventsSyncOptions {
877
916
  * consumer should feed these events into their state machine
878
917
  * (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
879
918
  * so that the machine's state reflects the latest log position.
919
+ *
920
+ * **Must be atomic across the batch: apply every event, or none.** There is
921
+ * no rollback here and none is possible — the state machine is the
922
+ * consumer's. A call that mutates derived state and then throws partway is
923
+ * re-delivered WHOLE on the next poll (the watermark only advances past a
924
+ * batch that fully succeeded, and a replay that threw is not recorded), so a
925
+ * non-atomic implementation applies the events before the throw twice. A
926
+ * call that RETURNS is never re-delivered: {@link EventsSync} tracks the
927
+ * highest applied `seq` separately from the watermark, so a batch whose
928
+ * replay succeeded and whose mirror fan-out then failed is not replayed.
880
929
  */
881
930
  applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
882
931
  /**
883
- * Fetch all events whose `seq >= sinceSeq`.
932
+ * Fetch the next batch of events whose `seq >= sinceSeq`.
884
933
  *
885
- * In a server-side context, this typically wraps
886
- * {@link import("@lunora/replica").EventLogDOClient.getSince |
887
- * EventLogDOClient.getSince()}.
888
- * In a client context it could call a Lunora action that proxies to the
934
+ * It does NOT have to return the whole backlog: a bounded batch is
935
+ * preferred and is what the DO-backed transport gives you
936
+ * ({@link import("@lunora/replica").EventLogDOClient.getSince |
937
+ * EventLogDOClient.getSince()} answers one page). {@link EventsSync} keeps
938
+ * calling with the advanced watermark until a call returns nothing, so the
939
+ * whole log is applied either way — one bounded atom at a time.
940
+ * In a client context this could call a Lunora action that proxies to the
889
941
  * event log, or read from an IndexedDB cache.
890
942
  *
891
943
  * Return an empty array when there are no new events.
@@ -895,8 +947,17 @@ interface EventsSyncOptions {
895
947
  * Produce {@link TableDiff | TableDiffs} from the current derived state.
896
948
  *
897
949
  * Called after every batch of events has been applied. The consumer
898
- * compares the state _before_ and _after_ the batch and returns the
899
- * diffs needed to bring the LocalMirror up to date.
950
+ * **recomputes a full diff from the current mirror-vs-source state** and
951
+ * returns the diffs needed to bring the LocalMirror up to date.
952
+ *
953
+ * **MUST be idempotent** — it must NOT advance a one-shot cursor as a side
954
+ * effect. A batch that fails partway (a `mirror.applyDiff` throws) is
955
+ * retried on the next poll from the same watermark; if this call consumed a
956
+ * cursor on the first attempt it would return `[]` on the retry and the
957
+ * un-mirrored diffs would be lost forever. Recompute-from-current-state has
958
+ * no such hazard: calling it again with no new events returns the same
959
+ * diffs, and calling it after a partial mirror write returns exactly the
960
+ * diffs still missing from the mirror.
900
961
  *
901
962
  * Return an empty array when there are no changes to push to the mirror.
902
963
  */
package/dist/index.mjs CHANGED
@@ -1,20 +1 @@
1
- export { createBetterSqlite3Adapter } from './adapters/better-sqlite3.mjs';
2
- export { createSqliteWasmAdapter } from './adapters/sqlite-wasm.mjs';
3
- export { createSqlJsAdapter } from './adapters/sqljs.mjs';
4
- export { applyDiff, applyDiffToSnapshot, applyDiffs } from './packem_shared/applyDiff-98tKzmiW.mjs';
5
- export { defineEvents } from './packem_shared/defineEvents-DiBkPTh_.mjs';
6
- export { MaterializerRuntime, defineMaterializer } from './packem_shared/MaterializerRuntime-BoIrsMYB.mjs';
7
- export { applyDiffToDb, applyDiffsToDb } from './packem_shared/applyDiffToDb-DQ1xZp5J.mjs';
8
- export { EventEmitter } from './packem_shared/EventEmitter-CMZfct03.mjs';
9
- export { EventLog } from './packem_shared/EventLog-CnK-3Wge.mjs';
10
- export { EventLogDO } from './packem_shared/EventLogDO-DqlsVx0H.mjs';
11
- export { EventLogDOClient } from './packem_shared/EventLogDOClient-F4FO8Si4.mjs';
12
- export { EventSource, UNHANDLED } from './packem_shared/EventSource-D5yO9_aI.mjs';
13
- export { eventsContext } from './packem_shared/eventsContext-Bk_p48hj.mjs';
14
- export { LocalMirror } from './packem_shared/LocalMirror-a-5jEqFN.mjs';
15
- export { isClientSeq, isGlobalSeq, isInputEvent } from './packem_shared/isClientSeq-C46BkzqJ.mjs';
16
- export { InMemorySnapshotStore } from './packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs';
17
- export { subscribeToMirror } from './packem_shared/subscribeToMirror-CiaM-nQ7.mjs';
18
- export { SubscriptionManager } from './packem_shared/SubscriptionManager-C5xbw0pg.mjs';
19
- export { EventsSync } from './packem_shared/EventsSync-BP36tC9O.mjs';
20
- export { classifyChanges, createTableDiff, diffSize, isDiffEmpty, mergeDiffs } from './packem_shared/classifyChanges-RcqLBpLs.mjs';
1
+ import{createBetterSqlite3Adapter as o}from"./adapters/better-sqlite3.mjs";import{createSqliteWasmAdapter as f}from"./adapters/sqlite-wasm.mjs";import{createSqlJsAdapter as i}from"./adapters/sqljs.mjs";import{applyDiff as m,applyDiffToSnapshot as n,applyDiffs as x}from"./packem_shared/applyDiff-DRJ1gap3.mjs";import{defineEvents as l}from"./packem_shared/defineEvents-DHo-VK7G.mjs";import{MaterializerRuntime as S,defineMaterializer as E}from"./packem_shared/MaterializerRuntime-S-Knx6BM.mjs";import{applyDiffToDb as v,applyDiffsToDb as y}from"./packem_shared/applyDiffToDb-C6ek5Elp.mjs";import{EventEmitter as d}from"./packem_shared/EventEmitter-uo75adUL.mjs";import{EventLog as M}from"./packem_shared/EventLog-B1-yhArT.mjs";import{EventLogDO as u}from"./packem_shared/EventLogDO-BF9ZWc6C.mjs";import{EventLogDOClient as T}from"./packem_shared/EventLogDOClient-DWerZ3_n.mjs";import{EventSource as C,UNHANDLED as h}from"./packem_shared/EventSource-BC0hKJSA.mjs";import{eventsContext as I}from"./packem_shared/eventsContext-Dxow9Y7S.mjs";import{LocalMirror as O}from"./packem_shared/LocalMirror-ZmB8SJFe.mjs";import{isClientSeq as G,isGlobalSeq as H,isInputEvent as J}from"./packem_shared/isClientSeq-D2Xm0_lj.mjs";import{InMemorySnapshotStore as U}from"./packem_shared/InMemorySnapshotStore-C4taIG5K.mjs";import{subscribeToMirror as j}from"./packem_shared/subscribeToMirror-BT4oOBng.mjs";import{SubscriptionManager as w}from"./packem_shared/SubscriptionManager-AhPw3lFc.mjs";import{EventsSync as K}from"./packem_shared/EventsSync-B3wzXm-b.mjs";import{classifyChanges as Q,createTableDiff as V,diffSize as X,isDiffEmpty as Y,mergeDiffs as Z}from"./packem_shared/classifyChanges-BBc0-770.mjs";export{d as EventEmitter,M as EventLog,u as EventLogDO,T as EventLogDOClient,C as EventSource,K as EventsSync,U as InMemorySnapshotStore,O as LocalMirror,S as MaterializerRuntime,w as SubscriptionManager,h as UNHANDLED,m as applyDiff,v as applyDiffToDb,n as applyDiffToSnapshot,x as applyDiffs,y as applyDiffsToDb,Q as classifyChanges,o as createBetterSqlite3Adapter,i as createSqlJsAdapter,f as createSqliteWasmAdapter,V as createTableDiff,l as defineEvents,E as defineMaterializer,X as diffSize,I as eventsContext,G as isClientSeq,Y as isDiffEmpty,H as isGlobalSeq,J as isInputEvent,Z as mergeDiffs,j as subscribeToMirror};
@@ -0,0 +1 @@
1
+ class h{#t=new Map;#e=new Set;on(t,s){let e=this.#t.get(t);return e||(e=new Set,this.#t.set(t,e)),e.add(s),()=>{this.off(t,s)}}off(t,s){this.#t.get(t)?.delete(s)}onAny(t){return this.#e.add(t),()=>{this.#e.delete(t)}}offAny(t){this.#e.delete(t)}emit(t,s){let e=!1;const r=this.#t.get(t);if(r)for(const i of r)try{i(s),e=!0}catch{}for(const i of this.#e)try{i(t,s),e=!0}catch{}return e}hasListeners(t){return(this.#t.get(t)?.size??0)>0||this.#e.size>0}listenerCount(t){return this.#t.get(t)?.size??0}clear(){this.#t.clear(),this.#e.clear()}}export{h as EventEmitter};
@@ -0,0 +1 @@
1
+ class l{#e=[];#t=0;#i=0;#s=null;#n;constructor(t){this.#n=l.#o(t?.maxEntries)}static#o(t){if(t!==void 0){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("maxEntries must be a non-negative safe integer");return t}}#r(){return this.#e.slice(this.#t)}#h(){if(this.#n===void 0)return;const e=this.#e.length-this.#t-this.#n;e>0&&(this.#t+=e),this.#t>0&&this.#t>=this.#e.length-this.#t&&(this.#e=this.#e.slice(this.#t),this.#t=0)}append(t,e,s,n){const r=typeof t=="string"?t:t.type,o=typeof t=="string"?e:t.payload;let h,i;typeof t=="string"?(h=s,i=n):(h=void 0,i=e);const a=i?.parentSeqNum??this.#s??void 0,f=this.#i;this.#i+=1;const c={seq:f,type:r,payload:o,timestamp:i?.timestamp??Date.now(),tableDiffs:h,clientId:i?.clientId,sessionId:i?.sessionId,parentSeqNum:a};return this.#e.push(c),this.#s=c.seq,this.#h(),c}commitAll(t){if(t.length===0)return[];const e=[];for(const s of t){const{type:n}=s,r="payload"in s?s.payload:void 0,o="timestamp"in s?s.timestamp:Date.now(),h=e.at(-1)?.seq??this.#s??void 0,i=this.#i;this.#i+=1;const a={seq:i,type:n,payload:r,timestamp:o,parentSeqNum:h};this.#e.push(a),e.push(a)}return this.#s=e.at(-1)?.seq??this.#s,this.#h(),e}load(t){this.#e=[...t.entries],this.#t=0,this.#i=t.nextSeq,this.#s=t.headSeq,this.#h()}getSince(t){const e=this.#r();if(t<=0)return e;const s=e.findIndex(n=>n.seq>=t);return s===-1?[]:e.slice(s)}getFrom(t,e=50){const s=this.#e,n=this.#t,r=s.findIndex((h,i)=>i>=n&&h.seq>=t);if(r===-1)return{entries:[],hasMore:!1};const o=Math.min(r+e,s.length);return{entries:s.slice(r,o),hasMore:o<s.length}}snapshot(){return{entries:this.#r(),nextSeq:this.#i,headSeq:this.#s}}get size(){return this.#e.length-this.#t}get nextSeq(){return this.#i}get isEmpty(){return this.size===0}get headSeq(){return this.#s}clear(){this.#e=[],this.#t=0,this.#i=0,this.#s=null}truncateBelow(t){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("floorSeq must be a non-negative safe integer");const e=this.#r(),s=e.findIndex(n=>n.seq>=t);this.#e=s===-1?[]:e.slice(s),this.#t=0}async*events(t=0){const e=this.getSince(t);for(const s of e)yield s}}export{l as EventLog};
@@ -0,0 +1 @@
1
+ const y=Symbol("lunora.replica.event-log-do.idempotency-conflict"),q=n=>{const t=new Error(n);return Object.defineProperty(t,y,{value:!0}),t},N=n=>n instanceof Error&&y in n,T=n=>{if(Array.isArray(n))return n.map(t=>T(t));if(n!==null&&typeof n=="object"){const t=n,e=Object.keys(t);e.sort();const s={};for(const r of e)s[r]=T(t[r]);return s}return n},I=async n=>{const t=JSON.stringify(T(n)),e=new TextEncoder().encode(t),s=await crypto.subtle.digest("SHA-256",e);return[...new Uint8Array(s)].map(r=>r.toString(16).padStart(2,"0")).join("")},g=500,A=1e3,f=(n,t=200)=>Response.json(n,{status:t,headers:{"content-type":"application/json"}}),d=(n,t,e)=>f({error:{code:t,message:e}},n),m=n=>typeof n.toArray=="function"?n.toArray():typeof n[Symbol.iterator]=="function"?[...n]:[],E=n=>m(n).map(e=>({seq:e.seq,type:e.type,payload:JSON.parse(e.payload),timestamp:e.timestamp,clientId:e.client_id??void 0,sessionId:e.session_id??void 0,parentSeqNum:e.parent_seq??void 0}));class p{state;env;#t=!1;constructor(t,e){this.state=t,this.env=e}async fetch(t){this.#c();const e=new URL(t.url);try{if(t.method==="POST"&&e.pathname==="/append")return await this.#e(t);if(t.method==="GET"&&e.pathname==="/since")return this.#i(e);if(t.method==="GET"&&e.pathname==="/size")return this.#o();if(t.method==="GET"&&e.pathname==="/state")return this.#a()}catch(s){return console.error("[event-log-do] request failed:",s),d(500,"INTERNAL_ERROR","internal error")}return d(404,"NOT_FOUND","unknown route")}async#e(t){let e;try{e=await t.json()}catch{return d(400,"BAD_REQUEST","invalid JSON body")}const s=p.#n(e);if(s)return d(400,"BAD_REQUEST",s);const{sql:r}=this.state.storage,{batchId:o}=e,a=()=>p.#s(r,e,o),{transaction:i}=this.state.storage;let c;try{c=typeof i=="function"?await i(a):await a()}catch(u){if(N(u))return d(409,"CONFLICT",u.message);throw u}return f({entries:c})}static async#s(t,e,s){let r;if(typeof s=="string"){r=await I(e.events);const i=p.#p(t,s);if(i){if(i.fingerprint!==r)throw q(`batchId "${s}" was already used for a different event batch`);return i.entries}}const o=Date.now(),a=[];for(const i of e.events){const l={seq:p.#d(t),type:i.type,payload:i.payload,timestamp:i.timestamp??o,clientId:i.clientId,sessionId:i.sessionId,parentSeqNum:i.parentSeqNum};p.#l(t,l),a.push(l)}if(typeof s=="string"&&r!==void 0){const i=a[0]?.seq,c=a.at(-1)?.seq;i!==void 0&&c!==void 0&&p.#u(t,s,i,c,r)}return a}static#n(t){if(!Array.isArray(t.events)||t.events.length===0)return"events[] with a non-empty string `type` required";if(t.batchId!==void 0&&(typeof t.batchId!="string"||t.batchId.length===0))return"batchId must be a non-empty string";for(const e of t.events){const s=p.#r(e);if(s!==void 0)return s}}static#r(t){if(typeof t.type!="string"||t.type.length===0)return"events[] with a non-empty string `type` required";if(t.timestamp!==void 0&&!Number.isFinite(t.timestamp))return"events[].timestamp must be a finite number";if(t.clientId!==void 0&&typeof t.clientId!="string")return"events[].clientId must be a string";if(t.sessionId!==void 0&&typeof t.sessionId!="string")return"events[].sessionId must be a string";if(t.parentSeqNum!==void 0&&(typeof t.parentSeqNum!="number"||!Number.isInteger(t.parentSeqNum)||t.parentSeqNum<0))return"events[].parentSeqNum must be a non-negative integer"}#i(t){const e=t.searchParams.get("seq"),s=e===null?0:Number(e),r=t.searchParams.get("limit"),o=r===null?g:Number(r);if(!Number.isSafeInteger(s)||s<0)return d(400,"BAD_REQUEST","invalid seq");if(!Number.isSafeInteger(o)||o<1||o>A)return d(400,"BAD_REQUEST","invalid limit");const{sql:a}=this.state.storage,i=a.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC LIMIT ?",s,o+1),c=E(i),l=c.length>o,u=l?c.slice(0,o):c,h=u.at(-1),S=l&&h!==void 0?{entries:u,truncated:!0,cursor:h.seq+1}:{entries:u,truncated:!1};return f(S)}#o(){const{sql:t}=this.state.storage,e=t.exec("SELECT COUNT(*) AS count FROM events"),r=m(e)[0]?.count??0;return f({count:r})}#a(){const{sql:t}=this.state.storage,e=t.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events ORDER BY seq ASC"),s=E(e),r=(s.at(-1)?.seq??-1)+1;return f({entries:s,nextSeq:r})}#c(){if(this.#t)return;const{sql:t}=this.state.storage;t.exec("CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, payload TEXT NOT NULL, timestamp INTEGER NOT NULL, client_id TEXT, session_id TEXT, parent_seq INTEGER)"),t.exec("CREATE TABLE IF NOT EXISTS event_batches (batch_id TEXT PRIMARY KEY, first_seq INTEGER NOT NULL, last_seq INTEGER NOT NULL, fingerprint TEXT NOT NULL)"),this.#t=!0}static#p(t,e){const s=t.exec("SELECT first_seq, last_seq, fingerprint FROM event_batches WHERE batch_id = ?",e),o=m(s)[0];if(!o)return;const a=t.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? AND seq <= ? ORDER BY seq ASC",o.first_seq,o.last_seq);return{entries:E(a),fingerprint:o.fingerprint}}static#u(t,e,s,r,o){t.exec("INSERT INTO event_batches (batch_id, first_seq, last_seq, fingerprint) VALUES (?, ?, ?, ?)",e,s,r,o)}static#d(t){const e=t.exec("SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM events");return m(e)[0]?.next_seq??0}static#l(t,e){const s=typeof e.parentSeqNum=="number"?e.parentSeqNum:null;t.exec("INSERT INTO events (seq, type, payload, timestamp, client_id, session_id, parent_seq) VALUES (?, ?, ?, ?, ?, ?, ?)",e.seq,e.type,JSON.stringify(e.payload),e.timestamp,e.clientId??null,e.sessionId??null,s)}}export{p as EventLogDO};
@@ -0,0 +1 @@
1
+ class n{#t;constructor(t){this.#t=t.fetch}async append(t,a){const e=JSON.stringify({events:t,batchId:a?.batchId}),s=await this.#t(new Request("https://do/append",{method:"POST",headers:{"content-type":"application/json"},body:e}));if(!s.ok)throw await n.#e(s,"append");return(await s.json()).entries}async getSince(t,a){const e=a===void 0?"":`&limit=${String(a)}`;return this.#a(`/since?seq=${String(t)}${e}`,"getSince")}async getSize(){return(await this.#a("/size","getSize")).count}async getState(){return this.#a("/state","getState")}async#a(t,a){const e=await this.#t(new Request(`https://do${t}`));if(!e.ok)throw await n.#e(e,a);return await e.json()}static async#e(t,a){try{const s=(await t.json()).error?.message??t.statusText;return new Error(`EventLogDO.${a} failed (${String(t.status)}): ${s}`)}catch{return new Error(`EventLogDO.${a} failed (${String(t.status)}): ${t.statusText}`)}}}export{n as EventLogDOClient};
@@ -0,0 +1 @@
1
+ import{EventEmitter as p}from"./EventEmitter-uo75adUL.mjs";import{EventLog as f}from"./EventLog-B1-yhArT.mjs";const d=Symbol("lunora.replica.event-source.unhandled");class w{emitter=new p;log;#e;#t;#n=!1;#i;#r=-1;constructor(e,t,n){this.#e={...e},this.#t=t,this.#i=n?.unknownEventHandling??"warn",this.log=new f({maxEntries:n?.maxLogEntries})}get state(){return this.#e}get replayed(){return this.#n}applyEvent(e,t,n){let r,o,i;typeof e=="string"?(r=e,o=t,i=n):(r=e.type,o=e.payload,i=t);const s={seq:this.log.nextSeq,type:r,payload:o,timestamp:Date.now(),clientId:i?.clientId,sessionId:i?.sessionId,parentSeqNum:i?.parentSeqNum??this.log.headSeq??void 0};let a;try{a=this.#t(this.#e,s)}catch(h){const u=h instanceof Error?h:new Error(String(h));throw this.emitter.emit("replay-error",{entry:s,error:u}),u}let c=!0;a===d&&(c=this.#s(s));const l=this.log.append(r,o,void 0,{...i,timestamp:s.timestamp});return a===d?(c&&this.emitter.emit("state-changed",{state:this.#e,entry:l}),l):(this.#e=a,this.emitter.emit("state-changed",{state:this.#e,entry:l}),l)}replayFromLog(e){const t=e.getSince(this.#r+1);for(const n of t)try{const r=this.#t(this.#e,n);r!==d&&(this.#e=r),this.log.append(n.type,n.payload,n.tableDiffs,{clientId:n.clientId,sessionId:n.sessionId,parentSeqNum:n.parentSeqNum})}catch(r){this.emitter.emit("replay-error",{entry:n,error:r instanceof Error?r:new Error(String(r))})}finally{this.#r=n.seq}this.#n=!0,this.emitter.emit("ready",{entryCount:this.log.size})}reset(e,t=-1){this.#e={...e},this.#r=t,this.#n=t>=0}async*events(e){const t=[];let n;const r=this.emitter.on("state-changed",({entry:i})=>{t.push(i),n?.()}),o=()=>{n?.()};e?.addEventListener("abort",o,{once:!0});try{let i=0;for(;i<this.log.size;){if(e?.aborted)return;for(const s of this.log.getSince(i))yield s;i=this.log.nextSeq}for(;!e?.aborted;){for(;t.length>0;){const s=t.shift();s&&(yield s)}t.length===0&&await new Promise(s=>{n=s})}}finally{r(),e?.removeEventListener("abort",o)}}#s(e){const t=this.#i;if(typeof t=="function")return t(e);switch(t){case"ignore":return!1;case"fail":throw new Error(`EventSource: unhandled event type "${e.type}" (seq ${String(e.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:return console.warn(`[EventSource] unhandled event type "${e.type}" (seq ${String(e.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`),!1}}}export{w as EventSource,d as UNHANDLED};