@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.
@@ -6,21 +6,31 @@ import { S as SqliteAdapter } from "../packem_shared/types.d-BuAWjEY5.mjs";
6
6
  * The adapter takes a structural projection of the OO API (`sqlite3.oo1.DB`) so
7
7
  * the package never imports from `@sqlite.org/sqlite-wasm` directly — consumers
8
8
  * install it themselves and pass in their database instance.
9
+ *
10
+ * IMPORTANT (REPLICA-01): the real `oo1.DB.exec()` does NOT return sql.js's
11
+ * `{ columns, values }[]` result shape — with `rowMode: "object"` and
12
+ * `returnValue: "resultRows"` it returns the rows directly, as
13
+ * `Record<string, unknown>[]`. This adapter is written against that real
14
+ * shape; `lastInsertRowId` uses the driver's `selectValue()` convenience
15
+ * method (a single-scalar query helper) rather than parsing a result-row
16
+ * array.
9
17
  * @param database An already-initialised `sqlite3.oo1.DB` instance.
10
18
  * @param database.close Tear down the database connection.
11
- * @param database.exec Execute SQL with optional bind params and return rows
12
- * as `{ columns, values }` result objects.
19
+ * @param database.exec Execute SQL with optional bind params. With
20
+ * `{ returnValue: "resultRows", rowMode: "object" }` it returns the matched
21
+ * rows directly (`Record<string, unknown>[]`); otherwise (DDL/DML/BEGIN/
22
+ * COMMIT/ROLLBACK) its return value is unused here.
23
+ * @param database.selectValue Run a query and return the first column of the
24
+ * first row as a single scalar — used for `SELECT last_insert_rowid()`.
13
25
  * @experimental
14
26
  */
15
27
  declare const createSqliteWasmAdapter: (database: {
16
28
  close: () => void;
17
29
  exec: (sql: string, options?: {
18
30
  bind?: unknown[];
19
- returnValue?: "resultRows" | "simple";
20
- rowMode?: "array" | "object";
21
- }) => {
22
- columns: string[];
23
- values: unknown[][];
24
- }[];
31
+ returnValue?: "resultRows";
32
+ rowMode?: "object";
33
+ }) => Record<string, unknown>[] | undefined;
34
+ selectValue: (sql: string, bind?: unknown[]) => unknown;
25
35
  }) => SqliteAdapter;
26
36
  export { createSqliteWasmAdapter };
@@ -6,21 +6,31 @@ import { S as SqliteAdapter } from "../packem_shared/types.d-BuAWjEY5.js";
6
6
  * The adapter takes a structural projection of the OO API (`sqlite3.oo1.DB`) so
7
7
  * the package never imports from `@sqlite.org/sqlite-wasm` directly — consumers
8
8
  * install it themselves and pass in their database instance.
9
+ *
10
+ * IMPORTANT (REPLICA-01): the real `oo1.DB.exec()` does NOT return sql.js's
11
+ * `{ columns, values }[]` result shape — with `rowMode: "object"` and
12
+ * `returnValue: "resultRows"` it returns the rows directly, as
13
+ * `Record&lt;string, unknown>[]`. This adapter is written against that real
14
+ * shape; `lastInsertRowId` uses the driver's `selectValue()` convenience
15
+ * method (a single-scalar query helper) rather than parsing a result-row
16
+ * array.
9
17
  * @param database An already-initialised `sqlite3.oo1.DB` instance.
10
18
  * @param database.close Tear down the database connection.
11
- * @param database.exec Execute SQL with optional bind params and return rows
12
- * as `{ columns, values }` result objects.
19
+ * @param database.exec Execute SQL with optional bind params. With
20
+ * `{ returnValue: "resultRows", rowMode: "object" }` it returns the matched
21
+ * rows directly (`Record&lt;string, unknown>[]`); otherwise (DDL/DML/BEGIN/
22
+ * COMMIT/ROLLBACK) its return value is unused here.
23
+ * @param database.selectValue Run a query and return the first column of the
24
+ * first row as a single scalar — used for `SELECT last_insert_rowid()`.
13
25
  * @experimental
14
26
  */
