@lunora/replica 0.0.0 → 1.0.0-alpha.3

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 (39) hide show
  1. package/LICENSE.md +231 -0
  2. package/README.md +194 -29
  3. package/dist/adapters/better-sqlite3.d.mts +28 -0
  4. package/dist/adapters/better-sqlite3.d.ts +28 -0
  5. package/dist/adapters/better-sqlite3.mjs +29 -0
  6. package/dist/adapters/sqlite-wasm.d.mts +26 -0
  7. package/dist/adapters/sqlite-wasm.d.ts +26 -0
  8. package/dist/adapters/sqlite-wasm.mjs +56 -0
  9. package/dist/adapters/sqljs.d.mts +20 -0
  10. package/dist/adapters/sqljs.d.ts +20 -0
  11. package/dist/adapters/sqljs.mjs +55 -0
  12. package/dist/index.d.mts +910 -0
  13. package/dist/index.d.ts +910 -0
  14. package/dist/index.mjs +20 -0
  15. package/dist/packem_shared/EventEmitter-CMZfct03.mjs +92 -0
  16. package/dist/packem_shared/EventLog-zMy7AYP4.mjs +162 -0
  17. package/dist/packem_shared/EventLogDO-CZYUvvSr.mjs +235 -0
  18. package/dist/packem_shared/EventLogDOClient-DGiEdi96.mjs +86 -0
  19. package/dist/packem_shared/EventSource-DfV4VoRD.mjs +195 -0
  20. package/dist/packem_shared/EventsSync-DkVbU0WV.mjs +91 -0
  21. package/dist/packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs +24 -0
  22. package/dist/packem_shared/LocalMirror-GeJ26eNe.mjs +188 -0
  23. package/dist/packem_shared/MaterializerRuntime-HqNXqJxp.mjs +204 -0
  24. package/dist/packem_shared/SubscriptionManager-C5xbw0pg.mjs +75 -0
  25. package/dist/packem_shared/applyDiff-BtbIl1D3.mjs +40 -0
  26. package/dist/packem_shared/applyDiffToDb-DQ1xZp5J.mjs +58 -0
  27. package/dist/packem_shared/classifyChanges-aZmkxgVI.mjs +38 -0
  28. package/dist/packem_shared/defineEvents-DiBkPTh_.mjs +28 -0
  29. package/dist/packem_shared/eventsContext-Bk_p48hj.mjs +6 -0
  30. package/dist/packem_shared/isClientSeq-C46BkzqJ.mjs +5 -0
  31. package/dist/packem_shared/local-mirror.d-BUeOe5KC.d.mts +439 -0
  32. package/dist/packem_shared/local-mirror.d-Cd8tAg-W.d.ts +439 -0
  33. package/dist/packem_shared/subscribeToMirror-CiaM-nQ7.mjs +45 -0
  34. package/dist/packem_shared/types.d-BuAWjEY5.d.mts +24 -0
  35. package/dist/packem_shared/types.d-BuAWjEY5.d.ts +24 -0
  36. package/dist/react.d.mts +67 -0
  37. package/dist/react.d.ts +67 -0
  38. package/dist/react.mjs +15 -0
  39. package/package.json +88 -7
