@lunora/replica 1.0.0-alpha.11 → 1.0.0-alpha.110

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 (52) hide show
  1. package/README.md +55 -10
  2. package/dist/adapters/better-sqlite3.d.mts +2 -1
  3. package/dist/adapters/better-sqlite3.d.ts +2 -1
  4. package/dist/adapters/better-sqlite3.mjs +1 -1
  5. package/dist/adapters/sqlite-wasm.d.mts +3 -9
  6. package/dist/adapters/sqlite-wasm.d.ts +3 -9
  7. package/dist/adapters/sqlite-wasm.mjs +1 -1
  8. package/dist/adapters/sqljs.d.mts +4 -2
  9. package/dist/adapters/sqljs.d.ts +4 -2
  10. package/dist/adapters/sqljs.mjs +1 -1
  11. package/dist/index.d.mts +171 -67
  12. package/dist/index.d.ts +171 -67
  13. package/dist/index.mjs +1 -1
  14. package/dist/packem_shared/{EventEmitter-ovTsLeAj.mjs → EventEmitter-uo75adUL.mjs} +1 -1
  15. package/dist/packem_shared/EventLog-SuC_BKwj.mjs +1 -0
  16. package/dist/packem_shared/EventLogDO-BgUx2GGL.mjs +1 -0
  17. package/dist/packem_shared/EventLogDOClient-DWerZ3_n.mjs +1 -0
  18. package/dist/packem_shared/EventSource-Bg6zNmRn.mjs +1 -0
  19. package/dist/packem_shared/EventsSync-B3wzXm-b.mjs +1 -0
  20. package/dist/packem_shared/LocalMirror-Bn22hyEB.mjs +4 -0
  21. package/dist/packem_shared/MaterializerRuntime-DFXi-aqd.mjs +1 -0
  22. package/dist/packem_shared/SubscriptionManager-AhPw3lFc.mjs +1 -0
  23. package/dist/packem_shared/applyDiff-CZAqAC8Y.mjs +1 -0
  24. package/dist/packem_shared/applyDiffToDb-CK-Dcy17.mjs +1 -0
  25. package/dist/packem_shared/classifyChanges-BBc0-770.mjs +1 -0
  26. package/dist/packem_shared/defineEvents-DHo-VK7G.mjs +1 -0
  27. package/dist/packem_shared/fnv1a-BNN96GYb.mjs +1 -0
  28. package/dist/packem_shared/int64-CCVxepl4.mjs +1 -0
  29. package/dist/packem_shared/{isClientSeq-DSXBJskD.mjs → isClientSeq-D2Xm0_lj.mjs} +1 -1
  30. package/dist/packem_shared/{local-mirror.d-DTavX_y0.d.mts → local-mirror.d-DzREvGNM.d.mts} +54 -22
  31. package/dist/packem_shared/{local-mirror.d-CyGOpUES.d.ts → local-mirror.d-wbGXkXaE.d.ts} +54 -22
  32. package/dist/packem_shared/subscribeToMirror-DSn8n1IR.mjs +1 -0
  33. package/dist/packem_shared/{types.d-CkMkSwLJ.d.mts → types.d-BuLTPLaQ.d.mts} +0 -2
  34. package/dist/packem_shared/{types.d-CkMkSwLJ.d.ts → types.d-BuLTPLaQ.d.ts} +0 -2
  35. package/dist/packem_shared/wire-key-DfMHAtqH.mjs +1 -0
  36. package/dist/react.d.mts +39 -29
  37. package/dist/react.d.ts +39 -29
  38. package/dist/react.mjs +1 -1
  39. package/package.json +1 -1
  40. package/dist/packem_shared/EventLog-DmlRY_4Z.mjs +0 -1
  41. package/dist/packem_shared/EventLogDO-Cb-7iN9w.mjs +0 -1
  42. package/dist/packem_shared/EventLogDOClient-C6gA3b3u.mjs +0 -1
  43. package/dist/packem_shared/EventSource-B5UTlkl9.mjs +0 -1
  44. package/dist/packem_shared/EventsSync-pK_hg9KP.mjs +0 -1
  45. package/dist/packem_shared/LocalMirror-Q7cPH_rX.mjs +0 -4
  46. package/dist/packem_shared/MaterializerRuntime-B-I9jKDe.mjs +0 -1
  47. package/dist/packem_shared/SubscriptionManager-CbSjG_GA.mjs +0 -1
  48. package/dist/packem_shared/applyDiff-BUzddc6r.mjs +0 -1
  49. package/dist/packem_shared/applyDiffToDb-1M2I3BHi.mjs +0 -1
  50. package/dist/packem_shared/classifyChanges-BnOMcDGj.mjs +0 -1
  51. package/dist/packem_shared/defineEvents-CJZV8Bgi.mjs +0 -1
  52. package/dist/packem_shared/subscribeToMirror-Dru_AYBu.mjs +0 -1
@@ -1,4 +1,4 @@
1
- import { S as SqliteAdapter } from "./types.d-CkMkSwLJ.mjs";
1
+ import { S as SqliteAdapter } from "./types.d-BuLTPLaQ.mjs";
2
2
  /**
3
3
  * Row-level change kind within a TableDiff.
4
4
  *
@@ -31,13 +31,14 @@ interface TableDiff {
31
31
  /**
32
32
  * Optional stable identity for this diff, distinct from `timestamp`
33
33
  * (multiple diffs can legitimately share a millisecond, so `timestamp`
34
- * alone is not a unique diff identity). Used by `deriveInsertId` in
35
- * `apply-diff.ts` to derive deterministic row ids for id-less inserts:
36
- * replaying the SAME diff (same `id`) must always mint the SAME id,
37
- * while two DIFFERENT diffs emitted in the same millisecond must not
38
- * alias onto the same one. `createTableDiff` auto-generates one when
39
- * omitted; diffs built as plain object literals (bypassing the helper)
40
- * simply fall back to `timestamp` for that derivation.
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.
41
42
  */
42
43
  readonly id?: string;
43
44
  /** Logical table name (matches the schema table name). */
@@ -140,7 +141,7 @@ declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
140
141
  *
