@lunora/replica 1.0.0-alpha.78 → 1.0.0-alpha.79
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/README.md +7 -4
- package/dist/index.d.mts +71 -37
- package/dist/index.d.ts +71 -37
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/EventLogDO-BF9ZWc6C.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
- package/dist/packem_shared/EventsSync-B3wzXm-b.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/{local-mirror.d-C1pwwvwS.d.ts → local-mirror.d-CErKffFW.d.ts} +17 -9
- package/dist/packem_shared/{local-mirror.d-N1dkxd9A.d.mts → local-mirror.d-CtQovAv_.d.mts} +17 -9
- package/dist/packem_shared/subscribeToMirror-BT4oOBng.mjs +1 -0
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/package.json +1 -1
- package/dist/packem_shared/EventLogDO-CaZvpgJN.mjs +0 -1
- package/dist/packem_shared/EventLogDOClient-DGDMj5L5.mjs +0 -1
- package/dist/packem_shared/EventsSync-DWyGGZQZ.mjs +0 -1
- package/dist/packem_shared/LocalMirror-BIURA-He.mjs +0 -4
- package/dist/packem_shared/MaterializerRuntime-D0HOAe24.mjs +0 -1
- package/dist/packem_shared/subscribeToMirror-Rq100MXf.mjs +0 -1
package/README.md
CHANGED
|
@@ -142,7 +142,9 @@ machine, and pushes the resulting diffs to the mirror:
|
|
|
142
142
|
import { EventsSync } from "@lunora/replica";
|
|
143
143
|
|
|
144
144
|
const sync = new EventsSync({
|
|
145
|
-
|
|
145
|
+
// `getSince` answers ONE bounded page; EventsSync keeps calling with the
|
|
146
|
+
// advanced watermark until the log is exhausted.
|
|
147
|
+
fetchEventsSince: async (seq) => (await eventLogClient.getSince(seq)).entries,
|
|
146
148
|
applyEvents: (events) => {
|
|
147
149
|
/* feed events into your state machine */
|
|
148
150
|
},
|
|
@@ -235,9 +237,10 @@ const client = new EventLogDOClient({
|
|
|
235
237
|
const [entry] = await client.append([{ type: "order:placed", payload: { orderId: "123" } }]);
|
|
236
238
|
|
|
237
239
|
// Read back by sequence number — the log is append-only and ordered, so
|
|
238
|
-
// there is no filter-by-type query.
|
|
239
|
-
|
|
240
|
-
const { entries,
|
|
240
|
+
// there is no filter-by-type query. Every read is ONE bounded page (500
|
|
241
|
+
// entries by default, 1000 max): walk `cursor` while `truncated` is true.
|
|
242
|
+
const { entries, truncated, cursor } = await client.getSince(entry.seq);
|
|
243
|
+
const page = await client.getSince(0, 50);
|
|
241
244
|
const size = await client.getSize();
|
|
242
245
|
```
|
|
243
246
|
|
package/dist/index.d.mts
CHANGED
|
@@ -17,8 +17,8 @@ createBetterSqlite3Adapter } from "./adapters/better-sqlite3.mjs";
|
|
|
17
17
|
export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.mjs";
|
|
18
18
|
export { createSqlJsAdapter } from "./adapters/sqljs.mjs";
|
|
19
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-
|
|
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-
|
|
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";
|
|
22
22
|
/**
|
|
23
23
|
* Apply a single {@link TableDiff} to an in-memory row map and return
|
|
24
24
|
* the updated map.
|
|
@@ -194,19 +194,28 @@ declare class EventLogDOClient {
|
|
|
194
194
|
batchId?: string;
|
|
195
195
|
}): Promise<EventLogEntry[]>;
|
|
196
196
|
/**
|
|
197
|
-
* Fetch
|
|
197
|
+
* Fetch ONE page of entries with `seq >= sinceSeq`.
|
|
198
198
|
*
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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`.
|
|
206
214
|
*/
|
|
207
|
-
|
|
215
|
+
getSince(sinceSeq: number, limit?: number): Promise<{
|
|
216
|
+
cursor?: number;
|
|
208
217
|
entries: EventLogEntry[];
|
|
209
|
-
|
|
218
|
+
truncated: boolean;
|
|
210
219
|
}>;
|
|
211
220
|
/**
|
|
212
221
|
* Return the total number of entries currently in the log.
|
|
@@ -603,12 +612,24 @@ declare class MaterializerRuntime {
|
|
|
603
612
|
*
|
|
604
613
|
* 1. Recover materialized state from snapshots (if a snapshotStore is
|
|
605
614
|
* configured).
|
|
606
|
-
* 2. Fetch
|
|
615
|
+
* 2. Fetch entries since the MINIMUM per-materializer watermark from
|
|
607
616
|
* the DO — not the maximum — so a materializer with no snapshot (or a
|
|
608
617
|
* lower one) still receives every event it hasn't seen (REPLICA-04).
|
|
609
618
|
* 3. Apply them through the materializers; `applyEntries` skips each
|
|
610
619
|
* entry for any materializer already past it, so nothing is double-applied.
|
|
611
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
|
+
*
|
|
612
633
|
* Call this once on startup / after the DO binding is available.
|
|
613
634
|
* @returns The number of entries applied during catch-up.
|
|
614
635
|
*/
|
|
@@ -620,7 +641,8 @@ declare class MaterializerRuntime {
|
|
|
620
641
|
* This is a convenience over calling `doClient.append(...)` +
|
|
621
642
|
* `runtime.applyEntries(...)` yourself — it persists the event
|
|
622
643
|
* **then** applies the returned entry (with its assigned seq).
|
|
623
|
-
* @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).
|
|
624
646
|
*/
|
|
625
647
|
appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
|
|
626
648
|
/**
|
|
@@ -714,20 +736,15 @@ interface EventsFacade {
|
|
|
714
736
|
type: string;
|
|
715
737
|
}[]) => Promise<EventLogEntry[]>;
|
|
716
738
|
/**
|
|
717
|
-
* Fetch
|
|
718
|
-
* @returns `{ entries,
|
|
719
|
-
*
|
|
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.
|
|
720
742
|
*/
|
|
721
|
-
|
|
743
|
+
getSince: (sinceSeq: number, limit?: number) => Promise<{
|
|
744
|
+
cursor?: number;
|
|
722
745
|
entries: EventLogEntry[];
|
|
723
|
-
|
|
746
|
+
truncated: boolean;
|
|
724
747
|
}>;
|
|
725
|
-
/**
|
|
726
|
-
* Fetch all entries with `seq >= sinceSeq`.
|
|
727
|
-
*
|
|
728
|
-
* Pass `sinceSeq = 0` to fetch the entire log.
|
|
729
|
-
*/
|
|
730
|
-
getSince: (sinceSeq: number) => Promise<EventLogEntry[]>;
|
|
731
748
|
/** Return the total number of entries currently in the log. */
|
|
732
749
|
getSize: () => Promise<number>;
|
|
733
750
|
/** Return the full log state — all entries plus the next seq number. */
|
|
@@ -748,7 +765,7 @@ interface EventsContextOutput {
|
|
|
748
765
|
* Create a middleware that attaches a typed `ctx.events` facade backed by
|
|
749
766
|
* the given {@link EventLogDOClient}.
|
|
750
767
|
*
|
|
751
|
-
* The facade surfaces `append`, `getSince`, `
|
|
768
|
+
* The facade surfaces `append`, `getSince`, `getSize`, and
|
|
752
769
|
* `getState` — every method the DO client exposes — so handlers can read
|
|
753
770
|
* and write the event log without reaching for the DO stub directly.
|
|
754
771
|
*
|
|
@@ -790,12 +807,16 @@ interface SubscriptionClient {
|
|
|
790
807
|
* is applied to the local SQLite store.
|
|
791
808
|
*
|
|
792
809
|
* Each frame from a Lunora live query is the FULL current result set, so the
|
|
793
|
-
* callback
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
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.
|
|
799
820
|
*
|
|
800
821
|
* The mirror table name is derived from the function ref alone (not `args`), so
|
|
801
822
|
* do NOT mirror two subscriptions to the same function with different `args`
|
|
@@ -895,15 +916,28 @@ interface EventsSyncOptions {
|
|
|
895
916
|
* consumer should feed these events into their state machine
|
|
896
917
|
* (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
|
|
897
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.
|
|
898
929
|
*/
|
|
899
930
|
applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
|
|
900
931
|
/**
|
|
901
|
-
* Fetch
|
|
932
|
+
* Fetch the next batch of events whose `seq >= sinceSeq`.
|
|
902
933
|
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* EventLogDOClient.getSince
|
|
906
|
-
*
|
|
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
|
|
907
941
|
* event log, or read from an IndexedDB cache.
|
|
908
942
|
*
|
|
909
943
|
* Return an empty array when there are no new events.
|
package/dist/index.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ createBetterSqlite3Adapter } from "./adapters/better-sqlite3.js";
|
|
|
17
17
|
export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.js";
|
|
18
18
|
export { createSqlJsAdapter } from "./adapters/sqljs.js";
|
|
19
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-
|
|
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-
|
|
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";
|
|
22
22
|
/**
|
|
23
23
|
* Apply a single {@link TableDiff} to an in-memory row map and return
|
|
24
24
|
* the updated map.
|
|
@@ -194,19 +194,28 @@ declare class EventLogDOClient {
|
|
|
194
194
|
batchId?: string;
|
|
195
195
|
}): Promise<EventLogEntry[]>;
|
|
196
196
|
/**
|
|
197
|
-
* Fetch
|
|
197
|
+
* Fetch ONE page of entries with `seq >= sinceSeq`.
|
|
198
198
|
*
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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`.
|
|
206
214
|
*/
|
|
207
|
-
|
|
215
|
+
getSince(sinceSeq: number, limit?: number): Promise<{
|
|
216
|
+
cursor?: number;
|
|
208
217
|
entries: EventLogEntry[];
|
|
209
|
-
|
|
218
|
+
truncated: boolean;
|
|
210
219
|
}>;
|
|
211
220
|
/**
|
|
212
221
|
* Return the total number of entries currently in the log.
|
|
@@ -603,12 +612,24 @@ declare class MaterializerRuntime {
|
|
|
603
612
|
*
|
|
604
613
|
* 1. Recover materialized state from snapshots (if a snapshotStore is
|
|
605
614
|
* configured).
|
|
606
|
-
* 2. Fetch
|
|
615
|
+
* 2. Fetch entries since the MINIMUM per-materializer watermark from
|
|
607
616
|
* the DO — not the maximum — so a materializer with no snapshot (or a
|
|
608
617
|
* lower one) still receives every event it hasn't seen (REPLICA-04).
|
|
609
618
|
* 3. Apply them through the materializers; `applyEntries` skips each
|
|
610
619
|
* entry for any materializer already past it, so nothing is double-applied.
|
|
611
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
|
+
*
|
|
612
633
|
* Call this once on startup / after the DO binding is available.
|
|
613
634
|
* @returns The number of entries applied during catch-up.
|
|
614
635
|
*/
|
|
@@ -620,7 +641,8 @@ declare class MaterializerRuntime {
|
|
|
620
641
|
* This is a convenience over calling `doClient.append(...)` +
|
|
621
642
|
* `runtime.applyEntries(...)` yourself — it persists the event
|
|
622
643
|
* **then** applies the returned entry (with its assigned seq).
|
|
623
|
-
* @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).
|
|
624
646
|
*/
|
|
625
647
|
appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
|
|
626
648
|
/**
|
|
@@ -714,20 +736,15 @@ interface EventsFacade {
|
|
|
714
736
|
type: string;
|
|
715
737
|
}[]) => Promise<EventLogEntry[]>;
|
|
716
738
|
/**
|
|
717
|
-
* Fetch
|
|
718
|
-
* @returns `{ entries,
|
|
719
|
-
*
|
|
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.
|
|
720
742
|
*/
|
|
721
|
-
|
|
743
|
+
getSince: (sinceSeq: number, limit?: number) => Promise<{
|
|
744
|
+
cursor?: number;
|
|
722
745
|
entries: EventLogEntry[];
|
|
723
|
-
|
|
746
|
+
truncated: boolean;
|
|
724
747
|
}>;
|
|
725
|
-
/**
|
|
726
|
-
* Fetch all entries with `seq >= sinceSeq`.
|
|
727
|
-
*
|
|
728
|
-
* Pass `sinceSeq = 0` to fetch the entire log.
|
|
729
|
-
*/
|
|
730
|
-
getSince: (sinceSeq: number) => Promise<EventLogEntry[]>;
|
|
731
748
|
/** Return the total number of entries currently in the log. */
|
|
732
749
|
getSize: () => Promise<number>;
|
|
733
750
|
/** Return the full log state — all entries plus the next seq number. */
|
|
@@ -748,7 +765,7 @@ interface EventsContextOutput {
|
|
|
748
765
|
* Create a middleware that attaches a typed `ctx.events` facade backed by
|
|
749
766
|
* the given {@link EventLogDOClient}.
|
|
750
767
|
*
|
|
751
|
-
* The facade surfaces `append`, `getSince`, `
|
|
768
|
+
* The facade surfaces `append`, `getSince`, `getSize`, and
|
|
752
769
|
* `getState` — every method the DO client exposes — so handlers can read
|
|
753
770
|
* and write the event log without reaching for the DO stub directly.
|
|
754
771
|
*
|
|
@@ -790,12 +807,16 @@ interface SubscriptionClient {
|
|
|
790
807
|
* is applied to the local SQLite store.
|
|
791
808
|
*
|
|
792
809
|
* Each frame from a Lunora live query is the FULL current result set, so the
|
|
793
|
-
* callback
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
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.
|
|
799
820
|
*
|
|
800
821
|
* The mirror table name is derived from the function ref alone (not `args`), so
|
|
801
822
|
* do NOT mirror two subscriptions to the same function with different `args`
|
|
@@ -895,15 +916,28 @@ interface EventsSyncOptions {
|
|
|
895
916
|
* consumer should feed these events into their state machine
|
|
896
917
|
* (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
|
|
897
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.
|
|
898
929
|
*/
|
|
899
930
|
applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
|
|
900
931
|
/**
|
|
901
|
-
* Fetch
|
|
932
|
+
* Fetch the next batch of events whose `seq >= sinceSeq`.
|
|
902
933
|
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* EventLogDOClient.getSince
|
|
906
|
-
*
|
|
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
|
|
907
941
|
* event log, or read from an IndexedDB cache.
|
|
908
942
|
*
|
|
909
943
|
* Return an empty array when there are no new events.
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
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-C6A5aCk4.mjs";import{defineEvents as l}from"./packem_shared/defineEvents-DHo-VK7G.mjs";import{MaterializerRuntime as S,defineMaterializer as E}from"./packem_shared/MaterializerRuntime-
|
|
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-C6A5aCk4.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-BDEt9_BL.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
|
+
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
|
+
class c{#t;#r=0;#i=0;#s;#e;constructor(t){this.#t=t}get watermark(){return this.#r}start(){if(this.#s!==void 0)return;const t=this.#t.pollInterval??5e3;this.#s=setInterval(()=>{this.#n().catch(()=>{})},t)}stop(){this.#s!==void 0&&(clearInterval(this.#s),this.#s=void 0)}async sync(){return this.#n()}async#n(){if(this.#e)return this.#e;const t=this.#o().finally(()=>{this.#e=void 0});return this.#e=t,t}async#o(){let t=0;try{for(let r=0;r<1e3;r+=1){const s=await this.#t.fetchEventsSince(this.#r);if(s.length===0)return t;const e=s.filter(i=>i.seq>=this.#i);e.length>0&&(this.#t.applyEvents(e),this.#i=e[e.length-1].seq+1);const o=this.#t.getTableDiffs();for(const i of o)this.#t.mirror.applyDiff(i);t+=s.length;const n=s[s.length-1].seq+1;if(n<=this.#r)return t;this.#r=n}return t}catch(r){return(this.#t.onError??console.error)(r),t}}}export{c as EventsSync};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{createSqlJsAdapter as y}from"../adapters/sqljs.mjs";import{applyDiffToDb as m,escapeIdentifier as a}from"./applyDiffToDb-C6ek5Elp.mjs";import{EventLog as b}from"./EventLog-B1-yhArT.mjs";const A=1e3,l="__lunora_mirror_meta",R=o=>{o.exec(`CREATE TABLE IF NOT EXISTS ${l} (
|
|
2
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
3
|
+
value TEXT NOT NULL
|
|
4
|
+
)`)},u="schema_version",f=3,h=o=>typeof o=="bigint"||typeof o=="boolean"?"INTEGER":typeof o=="number"?Number.isInteger(o)?"INTEGER":"REAL":"TEXT";class E{#e;#t;#s;#n=new Set;#i=0;static create(e,s){const t=y(e);return new E({db:t,tables:s?.tables})}constructor(e){this.#e=e.db,this.#t={...e.tables},this.#s=new b({maxEntries:e.maxEventLogEntries??A}),R(this.#e),this.#o()}onChange(e){return this.#n.add(e),()=>{this.#n.delete(e)}}get eventLog(){return this.#s}get db(){return this.#e}get version(){return this.#i}applyDiff(e){e.changes.length!==0&&(this.#l(e),m(this.#e,e,this.primaryKeyOf(e.table)),this.#s.append("table-diff",e,[e]),this.#r())}query(e,s){return this.#e.query(e,s)}clearData(){const e=this.#a();this.#e.transaction(()=>{for(const{name:s}of e)this.#e.exec(`DELETE FROM ${a(s)}`)}),this.#r()}#r(){this.#i+=1;for(const e of this.#n)try{e()}catch{}}close(){this.#e.close(),this.#s.clear(),this.#n.clear()}registerTable(e,s){this.#t[e]={...this.#t[e],...s}}primaryKeyOf(e){return this.#t[e]?.primaryKey??"id"}get mirroredTables(){return Object.keys(this.#t)}#a(){return this.#e.query(String.raw`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\_\_lunora\_%' ESCAPE '\' AND name NOT LIKE 'sqlite\_%' ESCAPE '\'`)}#o(){if(this.#e.query(`SELECT key, value FROM ${l}`).find(n=>n.key===u)?.value===String(f))return;const t=this.#a();this.#e.transaction(()=>{for(const{name:n}of t)this.#e.exec(`DROP TABLE IF EXISTS ${a(n)}`);this.#e.exec(`INSERT OR REPLACE INTO ${l} (key, value) VALUES (?, ?)`,[u,String(f)])})}static#c(e,s){const t=new Set;for(const n of e.changes)if(n.type!=="delete")for(const c of Object.keys(n.data))c!==s&&t.add(c);return t}static#E(e,s,t){const n=new Map;for(const c of e.changes)if(!(c.type==="delete"||n.size===t.size))for(const r of t){if(r===s||n.has(r))continue;const i=c.data[r];i!=null&&n.set(r,h(i))}return n}static#h(e,s){for(const t of e.changes){const n=t.type==="delete"?t.id:t.data[s];if(n!=null)return h(n);if(t.type==="update")return h(t.id)}return"TEXT"}#l(e){const s=this.primaryKeyOf(e.table),t=E.#c(e,s),n=E.#E(e,s,t);if(this.#e.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[e.table]).length===0){const r=E.#h(e,s);let i=`${a(s)} ${r==="INTEGER"?"INT":r} PRIMARY KEY NOT NULL`;for(const T of t)i+=`, ${a(T)} ${n.get(T)??"TEXT"}`;this.#e.exec(`CREATE TABLE IF NOT EXISTS ${a(e.table)} (${i})`)}else if(t.size>0){const r=new Set(this.#e.query(`PRAGMA table_info(${a(e.table)})`).map(i=>i.name));for(const i of t)r.has(i)||this.#e.exec(`ALTER TABLE ${a(e.table)} ADD COLUMN ${a(i)} ${n.get(i)??"TEXT"}`)}}}export{E as LocalMirror};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c=a=>{let e=a.initial();return{def:a,get state(){return Object.freeze(e)},setState(t){e=t},apply(t){e=a.handle(e,t)},reset(){e=a.initial()}}};class u{#e;#i;#n;#s;#t;constructor(e,t={}){this.#e=[...e],this.#t=this.#e.map(()=>0),this.#i=t.snapshotStore,this.#n=t.doClient,this.#s=t.unknownEventHandling??"warn"}get appliedSeq(){return this.#t.length>0?Math.min(...this.#t):0}applyEntries(e){let t=0;for(const i of e){let s=!1,n=!1;const r=[];for(const[o,p]of this.#e.entries()){const h=this.#t[o]??0;if(i.seq<h)continue;const l=p.state;p.apply(i),s=!0,p.state!==l&&(n=!0),r.push(o)}if(s){n||this.#a(i);for(const o of r)this.#t[o]=i.seq+1;t+=1}}return t}#a(e){const t=this.#s;if(typeof t=="function"){t(e);return}switch(t){case"ignore":return;case"fail":throw new Error(`MaterializerRuntime: unhandled event type "${e.type}" (seq ${String(e.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:console.warn(`[MaterializerRuntime] unhandled event type "${e.type}" (seq ${String(e.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`)}}async recoverFromSnapshots(){if(!this.#i)return 0;let e=0;for(const[t,i]of this.#e.entries()){const s=await this.#i.load(i.def.name);if(s!==null&&typeof s=="object"){const n=s;if(Number.isSafeInteger(n.appliedSeq)&&n.appliedSeq>=0&&n.state!==void 0){const r=n.appliedSeq;i.setState(n.state),this.#t[t]=r,r>e&&(e=r)}}}return e}async persistSnapshots(){if(this.#i)for(const[e,t]of this.#e.entries())await this.#i.save(t.def.name,{appliedSeq:this.#t[e]??0,state:t.state})}async initialize(){return this.#n?(await this.recoverFromSnapshots(),this.#r()):0}async#r(){const e=this.#n;if(!e)return 0;let t=this.appliedSeq,i=0;for(let s=0;s<1e3;s+=1){const n=await e.getSince(t);if(i+=this.applyEntries(n.entries),!n.truncated||n.cursor===void 0||n.cursor<=t)return i;t=n.cursor}return i}async appendEvent(e){if(!this.#n)throw new Error("MaterializerRuntime.appendEvent requires a doClient — pass one in the constructor options.");const i=(await this.#n.append([e]))[0];if(!i)throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");return this.#e.length>0&&this.appliedSeq<i.seq&&(await this.#r(),this.appliedSeq<i.seq)||this.applyEntries([i]),i}reset(){for(const[e,t]of this.#e.entries())this.#t[e]=0,t.reset()}get materializers(){return this.#e}}export{u as MaterializerRuntime,c as defineMaterializer};
|
|
@@ -381,15 +381,18 @@ interface LocalMirrorOptions {
|
|
|
381
381
|
/** Platform-specific SQLite adapter. */
|
|
382
382
|
readonly db: SqliteAdapter;
|
|
383
383
|
/**
|
|
384
|
-
* Cap the mirror's internal {@link EventLog} to this many entries
|
|
385
|
-
*
|
|
386
|
-
*
|
|
384
|
+
* Cap the mirror's internal {@link EventLog} to this many entries.
|
|
385
|
+
* Every applied diff is recorded in the log, so an uncapped log grows by
|
|
386
|
+
* one entry (holding every changed row) per diff for the life of the
|
|
387
|
+
* mirror — a leak by construction on a long-lived client.
|
|
387
388
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* a
|
|
389
|
+
* Defaults to {@link DEFAULT_MAX_EVENT_LOG_ENTRIES}. On overflow the
|
|
390
|
+
* OLDEST entries are dropped; nothing in the mirror replays its own log,
|
|
391
|
+
* so a drop loses nothing the mirror needs. A consumer that does replay
|
|
392
|
+
* it (`eventLog.getSince(watermark)` from another tab / service worker)
|
|
393
|
+
* detects a gap when the first returned entry's `seq` is above its
|
|
394
|
+
* watermark, and should re-seed from the mirror's rows (`query`) instead
|
|
395
|
+
* of applying the partial window.
|
|
393
396
|
*/
|
|
394
397
|
readonly maxEventLogEntries?: number;
|
|
395
398
|
/**
|
|
@@ -519,9 +522,14 @@ declare class LocalMirror {
|
|
|
519
522
|
close(): void;
|
|
520
523
|
/**
|
|
521
524
|
* Register a table schema so the mirror can create the table on
|
|
522
|
-
* first use.
|
|
525
|
+
* first use. Merges into any definition already registered for `name`
|
|
526
|
+
* (from the constructor's `tables` or an earlier call), so a helper that
|
|
527
|
+
* registers `{}` just to make the table known does not erase a
|
|
528
|
+
* user-supplied `primaryKey`.
|
|
523
529
|
*/
|
|
524
530
|
registerTable(name: string, definition: MirrorTableDef): void;
|
|
531
|
+
/** The primary-key column of a mirrored table (`"id"` unless registered otherwise). */
|
|
532
|
+
primaryKeyOf(table: string): string;
|
|
525
533
|
/**
|
|
526
534
|
* Return the list of mirrored table names.
|
|
527
535
|
*/
|
|
@@ -381,15 +381,18 @@ interface LocalMirrorOptions {
|
|
|
381
381
|
/** Platform-specific SQLite adapter. */
|
|
382
382
|
readonly db: SqliteAdapter;
|
|
383
383
|
/**
|
|
384
|
-
* Cap the mirror's internal {@link EventLog} to this many entries
|
|
385
|
-
*
|
|
386
|
-
*
|
|
384
|
+
* Cap the mirror's internal {@link EventLog} to this many entries.
|
|
385
|
+
* Every applied diff is recorded in the log, so an uncapped log grows by
|
|
386
|
+
* one entry (holding every changed row) per diff for the life of the
|
|
387
|
+
* mirror — a leak by construction on a long-lived client.
|
|
387
388
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* a
|
|
389
|
+
* Defaults to {@link DEFAULT_MAX_EVENT_LOG_ENTRIES}. On overflow the
|
|
390
|
+
* OLDEST entries are dropped; nothing in the mirror replays its own log,
|
|
391
|
+
* so a drop loses nothing the mirror needs. A consumer that does replay
|
|
392
|
+
* it (`eventLog.getSince(watermark)` from another tab / service worker)
|
|
393
|
+
* detects a gap when the first returned entry's `seq` is above its
|
|
394
|
+
* watermark, and should re-seed from the mirror's rows (`query`) instead
|
|
395
|
+
* of applying the partial window.
|
|
393
396
|
*/
|
|
394
397
|
readonly maxEventLogEntries?: number;
|
|
395
398
|
/**
|
|
@@ -519,9 +522,14 @@ declare class LocalMirror {
|
|
|
519
522
|
close(): void;
|
|
520
523
|
/**
|
|
521
524
|
* Register a table schema so the mirror can create the table on
|
|
522
|
-
* first use.
|
|
525
|
+
* first use. Merges into any definition already registered for `name`
|
|
526
|
+
* (from the constructor's `tables` or an earlier call), so a helper that
|
|
527
|
+
* registers `{}` just to make the table known does not erase a
|
|
528
|
+
* user-supplied `primaryKey`.
|
|
523
529
|
*/
|
|
524
530
|
registerTable(name: string, definition: MirrorTableDef): void;
|
|
531
|
+
/** The primary-key column of a mirrored table (`"id"` unless registered otherwise). */
|
|
532
|
+
primaryKeyOf(table: string): string;
|
|
525
533
|
/**
|
|
526
534
|
* Return the list of mirrored table names.
|
|
527
535
|
*/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const k=/["\\\u0000-\u001F\uD800-\uDFFF]/,O=r=>k.test(r)?JSON.stringify(r):`"${r}"`,w=r=>{if(r===void 0)return"null";if(typeof r=="bigint")throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");if(typeof r=="number"){if(Number.isNaN(r))return"nan";if(r===1/0)return"inf";if(r===-1/0)return"-inf";if(Object.is(r,-0))return"-0"}if(typeof r=="string")return O(r);if(r===null||typeof r!="object")return JSON.stringify(r);if(Array.isArray(r)){let e="[";for(let o=0;o<r.length;o++)o>0&&(e+=","),e+=w(r[o]);return e+"]"}const n=Object.getPrototypeOf(r);if(n!==null&&n!==Object.prototype){const e=r.constructor?.name??"value";throw new TypeError(`stableStringify: cannot use a ${e} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`)}const f=r,s=Object.keys(f).sort();let c="{",t=!0;for(const e of s){const o=f[e];o!==void 0&&(t?t=!1:c+=",",c+=O(e),c+=":",c+=w(o))}return c+"}"},l=r=>{let n="";for(let s=0;s<r.length;s+=32768)n+=String.fromCharCode(...r.subarray(s,s+32768));return btoa(n)},i="$lunora.wire$";const j="__proto__",E=r=>{if(r===null||typeof r!="object")return!1;const n=Object.getPrototypeOf(r);return n===null||n===Object.prototype},y=(r,n=0)=>{if(n>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(r===void 0)return[i,"undefined"];if(r===null)return null;const f=typeof r;if(f==="bigint")return[i,"bigint",r.toString()];if(f==="number"){const t=r;return Number.isNaN(t)?[i,"nan"]:t===1/0?[i,"inf"]:t===-1/0?[i,"-inf"]:t}if(f!=="object")return r;if(r instanceof Date)return[i,"date",y(r.getTime(),n+1)];if(r instanceof Error){const t=r,e={};for(const b of Object.keys(t))t[b]!==void 0&&(e[b]=y(t[b],n+1));const o=[i,"error",t.name,t.message,e];return t.cause!==void 0&&o.push(y(t.cause,n+1)),o}if(r instanceof URL)return[i,"url",r.href];if(r instanceof Map)return[i,"map",[...r.entries()].map(([t,e])=>[y(t,n+1),y(e,n+1)])];if(r instanceof Set)return[i,"set",[...r].map(t=>y(t,n+1))];if(r instanceof ArrayBuffer)return[i,"bytes",l(new Uint8Array(r)),"ArrayBuffer"];if(ArrayBuffer.isView(r)){const t=r,e=t.constructor.name,o=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return e==="Uint8Array"?[i,"bytes",l(o)]:[i,"bytes",l(o),e]}if(Array.isArray(r)){const t=r.map(e=>y(e,n+1));return t.length>0&&t[0]===i?[i,"arr",t]:t}if(!E(r)){const t=r.constructor?.name??"value";throw new TypeError(`wire-codec: cannot encode a ${t} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`)}const s=r,c={};for(const t of Object.keys(s)){const e=s[t];if(e===void 0)continue;const o=y(e,n+1);t===j?Object.defineProperty(c,t,{configurable:!0,enumerable:!0,value:o,writable:!0}):c[t]=o}return c},N=r=>w(y(r)),S=r=>`fn_${r.replaceAll(/[/:.]/g,"_")}`,_=r=>Array.isArray(r)?r:r!==null&&typeof r=="object"?[r]:[],T=(r,n,f,s,c)=>{const t=S(f.__lunoraRef);n.registerTable(t,{});const e=n.primaryKeyOf(t);let o=new Map;return r.subscribe(f,s,b=>{const d=new Map,m=new Map,p=[];for(const a of _(b)){const u=a,g=u[e];if(typeof g!="string"&&typeof g!="number"&&typeof g!="bigint"){p.push({type:"insert",data:u});continue}const A=String(g),h=N(u);d.set(A,h),m.set(A,u)}for(const[a,u]of d)o.get(a)!==u&&p.push({data:m.get(a),type:"insert"});for(const a of o.keys())d.has(a)||p.push({type:"delete",id:a});p.length>0&&n.applyDiff({table:t,changes:p,timestamp:Date.now()}),o=d},{shardKey:c})};export{T as subscribeToMirror};
|
package/dist/react.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
1
|
+
import { L as LocalMirror } from "./packem_shared/local-mirror.d-CtQovAv_.mjs";
|
|
2
2
|
import "./packem_shared/types.d-BuLTPLaQ.mjs";
|
|
3
3
|
/**
|
|
4
4
|
* Result of {@link useLocalQuery} — a discriminated union so callers get a
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LocalMirror } from "./packem_shared/local-mirror.d-
|
|
1
|
+
import { L as LocalMirror } from "./packem_shared/local-mirror.d-CErKffFW.js";
|
|
2
2
|
import "./packem_shared/types.d-BuLTPLaQ.js";
|
|
3
3
|
/**
|
|
4
4
|
* Result of {@link useLocalQuery} — a discriminated union so callers get a
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const T=Symbol("lunora.replica.event-log-do.idempotency-conflict"),q=n=>{const t=new Error(n);return Object.defineProperty(t,T,{value:!0}),t},N=n=>n instanceof Error&&T in n,h=n=>{if(Array.isArray(n))return n.map(t=>h(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]=h(t[r]);return s}return n},S=async n=>{const t=JSON.stringify(h(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("")},l=(n,t=200)=>Response.json(n,{status:t,headers:{"content-type":"application/json"}}),u=(n,t,e)=>l({error:{code:t,message:e}},n),E=n=>typeof n.toArray=="function"?n.toArray():typeof n[Symbol.iterator]=="function"?[...n]:[],f=n=>E(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.#p();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==="/range")return this.#a(e);if(t.method==="GET"&&e.pathname==="/size")return this.#o();if(t.method==="GET"&&e.pathname==="/state")return this.#c()}catch(s){return console.error("[event-log-do] request failed:",s),u(500,"INTERNAL_ERROR","internal error")}return u(404,"NOT_FOUND","unknown route")}async#e(t){let e;try{e=await t.json()}catch{return u(400,"BAD_REQUEST","invalid JSON body")}const s=p.#n(e);if(s)return u(400,"BAD_REQUEST",s);const{sql:r}=this.state.storage,{batchId:i}=e,o=()=>p.#s(r,e,i),{transaction:a}=this.state.storage;let c;try{c=typeof a=="function"?await a(o):await o()}catch(m){if(N(m))return u(409,"CONFLICT",m.message);throw m}return l({entries:c})}static async#s(t,e,s){let r;if(typeof s=="string"){r=await S(e.events);const a=p.#u(t,s);if(a){if(a.fingerprint!==r)throw q(`batchId "${s}" was already used for a different event batch`);return a.entries}}const i=Date.now(),o=[];for(const a of e.events){const d={seq:p.#l(t),type:a.type,payload:a.payload,timestamp:a.timestamp??i,clientId:a.clientId,sessionId:a.sessionId,parentSeqNum:a.parentSeqNum};p.#m(t,d),o.push(d)}if(typeof s=="string"&&r!==void 0){const a=o[0]?.seq,c=o.at(-1)?.seq;a!==void 0&&c!==void 0&&p.#d(t,s,a,c,r)}return o}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);if(!Number.isFinite(s)||s<0)return u(400,"BAD_REQUEST","invalid seq");const{sql:r}=this.state.storage,i=r.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC",s);return l({entries:f(i)})}#a(t){const e=t.searchParams.get("from"),s=e===null?0:Number(e),r=t.searchParams.get("limit"),i=r===null?50:Number(r);if(!Number.isFinite(s)||s<0||!Number.isFinite(i)||i<1||i>1e3)return u(400,"BAD_REQUEST","invalid from/limit");const{sql:o}=this.state.storage,a=o.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC LIMIT ?",s,i+1),c=f(a),d=c.length>i,y={entries:d?c.slice(0,i):c,hasMore:d};return l(y)}#o(){const{sql:t}=this.state.storage,e=t.exec("SELECT COUNT(*) AS count FROM events"),r=E(e)[0]?.count??0;return l({count:r})}#c(){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=f(e),r=(s.at(-1)?.seq??-1)+1;return l({entries:s,nextSeq:r})}#p(){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#u(t,e){const s=t.exec("SELECT first_seq, last_seq, fingerprint FROM event_batches WHERE batch_id = ?",e),i=E(s)[0];if(!i)return;const o=t.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? AND seq <= ? ORDER BY seq ASC",i.first_seq,i.last_seq);return{entries:f(o),fingerprint:i.fingerprint}}static#d(t,e,s,r,i){t.exec("INSERT INTO event_batches (batch_id, first_seq, last_seq, fingerprint) VALUES (?, ?, ?, ?)",e,s,r,i)}static#l(t){const e=t.exec("SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM events");return E(e)[0]?.next_seq??0}static#m(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};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
class s{#a;constructor(t){this.#a=t.fetch}async append(t,a){const e=JSON.stringify({events:t,batchId:a?.batchId}),n=await this.#a(new Request("https://do/append",{method:"POST",headers:{"content-type":"application/json"},body:e}));if(!n.ok)throw await s.#e(n,"append");return(await n.json()).entries}async getSince(t){return(await this.#t(`/since?seq=${String(t)}`,"getSince")).entries}async getRange(t,a=50){return this.#t(`/range?from=${String(t)}&limit=${String(a)}`,"getRange")}async getSize(){return(await this.#t("/size","getSize")).count}async getState(){return this.#t("/state","getState")}async#t(t,a){const e=await this.#a(new Request(`https://do${t}`));if(!e.ok)throw await s.#e(e,a);return await e.json()}static async#e(t,a){try{const n=(await t.json()).error?.message??t.statusText;return new Error(`EventLogDO.${a} failed (${String(t.status)}): ${n}`)}catch{return new Error(`EventLogDO.${a} failed (${String(t.status)}): ${t.statusText}`)}}}export{s as EventLogDOClient};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
class e{#t;#i=0;#s;#r;constructor(t){this.#t=t}get watermark(){return this.#i}start(){if(this.#s!==void 0)return;const t=this.#t.pollInterval??5e3;this.#s=setInterval(()=>{this.#n().catch(()=>{})},t)}stop(){this.#s!==void 0&&(clearInterval(this.#s),this.#s=void 0)}async sync(){return this.#n()}async#n(){if(this.#r)return this.#r;const t=this.#e().finally(()=>{this.#r=void 0});return this.#r=t,t}async#e(){try{const t=await this.#t.fetchEventsSince(this.#i);if(t.length===0)return 0;this.#t.applyEvents(t);const s=this.#t.getTableDiffs();for(const i of s)this.#t.mirror.applyDiff(i);const r=t[t.length-1];return this.#i=r.seq+1,t.length}catch(t){return(this.#t.onError??console.error)(t),0}}}export{e as EventsSync};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import{createSqlJsAdapter as y}from"../adapters/sqljs.mjs";import{applyDiffToDb as m,escapeIdentifier as a}from"./applyDiffToDb-C6ek5Elp.mjs";import{EventLog as b}from"./EventLog-B1-yhArT.mjs";const h="__lunora_mirror_meta",g=o=>{o.exec(`CREATE TABLE IF NOT EXISTS ${h} (
|
|
2
|
-
key TEXT PRIMARY KEY NOT NULL,
|
|
3
|
-
value TEXT NOT NULL
|
|
4
|
-
)`)},T="schema_version",f=3,l=o=>typeof o=="bigint"||typeof o=="boolean"?"INTEGER":typeof o=="number"?Number.isInteger(o)?"INTEGER":"REAL":"TEXT";class E{#e;#t;#n;#s=new Set;#i=0;static create(e,t){const n=y(e);return new E({db:n,tables:t?.tables})}constructor(e){this.#e=e.db,this.#t={...e.tables},this.#n=new b({maxEntries:e.maxEventLogEntries}),g(this.#e),this.#o()}onChange(e){return this.#s.add(e),()=>{this.#s.delete(e)}}get eventLog(){return this.#n}get db(){return this.#e}get version(){return this.#i}applyDiff(e){if(e.changes.length===0)return;const t=this.#t[e.table]?.primaryKey??"id";this.#h(e),m(this.#e,e,t),this.#n.append("table-diff",e,[e]),this.#r()}query(e,t){return this.#e.query(e,t)}clearData(){const e=this.#a();this.#e.transaction(()=>{for(const{name:t}of e)this.#e.exec(`DELETE FROM ${a(t)}`)}),this.#r()}#r(){this.#i+=1;for(const e of this.#s)try{e()}catch{}}close(){this.#e.close(),this.#n.clear(),this.#s.clear()}registerTable(e,t){this.#t[e]=t}get mirroredTables(){return Object.keys(this.#t)}#a(){return this.#e.query(String.raw`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\_\_lunora\_%' ESCAPE '\' AND name NOT LIKE 'sqlite\_%' ESCAPE '\'`)}#o(){if(this.#e.query(`SELECT key, value FROM ${h}`).find(s=>s.key===T)?.value===String(f))return;const n=this.#a();this.#e.transaction(()=>{for(const{name:s}of n)this.#e.exec(`DROP TABLE IF EXISTS ${a(s)}`);this.#e.exec(`INSERT OR REPLACE INTO ${h} (key, value) VALUES (?, ?)`,[T,String(f)])})}static#c(e,t){const n=new Set;for(const s of e.changes)if(s.type!=="delete")for(const c of Object.keys(s.data))c!==t&&n.add(c);return n}static#E(e,t,n){const s=new Map;for(const c of e.changes)if(!(c.type==="delete"||s.size===n.size))for(const r of n){if(r===t||s.has(r))continue;const i=c.data[r];i!=null&&s.set(r,l(i))}return s}static#l(e,t){for(const n of e.changes){const s=n.type==="delete"?n.id:n.data[t];if(s!=null)return l(s);if(n.type==="update")return l(n.id)}return"TEXT"}#h(e){const t=this.#t[e.table]?.primaryKey??"id",n=E.#c(e,t),s=E.#E(e,t,n);if(this.#e.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[e.table]).length===0){const r=E.#l(e,t);let i=`${a(t)} ${r==="INTEGER"?"INT":r} PRIMARY KEY NOT NULL`;for(const u of n)i+=`, ${a(u)} ${s.get(u)??"TEXT"}`;this.#e.exec(`CREATE TABLE IF NOT EXISTS ${a(e.table)} (${i})`)}else if(n.size>0){const r=new Set(this.#e.query(`PRAGMA table_info(${a(e.table)})`).map(i=>i.name));for(const i of n)r.has(i)||this.#e.exec(`ALTER TABLE ${a(e.table)} ADD COLUMN ${a(i)} ${s.get(i)??"TEXT"}`)}}}export{E as LocalMirror};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const c=a=>{let e=a.initial();return{def:a,get state(){return Object.freeze(e)},setState(t){e=t},apply(t){e=a.handle(e,t)},reset(){e=a.initial()}}};class u{#t;#n;#i;#s;#e;constructor(e,t={}){this.#t=[...e],this.#e=this.#t.map(()=>0),this.#n=t.snapshotStore,this.#i=t.doClient,this.#s=t.unknownEventHandling??"warn"}get appliedSeq(){return this.#e.length>0?Math.min(...this.#e):0}applyEntries(e){let t=0;for(const n of e){let s=!1,i=!1;const r=[];for(const[o,h]of this.#t.entries()){const l=this.#e[o]??0;if(n.seq<l)continue;const p=h.state;h.apply(n),s=!0,h.state!==p&&(i=!0),r.push(o)}if(s){i||this.#r(n);for(const o of r)this.#e[o]=n.seq+1;t+=1}}return t}#r(e){const t=this.#s;if(typeof t=="function"){t(e);return}switch(t){case"ignore":return;case"fail":throw new Error(`MaterializerRuntime: unhandled event type "${e.type}" (seq ${String(e.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:console.warn(`[MaterializerRuntime] unhandled event type "${e.type}" (seq ${String(e.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`)}}async recoverFromSnapshots(){if(!this.#n)return 0;let e=0;for(const[t,n]of this.#t.entries()){const s=await this.#n.load(n.def.name);if(s!==null&&typeof s=="object"){const i=s;if(Number.isSafeInteger(i.appliedSeq)&&i.appliedSeq>=0&&i.state!==void 0){const r=i.appliedSeq;n.setState(i.state),this.#e[t]=r,r>e&&(e=r)}}}return e}async persistSnapshots(){if(this.#n)for(const[e,t]of this.#t.entries())await this.#n.save(t.def.name,{appliedSeq:this.#e[e]??0,state:t.state})}async initialize(){if(!this.#i)return 0;await this.recoverFromSnapshots();const e=this.#e.length>0?Math.min(...this.#e):0,t=await this.#i.getSince(e);return t.length===0?0:this.applyEntries(t)}async appendEvent(e){if(!this.#i)throw new Error("MaterializerRuntime.appendEvent requires a doClient — pass one in the constructor options.");const n=(await this.#i.append([e]))[0];if(!n)throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");return this.applyEntries([n]),n}reset(){for(const[e,t]of this.#t.entries())this.#e[e]=0,t.reset()}get materializers(){return this.#t}}export{u as MaterializerRuntime,c as defineMaterializer};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const y=e=>`fn_${e.replaceAll(/[/:.]/g,"_")}`,w=e=>Array.isArray(e)?e:e!==null&&typeof e=="object"?[e]:[],d=(e,c,i,a,p)=>{const f=y(i.__lunoraRef);c.registerTable(f,{});let s=new Set;return e.subscribe(i,a,u=>{const b=w(u),r=new Set,t=[];for(const n of b){const l=n,o=l.id;(typeof o=="string"||typeof o=="number")&&r.add(String(o)),t.push({type:"insert",data:l})}for(const n of s)r.has(n)||t.push({type:"delete",id:n});if(t.length===0){s=r;return}c.applyDiff({table:f,changes:t,timestamp:Date.now()}),s=r},{shardKey:p})};export{d as subscribeToMirror};
|