@absolutejs/sync 1.21.0 → 1.22.0

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.
@@ -297,6 +297,33 @@ export type SyncEngine = {
297
297
  * Added in 1.19.0.
298
298
  */
299
299
  importChangeLog: (snapshot: ChangeLogSnapshot) => number;
300
+ /**
301
+ * Reconstruct the state of registered tables as of a target
302
+ * timestamp by walking the change log forward and folding each op
303
+ * into a per-table view. Useful for forensic incident response
304
+ * ("what did the tenant see at 14:32?") and the "I deleted prod
305
+ * — restore us to 2h ago" recovery story.
306
+ *
307
+ * The reconstruction is exact when the log spans `targetAt` (i.e.
308
+ * the log's oldest entry is at version 1). When the log has been
309
+ * trimmed (`changeLogSize` / `changeLogRetainMs` evicted older
310
+ * entries) AND `targetAt` falls in the gap, the result is
311
+ * best-effort: state walked forward from the OLDEST retained
312
+ * entry, with `truncated: true` so the caller knows.
313
+ *
314
+ * Added in 1.22.0.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1000;
319
+ * const result = await engine.replayTo({ at: twoHoursAgo, tables: ['orders'] });
320
+ * if (result.truncated) {
321
+ * console.warn('Replay truncated — log retention window too short.');
322
+ * }
323
+ * console.log(result.rows.orders); // orders as of two hours ago
324
+ * ```
325
+ */
326
+ replayTo: (options: ReplayOptions) => Promise<ReplayResult>;
300
327
  /**
301
328
  * Subscribe to the live engine activity stream (changes, mutation outcomes,
302
329
  * subscribe/unsubscribe). Returns an unsubscribe. Powers the devtools feed.
@@ -467,6 +494,48 @@ export type ChangeLogSnapshot = {
467
494
  */
468
495
  exportedAt?: number;
469
496
  };
497
+ /**
498
+ * Options for {@link SyncEngine.replayTo}. Added in 1.22.0.
499
+ */
500
+ export type ReplayOptions = {
501
+ /**
502
+ * Target timestamp (`Date.now()`-shaped). The engine walks the
503
+ * change log forward, applying entries with `at <= targetAt`. The
504
+ * result is the state as-of `targetAt` (or as close as the log
505
+ * permits — see `truncated`).
506
+ */
507
+ at: number;
508
+ /**
509
+ * Optional table filter. When set, only entries whose `table` is
510
+ * in this list are folded into the result; entries for other
511
+ * tables are skipped. Useful for "show me what `tasks` looked
512
+ * like at T" without paying to reconstruct every table.
513
+ */
514
+ tables?: ReadonlyArray<string>;
515
+ };
516
+ /**
517
+ * Returned by {@link SyncEngine.replayTo}. Added in 1.22.0.
518
+ *
519
+ * - `rows` — per-table arrays of rows that existed as of `asOfAt`.
520
+ * Keys are table names; values are the row objects (in last-write
521
+ * order — last write wins for duplicate-keyed inserts).
522
+ * - `asOfVersion` / `asOfAt` — the version + wall-clock of the LAST
523
+ * entry folded into the result. May be earlier than `targetAt` if
524
+ * no entries existed between the last-included entry and the
525
+ * target.
526
+ * - `truncated` — `true` when the log has been trimmed past the
527
+ * target window (`changeLog[0].version > 1 && changeLog[0].at >
528
+ * targetAt`). In this case, `rows` represents the state walked
529
+ * forward from the OLDEST retained entry — NOT the actual state
530
+ * at `targetAt`. The caller should treat the result as
531
+ * "best-effort given retention window" and warn the operator.
532
+ */
533
+ export type ReplayResult = {
534
+ asOfVersion: number;
535
+ asOfAt: number;
536
+ rows: Record<string, ReadonlyArray<unknown>>;
537
+ truncated: boolean;
538
+ };
470
539
  export type SyncEngineOptions = {
471
540
  /**
472
541
  * Stable identifier for this engine instance. Defaults to a per-process
package/dist/index.js CHANGED
@@ -2801,6 +2801,43 @@ var createSyncEngine = (options = {}) => {
2801
2801
  version
2802
2802
  }),
2803
2803
  importChangeLog,
2804
+ replayTo: async ({ at, tables }) => {
2805
+ const filterTables = tables !== undefined ? new Set(tables) : undefined;
2806
+ const state = new Map;
2807
+ let asOfVersion = 0;
2808
+ let asOfAt = 0;
2809
+ const oldest = changeLog[0];
2810
+ const truncated = oldest !== undefined && oldest.version > 1 && oldest.at > at;
2811
+ for (const entry of changeLog) {
2812
+ if (entry.at > at)
2813
+ break;
2814
+ if (filterTables !== undefined && !filterTables.has(entry.table)) {
2815
+ continue;
2816
+ }
2817
+ let tableState = state.get(entry.table);
2818
+ if (tableState === undefined) {
2819
+ tableState = new Map;
2820
+ state.set(entry.table, tableState);
2821
+ }
2822
+ const reader = readers.get(entry.table);
2823
+ const key = reader?.key?.(entry.change.row) ?? entry.change.row?.id;
2824
+ if (key === undefined) {
2825
+ continue;
2826
+ }
2827
+ if (entry.change.op === "delete") {
2828
+ tableState.delete(key);
2829
+ } else {
2830
+ tableState.set(key, entry.change.row);
2831
+ }
2832
+ asOfVersion = entry.version;
2833
+ asOfAt = entry.at;
2834
+ }
2835
+ const rows = {};
2836
+ for (const [table, map] of state) {
2837
+ rows[table] = [...map.values()];
2838
+ }
2839
+ return { asOfAt, asOfVersion, rows, truncated };
2840
+ },
2804
2841
  metrics: () => {
2805
2842
  const now = Date.now();
2806
2843
  const byCollection = {};
@@ -3218,5 +3255,5 @@ export {
3218
3255
  createPresenceHub
3219
3256
  };
3220
3257
 
3221
- //# debugId=06ECBF0C02EA0B8464756E2164756E21
3258
+ //# debugId=6DBA546C7A7B1DC564756E2164756E21
3222
3259
  //# sourceMappingURL=index.js.map