15
27
  declare const createSqliteWasmAdapter: (database: {
16
28
  close: () => void;
17
29
  exec: (sql: string, options?: {
18
30
  bind?: unknown[];
19
- returnValue?: "resultRows" | "simple";
20
- rowMode?: "array" | "object";
21
- }) => {
22
- columns: string[];
23
- values: unknown[][];
24
- }[];
31
+ returnValue?: "resultRows";
32
+ rowMode?: "object";
33
+ }) => Record<string, unknown>[] | undefined;
34
+ selectValue: (sql: string, bind?: unknown[]) => unknown;
25
35
  }) => SqliteAdapter;
26
36
  export { createSqliteWasmAdapter };
@@ -8,22 +8,12 @@ const createSqliteWasmAdapter = (database) => {
8
8
  }
9
9
  },
10
10
  query(sql, params) {
11
- const options = params && params.length > 0 ? { bind: [...params], returnValue: "resultRows" } : { returnValue: "resultRows" };
12
- const result = database.exec(sql, options);
13
- const first = result[0];
14
- if (!first) {
15
- return [];
16
- }
17
- const colNames = first.columns;
18
- const rows = [];
19
- for (const row of first.values) {
20
- const object = {};
21
- for (const [i, column] of colNames.entries()) {
22
- object[column] = row[i];
23
- }
24
- rows.push(object);
25
- }
26
- return rows;
11
+ const rows = database.exec(sql, {
12
+ bind: params && params.length > 0 ? [...params] : void 0,
13
+ returnValue: "resultRows",
14
+ rowMode: "object"
15
+ });
16
+ return rows ?? [];
27
17
  },
28
18
  transaction(function_) {
29
19
  database.exec("BEGIN");
@@ -36,16 +26,14 @@ const createSqliteWasmAdapter = (database) => {
36
26
  }
37
27
  },
