@lunora/replica 1.0.0-alpha.3 → 1.0.0-alpha.5

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.
@@ -1,6 +1,6 @@
1
1
  import { createSqlJsAdapter } from '../adapters/sqljs.mjs';
2
2
  import { applyDiffToDb as applyDiffToDatabase, escapeIdentifier } from './applyDiffToDb-DQ1xZp5J.mjs';
3
- import { EventLog } from './EventLog-zMy7AYP4.mjs';
3
+ import { EventLog } from './EventLog-CnK-3Wge.mjs';
4
4
 
5
5
  const MIRROR_META_TABLE = "__lunora_mirror_meta";
6
6
  const ensureMetaTable = (database) => {
@@ -14,8 +14,16 @@ const ensureMetaTable = (database) => {
14
14
  class LocalMirror {
15
15
  #db;
16
16
  #tables;
17
- #eventLog = new EventLog();
17
+ #eventLog;
18
18
  #changeListeners = /* @__PURE__ */ new Set();
19
+ /**
20
+ * Monotonically increasing counter bumped on every state-changing
21
+ * operation (`applyDiff`, `clearData`) — independent of `eventLog.size`.
22
+ * `clearData` doesn't grow the log (REPLICA-09), so a consumer that used
23
+ * `eventLog.size` as its `useSyncExternalStore` snapshot would never
24
+ * re-render after a clear; `version` changes on both operations.
25
+ */
26
+ #version = 0;
19
27
  /**
20
28
  * Convenience factory that creates a {@link LocalMirror} backed by a
21
29
  * {@link createSqlJsAdapter sql.js adapter} without needing to import
@@ -40,6 +48,7 @@ class LocalMirror {
40
48
  constructor(options) {
41
49
  this.#db = options.db;
42
50
  this.#tables = { ...options.tables };
51
+ this.#eventLog = new EventLog({ maxEntries: options.maxEventLogEntries });
43
52
  ensureMetaTable(this.#db);
44
53
  }
45
54
  /**
@@ -68,6 +77,15 @@ class LocalMirror {
68
77
  get db() {
69
78
  return this.#db;
70
79
  }
80
+ /**
81
+ * Monotonically increasing version counter, bumped on every operation
82
+ * that changes mirrored data (`applyDiff`, `clearData`). Use this — not
83
+ * `eventLog.size` — as a `useSyncExternalStore` snapshot so operations
84
+ * that don't append to the log still trigger a re-render.
85
+ */
86
+ get version() {
87
+ return this.#version;
88
+ }
71
89
  /**
72
90
  * Apply a server-side diff to the local SQLite mirror.
73
91
  *
@@ -82,6 +100,7 @@ class LocalMirror {
82
100
  this.#ensureTableSchema(diff);
83
101
  applyDiffToDatabase(this.#db, diff, pkColumn);
84
102
  this.#eventLog.append("table-diff", diff, [diff]);
103
+ this.#version += 1;
85
104
  for (const listener of this.#changeListeners) {
86
105
  try {
87
106
  listener();
@@ -106,16 +125,28 @@ class LocalMirror {
106
125
  /**
107
126
  * Delete every row from all known tables (preserves the event log
108
127
  * and schema). Useful when re-syncing from scratch.
128
+ *
129
+ * Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
130
+ * (REPLICA-09) even though nothing is appended to the event log — a
131
+ * consumer keyed only on `eventLog.size` would otherwise never learn the
132
+ * mirror was cleared and keep rendering deleted rows.
109
133
  */
110
134
  clearData() {
111
135
  const tables = this.#db.query(
112
- `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '__lunora_%' AND name NOT LIKE 'sqlite_%'`
136
+ String.raw`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE '\_\_lunora\_%' ESCAPE '\' AND name NOT LIKE 'sqlite\_%' ESCAPE '\'`
113
137
  );
114
138
  this.#db.transaction(() => {
115
139
  for (const { name } of tables) {
116
140
  this.#db.exec(`DELETE FROM ${escapeIdentifier(name)}`);
117
141
  }
118
142
  });
143
+ this.#version += 1;
144
+ for (const listener of this.#changeListeners) {
145
+ try {
146
+ listener();
147
+ } catch {
148
+ }
149
+ }
119
150
  }