@@ -0,0 +1,439 @@
1
+ import { S as SqliteAdapter } from "./types.d-BuAWjEY5.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
+ /** Logical table name (matches the schema table name). */
32
+ readonly table: string;
33
+ /** Monotonic server timestamp (ms since epoch) when this diff was emitted. */
34
+ readonly timestamp: number;
35
+ }
36
+ /**
37
+ * Create a {@link TableDiff} with a snapshot of the current time.
38
+ * @experimental
39
+ */
40
+ declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number) => TableDiff;
41
+ /**
42
+ * Return `true` when the diff contains no row changes.
43
+ * @experimental
44
+ */
45
+ declare const isDiffEmpty: (diff: TableDiff) => boolean;
46
+ /**
47
+ * Return the number of rows touched by the diff (inserts + updates + deletes).
48
+ * @experimental
49
+ */
50
+ declare const diffSize: (diff: TableDiff) => number;
51
+ /**
52
+ * Partition a {@link TableDiff} into three categories for batch processing.
53
+ * @experimental
54
+ */
55
+ declare const classifyChanges: (diff: TableDiff) => {
56
+ deletes: RowChange[];
57
+ inserts: RowChange[];
58
+ updates: RowChange[];
59
+ };
60
+ /**
61
+ * Merge several diffs for the same table into one (ordering preserved).
62
+ *
63
+ * Returns `null` when the input list is empty.
64
+ * @experimental
65
+ */
66
+ declare const mergeDiffs: (diffs: ReadonlyArray<TableDiff>) => TableDiff | null;
67
+ /**
68
+ * Sequence-number types for the event log.
69
+ *
70
+ * Three namespaces match the vocabulary established by event-sourcing:
71
+ *
72
+ * | Namespace | Shape | Producer | Consumer |
73
+ * |-----------|--------------------|---------------------------|------------------------------|
74
+ * | `Global` | `number` | `EventLog` / `EventLogDO` | Materializers, subscribers |
75
+ * | `Input` | No seq (optimistic) | `defineEvents` factories | `EventLog.append` / DO client|
76
+ * | `Client` | `{generation,seq}` | _(future: rebase engine)_ | _(future: rebase-aware code)_|
77
+ * @module
78
+ */
79
+ /**
80
+ * A server-authoritative (global) sequence number.
81
+ *
82
+ * Monotonically increasing, assigned by `EventLog` (in-memory) or
83
+ * `EventLogDO` (Durable Object). All confirmed log entries carry a
84
+ * `GlobalSeq`.
85
+ * @experimental
86
+ */
87
+ type GlobalSeq = number;
88
+ /**
89
+ * A client-originated composite sequence number designed to survive rebase.
90
+ *
91
+ * Carries the last-confirmed `global` seq, a monotonically increasing
92
+ * `client` counter, and a `rebaseGeneration` that increments whenever the
93
+ * client's local events are rebased onto a new upstream baseline.
94
+ * @experimental
95
+ */
96
+ interface ClientSeq {
97
+ /** Client-local monotonically increasing counter. */
98
+ readonly client: number;
99
+ /** The last-confirmed global sequence number. 0 for unconfirmed events. */
100
+ readonly global: number;
101
+ /** Incremented on every rebase. */
102
+ readonly rebaseGeneration: number;
103
+ }
104
+ /**
105
+ * Discriminated union of all sequence-number types.
106
+ * @experimental
107
+ */
108
+ type Seq = GlobalSeq | ClientSeq;
109
+ /**
110
+ * Narrow `Seq` to `GlobalSeq`.
111
+ * @experimental
112
+ */
113
+ declare const isGlobalSeq: (seq: Seq) => seq is GlobalSeq;
114
+ /**
115
+ * Narrow `Seq` to `ClientSeq`.
116
+ * @experimental
117
+ */
118
+ declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
119
+ /**
120
+ * An event that has **not** yet been assigned a sequence number.
121
+ *
122
+ * Input events represent optimistic / command payloads before the server
123
+ * confirms them. They carry `type`, `payload`, and `timestamp` but no
124
+ * `seq` — the log assigns one on append.
125
+ *
126
+ * Create input events via `defineEvents` factories:
127
+ *
128
+ * ```ts
129
+ * const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
130
+ * // event: InputEvent&lt;"chat.messageSent", { channelId: string; text: string }>
131
+ * ```
132
+ * @experimental
133
+ */
134
+ interface InputEvent<Type extends string = string, Payload = unknown> {
135
+ /** Arbitrary JSON-serialisable payload. */
136
+ readonly payload: Payload;
137
+ /** Millisecond timestamp (epoch) when the event was created. */
138
+ readonly timestamp: number;
139
+ /** Event type discriminator (e.g. `"chat.messageSent"`). */
140
+ readonly type: Type;
141
+ }
142
+ /**
143
+ * Type guard: check whether `value` is an {@link InputEvent}.
144
+ * @experimental
145
+ */
146
+ declare const isInputEvent: (value: unknown) => value is InputEvent;
147
+ /**
148
+ * A single entry in the append-only {@link EventLog}.
149
+ *
150
+ * Entries are immutable once appended; the `seq` field is assigned
151
+ * monotonically by the log and doubles as a watermark for catch-up
152
+ * replication between tabs or service-worker instances.
153
+ * @experimental
154
+ */
155
+ interface EventLogEntry {
156
+ /**
157
+ * Globally-unique client identifier that originated this event.
158
+ * Set by clients that support offline / optimistic writes.
159
+ * `undefined` when the event was created server-side.
160
+ */
161
+ readonly clientId?: string;
162
+ /**
163
+ * The sequence number of the causal parent event.
164
+ *
165
+ * A {@link GlobalSeq} for events confirmed by the server (pointing
166
+ * to the previous confirmed event), or a {@link ClientSeq} for
167
+ * optimistic / offline events pointing to the local predecessor.
168
+ * `undefined` for the first event in a log.
169
+ */
170
+ readonly parentSeqNum?: Seq;
171
+ /** Arbitrary JSON-serialisable payload. */
172
+ readonly payload: unknown;
173
+ /** Monotonically increasing sequence number (0-based). A {@link GlobalSeq}. */
174
+ readonly seq: GlobalSeq;
175
+ /**
176
+ * Session identifier from the originating client.
177
+ * Paired with `clientId` to disambiguate concurrent sessions.
178
+ */
179
+ readonly sessionId?: string;
180
+ /**
181
+ * Optional table diffs that this event produced.
182
+ * When present, a consumer can re-play the event by applying the diffs
183
+ * to its local mirror without re-executing the originating mutation.
184
+ */
185
+ readonly tableDiffs?: ReadonlyArray<TableDiff>;
186
+ /** Millisecond timestamp (epoch) when the entry was appended. */
187
+ readonly timestamp: number;
188
+ /** Event type discriminator (e.g. "row-insert", "mutation-apply"). */
189
+ readonly type: string;
190
+ }
191
+ /**
192
+ * Serialised snapshot of the log — used for persistence and transfer.
193
+ * @experimental
194
+ */
195
+ interface EventLogSnapshot {
196
+ readonly entries: ReadonlyArray<EventLogEntry>;
197
+ /** The sequence number of the last entry (head), or `null` for an empty log. */
198
+ readonly headSeq: GlobalSeq | null;
199
+ readonly nextSeq: number;
200
+ }
201
+ /**
202
+ * Optional metadata that can accompany an appended event.
203
+ * @experimental
204
+ */
205
+ interface AppendOptions {
206
+ /** Globally-unique client identifier. */
207
+ readonly clientId?: string;
208
+ /**
209
+ * Causal parent sequence number.
210
+ * Automatically set to the previous entry's seq when omitted.
211
+ */
212
+ readonly parentSeqNum?: Seq;
213
+ /** Session identifier within the client. */
214
+ readonly sessionId?: string;
215
+ }
216
+ /**
217
+ * An append-only, in-memory event log for local event sourcing.
218
+ *
219
+ * The log is the single source of truth for "what happened" and drives
220
+ * catch-up replication: a new tab or service worker asks for entries
221
+ * since its known `seq` watermark and re-applies them.
222
+ * @remarks This class is intentionally **not** a full SQLite-backed log.
223
+ * Persistence is the caller's responsibility (write the snapshot
224
+ * to IndexedDB / OPFS via {@link EventLog#snapshot}).
225
+ * @experimental
226
+ */
227
+ declare class EventLog {
228
+ #private;
229
+ /**
230
+ * Append a new entry to the log.
231
+ *
232
+ * Accepts either an {@link InputEvent} (e.g. from a `defineEvents` factory)
233
+ * or the traditional `(type, payload, tableDiffs?)` triple.
234
+ * @returns The newly created entry (already written to the log).
235
+ */
236
+ append(event: InputEvent, options?: AppendOptions): EventLogEntry;
237
+ append(type: string, payload: unknown, tableDiffs?: ReadonlyArray<TableDiff>, options?: AppendOptions): EventLogEntry;
238
+ /**
239
+ * Atomically append multiple events to the log.
240
+ *
241
+ * All events are assigned sequential global sequence numbers and
242
+ * automatically wired as a causal chain (each event's `parentSeqNum`
243
+ * points to the previous event in the batch, or to the log head for
244
+ * the first event).
245
+ * @param events An array of events to commit atomically.
246
+ * @returns The newly created entries in order.
247
+ */
248
+ commitAll(events: ReadonlyArray<InputEvent | {
249
+ payload: unknown;
250
+ type: string;
251
+ }>): EventLogEntry[];
252
+ /**
253
+ * Replace the log contents with a previously captured snapshot.
254
+ * This is the restore counterpart of {@link EventLog#snapshot}.
255
+ * Restores `headSeq` from the snapshot so auto-parenting continues
256
+ * after restore.
257
+ */
258
+ load(snapshot: EventLogSnapshot): void;
259
+ /**
260
+ * Return **all** entries whose `seq >= sinceSeq`.
261
+ * Useful for catch-up: "give me everything since my last watermark".
262
+ */
263
+ getSince(sinceSeq: number): ReadonlyArray<EventLogEntry>;
264
+ /**
265
+ * Paginated read starting at `fromSeq`.
266
+ * @returns `{ entries, hasMore }` where `hasMore` is `true` when more
267
+ * entries exist beyond the requested page.
268
+ */
269
+ getFrom(fromSeq: number, limit?: number): {
270
+ entries: ReadonlyArray<EventLogEntry>;
271
+ hasMore: boolean;
272
+ };
273
+ /**
274
+ * Return all entries as a snapshot suitable for serialisation.
275
+ */
276
+ snapshot(): EventLogSnapshot;
277
+ /** Number of entries currently in the log. */
278
+ get size(): number;
279
+ /** The next sequence number that will be assigned. */
280
+ get nextSeq(): number;
281
+ /** Return `true` when there are no entries. */
282
+ get isEmpty(): boolean;
283
+ /**
284
+ * The sequence number of the last (most recent) entry, or `null`
285
+ * when the log is empty. Used internally for auto-parenting and
286
+ * exposed for consumers that need the causal head.
287
+ */
288
+ get headSeq(): GlobalSeq | null;
289
+ /** Remove all entries (primarily for testing). */
290
+ clear(): void;
291
+ /**
292
+ * Return an async generator that yields every entry starting from
293
+ * `fromSeq` (default `0` = all entries).
294
+ *
295
+ * Because `EventLog` is purely in-memory, the generator yields all
296
+ * matching entries synchronously on first iteration and then completes.
297
+ * For a streaming / push-based variant see {@link EventSource.events}.
298
+ */
299
+ events(fromSeq?: number): AsyncGenerator<EventLogEntry>;
300
+ }
301
+ /**
302
+ * `MirrorTableDef` is part of the experimental `@lunora/replica` API and may change without a major version bump.
303
+ * @experimental
304
+ */
305
+ interface MirrorTableDef {
306
+ /** Primary key column name (defaults to `"id"`). */
307
+ readonly primaryKey?: string;
308
+ }
309
+ /**
310
+ * Options for constructing a {@link LocalMirror}.
311
+ * @experimental
312
+ */
313
+ interface LocalMirrorOptions {
314
+ /** Platform-specific SQLite adapter. */
315
+ readonly db: SqliteAdapter;
316
+ /**
317
+ * Table schemas the mirror should manage.
318
+ *
319
+ * On first use the mirror creates any missing tables automatically
320
+ * based on the columns observed in the first diff/row applied.
321
+ * If you want a fixed schema, pass it here with explicit column
322
+ * definitions in `columns`.
323
+ */
324
+ readonly tables?: Record<string, MirrorTableDef>;
325
+ }
326
+ /**
327
+ * Local SQLite mirror that maintains a client-side replica of server
328
+ * tables by applying {@link TableDiff} deltas.
329
+ *
330
+ * Usage:
331
+ * ```ts
332
+ * import { createSqlJsAdapter } from "@lunora/replica/adapters/sqljs";
333
+ * import initSqlJs from "sql.js";
334
+ *
335
+ * const SQL = await initSqlJs();
336
+ * const db = createSqlJsAdapter(new SQL.Database());
337
+ *
338
+ * const mirror = new LocalMirror({ db });
339
+ *
340
+ * // Apply a server diff:
341
+ * mirror.applyDiff(someDiff);
342
+ *
343
+ * // Query locally:
344
+ * const rows = mirror.query&lt;{ id: string; name: string }>(
345
+ * "SELECT id, name FROM users WHERE name LIKE ?",
346
+ * ["alice%"],
347
+ * );
348
+ * ```
349
+ */
350
+ type ChangeSubscriber = () => void;
351
+ /**
352
+ * `LocalMirror` is part of the experimental `@lunora/replica` API and may change without a major version bump.
353
+ * @experimental
354
+ */
355
+ declare class LocalMirror {
356
+ #private;
357
+ /**
358
+ * Convenience factory that creates a {@link LocalMirror} backed by a
359
+ * {@link createSqlJsAdapter sql.js adapter} without needing to import
360
+ * and wire sql.js manually.
361
+ *
362
+ * The caller provides an initialised sql.js database — this method wraps
363
+ * it in an adapter and constructs the mirror.
364
+ * @example
365
+ * ```ts
366
+ * import initSqlJs from "sql.js";
367
+ *
368
+ * const SQL = await initSqlJs();
369
+ * const mirror = LocalMirror.create(new SQL.Database(), {
370
+ * tables: { todos: { primaryKey: "id" } },
371
+ * });
372
+ * ```
373
+ */
374
+ static create(sqlJsDatabase: {
375
+ close: () => void;
376
+ exec: (sql: string) => {
377
+ columns: string[];
378
+ values: unknown[][];
379
+ }[];
380
+ run: (sql: string, params?: unknown[]) => void;
381
+ }, options?: {
382
+ tables?: Record<string, MirrorTableDef>;
383
+ }): LocalMirror;
384
+ constructor(options: LocalMirrorOptions);
385
+ /**
386
+ * Subscribe to data-change notifications. Fires after every {@link applyDiff}.
387
+ * Returns an unsubscribe function.
388
+ */
389
+ onChange(callback: ChangeSubscriber): () => void;
390
+ /**
391
+ * The in-memory event log tracking every diff applied to this mirror.
392
+ * Use {@link EventLog.getSince} for catch-up replication across tabs
393
+ * or service-worker instances.
394
+ */
395
+ get eventLog(): EventLog;
396
+ /**
397
+ * The raw SQLite adapter. Advanced consumers (e.g. the React hook)
398
+ * can use it for ad-hoc queries or bulk operations.
399
+ */
400
+ get db(): SqliteAdapter;
401
+ /**
402
+ * Apply a server-side diff to the local SQLite mirror.
403
+ *
404
+ * The diff is applied in a transaction and recorded in the event log
405
+ * so other tabs or the SW can catch up.
406
+ */
407
+ applyDiff(diff: TableDiff): void;
408
+ /**
409
+ * Run an arbitrary SQL query against the local mirror and return
410
+ * typed results.
411
+ * @example
412
+ * ```ts
413
+ * const users = mirror.query<{ id: string; name: string }>(
414
+ * "SELECT id, name FROM users WHERE active = ?",
415
+ * [true],
416
+ * );
417
+ * ```
418
+ */
419
+ query<T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>): T[];
420
+ /**
421
+ * Delete every row from all known tables (preserves the event log
422
+ * and schema). Useful when re-syncing from scratch.
423
+ */
424
+ clearData(): void;
425
+ /**
426
+ * Dispose the mirror and close the database connection.
427
+ */
428
+ close(): void;
429
+ /**
430
+ * Register a table schema so the mirror can create the table on
431
+ * first use.
432
+ */
433
+ registerTable(name: string, definition: MirrorTableDef): void;
434
+ /**
435
+ * Return the list of mirrored table names.
436
+ */
437
+ get mirroredTables(): ReadonlyArray<string>;
438
+ }
439
+ 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, EventLogSnapshot as b, LocalMirrorOptions as c, classifyChanges as d, createTableDiff as e, diffSize as f, isDiffEmpty as g, isGlobalSeq as h, isClientSeq as i, isInputEvent as j, mergeDiffs as m };
@@ -0,0 +1,45 @@
1
+ const deriveTableName = (functionRef) => `fn_${functionRef.replaceAll(/[/:.]/g, "_")}`;
2
+ const asRowArray = (data) => {
3
+ if (Array.isArray(data)) {
4
+ return data;
5
+ }
6
+ if (data !== null && typeof data === "object") {
7
+ return [data];
8
+ }
9
+ return [];
10
+ };
11
+ const subscribeToMirror = (client, mirror, functionRef, args, shardKey) => {
12
+ const tableName = deriveTableName(functionRef.__lunoraRef);
13
+ mirror.registerTable(tableName, {});
14
+ let knownIds = /* @__PURE__ */ new Set();
15
+ return client.subscribe(
16
+ functionRef,
17
+ args,
18
+ (data) => {
19
+ const rows = asRowArray(data);
20
+ const nextIds = /* @__PURE__ */ new Set();
21
+ const changes = [];
22
+ for (const row of rows) {
23
+ const record = row;
24
+ const rawId = record.id;
25
+ if (typeof rawId === "string" || typeof rawId === "number") {
26
+ nextIds.add(String(rawId));
27
+ }
28
+ changes.push({ type: "insert", data: record });
29
+ }
30
+ for (const id of knownIds) {
31
+ if (!nextIds.has(id)) {
32
+ changes.push({ type: "delete", id });
33
+ }
34
+ }
35
+ knownIds = nextIds;
36
+ if (changes.length === 0) {
37
+ return;
38
+ }
39
+ mirror.applyDiff({ table: tableName, changes, timestamp: Date.now() });
40
+ },
41
+ { shardKey }
42
+ );
43
+ };
44
+
45
+ export { subscribeToMirror };
@@ -0,0 +1,24 @@
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
+ /** Return the id of the last inserted row. */
15
+ lastInsertRowId: () => number;
16
+ /**
17
+ * Execute a SQL statement and return the result rows.
18
+ * Columns can be accessed by index or by name.
19
+ */
20
+ query: <T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>) => T[];
21
+ /** Run all statements in a transaction. */
22
+ transaction: (function_: () => void) => void;
23
+ }
24
+ export { SqliteAdapter as S };
@@ -0,0 +1,24 @@
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
+ /** Return the id of the last inserted row. */
15
+ lastInsertRowId: () => number;
16
+ /**
17
+ * Execute a SQL statement and return the result rows.
18
+ * Columns can be accessed by index or by name.
19
+ */
20
+ query: <T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>) => T[];
21
+ /** Run all statements in a transaction. */
22
+ transaction: (function_: () => void) => void;
23
+ }
24
+ export { SqliteAdapter as S };
@@ -0,0 +1,67 @@
1
+ import { L as LocalMirror } from "./packem_shared/local-mirror.d-BUeOe5KC.mjs";
2
+ import "./packem_shared/types.d-BuAWjEY5.mjs";
3
+ /**
4
+ * Options for the {@link useLocalQuery} hook.
5
+ * @experimental
6
+ */
7
+ interface UseLocalQueryOptions {
8
+ /**
9
+ * Optional shard key (reserved for future use; currently unused).
10
+ *
11
+ * Intended for multi-mirror setups where a single app maintains multiple
12
+ * SQLite databases sharded by a key (e.g. user id, tenant id). Currently
13
+ * has no effect — the hook always queries the mirror passed as the first
14
+ * argument.
15
+ */
16
+ shardKey?: string;
17
+ }
18
+ /**
19
+ * React hook that subscribes to a local SQLite query and returns
20
+ * live-updating results whenever the mirror applies a diff.
21
+ *
22
+ * Uses `useSyncExternalStore` to subscribe to the mirror's `onChange`
23
+ * callback — every diff triggers a re-query against the local SQLite.
24
+ *
25
+ * The hook works with React 18+ concurrent features, Suspense, and
26
+ * server-side rendering. During SSR the same value is returned as on
27
+ * the client (the mirror's current state at render time).
28
+ * @param mirror The {@link LocalMirror} instance to query. Must have been
29
+ * constructed with an {@link import("./adapters/types").SqliteAdapter}.
30
+ * @param sql Parameterised SQL query string. Use `?` placeholders for
31
+ * bound parameters (the adapter forwards them to the underlying SQLite
32
+ * engine without rewriting).
33
+ * @param params Optional positional bound parameters matching `?`
34
+ * placeholders in `sql`.
35
+ * @param _options Optional configuration (currently unused; reserved for
36
+ * future features like shard key routing).
37
+ * @returns An array of result rows typed via the generic parameter `T`, or
38
+ * `undefined` if the query fails (e.g. the target table doesn't exist yet
39
+ * because no matching diff has been applied). Treat `undefined` as a
40
+ * "loading" or "no data yet" signal in your component.
41
+ *
42
+ * **Error handling**: The hook catches SQL errors internally and returns
43
+ * `undefined`. Use a try-catch around `mirror.query(...)` directly if you
44
+ * need finer-grained error diagnostics.
45
+ * @example
46
+ * ```tsx
47
+ * import { useLocalQuery } from "@lunora/replica/react";
48
+ * import { mirror } from "./mirror";
49
+ *
50
+ * function UserList() {
51
+ * const users = useLocalQuery<{ id: string; name: string }>(
52
+ * mirror,
53
+ * "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
54
+ * ["%alice%"],
55
+ * );
56
+ *
57
+ * if (users === undefined) {
58
+ * return <p>Waiting for data…</p>;
59
+ * }
60
+ *
61
+ * return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
62
+ * }
63
+ * ```
64
+ * @experimental
65
+ */
66
+ declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => T[] | undefined;
67
+ export { UseLocalQueryOptions, useLocalQuery };
@@ -0,0 +1,67 @@
1
+ import { L as LocalMirror } from "./packem_shared/local-mirror.d-Cd8tAg-W.js";
2
+ import "./packem_shared/types.d-BuAWjEY5.js";
3
+ /**
4
+ * Options for the {@link useLocalQuery} hook.
5
+ * @experimental
6
+ */
7
+ interface UseLocalQueryOptions {
8
+ /**
9
+ * Optional shard key (reserved for future use; currently unused).
10
+ *
11
+ * Intended for multi-mirror setups where a single app maintains multiple
12
+ * SQLite databases sharded by a key (e.g. user id, tenant id). Currently
13
+ * has no effect — the hook always queries the mirror passed as the first
14
+ * argument.
15
+ */
16
+ shardKey?: string;
17
+ }
18
+ /**
19
+ * React hook that subscribes to a local SQLite query and returns
20
+ * live-updating results whenever the mirror applies a diff.
21
+ *
22
+ * Uses `useSyncExternalStore` to subscribe to the mirror's `onChange`
23
+ * callback — every diff triggers a re-query against the local SQLite.
24
+ *
25
+ * The hook works with React 18+ concurrent features, Suspense, and
26
+ * server-side rendering. During SSR the same value is returned as on
27
+ * the client (the mirror's current state at render time).
28
+ * @param mirror The {@link LocalMirror} instance to query. Must have been
29
+ * constructed with an {@link import("./adapters/types").SqliteAdapter}.
30
+ * @param sql Parameterised SQL query string. Use `?` placeholders for
31
+ * bound parameters (the adapter forwards them to the underlying SQLite
32
+ * engine without rewriting).
33
+ * @param params Optional positional bound parameters matching `?`
34
+ * placeholders in `sql`.
35
+ * @param _options Optional configuration (currently unused; reserved for
36
+ * future features like shard key routing).
37
+ * @returns An array of result rows typed via the generic parameter `T`, or
38
+ * `undefined` if the query fails (e.g. the target table doesn't exist yet
39
+ * because no matching diff has been applied). Treat `undefined` as a
40
+ * "loading" or "no data yet" signal in your component.
41
+ *
42
+ * **Error handling**: The hook catches SQL errors internally and returns
43
+ * `undefined`. Use a try-catch around `mirror.query(...)` directly if you
44
+ * need finer-grained error diagnostics.
45
+ * @example
46
+ * ```tsx
47
+ * import { useLocalQuery } from "@lunora/replica/react";
48
+ * import { mirror } from "./mirror";
49
+ *
50
+ * function UserList() {
51
+ * const users = useLocalQuery<{ id: string; name: string }>(
52
+ * mirror,
53
+ * "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
54
+ * ["%alice%"],
55
+ * );
56
+ *
57
+ * if (users === undefined) {
58
+ * return <p>Waiting for data…</p>;
59
+ * }
60
+ *
61
+ * return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
62
+ * }
63
+ * ```
64
+ * @experimental
65
+ */
66
+ declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => T[] | undefined;
67
+ export { UseLocalQueryOptions, useLocalQuery };
package/dist/react.mjs ADDED
@@ -0,0 +1,15 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ const useLocalQuery = (mirror, sql, params, _options) => {
4
+ const subscribe = (onStoreChange) => mirror.onChange(onStoreChange);
5
+ const getSnapshot = () => mirror.eventLog.size;
6
+ const getServerSnapshot = () => mirror.eventLog.size;
7
+ useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
8
+ try {
9
+ return mirror.query(sql, params);
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ };
14
+
15
+ export { useLocalQuery };