38
28
  lastInsertRowId() {
39
- const result = database.exec("SELECT last_insert_rowid() AS id", { returnValue: "resultRows" });
40
- const firstRow = result[0];
41
- if (result.length === 0 || !firstRow || firstRow.values.length === 0) {
42
- return -1;
29
+ const value = database.selectValue("SELECT last_insert_rowid()");
30
+ if (typeof value === "number") {
31
+ return value;
43
32
  }
44
- const value = firstRow.values[0];
45
- if (!value || value.length === 0) {
46
- return -1;
33
+ if (typeof value === "bigint") {
34
+ return Number(value);
47
35
  }
48
- return Number(value[0]);
36
+ return -1;
49
37
  },
50
38
  close() {
51
39
  database.close();
package/dist/index.d.mts CHANGED
@@ -2,8 +2,8 @@ export { createBetterSqlite3Adapter } from "./adapters/better-sqlite3.mjs";
2
2
  export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.mjs";
3
3
  export { createSqlJsAdapter } from "./adapters/sqljs.mjs";
4
4
  import { S as SqliteAdapter } from "./packem_shared/types.d-BuAWjEY5.mjs";
5
- import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-BUeOe5KC.mjs";
6
- export { type C as ClientSeq, type b as EventLogSnapshot, type G as GlobalSeq, type c as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, d as classifyChanges, e as createTableDiff, f as diffSize, i as isClientSeq, g as isDiffEmpty, h as isGlobalSeq, j as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-BUeOe5KC.mjs";
5
+ import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-DL1XJBB3.mjs";
6
+ export { type C as ClientSeq, type b as EventLogOptions, type c as EventLogSnapshot, type G as GlobalSeq, type d as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, e as classifyChanges, f as createTableDiff, g as diffSize, i as isClientSeq, h as isDiffEmpty, j as isGlobalSeq, k as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-DL1XJBB3.mjs";
7
7
  /**
8
8
  * Apply a single {@link TableDiff} to an in-memory row map and return
9
9
  * the updated map.
@@ -166,9 +166,17 @@ declare class EventLogDOClient {
166
166
  constructor(options: EventLogDOClientOptions);
167
167
  /**
168
168
  * Append one or more events to the log.
169
+ * @param events The events to append.
170
+ * @param options Idempotency controls for the batch.
171
+ * @param options.batchId Optional idempotency key for the whole batch — a
172
+ * retried `append` call with the same `batchId` (e.g. after a network
173
+ * timeout that hid a successful response) returns the originally-persisted
174
+ * entries instead of inserting duplicates.
169
175
  * @returns The persisted entries with their assigned `seq` numbers.
170
176
  */
171
- append(events: AppendEventInput[]): Promise<EventLogEntry[]>;
177
+ append(events: AppendEventInput[], options?: {
178
+ batchId?: string;
179
+ }): Promise<EventLogEntry[]>;
172
180
  /**
173
181
  * Fetch all entries with `seq >= sinceSeq`.
174
182
  *
@@ -284,18 +292,50 @@ type EventSourceEvents = {
284
292
  };
285
293
  };
286
294
  /**
295
+ * Sentinel a reducer can return to EXPLICITLY signal it does not handle a
296
+ * given event's `type` — as opposed to returning the current `state`
297
+ * reference unchanged to represent a legitimate, idempotent no-op for a type
298
+ * it DOES recognise.
299
+ *
300
+ * Reference equality alone can't tell these two cases apart (REPLICA-07): a
301
+ * reducer that intentionally returns `state` for a type it fully understands
302
+ * (e.g. "already applied this event, nothing to do") would otherwise be
303
+ * misclassified as "unhandled" and trigger {@link UnknownEventHandling} — a
304
+ * spurious warning, or worse, a thrown error under `"fail"`. Return `UNHANDLED`
305
+ * only for a `type` your reducer truly does not recognise; every other return
306
+ * (including a `state` returned by reference) is treated as handled.
307
+ *
308
+ * Reducers that always recognise every event they're given (a single
309
+ * always-matching type, or a catch-all) can ignore this entirely.
310
+ * @experimental
311
+ */
312
+ declare const UNHANDLED: unique symbol;
313
+ /**
287
314
  * A function that reduces an event into a state mutation.
288
315
  *
289
316
  * Pure functions are strongly encouraged: given the same event payload
290
- * and state, they must produce the same next state.
317
+ * and state, they must produce the same next state. Return {@link UNHANDLED}
318
+ * to explicitly mark an event `type` this reducer does not process — see
319
+ * {@link UNHANDLED} for why reference equality against the input `state`
320
+ * cannot be used for this instead.
291
321
  * @experimental
292
322
  */
293
- type EventReducer<S> = (state: S, entry: EventLogEntry) => S;
323
+ type EventReducer<S> = (state: S, entry: EventLogEntry) => S | typeof UNHANDLED;
294
324
  /**
295
325
  * Options for constructing an {@link EventSource}.
296
326
  * @experimental
297
327
  */
298
328
  interface EventSourceOptions {
329
+ /**
330
+ * Cap this runtime's internal `log` to this many entries (REPLICA-06).
331
+ * `replayFromLog` copies every entry it replays from the source log into
332
+ * `this.log` too — a second, uncapped copy of the same history — so a
333
+ * long-lived `EventSource` fed by repeated replay accumulates entries in
334
+ * both places forever without a cap.
335
+ *
336
+ * `undefined` (the default) preserves unbounded retention.
337
+ */
338
+ maxLogEntries?: number;
299
339
  /**
300
340
  * How to handle events whose `type` is not recognised by the reducer.
301
341
  * @default "warn"
@@ -506,27 +546,38 @@ declare class MaterializerRuntime {
506
546
  #private;
507
547
  constructor(materializers: AnyMaterializer[], options?: MaterializerRuntimeOptions);
508
548
  /**
509
- * The sequence number of the last event applied to all materializers.
549
+ * The lowest per-materializer watermark the seq of the next event that
550
+ * at least one materializer has not yet applied. `0` when there are no
551
+ * materializers.
510
552
  */
511
553
  get appliedSeq(): number;
512
554
  /**
513
- * Replay a batch of entries through all materializers.
514
- *
515
- * Entries with `seq < this.appliedSeq` are silently skipped (idempotent).
516
- * @returns The number of entries actually applied.
555
+ * Replay a batch of entries, applying each entry only to the
556
+ * materializers whose own watermark is behind it — a materializer at or
557
+ * past an entry's seq (e.g. recovered from a snapshot, or already caught
558
+ * up) skips it, so no materializer ever double-applies an event.
559
+ * @returns The number of entries applied to at least one materializer.
517
560
  */
518
561
  applyEntries(entries: ReadonlyArray<EventLogEntry>): number;
519
562
  /**
520
563
  * Attempt to recover materialized state from a snapshot store.
521
564
  *
522
- * When a snapshot is found for a materializer, its state is restored
523
- * and the snapshot's watermark (`appliedSeq`) is returned so the caller
524
- * can skip replaying entries up to that point.
525
- * @returns The highest `appliedSeq` across all recovered snapshots, or `0`.
565
+ * When a snapshot is found for a materializer, its state AND its own
566
+ * watermark are restored from that snapshot. A materializer with no
567
+ * snapshot keeps its current watermark (`0` for a fresh runtime) — it
568
+ * does NOT inherit another materializer's watermark, so it still catches
569
+ * up from the very beginning (REPLICA-04: previously a shared watermark
570
+ * was bumped to the MAX across snapshots, permanently skipping events 0..N
571
+ * for any un-snapshotted or lagging materializer).
572
+ * @returns The highest snapshot `appliedSeq` across all materializers, or
573
+ * `0` — kept for backward compatibility; callers that need the fetch
574
+ * watermark for catch-up should use the per-materializer minimum instead
575
+ * (see `initialize`).
526
576
  */
527
577
  recoverFromSnapshots(): Promise<number>;
528
578
  /**
529
- * Persist the current state of all materializers as snapshots.
579
+ * Persist the current state of all materializers as snapshots, each
580
+ * tagged with ITS OWN watermark (not a shared one).
530
581
  */
531
582
  persistSnapshots(): Promise<void>;
532
583
  /**
@@ -534,8 +585,11 @@ declare class MaterializerRuntime {
534
585
  *
535
586
  * 1. Recover materialized state from snapshots (if a snapshotStore is
536
587
  * configured).
537
- * 2. Fetch all entries since the recovered watermark from the DO.
538
- * 3. Apply them through the materializers.
588
+ * 2. Fetch all entries since the MINIMUM per-materializer watermark from
589
+ * the DO not the maximum — so a materializer with no snapshot (or a
590
+ * lower one) still receives every event it hasn't seen (REPLICA-04).
591
+ * 3. Apply them through the materializers; `applyEntries` skips each
592
+ * entry for any materializer already past it, so nothing is double-applied.
539
593
  *
540
594
  * Call this once on startup / after the DO binding is available.
541
595
  * @returns The number of entries applied during catch-up.
@@ -584,6 +638,13 @@ interface EventLogDOState {
584
638
  sql: {
585
639
  exec: (query: string, ...params: unknown[]) => unknown;
586
640
  };
641
+ /**
642
+ * The DO platform's native atomic-transaction primitive (async;
643
+ * commits on resolve, rolls back on throw/reject). Test doubles that
644
+ * omit it fall back to a bare (non-transactional) call — see
645
+ * `#handleAppend`.
646
+ */
647
+ transaction?: <T>(closure: () => Promise<T> | T) => Promise<T>;
587
648
  };
588
649
  }
589
650
  /**
@@ -907,4 +968,4 @@ declare class EventsSync {
907
968
  */
908
969
  sync(): Promise<number>;
909
970
  }
910
- export { type AppendEventInput, type AppendOptions, type EventCallback, EventEmitter, type EventFactory, EventLog, EventLogDO, EventLogDOClient, type EventLogDOClientOptions, type EventLogEntry, type EventNamespace, type EventReducer, EventSource, type EventSourceEvents, type EventSourceOptions, type EventsContextOutput, type EventsDefinition, type EventsFacade, EventsSync, type EventsSyncOptions, InMemorySnapshotStore, type InputEvent, LocalMirror, type Materializer, type MaterializerDef, type MaterializerReducer, MaterializerRuntime, type MaterializerRuntimeOptions, type Seq, type SnapshotStore, type SqliteAdapter, type StateChangeCallback, type SubscriptionClient, SubscriptionManager, type TableDiff, type UnknownEventHandling, applyDiff, applyDiffToDatabase as applyDiffToDb, applyDiffToSnapshot, applyDiffs, applyDiffsToDatabase as applyDiffsToDb, defineEvents, defineMaterializer, eventsContext, subscribeToMirror };
971
+ export { type AppendEventInput, type AppendOptions, type EventCallback, EventEmitter, type EventFactory, EventLog, EventLogDO, EventLogDOClient, type EventLogDOClientOptions, type EventLogEntry, type EventNamespace, type EventReducer, EventSource, type EventSourceEvents, type EventSourceOptions, type EventsContextOutput, type EventsDefinition, type EventsFacade, EventsSync, type EventsSyncOptions, InMemorySnapshotStore, type InputEvent, LocalMirror, type Materializer, type MaterializerDef, type MaterializerReducer, MaterializerRuntime, type MaterializerRuntimeOptions, type Seq, type SnapshotStore, type SqliteAdapter, type StateChangeCallback, type SubscriptionClient, SubscriptionManager, type TableDiff, UNHANDLED, type UnknownEventHandling, applyDiff, applyDiffToDatabase as applyDiffToDb, applyDiffToSnapshot, applyDiffs, applyDiffsToDatabase as applyDiffsToDb, defineEvents, defineMaterializer, eventsContext, subscribeToMirror };
package/dist/index.d.ts CHANGED
@@ -2,8 +2,8 @@ export { createBetterSqlite3Adapter } from "./adapters/better-sqlite3.js";
2
2
  export { createSqliteWasmAdapter } from "./adapters/sqlite-wasm.js";
3
3
  export { createSqlJsAdapter } from "./adapters/sqljs.js";
4
4
  import { S as SqliteAdapter } from "./packem_shared/types.d-BuAWjEY5.js";
5
- import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-Cd8tAg-W.js";
6
- export { type C as ClientSeq, type b as EventLogSnapshot, type G as GlobalSeq, type c as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, d as classifyChanges, e as createTableDiff, f as diffSize, i as isClientSeq, g as isDiffEmpty, h as isGlobalSeq, j as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-Cd8tAg-W.js";
5
+ import { T as TableDiff, I as InputEvent, S as Seq, E as EventLogEntry, a as EventLog, A as AppendOptions, L as LocalMirror } from "./packem_shared/local-mirror.d-ByIjd7sW.js";
6
+ export { type C as ClientSeq, type b as EventLogOptions, type c as EventLogSnapshot, type G as GlobalSeq, type d as LocalMirrorOptions, type M as MirrorTableDef, type R as RowChange, e as classifyChanges, f as createTableDiff, g as diffSize, i as isClientSeq, h as isDiffEmpty, j as isGlobalSeq, k as isInputEvent, m as mergeDiffs } from "./packem_shared/local-mirror.d-ByIjd7sW.js";
7
7
  /**
8
8
  * Apply a single {@link TableDiff} to an in-memory row map and return
9
9
  * the updated map.
@@ -166,9 +166,17 @@ declare class EventLogDOClient {
166
166
  constructor(options: EventLogDOClientOptions);
167
167
  /**
168
168
  * Append one or more events to the log.
169
+ * @param events The events to append.
170
+ * @param options Idempotency controls for the batch.
171
+ * @param options.batchId Optional idempotency key for the whole batch — a
172
+ * retried `append` call with the same `batchId` (e.g. after a network
173
+ * timeout that hid a successful response) returns the originally-persisted
174
+ * entries instead of inserting duplicates.
169
175
  * @returns The persisted entries with their assigned `seq` numbers.
170
176
  */
171
- append(events: AppendEventInput[]): Promise<EventLogEntry[]>;
177
+ append(events: AppendEventInput[], options?: {
178
+ batchId?: string;
179
+ }): Promise<EventLogEntry[]>;
172
180
  /**
173
181
  * Fetch all entries with `seq >= sinceSeq`.
174
182
  *
@@ -284,18 +292,50 @@ type EventSourceEvents = {
284
292
  };
285
293
  };
286
294
  /**
295
+ * Sentinel a reducer can return to EXPLICITLY signal it does not handle a
296
+ * given event's `type` — as opposed to returning the current `state`
297
+ * reference unchanged to represent a legitimate, idempotent no-op for a type
298
+ * it DOES recognise.
299
+ *
300
+ * Reference equality alone can't tell these two cases apart (REPLICA-07): a
301
+ * reducer that intentionally returns `state` for a type it fully understands
302
+ * (e.g. "already applied this event, nothing to do") would otherwise be
303
+ * misclassified as "unhandled" and trigger {@link UnknownEventHandling} — a
304
+ * spurious warning, or worse, a thrown error under `"fail"`. Return `UNHANDLED`
305
+ * only for a `type` your reducer truly does not recognise; every other return
306
+ * (including a `state` returned by reference) is treated as handled.
307
+ *
308
+ * Reducers that always recognise every event they're given (a single
309
+ * always-matching type, or a catch-all) can ignore this entirely.
310
+ * @experimental
311
+ */
312
+ declare const UNHANDLED: unique symbol;
313
+ /**
287
314
  * A function that reduces an event into a state mutation.
288
315
  *
289
316
  * Pure functions are strongly encouraged: given the same event payload
290
- * and state, they must produce the same next state.
317
+ * and state, they must produce the same next state. Return {@link UNHANDLED}
318
+ * to explicitly mark an event `type` this reducer does not process — see
319
+ * {@link UNHANDLED} for why reference equality against the input `state`
320
+ * cannot be used for this instead.
291
321
  * @experimental
292
322
  */
293
- type EventReducer<S> = (state: S, entry: EventLogEntry) => S;
323
+ type EventReducer<S> = (state: S, entry: EventLogEntry) => S | typeof UNHANDLED;
294
324
  /**
295
325
  * Options for constructing an {@link EventSource}.
296
326
  * @experimental
297
327
  */
298
328
  interface EventSourceOptions {
329
+ /**
330
+ * Cap this runtime's internal `log` to this many entries (REPLICA-06).
331
+ * `replayFromLog` copies every entry it replays from the source log into
332
+ * `this.log` too — a second, uncapped copy of the same history — so a
333
+ * long-lived `EventSource` fed by repeated replay accumulates entries in
334
+ * both places forever without a cap.
335
+ *
336
+ * `undefined` (the default) preserves unbounded retention.
337
+ */
338
+ maxLogEntries?: number;
299
339
  /**
300
340
  * How to handle events whose `type` is not recognised by the reducer.
301
341
  * @default "warn"
@@ -506,27 +546,38 @@ declare class MaterializerRuntime {
506
546
  #private;
507
547
  constructor(materializers: AnyMaterializer[], options?: MaterializerRuntimeOptions);
508
548
  /**
509
- * The sequence number of the last event applied to all materializers.
549
+ * The lowest per-materializer watermark the seq of the next event that
550
+ * at least one materializer has not yet applied. `0` when there are no
551
+ * materializers.
510
552
  */
511
553
  get appliedSeq(): number;
512
554
  /**
513
- * Replay a batch of entries through all materializers.
514
- *
515
- * Entries with `seq < this.appliedSeq` are silently skipped (idempotent).
516
- * @returns The number of entries actually applied.
555
+ * Replay a batch of entries, applying each entry only to the
556
+ * materializers whose own watermark is behind it — a materializer at or
557
+ * past an entry's seq (e.g. recovered from a snapshot, or already caught
558
+ * up) skips it, so no materializer ever double-applies an event.
559
+ * @returns The number of entries applied to at least one materializer.
517
560
  */
518
561
  applyEntries(entries: ReadonlyArray<EventLogEntry>): number;
519
562
  /**
520
563
  * Attempt to recover materialized state from a snapshot store.
521
564
  *
522
- * When a snapshot is found for a materializer, its state is restored
523
- * and the snapshot's watermark (`appliedSeq`) is returned so the caller
524
- * can skip replaying entries up to that point.
525
- * @returns The highest `appliedSeq` across all recovered snapshots, or `0`.
565
+ * When a snapshot is found for a materializer, its state AND its own
566
+ * watermark are restored from that snapshot. A materializer with no
567
+ * snapshot keeps its current watermark (`0` for a fresh runtime) — it
568
+ * does NOT inherit another materializer's watermark, so it still catches
569
+ * up from the very beginning (REPLICA-04: previously a shared watermark
570
+ * was bumped to the MAX across snapshots, permanently skipping events 0..N
571
+ * for any un-snapshotted or lagging materializer).
572
+ * @returns The highest snapshot `appliedSeq` across all materializers, or
573
+ * `0` — kept for backward compatibility; callers that need the fetch
574
+ * watermark for catch-up should use the per-materializer minimum instead
575
+ * (see `initialize`).
526
576
  */
527
577
  recoverFromSnapshots(): Promise<number>;
528
578
  /**
529
- * Persist the current state of all materializers as snapshots.
579
+ * Persist the current state of all materializers as snapshots, each
580
+ * tagged with ITS OWN watermark (not a shared one).
530
581
  */
531
582
  persistSnapshots(): Promise<void>;
532
583
  /**
@@ -534,8 +585,11 @@ declare class MaterializerRuntime {
534
585
  *
535
586
  * 1. Recover materialized state from snapshots (if a snapshotStore is
536
587
  * configured).
537
- * 2. Fetch all entries since the recovered watermark from the DO.
538
- * 3. Apply them through the materializers.
588
+ * 2. Fetch all entries since the MINIMUM per-materializer watermark from
589
+ * the DO not the maximum — so a materializer with no snapshot (or a
590
+ * lower one) still receives every event it hasn't seen (REPLICA-04).
591
+ * 3. Apply them through the materializers; `applyEntries` skips each
592
+ * entry for any materializer already past it, so nothing is double-applied.
539
593
  *
540
594
  * Call this once on startup / after the DO binding is available.
541
595
  * @returns The number of entries applied during catch-up.
@@ -584,6 +638,13 @@ interface EventLogDOState {
584
638
  sql: {
585
639
  exec: (query: string, ...params: unknown[]) => unknown;
586
640
  };
641
+ /**
642
+ * The DO platform's native atomic-transaction primitive (async;
643
+ * commits on resolve, rolls back on throw/reject). Test doubles that
644
+ * omit it fall back to a bare (non-transactional) call — see
645
+ * `#handleAppend`.
646
+ */
647
+ transaction?: <T>(closure: () => Promise<T> | T) => Promise<T>;
587
648
  };
588
649
  }
589
650
  /**
@@ -907,4 +968,4 @@ declare class EventsSync {
907
968
  */
908
969
  sync(): Promise<number>;
909
970
  }
910
- export { type AppendEventInput, type AppendOptions, type EventCallback, EventEmitter, type EventFactory, EventLog, EventLogDO, EventLogDOClient, type EventLogDOClientOptions, type EventLogEntry, type EventNamespace, type EventReducer, EventSource, type EventSourceEvents, type EventSourceOptions, type EventsContextOutput, type EventsDefinition, type EventsFacade, EventsSync, type EventsSyncOptions, InMemorySnapshotStore, type InputEvent, LocalMirror, type Materializer, type MaterializerDef, type MaterializerReducer, MaterializerRuntime, type MaterializerRuntimeOptions, type Seq, type SnapshotStore, type SqliteAdapter, type StateChangeCallback, type SubscriptionClient, SubscriptionManager, type TableDiff, type UnknownEventHandling, applyDiff, applyDiffToDatabase as applyDiffToDb, applyDiffToSnapshot, applyDiffs, applyDiffsToDatabase as applyDiffsToDb, defineEvents, defineMaterializer, eventsContext, subscribeToMirror };
971
+ export { type AppendEventInput, type AppendOptions, type EventCallback, EventEmitter, type EventFactory, EventLog, EventLogDO, EventLogDOClient, type EventLogDOClientOptions, type EventLogEntry, type EventNamespace, type EventReducer, EventSource, type EventSourceEvents, type EventSourceOptions, type EventsContextOutput, type EventsDefinition, type EventsFacade, EventsSync, type EventsSyncOptions, InMemorySnapshotStore, type InputEvent, LocalMirror, type Materializer, type MaterializerDef, type MaterializerReducer, MaterializerRuntime, type MaterializerRuntimeOptions, type Seq, type SnapshotStore, type SqliteAdapter, type StateChangeCallback, type SubscriptionClient, SubscriptionManager, type TableDiff, UNHANDLED, type UnknownEventHandling, applyDiff, applyDiffToDatabase as applyDiffToDb, applyDiffToSnapshot, applyDiffs, applyDiffsToDatabase as applyDiffsToDb, defineEvents, defineMaterializer, eventsContext, subscribeToMirror };
package/dist/index.mjs CHANGED
@@ -1,20 +1,20 @@
1
1
  export { createBetterSqlite3Adapter } from './adapters/better-sqlite3.mjs';
2
2
  export { createSqliteWasmAdapter } from './adapters/sqlite-wasm.mjs';
3
3
  export { createSqlJsAdapter } from './adapters/sqljs.mjs';
4
- export { applyDiff, applyDiffToSnapshot, applyDiffs } from './packem_shared/applyDiff-BtbIl1D3.mjs';
4
+ export { applyDiff, applyDiffToSnapshot, applyDiffs } from './packem_shared/applyDiff-98tKzmiW.mjs';
5
5
  export { defineEvents } from './packem_shared/defineEvents-DiBkPTh_.mjs';
6
- export { MaterializerRuntime, defineMaterializer } from './packem_shared/MaterializerRuntime-HqNXqJxp.mjs';
6
+ export { MaterializerRuntime, defineMaterializer } from './packem_shared/MaterializerRuntime-BoIrsMYB.mjs';
7
7
  export { applyDiffToDb, applyDiffsToDb } from './packem_shared/applyDiffToDb-DQ1xZp5J.mjs';
8
8
  export { EventEmitter } from './packem_shared/EventEmitter-CMZfct03.mjs';
9
- export { EventLog } from './packem_shared/EventLog-zMy7AYP4.mjs';
10
- export { EventLogDO } from './packem_shared/EventLogDO-CZYUvvSr.mjs';
11
- export { EventLogDOClient } from './packem_shared/EventLogDOClient-DGiEdi96.mjs';
12
- export { EventSource } from './packem_shared/EventSource-DfV4VoRD.mjs';
9
+ export { EventLog } from './packem_shared/EventLog-CnK-3Wge.mjs';
10
+ export { EventLogDO } from './packem_shared/EventLogDO-DqlsVx0H.mjs';
11
+ export { EventLogDOClient } from './packem_shared/EventLogDOClient-F4FO8Si4.mjs';
12
+ export { EventSource, UNHANDLED } from './packem_shared/EventSource-D5yO9_aI.mjs';
13
13
  export { eventsContext } from './packem_shared/eventsContext-Bk_p48hj.mjs';
14
- export { LocalMirror } from './packem_shared/LocalMirror-GeJ26eNe.mjs';
14
+ export { LocalMirror } from './packem_shared/LocalMirror-a-5jEqFN.mjs';
15
15
  export { isClientSeq, isGlobalSeq, isInputEvent } from './packem_shared/isClientSeq-C46BkzqJ.mjs';
16
16
  export { InMemorySnapshotStore } from './packem_shared/InMemorySnapshotStore-BHVAD-Bp.mjs';
17
17
  export { subscribeToMirror } from './packem_shared/subscribeToMirror-CiaM-nQ7.mjs';
18
18
  export { SubscriptionManager } from './packem_shared/SubscriptionManager-C5xbw0pg.mjs';
19
- export { EventsSync } from './packem_shared/EventsSync-DkVbU0WV.mjs';
20
- export { classifyChanges, createTableDiff, diffSize, isDiffEmpty, mergeDiffs } from './packem_shared/classifyChanges-aZmkxgVI.mjs';
19
+ export { EventsSync } from './packem_shared/EventsSync-BP36tC9O.mjs';
20
+ export { classifyChanges, createTableDiff, diffSize, isDiffEmpty, mergeDiffs } from './packem_shared/classifyChanges-RcqLBpLs.mjs';