120
151
  /**
121
152
  * Dispose the mirror and close the database connection.
@@ -22,50 +22,60 @@ class MaterializerRuntime {
22
22
  #doClient;
23
23
  #unknownEventHandling;
24
24
  /**
25
- * The highest event seq that has been applied to all materializers.
26
- * Starts at `0` and advances monotonically.
25
+ * Per-materializer watermark: the seq of the next event each materializer
26
+ * (by index, parallel to `#materializers`) has NOT yet applied. Starts at
27
+ * `0` for every materializer and advances independently — a materializer
28
+ * with no snapshot stays at `0` even when a sibling has recovered to a
29
+ * much higher watermark, so catch-up never skips events for it (REPLICA-04).
27
30
  */
28
- #appliedSeq = 0;
31
+ #watermarks;
29
32
  constructor(materializers, options = {}) {
30
33
  this.#materializers = [...materializers];
34
+ this.#watermarks = this.#materializers.map(() => 0);
31
35
  this.#snapshotStore = options.snapshotStore;
32
36
  this.#doClient = options.doClient;
33
37
  this.#unknownEventHandling = options.unknownEventHandling ?? "warn";
34
38
  }
35
39
  // ── Public API ──────────────────────────────────────────────────────
36
40
  /**
37
- * The sequence number of the last event applied to all materializers.
41
+ * The lowest per-materializer watermark — the seq of the next event that
42
+ * at least one materializer has not yet applied. `0` when there are no
43
+ * materializers.
38
44
  */
39
45
  get appliedSeq() {
40
- return this.#appliedSeq;
46
+ return this.#watermarks.length > 0 ? Math.min(...this.#watermarks) : 0;
41
47
  }
42
48
  /**
43
- * Replay a batch of entries through all materializers.
44
- *
45
- * Entries with `seq < this.appliedSeq` are silently skipped (idempotent).
46
- * @returns The number of entries actually applied.
49
+ * Replay a batch of entries, applying each entry only to the
50
+ * materializers whose own watermark is behind it — a materializer at or
51
+ * past an entry's seq (e.g. recovered from a snapshot, or already caught
52
+ * up) skips it, so no materializer ever double-applies an event.
53
+ * @returns The number of entries applied to at least one materializer.
47
54
  */