141
142
  * ```ts
142
143
  * const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
143
- * // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
144
+ * // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
144
145
  * ```
145
146
  * @experimental
146
147
  */
@@ -312,6 +313,11 @@ declare class EventLog {
312
313
  getSince(sinceSeq: number): ReadonlyArray<EventLogEntry>;
313
314
  /**
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.
315
321
  * @returns `{ entries, hasMore }` where `hasMore` is `true` when more
316
322
  * entries exist beyond the requested page.
317
323
  */
@@ -381,15 +387,18 @@ interface LocalMirrorOptions {
381
387
  /** Platform-specific SQLite adapter. */
382
388
  readonly db: SqliteAdapter;
383
389
  /**
384
- * Cap the mirror's internal {@link EventLog} to this many entries
385
- * (REPLICA-06). Every applied diff is recorded in the log — with no cap,
386
- * a long-running client accumulates one entry per diff forever.
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.
387
394
  *
388
- * `undefined` (the default) preserves unbounded retention. Set this when
389
- * catch-up replication only ever needs a bounded recent window; older
390
- * entries are silently evicted (oldest-first) once the cap is exceeded.
391
- * See {@link EventLog#truncateBelow} for caller-driven truncation tied to
392
- * a snapshot instead.
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.
393
402
  */
394
403
  readonly maxEventLogEntries?: number;
395
404
  /**
@@ -402,6 +411,15 @@ interface LocalMirrorOptions {
402
411
  */
403
412
  readonly tables?: Record<string, MirrorTableDef>;
404
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";
405
423
  /**
406
424
  * Local SQLite mirror that maintains a client-side replica of server
407
425
  * tables by applying {@link TableDiff} deltas.
@@ -420,13 +438,13 @@ interface LocalMirrorOptions {
420
438
  * mirror.applyDiff(someDiff);
421
439
  *
422
440
  * // Query locally:
423
- * const rows = mirror.query&lt;{ id: string; name: string }>(
441
+ * const rows = mirror.query<{ id: string; name: string }>(
424
442
  * "SELECT id, name FROM users WHERE name LIKE ?",
425
443
  * ["alice%"],
426
444
  * );
427
445
  * ```
428
446
  */
429
- type ChangeSubscriber = () => void;
447
+ type ChangeSubscriber = (reason: MirrorChangeReason) => void;
430
448
  /**
431
449
  * `LocalMirror` is part of the experimental `@lunora/replica` API and may change without a major version bump.
432
450
  * @experimental
@@ -504,8 +522,17 @@ declare class LocalMirror {
504
522
  */
505
523
  query<T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>): T[];
506
524
  /**
507
- * Delete every row from all known tables (preserves the event log
508
- * and schema). Useful when re-syncing from scratch.
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.
509
536
  *
510
537
  * Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
511
538
  * (REPLICA-09) even though nothing is appended to the event log — a
@@ -519,9 +546,14 @@ declare class LocalMirror {
519
546
  close(): void;
520
547
  /**
521
548
  * Register a table schema so the mirror can create the table on
522
- * first use.
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`.
523
553
  */
524
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;
525
557
  /**
526
558
  * Return the list of mirrored table names.
527
559
  */
@@ -1,4 +1,4 @@
1
- import { S as SqliteAdapter } from "./types.d-CkMkSwLJ.js";
1
+ import { S as SqliteAdapter } from "./types.d-BuLTPLaQ.js";
2
2
  /**
3
3
  * Row-level change kind within a TableDiff.
4
4
  *
@@ -31,13 +31,14 @@ interface TableDiff {
31
31
  /**
32
32
  * Optional stable identity for this diff, distinct from `timestamp`
33
33
  * (multiple diffs can legitimately share a millisecond, so `timestamp`
34
- * alone is not a unique diff identity). Used by `deriveInsertId` in
35
- * `apply-diff.ts` to derive deterministic row ids for id-less inserts:
36
- * replaying the SAME diff (same `id`) must always mint the SAME id,
37
- * while two DIFFERENT diffs emitted in the same millisecond must not
38
- * alias onto the same one. `createTableDiff` auto-generates one when
39
- * omitted; diffs built as plain object literals (bypassing the helper)
40
- * simply fall back to `timestamp` for that derivation.
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.
41
42
  */
42
43
  readonly id?: string;
43
44
  /** Logical table name (matches the schema table name). */
@@ -140,7 +141,7 @@ declare const isClientSeq: (seq: Seq) => seq is ClientSeq;
140
141
  *
141
142
  * ```ts
142
143
  * const event = events.chat.messageSent({ channelId: "c1", text: "hello" });
143
- * // event: InputEvent&lt;"chat.messageSent", { channelId: string; text: string }>
144
+ * // event: InputEvent<"chat.messageSent", { channelId: string; text: string }>
144
145
  * ```
145
146
  * @experimental
146
147
  */
@@ -312,6 +313,11 @@ declare class EventLog {
312
313
  getSince(sinceSeq: number): ReadonlyArray<EventLogEntry>;
313
314
  /**
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.
315
321
  * @returns `{ entries, hasMore }` where `hasMore` is `true` when more
316
322
  * entries exist beyond the requested page.
317
323
  */
@@ -381,15 +387,18 @@ interface LocalMirrorOptions {
381
387
  /** Platform-specific SQLite adapter. */
382
388
  readonly db: SqliteAdapter;
383
389
  /**
384
- * Cap the mirror's internal {@link EventLog} to this many entries
385
- * (REPLICA-06). Every applied diff is recorded in the log — with no cap,
386
- * a long-running client accumulates one entry per diff forever.
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.
387
394
  *
388
- * `undefined` (the default) preserves unbounded retention. Set this when
389
- * catch-up replication only ever needs a bounded recent window; older
390
- * entries are silently evicted (oldest-first) once the cap is exceeded.
391
- * See {@link EventLog#truncateBelow} for caller-driven truncation tied to
392
- * a snapshot instead.
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.
393
402
  */
394
403
  readonly maxEventLogEntries?: number;
395
404
  /**
@@ -402,6 +411,15 @@ interface LocalMirrorOptions {
402
411
  */
403
412
  readonly tables?: Record<string, MirrorTableDef>;
404
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";
405
423
  /**
406
424
  * Local SQLite mirror that maintains a client-side replica of server
407
425
  * tables by applying {@link TableDiff} deltas.
@@ -420,13 +438,13 @@ interface LocalMirrorOptions {
420
438
  * mirror.applyDiff(someDiff);
421
439
  *
422
440
  * // Query locally:
423
- * const rows = mirror.query&lt;{ id: string; name: string }>(
441
+ * const rows = mirror.query<{ id: string; name: string }>(
424
442
  * "SELECT id, name FROM users WHERE name LIKE ?",
425
443
  * ["alice%"],
426
444
  * );
427
445
  * ```
428
446
  */
429
- type ChangeSubscriber = () => void;
447
+ type ChangeSubscriber = (reason: MirrorChangeReason) => void;
430
448
  /**
431
449
  * `LocalMirror` is part of the experimental `@lunora/replica` API and may change without a major version bump.
432
450
  * @experimental
@@ -504,8 +522,17 @@ declare class LocalMirror {
504
522
  */
505
523
  query<T = Record<string, unknown>>(sql: string, params?: ReadonlyArray<unknown>): T[];
506
524
  /**
507
- * Delete every row from all known tables (preserves the event log
508
- * and schema). Useful when re-syncing from scratch.
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.
509
536
  *
510
537
  * Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
511
538
  * (REPLICA-09) even though nothing is appended to the event log — a
@@ -519,9 +546,14 @@ declare class LocalMirror {
519
546
  close(): void;
520
547
  /**
521
548
  * Register a table schema so the mirror can create the table on
522
- * first use.
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`.
523
553
  */
524
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;
525
557
  /**
526
558
  * Return the list of mirrored table names.
527
559
  */
@@ -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};
@@ -11,8 +11,6 @@ interface SqliteAdapter {
11
11
  close: () => void;
12
12
  /** Execute a SQL statement (with optional bound params). */
13
13
  exec: (sql: string, params?: ReadonlyArray<unknown>) => void;
14
- /** Return the id of the last inserted row. */
15
- lastInsertRowId: () => number;
16
14
  /**
17
15
  * Execute a SQL statement and return the result rows.
18
16
  * Columns can be accessed by index or by name.
@@ -11,8 +11,6 @@ interface SqliteAdapter {
11
11
  close: () => void;
12
12
  /** Execute a SQL statement (with optional bound params). */
13
13
  exec: (sql: string, params?: ReadonlyArray<unknown>) => void;
14
- /** Return the id of the last inserted row. */
15
- lastInsertRowId: () => number;
16
14
  /**
17
15
  * Execute a SQL statement and return the result rows.
18
16
  * Columns can be accessed by index or by name.
@@ -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};
package/dist/react.d.mts CHANGED
@@ -1,26 +1,36 @@
1
- import { L as LocalMirror } from "./packem_shared/local-mirror.d-DTavX_y0.mjs";
2
- import "./packem_shared/types.d-CkMkSwLJ.mjs";
1
+ import { L as LocalMirror } from "./packem_shared/local-mirror.d-DzREvGNM.mjs";
2
+ import "./packem_shared/types.d-BuLTPLaQ.mjs";
3
3
  /**
4
- * Options for the {@link useLocalQuery} hook.
4
+ * Result of {@link useLocalQuery} — a discriminated union so callers get a
5
+ * typed error instead of a swallowed `undefined`.
5
6
  * @experimental
6
7
  */
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
- }
8
+ type LocalQueryResult<T> = {
9
+ readonly data: T[];
10
+ readonly error?: undefined;
11
+ } | {
12
+ readonly data?: undefined;
13
+ readonly error: Error;
14
+ };
18
15
  /**
19
16
  * React hook that subscribes to a local SQLite query and returns
20
17
  * live-updating results whenever the mirror applies a diff.
21
18
  *
22
- * Uses `useSyncExternalStore` to subscribe to the mirror's `onChange`
23
- * callback — every diff triggers a re-query against the local SQLite.
19
+ * Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
20
+ * — a plain number, unconditionally `Object.is`-stable across calls with no
21
+ * mutation in between. The actual query runs in a `useMemo` keyed on
22
+ * `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
23
+ * when the mirror actually advances (or `sql`/`params` change) — not on
24
+ * every unrelated re-render.
25
+ *
26
+ * This intentionally does NOT cache query results inside {@link LocalMirror}
27
+ * itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
28
+ * entries once more than the cap's worth of distinct live queries are open
29
+ * on one mirror, which makes `getSnapshot` return a fresh (non-identical)
30
+ * object on the next read — `useSyncExternalStore` then force-re-renders,
31
+ * re-inserts, evicts another entry, and so on without bound. Keying the
32
+ * external-store snapshot on the version primitive instead of a cached
33
+ * query result has no such cap, so it can't loop.
24
34
  *
25
35
  * The hook works with React 18+ concurrent features, Suspense, and
26
36
  * server-side rendering. During SSR the same value is returned as on
@@ -32,28 +42,28 @@ interface UseLocalQueryOptions {
32
42
  * engine without rewriting).
33
43
  * @param params Optional positional bound parameters matching `?`
34
44
  * 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
+ * @returns `{ data }` with the result rows typed via the generic parameter
46
+ * `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
47
+ * target table doesn't exist yet because no matching diff has been applied
48
+ * — that specific case surfaces as a "no such table" `Error`). Never
49
+ * collapses a failure to `undefined` — check `error` explicitly rather than
50
+ * treating a missing `data` as "still loading".
45
51
  * @example
46
52
  * ```tsx
47
53
  * import { useLocalQuery } from "@lunora/replica/react";
48
54
  * import { mirror } from "./mirror";
49
55
  *
50
56
  * function UserList() {
51
- * const users = useLocalQuery<{ id: string; name: string }>(
57
+ * const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
52
58
  * mirror,
53
59
  * "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
54
60
  * ["%alice%"],
55
61
  * );
56
62
  *
63
+ * if (error) {
64
+ * return <p>Query failed: {error.message}</p>;
65
+ * }
66
+ *
57
67
  * if (users === undefined) {
58
68
  * return <p>Waiting for data…</p>;
59
69
  * }
@@ -63,5 +73,5 @@ interface UseLocalQueryOptions {
63
73
  * ```
64
74
  * @experimental
65
75
  */
66
- declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => T[] | undefined;
67
- export { UseLocalQueryOptions, useLocalQuery };
76
+ declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>) => LocalQueryResult<T>;
77
+ export { type LocalQueryResult, useLocalQuery };
package/dist/react.d.ts CHANGED
@@ -1,26 +1,36 @@
1
- import { L as LocalMirror } from "./packem_shared/local-mirror.d-CyGOpUES.js";
2
- import "./packem_shared/types.d-CkMkSwLJ.js";
1
+ import { L as LocalMirror } from "./packem_shared/local-mirror.d-wbGXkXaE.js";
2
+ import "./packem_shared/types.d-BuLTPLaQ.js";
3
3
  /**
4
- * Options for the {@link useLocalQuery} hook.
4
+ * Result of {@link useLocalQuery} — a discriminated union so callers get a
5
+ * typed error instead of a swallowed `undefined`.
5
6
  * @experimental
6
7
  */
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
- }
8
+ type LocalQueryResult<T> = {
9
+ readonly data: T[];
10
+ readonly error?: undefined;
11
+ } | {
12
+ readonly data?: undefined;
13
+ readonly error: Error;
14
+ };
18
15
  /**
19
16
  * React hook that subscribes to a local SQLite query and returns
20
17
  * live-updating results whenever the mirror applies a diff.
21
18
  *
22
- * Uses `useSyncExternalStore` to subscribe to the mirror's `onChange`
23
- * callback — every diff triggers a re-query against the local SQLite.
19
+ * Uses `useSyncExternalStore` with `getSnapshot` reading {@link LocalMirror.version}
20
+ * — a plain number, unconditionally `Object.is`-stable across calls with no
21
+ * mutation in between. The actual query runs in a `useMemo` keyed on
22
+ * `[mirror, version, sql, stableParamsKey(params)]`, so it only re-executes
23
+ * when the mirror actually advances (or `sql`/`params` change) — not on
24
+ * every unrelated re-render.
25
+ *
26
+ * This intentionally does NOT cache query results inside {@link LocalMirror}
27
+ * itself: an LRU-capped cache in the mirror core evicts still-mounted hooks'
28
+ * entries once more than the cap's worth of distinct live queries are open
29
+ * on one mirror, which makes `getSnapshot` return a fresh (non-identical)
30
+ * object on the next read — `useSyncExternalStore` then force-re-renders,
31
+ * re-inserts, evicts another entry, and so on without bound. Keying the
32
+ * external-store snapshot on the version primitive instead of a cached
33
+ * query result has no such cap, so it can't loop.
24
34
  *
25
35
  * The hook works with React 18+ concurrent features, Suspense, and
26
36
  * server-side rendering. During SSR the same value is returned as on
@@ -32,28 +42,28 @@ interface UseLocalQueryOptions {
32
42
  * engine without rewriting).
33
43
  * @param params Optional positional bound parameters matching `?`
34
44
  * 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
+ * @returns `{ data }` with the result rows typed via the generic parameter
46
+ * `T`, or `{ error }` when the query fails (e.g. malformed SQL, or the
47
+ * target table doesn't exist yet because no matching diff has been applied
48
+ * — that specific case surfaces as a "no such table" `Error`). Never
49
+ * collapses a failure to `undefined` — check `error` explicitly rather than
50
+ * treating a missing `data` as "still loading".
45
51
  * @example
46
52
  * ```tsx
47
53
  * import { useLocalQuery } from "@lunora/replica/react";
48
54
  * import { mirror } from "./mirror";
49
55
  *
50
56
  * function UserList() {
51
- * const users = useLocalQuery<{ id: string; name: string }>(
57
+ * const { data: users, error } = useLocalQuery<{ id: string; name: string }>(
52
58
  * mirror,
53
59
  * "SELECT id, name FROM fn_todos_list WHERE name LIKE ?",
54
60
  * ["%alice%"],
55
61
  * );
56
62
  *
63
+ * if (error) {
64
+ * return <p>Query failed: {error.message}</p>;
65
+ * }
66
+ *
57
67
  * if (users === undefined) {
58
68
  * return <p>Waiting for data…</p>;
59
69
  * }
@@ -63,5 +73,5 @@ interface UseLocalQueryOptions {
63
73
  * ```
64
74
  * @experimental
65
75
  */
