@lunora/replica 0.0.0 → 1.0.0-alpha.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE.md +445 -0
  2. package/README.md +239 -29
  3. package/dist/adapters/better-sqlite3.d.mts +29 -0
  4. package/dist/adapters/better-sqlite3.d.ts +29 -0
  5. package/dist/adapters/better-sqlite3.mjs +1 -0
  6. package/dist/adapters/sqlite-wasm.d.mts +30 -0
  7. package/dist/adapters/sqlite-wasm.d.ts +30 -0
  8. package/dist/adapters/sqlite-wasm.mjs +1 -0
  9. package/dist/adapters/sqljs.d.mts +22 -0
  10. package/dist/adapters/sqljs.d.ts +22 -0
  11. package/dist/adapters/sqljs.mjs +1 -0
  12. package/dist/index.d.mts +1074 -0
  13. package/dist/index.d.ts +1074 -0
  14. package/dist/index.mjs +1 -0
  15. package/dist/packem_shared/EventEmitter-uo75adUL.mjs +1 -0
  16. package/dist/packem_shared/EventLog-SuC_BKwj.mjs +1 -0
  17. package/dist/packem_shared/EventLogDO-BgUx2GGL.mjs +1 -0
  18. package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
  19. package/dist/packem_shared/EventSource-Bg6zNmRn.mjs +1 -0
  20. package/dist/packem_shared/EventsSync-B3wzXm-b.mjs +1 -0
  21. package/dist/packem_shared/InMemorySnapshotStore-C4taIG5K.mjs +1 -0
  22. package/dist/packem_shared/LocalMirror-Bn22hyEB.mjs +4 -0
  23. package/dist/packem_shared/MaterializerRuntime-DFXi-aqd.mjs +1 -0
  24. package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
  25. package/dist/packem_shared/applyDiff-CZAqAC8Y.mjs +1 -0
  26. package/dist/packem_shared/applyDiffToDb-CK-Dcy17.mjs +1 -0
  27. package/dist/packem_shared/classifyChanges-BBc0-770.mjs +1 -0
  28. package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
  29. package/dist/packem_shared/eventsContext-Dxow9Y7S.mjs +1 -0
  30. package/dist/packem_shared/fnv1a-BNN96GYb.mjs +1 -0
  31. package/dist/packem_shared/int64-CCVxepl4.mjs +1 -0
  32. package/dist/packem_shared/isClientSeq-D2Xm0_lj.mjs +1 -0
  33. package/dist/packem_shared/local-mirror.d-DzREvGNM.d.mts +562 -0
  34. package/dist/packem_shared/local-mirror.d-wbGXkXaE.d.ts +562 -0
  35. package/dist/packem_shared/subscribeToMirror-DSn8n1IR.mjs +1 -0
  36. package/dist/packem_shared/types.d-BuLTPLaQ.d.mts +22 -0
  37. package/dist/packem_shared/types.d-BuLTPLaQ.d.ts +22 -0
  38. package/dist/packem_shared/wire-key-DfMHAtqH.mjs +1 -0
  39. package/dist/react.d.mts +77 -0
  40. package/dist/react.d.ts +77 -0
  41. package/dist/react.mjs +1 -0
  42. package/package.json +88 -7