48
55
  applyEntries(entries) {
49
56
  let count = 0;
50
57
  for (const entry of entries) {
51
- if (entry.seq < this.#appliedSeq) {
52
- continue;
53
- }
54
- const statesBefore = this.#materializers.map((m) => m.state);
55
- let stateChanged = false;
56
- for (const m of this.#materializers) {
57
- m.apply(entry);
58
- }
59
- for (let i = 0; i < this.#materializers.length; i += 1) {
60
- if (this.#materializers[i]?.state !== statesBefore[i]) {
61
- stateChanged = true;
62
- break;
58
+ let appliedToAny = false;
59
+ let anyChanged = false;
60
+ for (const [i, materializer] of this.#materializers.entries()) {
61
+ const watermark = this.#watermarks[i] ?? 0;
62
+ if (entry.seq < watermark) {
63
+ continue;
63
64
  }
65
+ const stateBefore = materializer.state;
66
+ materializer.apply(entry);
67
+ appliedToAny = true;
68
+ if (materializer.state !== stateBefore) {
69
+ anyChanged = true;
70
+ }
71
+ this.#watermarks[i] = entry.seq + 1;
72
+ }
73
+ if (!appliedToAny) {
74
+ continue;
64
75
  }
65
- if (!stateChanged) {
76
+ if (!anyChanged) {
66
77
  this.#handleUnknownEvent(entry);
67
78
  }
68
- this.#appliedSeq = entry.seq + 1;
69
79
  count += 1;
70
80
  }
71
81
  return count;
@@ -99,44 +109,50 @@ class MaterializerRuntime {
99
109
  /**
100
110
  * Attempt to recover materialized state from a snapshot store.
101
111
  *
102
- * When a snapshot is found for a materializer, its state is restored
103
- * and the snapshot's watermark (`appliedSeq`) is returned so the caller
104
- * can skip replaying entries up to that point.
105
- * @returns The highest `appliedSeq` across all recovered snapshots, or `0`.
112
+ * When a snapshot is found for a materializer, its state AND its own
113
+ * watermark are restored from that snapshot. A materializer with no
114
+ * snapshot keeps its current watermark (`0` for a fresh runtime) — it
115
+ * does NOT inherit another materializer's watermark, so it still catches
116
+ * up from the very beginning (REPLICA-04: previously a shared watermark
117
+ * was bumped to the MAX across snapshots, permanently skipping events 0..N
118
+ * for any un-snapshotted or lagging materializer).
119
+ * @returns The highest snapshot `appliedSeq` across all materializers, or
120
+ * `0` — kept for backward compatibility; callers that need the fetch
121
+ * watermark for catch-up should use the per-materializer minimum instead
122
+ * (see `initialize`).
106
123
  */
107
124
  async recoverFromSnapshots() {
108
125
  if (!this.#snapshotStore) {
109
126
  return 0;
110
127
  }
111
128
  let maxSeq = 0;
112
- for (const m of this.#materializers) {
113
- const raw = await this.#snapshotStore.load(m.def.name);
129
+ for (const [i, materializer] of this.#materializers.entries()) {
130
+ const raw = await this.#snapshotStore.load(materializer.def.name);
114
131
  if (raw !== null && typeof raw === "object") {
115
132
  const snapshot = raw;
116
133
  if (snapshot.state !== void 0) {
117
- m.setState(snapshot.state);
134
+ materializer.setState(snapshot.state);
118
135
  }
136
+ this.#watermarks[i] = snapshot.appliedSeq;
119
137
  if (snapshot.appliedSeq > maxSeq) {
120
138
  maxSeq = snapshot.appliedSeq;
121
139
  }
122
140
  }
123
141
  }
124
- if (maxSeq > this.#appliedSeq) {
125
- this.#appliedSeq = maxSeq;
126
- }
127
142
  return maxSeq;
128
143
  }
129
144
  /**
130
- * Persist the current state of all materializers as snapshots.
145
+ * Persist the current state of all materializers as snapshots, each
146
+ * tagged with ITS OWN watermark (not a shared one).
131
147
  */
132
148
  async persistSnapshots() {
133
149
  if (!this.#snapshotStore) {
134
150
  return;
135
151
  }
136
- for (const m of this.#materializers) {
137
- await this.#snapshotStore.save(m.def.name, {
138
- appliedSeq: this.#appliedSeq,
139
- state: m.state
152
+ for (const [i, materializer] of this.#materializers.entries()) {
153
+ await this.#snapshotStore.save(materializer.def.name, {
154
+ appliedSeq: this.#watermarks[i] ?? 0,
155
+ state: materializer.state
140
156
  });
141
157
  }
142
158
  }
@@ -146,8 +162,11 @@ class MaterializerRuntime {
146
162
  *
147
163
  * 1. Recover materialized state from snapshots (if a snapshotStore is
148
164
  * configured).
149
- * 2. Fetch all entries since the recovered watermark from the DO.
150
- * 3. Apply them through the materializers.
165
+ * 2. Fetch all entries since the MINIMUM per-materializer watermark from
166
+ * the DO — not the maximum — so a materializer with no snapshot (or a
167
+ * lower one) still receives every event it hasn't seen (REPLICA-04).
168
+ * 3. Apply them through the materializers; `applyEntries` skips each
169
+ * entry for any materializer already past it, so nothing is double-applied.
151
170
  *
152
171
  * Call this once on startup / after the DO binding is available.
153
172
  * @returns The number of entries applied during catch-up.
@@ -156,8 +175,9 @@ class MaterializerRuntime {
156
175
  if (!this.#doClient) {
157
176
  return 0;
158
177
  }
159
- const snapshotSeq = await this.recoverFromSnapshots();
160
- const entries = await this.#doClient.getSince(snapshotSeq);
178
+ await this.recoverFromSnapshots();
179
+ const minWatermark = this.#watermarks.length > 0 ? Math.min(...this.#watermarks) : 0;
180
+ const entries = await this.#doClient.getSince(minWatermark);
161
181
  if (entries.length === 0) {
162
182
  return 0;
163
183
  }
@@ -188,9 +208,9 @@ class MaterializerRuntime {
188
208
  * Reset all materializers to their initial state and clear snapshots.
189
209
  */
190
210
  reset() {
191
- this.#appliedSeq = 0;
192
- for (const m of this.#materializers) {
193
- m.reset();
211
+ for (const [i, materializer] of this.#materializers.entries()) {
212
+ this.#watermarks[i] = 0;
213
+ materializer.reset();
194
214
  }
195
215
  }
196
216
  /**
@@ -0,0 +1,67 @@
1
+ const canonicalizeForHash = (value) => {
2
+ if (Array.isArray(value)) {
3
+ return value.map((item) => canonicalizeForHash(item));
4
+ }
5
+ if (value !== null && typeof value === "object") {
6
+ const record = value;
7
+ const sortedKeys = Object.keys(record).toSorted((a, b) => a.localeCompare(b));
8
+ const result = {};
9
+ for (const key of sortedKeys) {
10
+ result[key] = canonicalizeForHash(record[key]);
11
+ }
12
+ return result;
13
+ }
14
+ return value;
15
+ };
16
+ const deriveInsertId = (diff, changeIndex, data) => {
17
+ const diffIdentity = diff.id ?? String(diff.timestamp);
18
+ const input = `${diff.table}::${diffIdentity}::${String(changeIndex)}::${JSON.stringify(canonicalizeForHash(data))}`;
19
+ let hash = 0xcbf29ce484222325n;
20
+ const prime = 0x00000100000001b3n;
21
+ const mask64 = 0xffffffffffffffffn;
22
+ for (let index = 0; index < input.length; index += 1) {
23
+ hash ^= BigInt(input.codePointAt(index) ?? 0);
24
+ hash = hash * prime & mask64;
25
+ }
26
+ return `row-${hash.toString(16).padStart(16, "0")}`;
27
+ };
28
+ const applyDiff = (current, diff) => {
29
+ const next = new Map(current);
30
+ for (const [changeIndex, change] of diff.changes.entries()) {
31
+ switch (change.type) {
32
+ case "delete": {
33
+ next.delete(change.id);
34
+ break;
35
+ }
36
+ case "insert": {
37
+ const rawId = change.data.id;
38
+ const id = typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : deriveInsertId(diff, changeIndex, change.data);
39
+ next.set(id, { ...change.data, id });
40
+ break;
41
+ }
42
+ case "update": {
43
+ const existing = next.get(change.id);
44
+ if (existing) {
45
+ next.set(change.id, { ...existing, ...change.data });
46
+ }
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ return next;
52
+ };
53
+ const applyDiffs = (current, diffs) => {
54
+ let result = new Map(current);
55
+ for (const diff of diffs) {
56
+ result = applyDiff(result, diff);
57
+ }
58
+ return result;
59
+ };
60
+ const applyDiffToSnapshot = (snapshot, diff) => {
61
+ const next = new Map(snapshot);
62
+ const tableMap = next.get(diff.table) ?? /* @__PURE__ */ new Map();
63
+ next.set(diff.table, applyDiff(tableMap, diff));
64
+ return next;
65
+ };
66
+
67
+ export { applyDiff, applyDiffToSnapshot, applyDiffs };
@@ -1,8 +1,9 @@
1
- const createTableDiff = (table, changes, timestamp) => {
1
+ const createTableDiff = (table, changes, timestamp, id) => {
2
2
  return {
3
3
  table,
4
4
  changes,
5
- timestamp: timestamp ?? Date.now()
5
+ timestamp: timestamp ?? Date.now(),
6
+ id: id ?? crypto.randomUUID()
6
7
  };
7
8
  };
8
9
  const isDiffEmpty = (diff) => diff.changes.length === 0;
@@ -28,10 +29,12 @@ const mergeDiffs = (diffs) => {
28
29
  }
29
30
  const first = diffs[0];
30
31
  const last = diffs[diffs.length - 1];
32
+ const mergedId = `merge:${diffs.map((d) => d.id ?? String(d.timestamp)).join("|")}`;
31
33
  return createTableDiff(
32
34
  first.table,
33
35
  diffs.flatMap((d) => d.changes),
34
- last.timestamp
36
+ last.timestamp,
37
+ mergedId
35
38
  );
36
39
  };
37
40
 
@@ -28,16 +28,29 @@ type RowChange = {
28
28
  interface TableDiff {
29
29
  /** Ordered row changes — insert/update/delete, earliest first. */
30
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). 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.
41
+ */
42
+ readonly id?: string;
31
43
  /** Logical table name (matches the schema table name). */
32
44
  readonly table: string;
33
45
  /** Monotonic server timestamp (ms since epoch) when this diff was emitted. */
34
46
  readonly timestamp: number;
35
47
  }
36
48
  /**
37
- * Create a {@link TableDiff} with a snapshot of the current time.
49
+ * Create a {@link TableDiff} with a snapshot of the current time and a
50
+ * fresh stable `id` (unless one is explicitly provided).
38
51
  * @experimental
39
52
  */
40
- declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number) => TableDiff;
53
+ declare const createTableDiff: (table: string, changes: ReadonlyArray<RowChange>, timestamp?: number, id?: string) => TableDiff;
41
54
  /**
42
55
  * Return `true` when the diff contains no row changes.
43
56
  * @experimental
@@ -212,6 +225,37 @@ interface AppendOptions {
212
225
  readonly parentSeqNum?: Seq;
213
226
  /** Session identifier within the client. */
214
227
  readonly sessionId?: string;
228
+ /**
229
+ * Override the entry's `timestamp` instead of stamping `Date.now()` at
230
+ * append time.
231
+ *
232
+ * Used by callers (e.g. {@link import("./event-source").EventSource | EventSource})
233
+ * that must commit the EXACT entry a reducer already observed — a
234
+ * second, independently-drawn `Date.now()` at append time could produce
235
+ * a persisted entry a timestamp-dependent reducer cannot reproduce on
236
+ * replay.
237
+ */
238
+ readonly timestamp?: number;
239
+ }
240
+ /**
241
+ * Options for constructing an {@link EventLog}.
242
+ * @experimental
243
+ */
244
+ interface EventLogOptions {
245
+ /**
246
+ * Cap the number of entries retained in memory (REPLICA-06). When an
247
+ * append would exceed the cap, the OLDEST entries are evicted (ring
248
+ * buffer) — a `getSince`/`getFrom` call for a watermark below the oldest
249
+ * retained `seq` then returns only what's left, silently missing
250
+ * anything evicted.
251
+ *
252
+ * `undefined` (the default) preserves the original unbounded behavior.
253
+ * Set this only when you have another durable source of truth for
254
+ * anything older than the cap (a snapshot, a server-side `EventLogDO`) —
255
+ * see {@link EventLog#truncateBelow} for the caller-driven equivalent
256
+ * tied to snapshot persistence.
257
+ */
258
+ readonly maxEntries?: number;
215
259
  }
216
260
  /**
217
261
  * An append-only, in-memory event log for local event sourcing.
@@ -226,6 +270,7 @@ interface AppendOptions {
226
270
  */
227
271
  declare class EventLog {
228
272
  #private;
273
+ constructor(options?: EventLogOptions);
229
274
  /**
230
275
  * Append a new entry to the log.
231
276
  *
@@ -254,6 +299,10 @@ declare class EventLog {
254
299
  * This is the restore counterpart of {@link EventLog#snapshot}.
255
300
  * Restores `headSeq` from the snapshot so auto-parenting continues
256
301
  * after restore.
302
+ *
303
+ * Runs `#enforceCap()` after restoring so a snapshot captured under a
304
+ * different (or no) `maxEntries` can never leave this log over its
305
+ * configured capacity.
257
306
  */
258
307
  load(snapshot: EventLogSnapshot): void;
259
308
  /**
@@ -289,6 +338,24 @@ declare class EventLog {
289
338
  /** Remove all entries (primarily for testing). */
290
339
  clear(): void;
291
340
  /**
341
+ * Discard all entries with `seq < floorSeq` (REPLICA-06).
342
+ *
343
+ * `headSeq`/`nextSeq` are untouched (they're independent counters), so
344
+ * appends after a truncation continue the same sequence uninterrupted.
345
+ *
346
+ * **Caller-driven, not automatic**: only call this after the truncated
347
+ * range has already been durably captured elsewhere (a snapshot, a
348
+ * server-side `EventLogDO`) — truncating without such a floor makes any
349
+ * future `getSince`/`getFrom`/`EventSource.replayFromLog` call for a
350
+ * watermark below `floorSeq` silently miss the discarded entries. This is
351
+ * the hook the caller ties to snapshot persistence; the log itself has no
352
+ * concept of "already durably persisted".
353
+ *
354
+ * `floorSeq` must be a non-negative safe integer — `NaN` would make
355
+ * every comparison false and silently clear the entire log.
356
+ */
357
+ truncateBelow(floorSeq: number): void;
358
+ /**
292
359
  * Return an async generator that yields every entry starting from
293
360
  * `fromSeq` (default `0` = all entries).
294
361
  *
@@ -314,6 +381,18 @@ interface LocalMirrorOptions {
314
381
  /** Platform-specific SQLite adapter. */
315
382
  readonly db: SqliteAdapter;
316
383
  /**
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.
387
+ *
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.
393
+ */
394
+ readonly maxEventLogEntries?: number;
395
+ /**
317
396
  * Table schemas the mirror should manage.
318
397
  *
319
398
  * On first use the mirror creates any missing tables automatically
@@ -399,6 +478,13 @@ declare class LocalMirror {
399
478
  */
400
479
  get db(): SqliteAdapter;
401
480
  /**
481
+ * Monotonically increasing version counter, bumped on every operation
482
+ * that changes mirrored data (`applyDiff`, `clearData`). Use this — not
483
+ * `eventLog.size` — as a `useSyncExternalStore` snapshot so operations
484
+ * that don't append to the log still trigger a re-render.
485
+ */
486
+ get version(): number;
487
+ /**
402
488
  * Apply a server-side diff to the local SQLite mirror.
403
489
  *
404
490
  * The diff is applied in a transaction and recorded in the event log
@@ -420,6 +506,11 @@ declare class LocalMirror {
420
506
  /**
421
507
  * Delete every row from all known tables (preserves the event log
422
508
  * and schema). Useful when re-syncing from scratch.
509
+ *
510
+ * Notifies `onChange` subscribers and bumps {@link LocalMirror.version}
511
+ * (REPLICA-09) even though nothing is appended to the event log — a
512
+ * consumer keyed only on `eventLog.size` would otherwise never learn the
513
+ * mirror was cleared and keep rendering deleted rows.
423
514
  */
424
515
  clearData(): void;
425
516
  /**
@@ -436,4 +527,4 @@ declare class LocalMirror {
436
527
  */
437
528
  get mirroredTables(): ReadonlyArray<string>;
438
529
  }
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 };
530
+ 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 };