66
- declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>, _options?: UseLocalQueryOptions) => T[] | undefined;
67
- export { UseLocalQueryOptions, useLocalQuery };
76
+ declare const useLocalQuery: <T = Record<string, unknown>>(mirror: LocalMirror, sql: string, params?: ReadonlyArray<unknown>) => LocalQueryResult<T>;
77
+ export { type LocalQueryResult, useLocalQuery };
package/dist/react.mjs CHANGED
@@ -1 +1 @@
1
- import{useSyncExternalStore as t}from"react";const c=(r,e,n,s)=>{t(o=>r.onChange(o),()=>r.version,()=>r.version);try{return r.query(e,n)}catch{return}};export{c as useLocalQuery};
1
+ import{useSyncExternalStore as y,useMemo as i}from"react";const u=t=>JSON.stringify(t??[],(r,e)=>typeof e=="bigint"?`${e.toString()}n`:e),S=(t,r,e)=>{const s=n=>t.onChange(n),o=()=>t.version,c=y(s,o,o),a=u(e);return i(()=>{try{return{data:t.query(r,e)}}catch(n){return{error:n instanceof Error?n:new Error(String(n))}}},[t,c,r,a])};export{S as useLocalQuery};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/replica",
3
- "version": "1.0.0-alpha.11",
3
+ "version": "1.0.0-alpha.110",
4
4
  "description": "Local-first replica runtime + local SQLite mirror for Lunora",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- class c{#s=[];#t=0;#i=0;#e=null;#n;constructor(t){this.#n=c.#o(t?.maxEntries)}static#o(t){if(t!==void 0){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("maxEntries must be a non-negative safe integer");return t}}#h(){return this.#s.slice(this.#t)}#r(){if(this.#n===void 0)return;const t=this.#s.length-this.#t-this.#n;t>0&&(this.#t+=t),this.#t>0&&this.#t>=this.#s.length-this.#t&&(this.#s=this.#s.slice(this.#t),this.#t=0)}append(t,e,s,n){const h=typeof t=="string"?t:t.type,o=typeof t=="string"?e:t.payload;let r,i;typeof t=="string"?(r=s,i=n):(r=void 0,i=e);const a=i?.parentSeqNum??this.#e??void 0,p=this.#i;this.#i+=1;const l={seq:p,type:h,payload:o,timestamp:i?.timestamp??Date.now(),tableDiffs:r,clientId:i?.clientId,sessionId:i?.sessionId,parentSeqNum:a};return this.#s.push(l),this.#e=l.seq,this.#r(),l}commitAll(t){if(t.length===0)return[];const e=[];for(const s of t){const{type:n}=s,h="payload"in s?s.payload:void 0,o="timestamp"in s?s.timestamp:Date.now(),r=e.at(-1)?.seq??this.#e??void 0,i=this.#i;this.#i+=1;const a={seq:i,type:n,payload:h,timestamp:o,parentSeqNum:r};this.#s.push(a),e.push(a)}return this.#e=e.at(-1)?.seq??this.#e,this.#r(),e}load(t){this.#s=[...t.entries],this.#t=0,this.#i=t.nextSeq,this.#e=t.headSeq,this.#r()}getSince(t){const e=this.#h();if(t<=0)return e;const s=e.findIndex(n=>n.seq>=t);return s===-1?[]:e.slice(s)}getFrom(t,e=50){const s=this.#s,n=this.#t,h=s.findIndex((r,i)=>i>=n&&r.seq>=t);if(h===-1)return{entries:[],hasMore:!1};const o=Math.min(h+e,s.length);return{entries:s.slice(h,o),hasMore:o<s.length}}snapshot(){return{entries:this.#h(),nextSeq:this.#i,headSeq:this.#e}}get size(){return this.#s.length-this.#t}get nextSeq(){return this.#i}get isEmpty(){return this.size===0}get headSeq(){return this.#e}clear(){this.#s=[],this.#t=0,this.#i=0,this.#e=null}truncateBelow(t){if(!Number.isSafeInteger(t)||t<0)throw new RangeError("floorSeq must be a non-negative safe integer");const e=this.#h(),s=e.findIndex(n=>n.seq>=t);this.#s=s===-1?[]:e.slice(s),this.#t=0}async*events(t=0){const e=this.getSince(t);for(const s of e)yield s}}export{c as EventLog};
@@ -1 +0,0 @@
1
- const E=Symbol("lunora.replica.event-log-do.idempotency-conflict"),f=i=>{const e=new Error(i);return Object.defineProperty(e,E,{value:!0}),e},h=i=>i instanceof Error&&E in i,y=i=>{if(Array.isArray(i))return i.map(e=>y(e));if(i!==null&&typeof i=="object"){const e=i,t=Object.keys(e);t.sort();const s={};for(const n of t)s[n]=y(e[n]);return s}return i},q=async i=>{const e=JSON.stringify(y(i)),t=new TextEncoder().encode(e),s=await crypto.subtle.digest("SHA-256",t);return[...new Uint8Array(s)].map(n=>n.toString(16).padStart(2,"0")).join("")},m=i=>typeof i.toArray=="function"?i.toArray():typeof i[Symbol.iterator]=="function"?[...i]:[],l=i=>m(i).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 c{state;env;#e=!1;constructor(e,t){this.state=e,this.env=t}async fetch(e){this.#c();const t=new URL(e.url);try{if(e.method==="POST"&&t.pathname==="/append")return await this.#t(e);if(e.method==="GET"&&t.pathname==="/since")return this.#r(t);if(e.method==="GET"&&t.pathname==="/range")return this.#a(t);if(e.method==="GET"&&t.pathname==="/size")return this.#o();if(e.method==="GET"&&t.pathname==="/state")return this.#p()}catch(s){return console.error("[event-log-do] request failed:",s),Response.json({error:{code:"INTERNAL_ERROR",message:"internal error"}},{status:500,headers:{"content-type":"application/json"}})}return Response.json({error:{code:"NOT_FOUND",message:"unknown route"}},{status:404,headers:{"content-type":"application/json"}})}async#t(e){let t;try{t=await e.json()}catch{return Response.json({error:{code:"BAD_REQUEST",message:"invalid JSON body"}},{status:400,headers:{"content-type":"application/json"}})}const s=c.#n(t);if(s)return Response.json({error:{code:"BAD_REQUEST",message:s}},{status:400,headers:{"content-type":"application/json"}});const{sql:n}=this.state.storage,{batchId:a}=t,p=()=>c.#s(n,t,a),{transaction:r}=this.state.storage;let o;try{o=typeof r=="function"?await r(p):await p()}catch(d){if(h(d))return Response.json({error:{code:"CONFLICT",message:d.message}},{status:409,headers:{"content-type":"application/json"}});throw d}const u={entries:o};return Response.json(u,{status:200,headers:{"content-type":"application/json"}})}static async#s(e,t,s){let n;if(typeof s=="string"){n=await q(t.events);const r=c.#d(e,s);if(r){if(r.fingerprint!==n)throw f(`batchId "${s}" was already used for a different event batch`);return r.entries}}const a=Date.now(),p=[];for(const r of t.events){const o={seq:c.#l(e),type:r.type,payload:r.payload,timestamp:r.timestamp??a,clientId:r.clientId,sessionId:r.sessionId,parentSeqNum:r.parentSeqNum};c.#m(e,o),p.push(o)}if(typeof s=="string"&&n!==void 0){const r=p[0]?.seq,o=p.at(-1)?.seq;r!==void 0&&o!==void 0&&c.#u(e,s,r,o,n)}return p}static#n(e){if(!Array.isArray(e.events)||e.events.length===0)return"events[] with a non-empty string `type` required";if(e.batchId!==void 0&&(typeof e.batchId!="string"||e.batchId.length===0))return"batchId must be a non-empty string";for(const t of e.events){const s=c.#i(t);if(s!==void 0)return s}}static#i(e){if(typeof e.type!="string"||e.type.length===0)return"events[] with a non-empty string `type` required";if(e.timestamp!==void 0&&!Number.isFinite(e.timestamp))return"events[].timestamp must be a finite number";if(e.clientId!==void 0&&typeof e.clientId!="string")return"events[].clientId must be a string";if(e.sessionId!==void 0&&typeof e.sessionId!="string")return"events[].sessionId must be a string";if(e.parentSeqNum!==void 0&&(typeof e.parentSeqNum!="number"||!Number.isInteger(e.parentSeqNum)||e.parentSeqNum<0))return"events[].parentSeqNum must be a non-negative integer"}#r(e){const t=e.searchParams.get("seq"),s=t===null?0:Number(t);if(!Number.isFinite(s)||s<0)return Response.json({error:{code:"BAD_REQUEST",message:"invalid seq"}},{status:400,headers:{"content-type":"application/json"}});const{sql:n}=this.state.storage,a=n.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC",s),p=l(a);return Response.json({entries:p},{status:200,headers:{"content-type":"application/json"}})}#a(e){const t=e.searchParams.get("from"),s=t===null?0:Number(t),n=e.searchParams.get("limit"),a=n===null?50:Number(n);if(!Number.isFinite(s)||s<0||!Number.isFinite(a)||a<1||a>1e3)return Response.json({error:{code:"BAD_REQUEST",message:"invalid from/limit"}},{status:400,headers:{"content-type":"application/json"}});const{sql:p}=this.state.storage,r=p.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? ORDER BY seq ASC LIMIT ?",s,a+1),o=l(r),u=o.length>a,d={entries:u?o.slice(0,a):o,hasMore:u};return Response.json(d,{status:200,headers:{"content-type":"application/json"}})}#o(){const{sql:e}=this.state.storage,t=e.exec("SELECT COUNT(*) AS count FROM events"),s=m(t)[0]?.count??0;return Response.json({count:s},{status:200,headers:{"content-type":"application/json"}})}#p(){const{sql:e}=this.state.storage,t=e.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events ORDER BY seq ASC"),s=l(t),n=(s.at(-1)?.seq??-1)+1;return Response.json({entries:s,nextSeq:n},{status:200,headers:{"content-type":"application/json"}})}#c(){if(this.#e)return;const{sql:e}=this.state.storage;e.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)"),e.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.#e=!0}static#d(e,t){const s=e.exec("SELECT first_seq, last_seq, fingerprint FROM event_batches WHERE batch_id = ?",t),n=m(s)[0];if(!n)return;const a=e.exec("SELECT seq, type, payload, timestamp, client_id, session_id, parent_seq FROM events WHERE seq >= ? AND seq <= ? ORDER BY seq ASC",n.first_seq,n.last_seq);return{entries:l(a),fingerprint:n.fingerprint}}static#u(e,t,s,n,a){e.exec("INSERT INTO event_batches (batch_id, first_seq, last_seq, fingerprint) VALUES (?, ?, ?, ?)",t,s,n,a)}static#l(e){const t=e.exec("SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM events");return m(t)[0]?.next_seq??0}static#m(e,t){let s=null;typeof t.parentSeqNum=="number"&&(s=t.parentSeqNum),e.exec("INSERT INTO events (seq, type, payload, timestamp, client_id, session_id, parent_seq) VALUES (?, ?, ?, ?, ?, ?, ?)",t.seq,t.type,JSON.stringify(t.payload),t.timestamp,t.clientId??null,t.sessionId??null,s)}}export{c as EventLogDO};
@@ -1 +0,0 @@
1
- class s{#t;constructor(t){this.#t=t.fetch}async append(t,e){const a=JSON.stringify({events:t,batchId:e?.batchId}),n=await this.#t(new Request("https://do/append",{method:"POST",headers:{"content-type":"application/json"},body:a}));if(!n.ok)throw await s.#e(n,"append");return(await n.json()).entries}async getSince(t){const e=await this.#t(new Request(`https://do/since?seq=${String(t)}`));if(!e.ok)throw await s.#e(e,"getSince");return(await e.json()).entries}async getRange(t,e=50){const a=await this.#t(new Request(`https://do/range?from=${String(t)}&limit=${String(e)}`));if(!a.ok)throw await s.#e(a,"getRange");return await a.json()}async getSize(){const t=await this.#t(new Request("https://do/size"));if(!t.ok)throw await s.#e(t,"getSize");return(await t.json()).count}async getState(){const t=await this.#t(new Request("https://do/state"));if(!t.ok)throw await s.#e(t,"getState");return await t.json()}static async#e(t,e){try{const a=(await t.json()).error?.message??t.statusText;return new Error(`EventLogDO.${e} failed (${String(t.status)}): ${a}`)}catch{return new Error(`EventLogDO.${e} failed (${String(t.status)}): ${t.statusText}`)}}}export{s as EventLogDOClient};
@@ -1 +0,0 @@
1
- import{EventEmitter as g}from"./EventEmitter-ovTsLeAj.mjs";import{EventLog as y}from"./EventLog-DmlRY_4Z.mjs";const c=Symbol("lunora.replica.event-source.unhandled");class E{emitter=new g;log;#t;#e;#n=!1;#s;#r=-1;constructor(t,e,n){this.#t={...t},this.#e=e,this.#s=n?.unknownEventHandling??"warn",this.log=new y({maxEntries:n?.maxLogEntries})}get state(){return this.#t}get replayed(){return this.#n}applyEvent(t,e,n){let r,i,s;typeof t=="string"?(r=t,i=e,s=n):(r=t.type,i=t.payload,s=e);const o={seq:this.log.nextSeq,type:r,payload:i,timestamp:Date.now(),clientId:s?.clientId,sessionId:s?.sessionId,parentSeqNum:s?.parentSeqNum??this.log.headSeq??void 0};let a;try{a=this.#e(this.#t,o)}catch(l){const p=l instanceof Error?l:new Error(String(l));throw this.emitter.emit("replay-error",{entry:o,error:p}),p}let d=!0;a===c&&(d=this.#i(o));const h=this.log.append(r,i,void 0,{...s,timestamp:o.timestamp});return a===c?(d&&this.emitter.emit("state-changed",{state:this.#t,entry:h}),h):(this.#t=a,this.emitter.emit("state-changed",{state:this.#t,entry:h}),h)}replayFromLog(t){const e=t.getSince(this.#r+1);for(const n of e)try{const r=this.#e(this.#t,n);r!==c&&(this.#t=r),this.log.append(n.type,n.payload,n.tableDiffs,{clientId:n.clientId,sessionId:n.sessionId,parentSeqNum:n.parentSeqNum})}catch(r){this.emitter.emit("replay-error",{entry:n,error:r instanceof Error?r:new Error(String(r))})}finally{this.#r=n.seq}this.#n=!0,this.emitter.emit("ready",{entryCount:this.log.size})}reset(t,e=-1){this.#t={...t},this.#r=e,this.#n=e>=0}async*events(t){const e=[];let n;const r=this.emitter.on("state-changed",({entry:i})=>{e.push(i),n?.()});try{let i=0;for(;i<this.log.size;){if(t?.aborted)return;for(const s of this.log.getSince(i))yield s;i=this.log.nextSeq}for(;!t?.aborted;){for(;e.length>0;){const s=e.shift();s&&(yield s)}e.length===0&&await new Promise(s=>{n=s})}}finally{r()}}#i(t){const e=this.#s;if(typeof e=="function")return e(t);switch(e){case"ignore":return!1;case"fail":throw new Error(`EventSource: unhandled event type "${t.type}" (seq ${String(t.seq)}). Configure \`unknownEventHandling\` to handle this event or change the strategy.`);default:return console.warn(`[EventSource] unhandled event type "${t.type}" (seq ${String(t.seq)}). The event was skipped. Configure \`unknownEventHandling\` if this is expected.`),!1}}}export{E as EventSource,c as UNHANDLED};
@@ -1 +0,0 @@
1
- class o{#t;#i=0;#r;#s;constructor(t){this.#t=t}get watermark(){return this.#i}start(){if(this.#r!==void 0)return;const t=this.#t.pollInterval??5e3;this.#r=setInterval(()=>{this.#n().catch(()=>{})},t)}stop(){this.#r!==void 0&&(clearInterval(this.#r),this.#r=void 0)}async sync(){return this.#n()}async#n(){if(this.#s)return this.#s;const t=this.#e().finally(()=>{this.#s=void 0});return this.#s=t,t}async#e(){try{const t=await this.#t.fetchEventsSince(this.#i);if(t.length===0)return 0;let s=0;try{for(const r of t){this.#t.applyEvents([r]);const i=this.#t.getTableDiffs();for(const n of i)this.#t.mirror.applyDiff(n);this.#i=r.seq+1,s+=1}}catch(r){(this.#t.onError??console.error)(r)}return s}catch(t){return(this.#t.onError??console.error)(t),0}}}export{o as EventsSync};
@@ -1,4 +0,0 @@
1
- import{createSqlJsAdapter as c}from"../adapters/sqljs.mjs";import{applyDiffToDb as h,escapeIdentifier as a}from"./applyDiffToDb-1M2I3BHi.mjs";import{EventLog as E}from"./EventLog-DmlRY_4Z.mjs";const l="__lunora_mirror_meta",T=o=>{o.exec(`CREATE TABLE IF NOT EXISTS ${l} (
2
- key TEXT PRIMARY KEY NOT NULL,
3
- value TEXT NOT NULL
4
- )`)};class n{#t;#e;#s;#r=new Set;#i=0;static create(t,e){const s=c(t);return new n({db:s,tables:e?.tables})}constructor(t){this.#t=t.db,this.#e={...t.tables},this.#s=new E({maxEntries:t.maxEventLogEntries}),T(this.#t)}onChange(t){return this.#r.add(t),()=>{this.#r.delete(t)}}get eventLog(){return this.#s}get db(){return this.#t}get version(){return this.#i}applyDiff(t){if(t.changes.length===0)return;const e=this.#e[t.table]?.primaryKey??"id";this.#n(t),h(this.#t,t,e),this.#s.append("table-diff",t,[t]),this.#i+=1;for(const s of this.#r)try{s()}catch{}}query(t,e){return this.#t.query(t,e)}clearData(){const t=this.#t.query(String.raw`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\_\_lunora\_%' ESCAPE '\' AND name NOT LIKE 'sqlite\_%' ESCAPE '\'`);this.#t.transaction(()=>{for(const{name:e}of t)this.#t.exec(`DELETE FROM ${a(e)}`)}),this.#i+=1;for(const e of this.#r)try{e()}catch{}}close(){this.#t.close(),this.#s.clear()}registerTable(t,e){this.#e[t]=e}get mirroredTables(){return Object.keys(this.#e)}static#a(t,e){const s=new Set;for(const i of t.changes)if(i.type!=="delete")for(const r of Object.keys(i.data))r!==e&&s.add(r);return s}#n(t){const e=this.#e[t.table]?.primaryKey??"id",s=n.#a(t,e);if(this.#t.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?",[t.table]).length===0){let i=`${a(e)} TEXT PRIMARY KEY NOT NULL`;for(const r of s)i+=`, ${a(r)} TEXT`;this.#t.exec(`CREATE TABLE IF NOT EXISTS ${a(t.table)} (${i})`)}else if(s.size>0){const i=new Set(this.#t.query(`PRAGMA table_info(${a(t.table)})`).map(r=>r.name));for(const r of s)i.has(r)||this.#t.exec(`ALTER TABLE ${a(t.table)} ADD COLUMN ${a(r)} TEXT`)}}}export{n as LocalMirror};
@@ -1 +0,0 @@
1
- const l=r=>{let e=r.initial();return{def:r,get state(){return Object.freeze(e)},setState(t){e=t},apply(t){e=r.handle(e,t)},reset(){e=r.initial()}}};class u{#e;#n;#i;#s;#t;constructor(e,t={}){this.#e=[...e],this.#t=this.#e.map(()=>0),this.#n=t.snapshotStore,this.#i=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;for(const[o,a]of this.#e.entries()){const h=this.#t[o]??0;if(i.seq<h)continue;const p=a.state;a.apply(i),s=!0,a.state!==p&&(n=!0),this.#t[o]=i.seq+1}s&&(n||this.#r(i),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,i]of this.#e.entries()){const s=await this.#n.load(i.def.name);if(s!==null&&typeof s=="object"){const n=s;n.state!==void 0&&i.setState(n.state),this.#t[t]=n.appliedSeq,n.appliedSeq>e&&(e=n.appliedSeq)}}return e}async persistSnapshots(){if(this.#n)for(const[e,t]of this.#e.entries())await this.#n.save(t.def.name,{appliedSeq:this.#t[e]??0,state:t.state})}async initialize(){if(!this.#i)return 0;await this.recoverFromSnapshots();const e=this.#t.length>0?Math.min(...this.#t):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 t=(await this.#i.append([e]))[0];if(!t)throw new Error("MaterializerRuntime.appendEvent: DO returned empty result");return this.applyEntries([t]),t}reset(){for(const[e,t]of this.#e.entries())this.#t[e]=0,t.reset()}get materializers(){return this.#e}}export{u as MaterializerRuntime,l as defineMaterializer};
@@ -1 +0,0 @@
1
- class a{#t=new Map;#e=0;onStateChange(e){const t=String(this.#e);this.#e+=1;const n={kind:"state",id:t,callback:e};return this.#t.set(t,n),()=>{this.#t.delete(t)}}onEvent(e,t){const n=String(this.#e);this.#e+=1;const s={kind:"event",id:n,eventType:e,callback:t};return this.#t.set(n,s),()=>{this.#t.delete(n)}}notifyState(e){for(const t of this.#t.values())if(t.kind==="state")try{t.callback(e)}catch{}}notifyEvent(e){for(const t of this.#t.values())if(t.kind==="event"&&t.eventType===e.type)try{t.callback(e)}catch{}}get size(){return this.#t.size}clear(){this.#t.clear()}}export{a as SubscriptionManager};
@@ -1 +0,0 @@
1
- const i=t=>{if(Array.isArray(t))return t.map(e=>i(e));if(t!==null&&typeof t=="object"){const e=t,r=Object.keys(e);r.sort();const a={};for(const s of r)a[s]=i(e[s]);return a}return t},n=t=>t.toString(16).padStart(4,"0"),w=t=>{let e=8997,r=33826,a=40164,s=52210;for(let o=0;o<t.length;o+=1){const c=t.codePointAt(o)??0;e^=c&65535,r^=c>>>16&65535;const f=e*435,u=r*435,y=a*435+e*256,g=s*435+r*256,p=u+(f>>>16),d=y+(p>>>16),b=g+(d>>>16);e=f&65535,r=p&65535,a=d&65535,s=b&65535}return n(s)+n(a)+n(r)+n(e)},S=(t,e,r)=>{const a=t.id??String(t.timestamp),s=`${t.table}::${a}::${String(e)}::${JSON.stringify(i(r))}`;return`row-${w(s)}`},l=(t,e)=>{for(const[r,a]of e.changes.entries())switch(a.type){case"delete":{t.delete(a.id);break}case"insert":{const s=a.data.id,o=typeof s=="string"||typeof s=="number"?String(s):S(e,r,a.data);t.set(o,{...a.data,id:o});break}case"update":{const s=t.get(a.id);s&&t.set(a.id,{...s,...a.data});break}}},h=(t,e)=>{const r=new Map(t);return l(r,e),r},k=(t,e)=>{const r=new Map(t);for(const a of e)l(r,a);return r},M=(t,e)=>{const r=new Map(t),a=r.get(e.table)??new Map;return r.set(e.table,h(a,e)),r};export{h as applyDiff,M as applyDiffToSnapshot,k as applyDiffs,i as canonicalizeForHash,S as deriveInsertId,w as fnv1a64Hex};
@@ -1 +0,0 @@
1
- const o=e=>`\`${e.replaceAll("`","``")}\``,$=e=>e.map(a=>`${o(a)} = ?`).join(", "),d=e=>`(${e.map(a=>o(a)).join(", ")})`,b=e=>`(${Array.from({length:e}).fill("?").join(", ")})`,E=(e,a,t)=>{if(a.changes.length===0)return;const i=o(a.table),p=o(t);for(const c of a.changes)switch(c.type){case"delete":{e.exec(`DELETE FROM ${i} WHERE ${p} = ?`,[c.id]);break}case"insert":{const{data:s}=c,n=Object.keys(s);if(n.length===0)continue;const l=`INSERT OR REPLACE INTO ${i} ${d(n)} VALUES ${b(n.length)}`,r=n.map(f=>s[f]);e.exec(l,r);break}case"update":{const{data:s}=c,n=Object.keys(s);if(n.length===0)continue;const l=`UPDATE ${i} SET ${$(n)} WHERE ${p} = ?`,r=[...n.map(f=>s[f]),c.id];e.exec(l,r);break}}},g=(e,a,t)=>{e.transaction(()=>{E(e,a,t??"id")})},h=(e,a)=>{a.length!==0&&e.transaction(()=>{for(const t of a)E(e,t,"id")})};export{g as applyDiffToDb,h as applyDiffsToDb,o as escapeIdentifier};
@@ -1 +0,0 @@
1
- const i=(t,s,a,n)=>({table:t,changes:s,timestamp:a??Date.now(),id:n??crypto.randomUUID()}),r=t=>t.changes.length===0,p=t=>t.changes.length,g=t=>{const s=[],a=[],n=[];for(const e of t.changes)e.type==="insert"?s.push(e):e.type==="update"?a.push(e):n.push(e);return{inserts:s,updates:a,deletes:n}},f=t=>{if(t.length===0)return null;const s=t[0],a=t[t.length-1],n=`merge:${t.map(e=>e.id??String(e.timestamp)).join("|")}`;return i(s.table,t.flatMap(e=>e.changes),a.timestamp,n)};export{g as classifyChanges,i as createTableDiff,p as diffSize,r as isDiffEmpty,f as mergeDiffs};