@lunora/replica 1.0.0-alpha.9 → 1.0.0-alpha.91
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 +2 -1
- package/dist/adapters/better-sqlite3.d.ts +2 -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 +4 -2
- package/dist/adapters/sqljs.d.ts +4 -2
- package/dist/adapters/sqljs.mjs +1 -55
- package/dist/index.d.mts +158 -55
- package/dist/index.d.ts +158 -55
- package/dist/index.mjs +1 -20
- package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
- package/dist/packem_shared/EventLog-SuC_BKwj.mjs +1 -0
- package/dist/packem_shared/EventLogDO-BgUx2GGL.mjs +1 -0
- package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
- package/dist/packem_shared/EventSource-Bg6zNmRn.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-Bn22hyEB.mjs +4 -0
- package/dist/packem_shared/MaterializerRuntime-DFXi-aqd.mjs +1 -0
- package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
- package/dist/packem_shared/applyDiff-CZAqAC8Y.mjs +1 -0
- package/dist/packem_shared/applyDiffToDb-CK-Dcy17.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/int64-CCVxepl4.mjs +1 -0
- package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
- package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-DzREvGNM.d.mts} +54 -22
- package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-wbGXkXaE.d.ts} +54 -22
- package/dist/packem_shared/subscribeToMirror-DSn8n1IR.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/packem_shared/wire-key-DfMHAtqH.mjs +1 -0
- 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-DIJWdi7n.mjs +0 -382
- 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-Ctu04rYc.mjs +0 -88
- 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-DzREvGNM.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-DzREvGNM.mjs";
|
|
7
22
|
/**
|
|
8
23
|
* Apply a single {@link TableDiff} to an in-memory row map and return
|
|
9
24
|
* the updated map.
|
|
@@ -39,7 +54,7 @@ declare const applyDiffs: (current: ReadonlyMap<string, Record<string, unknown>>
|
|
|
39
54
|
/**
|
|
40
55
|
* Merge the row-level effect of a {@link TableDiff} into plain JSON
|
|
41
56
|
* state keyed by table name, returning a new snapshot.
|
|
42
|
-
* @param snapshot Current snapshot, e.g. `{ users: Map
|
|
57
|
+
* @param snapshot Current snapshot, e.g. `{ users: Map<id, row>, posts: Map<id, row> }`.
|
|
43
58
|
* @param diff Contains the target table name and the row-level changes to merge.
|
|
44
59
|
* @returns A shallow copy of `snapshot` with `diff.table`'s map updated.
|
|
45
60
|
* @experimental
|
|
@@ -106,7 +121,7 @@ interface DefineEventsOptions {
|
|
|
106
121
|
/**
|
|
107
122
|
* Optional version prefix for all event types.
|
|
108
123
|
*
|
|
109
|
-
* When set, every qualified event type is prefixed with `"v
|
|
124
|
+
* When set, every qualified event type is prefixed with `"v<N>."`, enabling
|
|
110
125
|
* versioned event naming like `"v1.chat.messageSent"` or `"v2.chat.messageSent"`.
|
|
111
126
|
* This allows materializers to evolve their handling logic based on the event
|
|
112
127
|
* version without breaking backward compatibility.
|
|
@@ -179,19 +194,28 @@ declare class EventLogDOClient {
|
|
|
179
194
|
batchId?: string;
|
|
180
195
|
}): Promise<EventLogEntry[]>;
|
|
181
196
|
/**
|
|
182
|
-
* Fetch
|
|
197
|
+
* Fetch ONE page of entries with `seq >= sinceSeq`.
|
|
183
198
|
*
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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`.
|
|
191
214
|
*/
|
|
192
|
-
|
|
215
|
+
getSince(sinceSeq: number, limit?: number): Promise<{
|
|
216
|
+
cursor?: number;
|
|
193
217
|
entries: EventLogEntry[];
|
|
194
|
-
|
|
218
|
+
truncated: boolean;
|
|
195
219
|
}>;
|
|
196
220
|
/**
|
|
197
221
|
* Return the total number of entries currently in the log.
|
|
@@ -199,6 +223,12 @@ declare class EventLogDOClient {
|
|
|
199
223
|
getSize(): Promise<number>;
|
|
200
224
|
/**
|
|
201
225
|
* Return the full log state — all entries plus the next seq number.
|
|
226
|
+
*
|
|
227
|
+
* Only for a log small enough to answer as one body: the DO refuses with a
|
|
228
|
+
* 413 past its page ceiling, since serialising an unbounded log into one
|
|
229
|
+
* response is what {@link EventLogDOClient.getSince} was bounded to avoid.
|
|
230
|
+
* A catch-up walks `getSince` instead.
|
|
231
|
+
* @throws Error when the log is too large to return in one body
|
|
202
232
|
*/
|
|
203
233
|
getState(): Promise<{
|
|
204
234
|
entries: EventLogEntry[];
|
|
@@ -272,8 +302,8 @@ type UnknownEventHandling = "warn" | "ignore" | "fail" | ((entry: EventLogEntry)
|
|
|
272
302
|
* Events emitted by the {@link EventSource} runtime.
|
|
273
303
|
*
|
|
274
304
|
* A `type` (not `interface`) so it satisfies `EventEmitter`'s
|
|
275
|
-
* `Record
|
|
276
|
-
* signature and aren't assignable to `Record
|
|
305
|
+
* `Record<string, unknown>` constraint — interfaces have no implicit index
|
|
306
|
+
* signature and aren't assignable to `Record<string, unknown>`.
|
|
277
307
|
* @experimental
|
|
278
308
|
*/
|
|
279
309
|
type EventSourceEvents = {
|
|
@@ -409,8 +439,10 @@ declare class EventSource<S extends Record<string, unknown> = Record<string, unk
|
|
|
409
439
|
* starting from the events currently in the log and continuing with
|
|
410
440
|
* every future `applyEvent` / `replayFromLog` call.
|
|
411
441
|
*
|
|
412
|
-
* The generator runs indefinitely
|
|
413
|
-
* break out of the `for await` loop or
|
|
442
|
+
* The generator runs indefinitely unless given a `signal` — callers
|
|
443
|
+
* should break out of the `for await` loop or pass an `AbortSignal` to
|
|
444
|
+
* stop it (an abort settles the generator, `done: true`, on its next
|
|
445
|
+
* iteration step; it does not throw).
|
|
414
446
|
* @example
|
|
415
447
|
* ```ts
|
|
416
448
|
* for await (const entry of source.events()) {
|
|
@@ -461,9 +493,16 @@ declare class InMemorySnapshotStore implements SnapshotStore {
|
|
|
461
493
|
*
|
|
462
494
|
* Pure functions are strongly encouraged: given the same event and state,
|
|
463
495
|
* they must produce the same next state for deterministic replay.
|
|
496
|
+
*
|
|
497
|
+
* Return {@link UNHANDLED} for an event `type` the reducer does not recognise —
|
|
498
|
+
* that, and only that, is what {@link MaterializerRuntimeOptions.unknownEventHandling}
|
|
499
|
+
* reacts to. Returning the current `state` is a legitimate, idempotent no-op for
|
|
500
|
+
* a type the reducer DOES handle; reference equality cannot tell the two apart
|
|
501
|
+
* (REPLICA-07), and reading it as "unhandled" warned about — or, under `"fail"`,
|
|
502
|
+
* threw on — an event type the reducer explicitly recognised.
|
|
464
503
|
* @experimental
|
|
465
504
|
*/
|
|
466
|
-
type MaterializerReducer<S> = (state: S, entry: EventLogEntry) => S;
|
|
505
|
+
type MaterializerReducer<S> = (state: S, entry: EventLogEntry) => S | typeof UNHANDLED;
|
|
467
506
|
/**
|
|
468
507
|
* Options for defining a single materializer.
|
|
469
508
|
* @experimental
|
|
@@ -472,7 +511,8 @@ interface MaterializerDef<S> {
|
|
|
472
511
|
/**
|
|
473
512
|
* Reducer invoked for every event in the log.
|
|
474
513
|
*
|
|
475
|
-
* Return the current state unchanged
|
|
514
|
+
* Return the current state unchanged for a recognised event with nothing to
|
|
515
|
+
* do; return {@link UNHANDLED} for a `type` this reducer does not process.
|
|
476
516
|
*/
|
|
477
517
|
handle: MaterializerReducer<S>;
|
|
478
518
|
/** Factory for the initial (empty) state. */
|
|
@@ -485,8 +525,12 @@ interface MaterializerDef<S> {
|
|
|
485
525
|
* @experimental
|
|
486
526
|
*/
|
|
487
527
|
interface Materializer<S> {
|
|
488
|
-
/**
|
|
489
|
-
|
|
528
|
+
/**
|
|
529
|
+
* Apply a single event entry through the reducer.
|
|
530
|
+
* @returns `false` when the reducer returned {@link UNHANDLED} (state left
|
|
531
|
+
* untouched), `true` otherwise.
|
|
532
|
+
*/
|
|
533
|
+
apply: (entry: EventLogEntry) => boolean;
|
|
490
534
|
readonly def: MaterializerDef<S>;
|
|
491
535
|
/** Reset to the initial state. */
|
|
492
536
|
reset: () => void;
|
|
@@ -507,9 +551,9 @@ declare const defineMaterializer: <S>(definition: MaterializerDef<S>) => Materia
|
|
|
507
551
|
* A materializer of any state shape. The {@link MaterializerRuntime} holds a
|
|
508
552
|
* heterogeneous collection and only ever calls `apply(entry)` / `setState(...)`
|
|
509
553
|
* (with cast values) / reads `def.name` — it never needs the concrete state
|
|
510
|
-
* type. `Materializer
|
|
511
|
-
* `Materializer
|
|
512
|
-
* to `Materializer
|
|
554
|
+
* type. `Materializer<unknown>` won't do: `setState(state: S)` makes
|
|
555
|
+
* `Materializer<S>` invariant in `S`, so `Materializer<number>` isn't assignable
|
|
556
|
+
* to `Materializer<unknown>`. Erasing the type param is the idiomatic fix.
|
|
513
557
|
*/
|
|
514
558
|
type AnyMaterializer = Materializer<any>;
|
|
515
559
|
/**
|
|
@@ -528,7 +572,21 @@ interface MaterializerRuntimeOptions {
|
|
|
528
572
|
/** Optional snapshot store for persisting/recovering materialized state. */
|
|
529
573
|
snapshotStore?: SnapshotStore;
|
|
530
574
|
/**
|
|
531
|
-
* How to handle
|
|
575
|
+
* How to handle an event that every materializer explicitly DECLINED — one
|
|
576
|
+
* for which each reducer returned {@link UNHANDLED}.
|
|
577
|
+
*
|
|
578
|
+
* A reducer that instead falls through to `return state` for a type it does
|
|
579
|
+
* not recognise has, as far as the runtime can tell, handled the event: it
|
|
580
|
+
* changed nothing, but it did not decline. `"fail"` and `"warn"` are inert
|
|
581
|
+
* for such a reducer, and no option here can make them otherwise — write the
|
|
582
|
+
* reducer's default branch as `return UNHANDLED` if you want to hear about
|
|
583
|
+
* unknown types.
|
|
584
|
+
*
|
|
585
|
+
* A materializer whose own watermark is already past the entry does not run
|
|
586
|
+
* for it, and does not count as declining it: an entry that was already
|
|
587
|
+
* applied has already been classified, so a catch-up replaying it for a
|
|
588
|
+
* LAGGING materializer alone never re-reports it. Without that, `"fail"`
|
|
589
|
+
* aborted a catch-up on events a snapshot-recovered sibling had processed.
|
|
532
590
|
* @default "warn"
|
|
533
591
|
*/
|
|
534
592
|
unknownEventHandling?: UnknownEventHandling;
|
|
@@ -586,12 +644,24 @@ declare class MaterializerRuntime {
|
|
|
586
644
|
*
|
|
587
645
|
* 1. Recover materialized state from snapshots (if a snapshotStore is
|
|
588
646
|
* configured).
|
|
589
|
-
* 2. Fetch
|
|
647
|
+
* 2. Fetch entries since the MINIMUM per-materializer watermark from
|
|
590
648
|
* the DO — not the maximum — so a materializer with no snapshot (or a
|
|
591
649
|
* lower one) still receives every event it hasn't seen (REPLICA-04).
|
|
592
650
|
* 3. Apply them through the materializers; `applyEntries` skips each
|
|
593
651
|
* entry for any materializer already past it, so nothing is double-applied.
|
|
594
652
|
*
|
|
653
|
+
* The DO answers one BOUNDED page per request, so step 2/3 walk pages until
|
|
654
|
+
* the log is exhausted — applying each page as it arrives, rather than
|
|
655
|
+
* holding the whole backlog in memory. Taking only the first page (and
|
|
656
|
+
* dropping `truncated`) would silently leave every materializer short of
|
|
657
|
+
* the log's head whenever the backlog exceeds a page.
|
|
658
|
+
*
|
|
659
|
+
* The walk is bounded by {@link MAX_CATCHUP_PAGES}: against a log written
|
|
660
|
+
* faster than it is read, "until the log is exhausted" never arrives and
|
|
661
|
+
* startup would never finish. Hitting the budget returns what was applied
|
|
662
|
+
* with every materializer's watermark advanced, so a later `initialize()`
|
|
663
|
+
* (or the ordinary append path) picks up exactly where this left off.
|
|
664
|
+
*
|
|
595
665
|
* Call this once on startup / after the DO binding is available.
|
|
596
666
|
* @returns The number of entries applied during catch-up.
|
|
597
667
|
*/
|
|
@@ -603,7 +673,8 @@ declare class MaterializerRuntime {
|
|
|
603
673
|
* This is a convenience over calling `doClient.append(...)` +
|
|
604
674
|
* `runtime.applyEntries(...)` yourself — it persists the event
|
|
605
675
|
* **then** applies the returned entry (with its assigned seq).
|
|
606
|
-
* @returns The persisted entry with its DO-assigned `seq
|
|
676
|
+
* @returns The persisted entry with its DO-assigned `seq` — always, whether
|
|
677
|
+
* or not the entry could be applied to the materializers (see below).
|
|
607
678
|
*/
|
|
608
679
|
appendEvent(input: AppendEventInput): Promise<EventLogEntry>;
|
|
609
680
|
/**
|
|
@@ -697,20 +768,15 @@ interface EventsFacade {
|
|
|
697
768
|
type: string;
|
|
698
769
|
}[]) => Promise<EventLogEntry[]>;
|
|
699
770
|
/**
|
|
700
|
-
* Fetch
|
|
701
|
-
* @returns `{ entries,
|
|
702
|
-
*
|
|
771
|
+
* Fetch ONE bounded page of entries with `seq >= sinceSeq`.
|
|
772
|
+
* @returns `{ entries, truncated, cursor }` — pass `cursor` back as
|
|
773
|
+
* `sinceSeq` while `truncated` is `true` to walk the whole log.
|
|
703
774
|
*/
|
|
704
|
-
|
|
775
|
+
getSince: (sinceSeq: number, limit?: number) => Promise<{
|
|
776
|
+
cursor?: number;
|
|
705
777
|
entries: EventLogEntry[];
|
|
706
|
-
|
|
778
|
+
truncated: boolean;
|
|
707
779
|
}>;
|
|
708
|
-
/**
|
|
709
|
-
* Fetch all entries with `seq >= sinceSeq`.
|
|
710
|
-
*
|
|
711
|
-
* Pass `sinceSeq = 0` to fetch the entire log.
|
|
712
|
-
*/
|
|
713
|
-
getSince: (sinceSeq: number) => Promise<EventLogEntry[]>;
|
|
714
780
|
/** Return the total number of entries currently in the log. */
|
|
715
781
|
getSize: () => Promise<number>;
|
|
716
782
|
/** Return the full log state — all entries plus the next seq number. */
|
|
@@ -731,7 +797,7 @@ interface EventsContextOutput {
|
|
|
731
797
|
* Create a middleware that attaches a typed `ctx.events` facade backed by
|
|
732
798
|
* the given {@link EventLogDOClient}.
|
|
733
799
|
*
|
|
734
|
-
* The facade surfaces `append`, `getSince`, `
|
|
800
|
+
* The facade surfaces `append`, `getSince`, `getSize`, and
|
|
735
801
|
* `getState` — every method the DO client exposes — so handlers can read
|
|
736
802
|
* and write the event log without reaching for the DO stub directly.
|
|
737
803
|
*
|
|
@@ -773,18 +839,33 @@ interface SubscriptionClient {
|
|
|
773
839
|
* is applied to the local SQLite store.
|
|
774
840
|
*
|
|
775
841
|
* Each frame from a Lunora live query is the FULL current result set, so the
|
|
776
|
-
* callback
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
842
|
+
* callback diffs it against the previous frame: a row that is new or whose
|
|
843
|
+
* content changed is upserted, a row that dropped out is deleted, and an
|
|
844
|
+
* unchanged row produces nothing. A frame identical to the last one therefore
|
|
845
|
+
* applies no diff — no event-log entry, no `version` bump, no re-query for the
|
|
846
|
+
* hooks subscribed to the mirror.
|
|
847
|
+
*
|
|
848
|
+
* Rows are keyed by the table's primary key (`id` unless the table was
|
|
849
|
+
* registered with another `primaryKey`). A row without one still lands — the
|
|
850
|
+
* apply path derives a key from the ROW's own content — but it cannot be
|
|
851
|
+
* diffed: it is re-emitted on every frame, and it is never reconciled on
|
|
852
|
+
* removal because only keyed rows are recorded for the delete pass. Repeating an
|
|
853
|
+
* identical frame is therefore a no-op upsert, but each distinct content the
|
|
854
|
+
* un-keyed row ever holds leaves a row behind for the life of the mirror.
|
|
855
|
+
* **Mirror a query that selects the primary key.** An un-keyed shape (an
|
|
856
|
+
* aggregate, or a projection that drops `id`) is supported only so that one such
|
|
857
|
+
* row cannot take the rest of the frame down with it.
|
|
782
858
|
*
|
|
783
859
|
* The mirror table name is derived from the function ref alone (not `args`), so
|
|
784
860
|
* do NOT mirror two subscriptions to the same function with different `args`
|
|
785
861
|
* into the same mirror: they'd share one table and the snapshot-delete pass of
|
|
786
862
|
* one could remove rows still live in the other.
|
|
787
863
|
*
|
|
864
|
+
* `mirror.clearData()` resets the remembered frame. Without that reset the next
|
|
865
|
+
* identical frame diffed clean against a mirror that no longer held the rows —
|
|
866
|
+
* no changes, no `applyDiff` — and the table stayed empty until some row's
|
|
867
|
+
* content happened to change or the page reloaded.
|
|
868
|
+
*
|
|
788
869
|
* Call the returned unsubscribe function to tear down both the client
|
|
789
870
|
* subscription and future mirror writes.
|
|
790
871
|
* @example
|
|
@@ -878,15 +959,28 @@ interface EventsSyncOptions {
|
|
|
878
959
|
* consumer should feed these events into their state machine
|
|
879
960
|
* (e.g. an {@link import("@lunora/replica").EventSource | EventSource})
|
|
880
961
|
* so that the machine's state reflects the latest log position.
|
|
962
|
+
*
|
|
963
|
+
* **Must be atomic across the batch: apply every event, or none.** There is
|
|
964
|
+
* no rollback here and none is possible — the state machine is the
|
|
965
|
+
* consumer's. A call that mutates derived state and then throws partway is
|
|
966
|
+
* re-delivered WHOLE on the next poll (the watermark only advances past a
|
|
967
|
+
* batch that fully succeeded, and a replay that threw is not recorded), so a
|
|
968
|
+
* non-atomic implementation applies the events before the throw twice. A
|
|
969
|
+
* call that RETURNS is never re-delivered: {@link EventsSync} tracks the
|
|
970
|
+
* highest applied `seq` separately from the watermark, so a batch whose
|
|
971
|
+
* replay succeeded and whose mirror fan-out then failed is not replayed.
|
|
881
972
|
*/
|
|
882
973
|
applyEvents: (events: ReadonlyArray<EventLogEntry>) => void;
|
|
883
974
|
/**
|
|
884
|
-
* Fetch
|
|
975
|
+
* Fetch the next batch of events whose `seq >= sinceSeq`.
|
|
885
976
|
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
888
|
-
* EventLogDOClient.getSince
|
|
889
|
-
*
|
|
977
|
+
* It does NOT have to return the whole backlog: a bounded batch is
|
|
978
|
+
* preferred and is what the DO-backed transport gives you
|
|
979
|
+
* ({@link import("@lunora/replica").EventLogDOClient.getSince |
|
|
980
|
+
* EventLogDOClient.getSince()} answers one page). {@link EventsSync} keeps
|
|
981
|
+
* calling with the advanced watermark until a call returns nothing, so the
|
|
982
|
+
* whole log is applied either way — one bounded atom at a time.
|
|
983
|
+
* In a client context this could call a Lunora action that proxies to the
|
|
890
984
|
* event log, or read from an IndexedDB cache.
|
|
891
985
|
*
|
|
892
986
|
* Return an empty array when there are no new events.
|
|
@@ -896,8 +990,17 @@ interface EventsSyncOptions {
|
|
|
896
990
|
* Produce {@link TableDiff | TableDiffs} from the current derived state.
|
|
897
991
|
*
|
|
898
992
|
* Called after every batch of events has been applied. The consumer
|
|
899
|
-
*
|
|
900
|
-
* diffs needed to bring the LocalMirror up to date.
|
|
993
|
+
* **recomputes a full diff from the current mirror-vs-source state** and
|
|
994
|
+
* returns the diffs needed to bring the LocalMirror up to date.
|
|
995
|
+
*
|
|
996
|
+
* **MUST be idempotent** — it must NOT advance a one-shot cursor as a side
|
|
997
|
+
* effect. A batch that fails partway (a `mirror.applyDiff` throws) is
|
|
998
|
+
* retried on the next poll from the same watermark; if this call consumed a
|
|
999
|
+
* cursor on the first attempt it would return `[]` on the retry and the
|
|
1000
|
+
* un-mirrored diffs would be lost forever. Recompute-from-current-state has
|
|
1001
|
+
* no such hazard: calling it again with no new events returns the same
|
|
1002
|
+
* diffs, and calling it after a partial mirror write returns exactly the
|
|
1003
|
+
* diffs still missing from the mirror.
|
|
901
1004
|
*
|
|
902
1005
|
* Return an empty array when there are no changes to push to the mirror.
|
|
903
1006
|
*/
|