@@ -0,0 +1,562 @@
1
+ import { S as SqliteAdapter } from "./types.d-BuLTPLaQ.js";
2
+ /**
3
+ * Row-level change kind within a TableDiff.
4
+ *
5
+ * Each change represents one row that was inserted, updated, or deleted on
6
+ * the server since the last sync tick.
7
+ * @experimental
8
+ */
9
+ type RowChange = {
10
+ data: Record<string, unknown>;
11
+ type: "insert";
12
+ } | {
13
+ data: Record<string, unknown>;
14
+ id: string;
15
+ type: "update";
16
+ } | {
17
+ id: string;
18
+ type: "delete";
19
+ };
20
+ /**
21
+ * A scoped, ordered set of row changes for a single table.
22
+ *
23
+ * `TableDiff` is the unit of replication between the server and the local
24
+ * SQLite mirror. The server pushes diffs over the poke protocol; the
25
+ * client applies them via `applyDiff`.
26
+ * @experimental
27
+ */
28
+ interface TableDiff {
29
+ /** Ordered row changes — insert/update/delete, earliest first. */
30
+ readonly changes: ReadonlyArray<RowChange>;
31
+ /**
32
+ * Optional stable identity for this diff, distinct from `timestamp`
33
+ * (multiple diffs can legitimately share a millisecond, so `timestamp`
34
+ * alone is not a unique diff identity). `createTableDiff` auto-generates
35
+ * one when omitted.
36
+ *
37
+ * **Nothing in the apply path reads it.** `deriveInsertId` (`apply-diff.ts`)
38
+ * keys an id-less insert off the ROW'S OWN content — hashing the diff id
39
+ * into it is what made `subscribeToMirror`'s per-frame re-emission mint a
40
+ * fresh row every second, so that derivation is gone. This field is
41
+ * carried for consumers that want to recognise a diff they have seen.
42
+ */
43
+ readonly id?: string;
44
+ /** Logical table name (matches the schema table name). */
45
+ readonly table: string;
46
+ /** Monotonic server timestamp (ms since epoch) when this diff was emitted. */
47
+ readonly timestamp: number;
48
+ }
49
+ /**
50
+ * Create a {@link TableDiff} with a snapshot of the current time and a
51
+ * fresh stable `id` (unless one is explicitly provided).
52
+ * @experimental
53
+ */
54
+ declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number, id?: string) => TableDiff;
55
+ /**
56
+ * Return `true` when the diff contains no row changes.
57
+ * @experimental
58
+ */
59
+ declare const isDiffEmpty: (diff: TableDiff) => boolean;
60
+ /**
61
+ * Return the number of rows touched by the diff (inserts + updates + deletes).
62
+ * @experimental
63
+ */
64
+ declare const diffSize: (diff: TableDiff) => number;
65
+ /**
66
+ * Partition a {@link TableDiff} into three categories for batch processing.
67
+ * @experimental
68
+ */
69
+ declare const classifyChanges: (diff: TableDiff) => {
70
+ deletes: RowChange[];
71
+ inserts: RowChange[];
72
+ updates: RowChange[];
73
+ };
74
+ /**
75
+ * Merge several diffs for the same table into one (ordering preserved).
76
+ *
77
+ * Returns `null` when the input list is empty.
78
+ * @experimental
79
+ */
80
+ declare const mergeDiffs: (diffs: ReadonlyArray<TableDiff>) => TableDiff | null;
81
+ /**
82
+ * Sequence-number types for the event log.
83
+ *
84
+ * Three namespaces match the vocabulary established by event-sourcing:
85
+ *
86
+ * | Namespace | Shape | Producer | Consumer |
87
+ * |-----------|--------------------|---------------------------|------------------------------|
88
+ * | `Global` | `number` | `EventLog` / `EventLogDO` | Materializers, subscribers |
89
+ * | `Input` | No seq (optimistic) | `defineEvents` factories | `EventLog.append` / DO client|
90
+ * | `Client` | `{generation,seq}` | _(future: rebase engine)_ | _(future: rebase-aware code)_|
91
+ * @module
92
+ */
93
+ /**
94
+ * A server-authoritative (global) sequence number.
95
+ *
96
+ * Monotonically increasing, assigned by `EventLog` (in-memory) or
97
+ * `EventLogDO` (Durable Object). All confirmed log entries carry a
98
+ * `GlobalSeq`.
99
+ * @experimental
100
+ */
101
+ type GlobalSeq = number;
102
+ /**
103
+ * A client-originated composite sequence number designed to survive rebase.
104
+ *
105
+ * Carries the last-confirmed `global` seq, a monotonically increasing
106
+ * `client` counter, and a `rebaseGeneration` that increments whenever the
107
+ * client's local events are rebased onto a new upstream baseline.
108
+ * @experimental
109
+ */
110
+ interface ClientSeq {
111
+ /** Client-local monotonically increasing counter. */
112
+ readonly client: number;
113
+ /** The last-confirmed global sequence number. 0 for unconfirmed events. */
114
+ readonly global: number;
115
+ /** Incremented on every rebase. */
116
+ readonly rebaseGeneration: number;
117
+ }
118
+ /**
119
+ * Discriminated union of all sequence-number types.
120
+ * @experimental
121
+ */
122
+ type Seq = GlobalSeq | ClientSeq;
123
+ /**
124
+ * Narrow `Seq` to `GlobalSeq`.
125
+ * @experimental
126
+ */
127
+ declare const isGlobalSeq: (seq: Seq) => seq is GlobalSeq;
128
+ /**
129
+ * Narrow `Seq` to `ClientSeq`.
130
+ * @experimental
131
+ */
132
+ declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
133
+ /**
134
+ * An event that has **not** yet been assigned a sequence number.
135
+ *
136
+ * Input events represent optimistic / command payloads before the server
137
+ * confirms them. They carry `type`, `payload`, and `timestamp` but no
138
+ * `seq` — the log assigns one on append.
139
+ *
140
+ * Create input events via `defineEvents` factories:
141
+ *
142
+ * ```ts
143
+ * const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
144
+ * // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
145
+ * ```
146
+ * @experimental
147
+ */
148
+ interface InputEvent<Type extends string = string, Payload = unknown> {
149
+ /** Arbitrary JSON-serialisable payload. */
150
+ readonly payload: Payload;
151
+ /** Millisecond timestamp (epoch) when the event was created. */
152
+ readonly timestamp: number;
153
+ /** Event type discriminator (e.g. `"chat.messageSent"`). */
154
+ readonly type: Type;
155
+ }
156
+ /**
157
+ * Type guard: check whether `value` is an {@link InputEvent}.
158
+ * @experimental
159
+ */
160
+ declare const isInputEvent: (value: unknown) => value is InputEvent;
161
+ /**
162
+ * A single entry in the append-only {@link EventLog}.
163
+ *
164
+ * Entries are immutable once appended; the `seq` field is assigned
165
+ * monotonically by the log and doubles as a watermark for catch-up
166
+ * replication between tabs or service-worker instances.
167
+ * @experimental
168
+ */
169
+ interface EventLogEntry {
170
+ /**
171
+ * Globally-unique client identifier that originated this event.
172
+ * Set by clients that support offline / optimistic writes.
173
+ * `undefined` when the event was created server-side.
174
+ */
175
+ readonly clientId?: string;
176
+ /**
177
+ * The sequence number of the causal parent event.
178
+ *
179
+ * A {@link GlobalSeq} for events confirmed by the server (pointing
180
+ * to the previous confirmed event), or a {@link ClientSeq} for
181
+ * optimistic / offline events pointing to the local predecessor.
182
+ * `undefined` for the first event in a log.
183
+ */
184
+ readonly parentSeqNum?: Seq;
185
+ /** Arbitrary JSON-serialisable payload. */
186
+ readonly payload: unknown;
187
+ /** Monotonically increasing sequence number (0-based). A {@link GlobalSeq}. */
188
+ readonly seq: GlobalSeq;
189
+ /**
190
+ * Session identifier from the originating client.
191
+ * Paired with `clientId` to disambiguate concurrent sessions.
192
+ */
193
+ readonly sessionId?: string;
194
+ /**
195
+ * Optional table diffs that this event produced.
196
+ * When present, a consumer can re-play the event by applying the diffs
197
+ * to its local mirror without re-executing the originating mutation.
198
+ */
199
+ readonly tableDiffs?: ReadonlyArray<TableDiff>;
200
+ /** Millisecond timestamp (epoch) when the entry was appended. */
201
+ readonly timestamp: number;
202
+ /** Event type discriminator (e.g. "row-insert", "mutation-apply"). */
203
+ readonly type: string;
204
+ }
205
+ /**
206
+ * Serialised snapshot of the log — used for persistence and transfer.
207
+ * @experimental
208
+ */
209
+ interface EventLogSnapshot {
210
+ readonly entries: ReadonlyArray<EventLogEntry>;
211
+ /** The sequence number of the last entry (head), or `null` for an empty log. */
212
+ readonly headSeq: GlobalSeq | null;
213
+ readonly nextSeq: number;
214
+ }
215
+ /**
216
+ * Optional metadata that can accompany an appended event.
217
+ * @experimental
218
+ */
219
+ interface AppendOptions {
220
+ /** Globally-unique client identifier. */
221
+ readonly clientId?: string;
222
+ /**
223
+ * Causal parent sequence number.
224
+ * Automatically set to the previous entry's seq when omitted.
225
+ */
226
+ readonly parentSeqNum?: Seq;
227
+ /** Session identifier within the client. */
228
+ readonly sessionId?: string;
229
+ /**
230
+ * Override the entry's `timestamp` instead of stamping `Date.now()` at
231
+ * append time.
232
+ *
233
+ * Used by callers (e.g. {@link import("./event-source").EventSource | EventSource})
234
+ * that must commit the EXACT entry a reducer already observed — a
235
+ * second, independently-drawn `Date.now()` at append time could produce
236
+ * a persisted entry a timestamp-dependent reducer cannot reproduce on
237
+ * replay.
238
+ */
239
+ readonly timestamp?: number;
240
+ }
241
+ /**
242
+ * Options for constructing an {@link EventLog}.
243
+ * @experimental
244
+ */
245
+ interface EventLogOptions {
246
+ /**
247
+ * Cap the number of entries retained in memory (REPLICA-06). When an
248
+ * append would exceed the cap, the OLDEST entries are evicted (ring
249
+ * buffer) — a `getSince`/`getFrom` call for a watermark below the oldest
250
+ * retained `seq` then returns only what's left, silently missing
251
+ * anything evicted.
252
+ *
253
+ * `undefined` (the default) preserves the original unbounded behavior.
254
+ * Set this only when you have another durable source of truth for
255
+ * anything older than the cap (a snapshot, a server-side `EventLogDO`) —
256
+ * see {@link EventLog#truncateBelow} for the caller-driven equivalent
257
+ * tied to snapshot persistence.
258
+ */
259
+ readonly maxEntries?: number;
260
+ }
261
+ /**
262
+ * An append-only, in-memory event log for local event sourcing.
263
+ *
264
+ * The log is the single source of truth for "what happened" and drives
265
+ * catch-up replication: a new tab or service worker asks for entries
266
+ * since its known `seq` watermark and re-applies them.
267
+ * @remarks This class is intentionally **not** a full SQLite-backed log.
268
+ * Persistence is the caller's responsibility (write the snapshot
269
+ * to IndexedDB / OPFS via {@link EventLog#snapshot}).
270
+ * @experimental
271
+ */
272
+ declare class EventLog {
273
+ #private;
274
+ constructor(options?: EventLogOptions);
275
+ /**
276
+ * Append a new entry to the log.
277
+ *
278
+ * Accepts either an {@link InputEvent} (e.g. from a `defineEvents` factory)
279
+ * or the traditional `(type, payload, tableDiffs?)` triple.
280
+ * @returns The newly created entry (already written to the log).
281
+ */
282
+ append(event: InputEvent, options?: AppendOptions): EventLogEntry;
283
+ append(type: string, payload: unknown, tableDiffs?: ReadonlyArray<TableDiff>, options?: AppendOptions): EventLogEntry;
284
+ /**
285
+ * Atomically append multiple events to the log.
286
+ *
287
+ * All events are assigned sequential global sequence numbers and
288
+ * automatically wired as a causal chain (each event's `parentSeqNum`
289
+ * points to the previous event in the batch, or to the log head for
290
+ * the first event).
291
+ * @param events An array of events to commit atomically.
292
+ * @returns The newly created entries in order.
293
+ */
294
+ commitAll(events: ReadonlyArray<InputEvent | {
295
+ payload: unknown;
296
+ type: string;
297
+ }>): EventLogEntry[];
298
+ /**
299
+ * Replace the log contents with a previously captured snapshot.
300
+ * This is the restore counterpart of {@link EventLog#snapshot}.
301
+ * Restores `headSeq` from the snapshot so auto-parenting continues
302
+ * after restore.
303
+ *
304
+ * Runs `#enforceCap()` after restoring so a snapshot captured under a
305
+ * different (or no) `maxEntries` can never leave this log over its
306
+ * configured capacity.
307
+ */
308
+ load(snapshot: EventLogSnapshot): void;
309
+ /**
310
+ * Return **all** entries whose `seq >= sinceSeq`.
311
+ * Useful for catch-up: "give me everything since my last watermark".
312
+ */
313
+ getSince(sinceSeq: number): ReadonlyArray<EventLogEntry>;
314
+ /**
315
+ * Paginated read starting at `fromSeq`.
316
+ *
317
+ * `limit` must be a positive safe integer, validated like
318
+ * {@link EventLog#truncateBelow}'s floor and the constructor's `maxEntries`:
319
+ * a `limit` of `0` returned an empty page with `hasMore: true`, which spins a
320
+ * paginating caller forever on a page it can never advance past.
321
+ * @returns `{ entries, hasMore }` where `hasMore` is `true` when more
322
+ * entries exist beyond the requested page.
323
+ */
324
+ getFrom(fromSeq: number, limit?: number): {
325
+ entries: ReadonlyArray<EventLogEntry>;
326
+ hasMore: boolean;
327
+ };
328
+ /**
329
+ * Return all entries as a snapshot suitable for serialisation.
330
+ */
331
+ snapshot(): EventLogSnapshot;
332
+ /** Number of entries currently in the log. */
333
+ get size(): number;
334
+ /** The next sequence number that will be assigned. */
335
+ get nextSeq(): number;
336
+ /** Return `true` when there are no entries. */
337
+ get isEmpty(): boolean;
338
+ /**
339
+ * The sequence number of the last (most recent) entry, or `null`
340
+ * when the log is empty. Used internally for auto-parenting and
341
+ * exposed for consumers that need the causal head.
342
+ */
343
+ get headSeq(): GlobalSeq | null;
344
+ /** Remove all entries (primarily for testing). */
345
+ clear(): void;
346
+ /**
347
+ * Discard all entries with `seq < floorSeq` (REPLICA-06).
348
+ *
349
+ * `headSeq`/`nextSeq` are untouched (they're independent counters), so
350
+ * appends after a truncation continue the same sequence uninterrupted.
351
+ *
352
+ * **Caller-driven, not automatic**: only call this after the truncated
353
+ * range has already been durably captured elsewhere (a snapshot, a
354
+ * server-side `EventLogDO`) — truncating without such a floor makes any
355
+ * future `getSince`/`getFrom`/`EventSource.replayFromLog` call for a
356
+ * watermark below `floorSeq` silently miss the discarded entries. This is
357
+ * the hook the caller ties to snapshot persistence; the log itself has no
358
+ * concept of "already durably persisted".
359
+ *
360
+ * `floorSeq` must be a non-negative safe integer — `NaN` would make
361
+ * every comparison false and silently clear the entire log.
362
+ */
363
+ truncateBelow(floorSeq: number): void;
364
+ /**
365
+ * Return an async generator that yields every entry starting from
366
+ * `fromSeq` (default `0` = all entries).
367
+ *
368
+ * Because `EventLog` is purely in-memory, the generator yields all
369
+ * matching entries synchronously on first iteration and then completes.
370
+ * For a streaming / push-based variant see {@link EventSource.events}.
371
+ */
372
+ events(fromSeq?: number): AsyncGenerator<EventLogEntry>;
373
+ }
374
+ /**
375
+ * `MirrorTableDef` is part of the experimental `@lunora/replica` API and may change without a major version bump.
376
+ * @experimental
377
+ */
378
+ interface MirrorTableDef {
379
+ /** Primary key column name (defaults to `"id"`). */
380
+ readonly primaryKey?: string;
381
+ }
382
+ /**
383
+ * Options for constructing a {@link LocalMirror}.
384
+ * @experimental
385
+ */
386
+ interface LocalMirrorOptions {
387
+ /** Platform-specific SQLite adapter. */
388
+ readonly db: SqliteAdapter;
389
+ /**
390
+ * Cap the mirror's internal {@link EventLog} to this many entries.
391
+ * Every applied diff is recorded in the log, so an uncapped log grows by
392
+ * one entry (holding every changed row) per diff for the life of the
393
+ * mirror — a leak by construction on a long-lived client.
394
+ *
395
+ * Defaults to {@link DEFAULT_MAX_EVENT_LOG_ENTRIES}. On overflow the
396
+ * OLDEST entries are dropped; nothing in the mirror replays its own log,
397
+ * so a drop loses nothing the mirror needs. A consumer that does replay
398
+ * it (`eventLog.getSince(watermark)` from another tab / service worker)
399
+ * detects a gap when the first returned entry's `seq` is above its
400
+ * watermark, and should re-seed from the mirror's rows (`query`) instead
401
+ * of applying the partial window.
402
+ */
403
+ readonly maxEventLogEntries?: number;
404
+ /**
405
+ * Table schemas the mirror should manage.
406
+ *
407
+ * On first use the mirror creates any missing tables automatically
408
+ * based on the columns observed in the first diff/row applied.
409
+ * If you want a fixed schema, pass it here with explicit column
410
+ * definitions in `columns`.
411
+ */
412
+ readonly tables?: Record<string, MirrorTableDef>;
413
+ }
414
+ /**
415
+ * Why the mirror changed: `"diff"` for the rows an {@link LocalMirror.applyDiff}
416
+ * wrote, `"clear"` for the wholesale {@link LocalMirror.clearData} sweep.
417
+ *
418
+ * A subscriber that caches what it believes the mirror holds has to tell the two
419
+ * apart: `"diff"` reports a change it usually made itself, while `"clear"` means
420
+ * every row it was tracking is gone regardless of who wrote it.
421
+ */
422
+ type MirrorChangeReason = "clear" | "diff";
423
+ /**
424
+ * Local SQLite mirror that maintains a client-side replica of server
425
+ * tables by applying {@link TableDiff} deltas.
426
+ *
427
+ * Usage:
428
+ * ```ts
429
+ * import { createSqlJsAdapter } from "@lunora/replica/adapters/sqljs";
430
+ * import initSqlJs from "sql.js";
431
+ *
432
+ * const SQL = await initSqlJs();
433
+ * const db = createSqlJsAdapter(new SQL.Database());
434
+ *
435
+ * const mirror = new LocalMirror({ db });
436
+ *
437
+ * // Apply a server diff:
438
+ * mirror.applyDiff(someDiff);
439
+ *
440
+ * // Query locally:
441
+ * const rows = mirror.query<{ id: string; name: string }>(
442
+ * "SELECT id, name FROM users WHERE name LIKE ?",
443
+ * ["alice%"],
444
+ * );
445
+ * ```
446
+ */
447
+ type ChangeSubscriber = (reason: MirrorChangeReason) => void;
448
+ /**
449
+ * `LocalMirror` is part of the experimental `@lunora/replica` API and may change without a major version bump.
450
+ * @experimental
451
+ */
452
+ declare class LocalMirror {
453
+ #private;
454
+ /**
455
+ * Convenience factory that creates a {@link LocalMirror} backed by a
456
+ * {@link createSqlJsAdapter sql.js adapter} without needing to import
457
+ * and wire sql.js manually.
458
+ *
459
+ * The caller provides an initialised sql.js database — this method wraps
460
+ * it in an adapter and constructs the mirror.
461
+ * @example
462
+ * ```ts
463
+ * import initSqlJs from "sql.js";
464
+ *
465
+ * const SQL = await initSqlJs();
466
+ * const mirror = LocalMirror.create(new SQL.Database(), {
467
+ * tables: { todos: { primaryKey: "id" } },
468
+ * });
469
+ * ```
470
+ */
471
+ static create(sqlJsDatabase: {
472
+ close: () => void;
473
+ exec: (sql: string) => {
474
+ columns: string[];
475
+ values: unknown[][];
476
+ }[];
477
+ run: (sql: string, params?: unknown[]) => void;
478
+ }, options?: {
479
+ tables?: Record<string, MirrorTableDef>;
480
+ }): LocalMirror;
481
+ constructor(options: LocalMirrorOptions);
482
+ /**
483
+ * Subscribe to data-change notifications. Fires after every {@link applyDiff}.
484
+ * Returns an unsubscribe function.
485
+ */
486
+ onChange(callback: ChangeSubscriber): () => void;
487
+ /**
488
+ * The in-memory event log tracking every diff applied to this mirror.
489
+ * Use {@link EventLog.getSince} for catch-up replication across tabs
490
+ * or service-worker instances.
491
+ */
492
+ get eventLog(): EventLog;
493
+ /**
494
+ * The raw SQLite adapter. Advanced consumers (e.g. the React hook)
495
+ * can use it for ad-hoc queries or bulk operations.
496
+ */
497
+ get db(): SqliteAdapter;
498
+ /**
499
+ * Monotonically increasing version counter, bumped on every operation
500
+ * that changes mirrored data (`applyDiff`, `clearData`). Use this — not
501
+ * `eventLog.size` — as a `useSyncExternalStore` snapshot so operations
502
+ * that don't append to the log still trigger a re-render.
503
+ */
504
+ get version(): number;
505
+ /**
506
+ * Apply a server-side diff to the local SQLite mirror.
507
+ *
508
+ * The diff is applied in a transaction and recorded in the event log
509
+ * so other tabs or the SW can catch up.
510
+ */
511
+ applyDiff(diff: TableDiff): void;
512
+ /**
513
+ * Run an arbitrary SQL query against the local mirror and return
514
+ * typed results.
515
+ * @example
516
+ * ```ts
517
+ * const users = mirror.query<{ id: string; name: string }>(
518
+ * "SELECT id, name FROM users WHERE active = ?",
519
+ * [true],
520
+ * );
521
+ * ```
522
+ */
523
+ query<T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>): T[];
524
+ /**
525
+ * Delete every row from every data table in the adapter's database
526
+ * (preserves the event log and schema). Useful when re-syncing from scratch.
527
+ *
528
+ * **The mirror owns its database.** The sweep is `sqlite_master` minus the
529
+ * reserved prefixes, NOT {@link LocalMirror.mirroredTables} — a table this
530
+ * mirror never registered is cleared too, and `#reconcileSchemaVersion`
531
+ * DROPs on the same list. It cannot be narrowed to the registered set: that
532
+ * runs from the constructor, before any `applyDiff` has re-registered the
533
+ * tables a previous session persisted, and those are exactly the
534
+ * stale-schema tables it exists to drop. So hand the adapter a database
535
+ * dedicated to the mirror, never one that also holds your own tables.
536
+ *
537
+ * Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
538
+ * (REPLICA-09) even though nothing is appended to the event log — a
539
+ * consumer keyed only on `eventLog.size` would otherwise never learn the
540
+ * mirror was cleared and keep rendering deleted rows.
541
+ */
542
+ clearData(): void;
543
+ /**
544
+ * Dispose the mirror and close the database connection.
545
+ */
546
+ close(): void;
547
+ /**
548
+ * Register a table schema so the mirror can create the table on
549
+ * first use. Merges into any definition already registered for `name`
550
+ * (from the constructor's `tables` or an earlier call), so a helper that
551
+ * registers `{}` just to make the table known does not erase a
552
+ * user-supplied `primaryKey`.
553
+ */
554
+ registerTable(name: string, definition: MirrorTableDef): void;
555
+ /** The primary-key column of a mirrored table (`"id"` unless registered otherwise). */
556
+ primaryKeyOf(table: string): string;
557
+ /**
558
+ * Return the list of mirrored table names.
559
+ */
560
+ get mirroredTables(): ReadonlyArray<string>;
561
+ }
562
+ export { AppendOptions as A, ClientSeq as C, EventLogEntry as E, GlobalSeq as G, InputEvent as I, LocalMirror as L, MirrorTableDef as M, RowChange as R, Seq as S, TableDiff as T, EventLog as a, EventLogOptions as b, EventLogSnapshot as c, LocalMirrorOptions as d, classifyChanges as e, createTableDiff as f, diffSize as g, isDiffEmpty as h, isClientSeq as i, isGlobalSeq as j, isInputEvent as k, mergeDiffs as m };
@@ -0,0 +1 @@
1
+ import{s as A}from"./wire-key-DfMHAtqH.mjs";const _=e=>`fn_${e.replaceAll(/[/:.]/g,"_")}`,k=e=>Array.isArray(e)?e:e!==null&&typeof e=="object"?[e]:[],x=(e,o,l,y,g)=>{const a=_(l.__lunoraRef);o.registerTable(a,{});const d=o.primaryKeyOf(a);let r=new Map,f=0;const w=o.onChange(p=>{p==="clear"&&(f+=1,r=new Map)}),h=e.subscribe(l,y,p=>{const M=f,c=new Map,b=new Map,s=[];for(const t of k(p)){const n=t,i=n[d];if(typeof i!="string"&&typeof i!="number"&&typeof i!="bigint"){s.push({type:"insert",data:n});continue}const u=String(i),m=A(n);c.set(u,m),b.set(u,n)}for(const[t,n]of c)r.get(t)!==n&&s.push({data:b.get(t),type:"insert"});for(const t of r.keys())c.has(t)||s.push({type:"delete",id:t});s.length>0&&o.applyDiff({table:a,changes:s,timestamp:Date.now()}),M===f&&(r=c)},{shardKey:g});return()=>{w(),h()}};export{x as subscribeToMirror};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Abstract SQLite driver interface used by the local mirror.
3
+ *
4
+ * Each runtime (browser via sql.js, React Native via expo-sqlite,
5
+ * Node via better-sqlite3) provides its own adapter implementing this
6
+ * interface so the rest of `@lunora/replica` stays platform-agnostic.
7
+ * @experimental
8
+ */
9
+ interface SqliteAdapter {
10
+ /** Close the database connection. */
11
+ close: () => void;
12
+ /** Execute a SQL statement (with optional bound params). */
13
+ exec: (sql: string, params?: ReadonlyArray<unknown>) => void;
14
+ /**
15
+ * Execute a SQL statement and return the result rows.
16
+ * Columns can be accessed by index or by name.
17
+ */
18
+ query: <T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>) => T[];
19
+ /** Run all statements in a transaction. */
20
+ transaction: (function_: () => void) => void;
21
+ }
22
+ export { SqliteAdapter as S };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Abstract SQLite driver interface used by the local mirror.
3
+ *
4
+ * Each runtime (browser via sql.js, React Native via expo-sqlite,
5
+ * Node via better-sqlite3) provides its own adapter implementing this
6
+ * interface so the rest of `@lunora/replica` stays platform-agnostic.
7
+ * @experimental
8
+ */
9
+ interface SqliteAdapter {
10
+ /** Close the database connection. */
11
+ close: () => void;
12
+ /** Execute a SQL statement (with optional bound params). */
13
+ exec: (sql: string, params?: ReadonlyArray<unknown>) => void;
14
+ /**
15
+ * Execute a SQL statement and return the result rows.
16
+ * Columns can be accessed by index or by name.
17
+ */
18
+ query: <T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>) => T[];
19
+ /** Run all statements in a transaction. */
20
+ transaction: (function_: () => void) => void;
21
+ }
22
+ export { SqliteAdapter as S };
@@ -0,0 +1 @@
1
+ const m=/["\\\u0000-\u001F\uD800-\uDFFF]/,d=r=>m.test(r)?JSON.stringify(r):`"${r}"`,b=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 d(r);if(r===null||typeof r!="object")return JSON.stringify(r);if(Array.isArray(r)){let e="[";for(let i=0;i<r.length;i++)i>0&&(e+=","),e+=b(r[i]);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 y=r,s=Object.keys(y).sort();let f="{",t=!0;for(const e of s){const i=y[e];i!==void 0&&(t?t=!1:f+=",",f+=d(e),f+=":",f+=b(i))}return f+"}"},u=r=>{let n="";for(let s=0;s<r.length;s+=32768)n+=String.fromCharCode(...r.subarray(s,s+32768));return btoa(n)},o="$lunora.wire$";const g="__proto__",w=r=>{if(r===null||typeof r!="object")return!1;const n=Object.getPrototypeOf(r);return n===null||n===Object.prototype},c=(r,n=0)=>{if(n>64)throw new RangeError("wire-codec: value nesting exceeds the 64-level limit");if(r===void 0)return[o,"undefined"];if(r===null)return null;const y=typeof r;if(y==="bigint")return[o,"bigint",r.toString()];if(y==="number"){const t=r;return Number.isNaN(t)?[o,"nan"]:t===1/0?[o,"inf"]:t===-1/0?[o,"-inf"]:t}if(y!=="object")return r;if(r instanceof Date)return[o,"date",c(r.getTime(),n+1)];if(r instanceof Error){const t=r,e={};for(const a of Object.keys(t)){if(t[a]===void 0)continue;const p=c(t[a],n+1);a===g?Object.defineProperty(e,a,{configurable:!0,enumerable:!0,value:p,writable:!0}):e[a]=p}const i=[o,"error",String(t.name),String(t.message),e];return t.cause!==void 0&&i.push(c(t.cause,n+1)),i}if(r instanceof URL)return[o,"url",r.href];if(r instanceof Map)return[o,"map",[...r.entries()].map(([t,e])=>[c(t,n+1),c(e,n+1)])];if(r instanceof Set)return[o,"set",[...r].map(t=>c(t,n+1))];if(r instanceof ArrayBuffer)return[o,"bytes",u(new Uint8Array(r)),"ArrayBuffer"];if(ArrayBuffer.isView(r)){const t=r,e=t.constructor.name,i=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return e==="Uint8Array"?[o,"bytes",u(i)]:[o,"bytes",u(i),e]}if(Array.isArray(r)){const t=r.map(e=>c(e,n+1));return t.length>0&&t[0]===o?[o,"arr",t]:t}if(!w(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,f={};for(const t of Object.keys(s)){const e=s[t];if(e===void 0)continue;const i=c(e,n+1);t===g?Object.defineProperty(f,t,{configurable:!0,enumerable:!0,value:i,writable:!0}):f[t]=i}return f},O=r=>b(c(r));export{O as s};