@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.
- package/LICENSE.md +214 -0
- package/README.md +55 -10
- package/dist/adapters/better-sqlite3.d.mts +1 -1
- package/dist/adapters/better-sqlite3.d.ts +1 -1
- package/dist/adapters/better-sqlite3.mjs +1 -29
- package/dist/adapters/sqlite-wasm.d.mts +3 -9
- package/dist/adapters/sqlite-wasm.d.ts +3 -9
- package/dist/adapters/sqlite-wasm.mjs +1 -44
- package/dist/adapters/sqljs.d.mts +1 -1
- package/dist/adapters/sqljs.d.ts +1 -1
- package/dist/adapters/sqljs.mjs +1 -55
- package/dist/index.d.mts +113 -52
- package/dist/index.d.ts +113 -52
- package/dist/index.mjs +1 -20
- package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
- package/dist/packem_shared/EventLog-B1-yhArT.mjs +1 -0
- package/dist/packem_shared/EventLogDO-BF9ZWc6C.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
- package/dist/packem_shared/EventSource-BC0hKJSA.mjs +1 -0
- package/dist/packem_shared/EventsSync-B3wzXm-b.mjs +1 -0
- package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
- package/dist/packem_shared/LocalMirror-ZmB8SJFe.mjs +4 -0
- package/dist/packem_shared/MaterializerRuntime-S-Knx6BM.mjs +1 -0
- package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
- package/dist/packem_shared/applyDiff-DRJ1gap3.mjs +1 -0
- package/dist/packem_shared/applyDiffToDb-C6ek5Elp.mjs +1 -0
- package/dist/packem_shared/classifyChanges-BBc0-770.mjs +1 -0
- package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
- package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
- package/dist/packem_shared/fnv1a-BNN96GYb.mjs +1 -0
- package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
- package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-CErKffFW.d.ts} +20 -12
- package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-CtQovAv_.d.mts} +20 -12
- package/dist/packem_shared/subscribeToMirror-BT4oOBng.mjs +1 -0
- package/dist/packem_shared/{types.d-CkMkSwLJ.d.mts → types.d-BuLTPLaQ.d.mts} +0 -2
- package/dist/packem_shared/{types.d-CkMkSwLJ.d.ts → types.d-BuLTPLaQ.d.ts} +0 -2
- package/dist/react.d.mts +39 -29
- package/dist/react.d.ts +39 -29
- package/dist/react.mjs +1 -15
- package/package.json +1 -1
- package/dist/packem_shared/EventEmitter-CMZfct03.mjs +0 -92
- package/dist/packem_shared/EventLog-CnK-3Wge.mjs +0 -264
- package/dist/packem_shared/EventLogDO-DqlsVx0H.mjs +0 -381
- package/dist/packem_shared/EventLogDOClient-F4FO8Si4.mjs +0 -92
- package/dist/packem_shared/EventSource-D5yO9_aI.mjs +0 -221
- package/dist/packem_shared/EventsSync-BP36tC9O.mjs +0 -123
- package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +0 -24
- package/dist/packem_shared/LocalMirror-a-5jEqFN.mjs +0 -219
- package/dist/packem_shared/MaterializerRuntime-BoIrsMYB.mjs +0 -224
- package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +0 -75
- package/dist/packem_shared/applyDiff-98tKzmiW.mjs +0 -67
- package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +0 -58
- package/dist/packem_shared/classifyChanges-RcqLBpLs.mjs +0 -41
- package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +0 -28
- package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +0 -6
- package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +0 -5
- package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +0 -45
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
export {
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
34
|
-
*
|
|
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
|
|
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
|
|
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
|
|
197
|
+
* Fetch ONE page of entries with `seq >= sinceSeq`.
|
|
182
198
|
*
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
|
|
215
|
+
getSince(sinceSeq: number, limit?: number): Promise<{
|
|
216
|
+
cursor?: number;
|
|
192
217
|
entries: EventLogEntry[];
|
|
193
|
-
|
|
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
|
|
275
|
-
* signature and aren't assignable to `Record
|
|
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
|
|
412
|
-
* break out of the `for await` loop or
|
|
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
|
|
510
|
-
* `Materializer
|
|
511
|
-
* to `Materializer
|
|
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
|
|
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
|
|
700
|
-
* @returns `{ entries,
|
|
701
|
-
*
|
|
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
|
-
|
|
743
|
+
getSince: (sinceSeq: number, limit?: number) => Promise<{
|
|
744
|
+
cursor?: number;
|
|
704
745
|
entries: EventLogEntry[];
|
|
705
|
-
|
|
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`, `
|
|
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
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
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
|
|
932
|
+
* Fetch the next batch of events whose `seq >= sinceSeq`.
|
|
884
933
|
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
* EventLogDOClient.getSince
|
|
888
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
34
|
-
*
|
|
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
|
|
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
|
|
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
|
|
197
|
+
* Fetch ONE page of entries with `seq >= sinceSeq`.
|
|
182
198
|
*
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
-
|
|
215
|
+
getSince(sinceSeq: number, limit?: number): Promise<{
|
|
216
|
+
cursor?: number;
|
|
192
217
|
entries: EventLogEntry[];
|
|
193
|
-
|
|
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
|
|
275
|
-
* signature and aren't assignable to `Record
|
|
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
|
|
412
|
-
* break out of the `for await` loop or
|
|
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
|
|
510
|
-
* `Materializer
|
|
511
|
-
* to `Materializer
|
|
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
|
|
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
|
|
700
|
-
* @returns `{ entries,
|
|
701
|
-
*
|
|
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
|
-
|
|
743
|
+
getSince: (sinceSeq: number, limit?: number) => Promise<{
|
|
744
|
+
cursor?: number;
|
|
704
745
|
entries: EventLogEntry[];
|
|
705
|
-
|
|
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`, `
|
|
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
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
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
|
|
932
|
+
* Fetch the next batch of events whose `seq >= sinceSeq`.
|
|
884
933
|
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
* EventLogDOClient.getSince
|
|
888
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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};
|