📄 src/models/replay.ts
D-OPEN SOVEREIGN

Documentation & Insights

Applies a decoded batch of patches to the **in-memory ORM** — the v2
replacement for v1's `deserializeOp` + `SyncRestoredPatches`.

The two v1 functions collapse in v2:
 - **decode** is `evmToPatch(payload, versionsFile)` (whole batch, version-driven);
 - **persist** is `deserializePatches()` (writes to IndexedDB);
 - this function is the missing **routing + apply** step only.

Each EVM edit op is routed to its target record by `(objectType, objectId)`
and applied via `record.patch(op)`, which mutates in-memory state, fires
reactivity, and propagates inverse relations. Records not currently
materialized in the LRU are **skipped** (their ops remain in IndexedDB and
are folded in when the record is next hydrated — the Direction-B path).
Local analytics ops carry no object edit and are ignored here (they feed the
stats store on boot).

`setReplay(true)` brackets the loop so no apply is mistaken for user
authoring (defensive: `record.patch()` does not itself queue, but inverse
propagation and future code paths might).

Idempotent: re-applying the same ops converges to the same state
(`patch()` is a last-writer-wins assignment / Set add-remove).

@returns counts of ops applied to live records vs. skipped (not materialized).
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import { ModelTypes } from './generated-src/index.ts';
16import { Patches } from "./patcher.ts";
17import { getRecord, hasRecord } from "./db.ts";
18import { isEvmPatch } from "./interfaces.ts";
19import type { QueueItem } from "./interfaces.ts";
20import { PatchOperation, type AnyPatch } from "./binary.ts";
21import { applyPatchWithContext, type PatchContext } from "./patchContext.ts";
22import { localRecordTable } from "./localRecordTable.ts";
23
24/**
25 * Applies a decoded batch of patches to the **in-memory ORM** — the v2
26 * replacement for v1's `deserializeOp` + `SyncRestoredPatches`.
27 *
28 * The two v1 functions collapse in v2:
29 *  - **decode** is `evmToPatch(payload, versionsFile)` (whole batch, version-driven);
30 *  - **persist** is `deserializePatches()` (writes to IndexedDB);
31 *  - this function is the missing **routing + apply** step only.
32 *
33 * Each EVM edit op is routed to its target record by `(objectType, objectId)`
34 * and applied via `record.patch(op)`, which mutates in-memory state, fires
35 * reactivity, and propagates inverse relations. Records not currently
36 * materialized in the LRU are **skipped** (their ops remain in IndexedDB and
37 * are folded in when the record is next hydrated — the Direction-B path).
38 * Local analytics ops carry no object edit and are ignored here (they feed the
39 * stats store on boot).
40 *
41 * `setReplay(true)` brackets the loop so no apply is mistaken for user
42 * authoring (defensive: `record.patch()` does not itself queue, but inverse
43 * propagation and future code paths might).
44 *
45 * Idempotent: re-applying the same ops converges to the same state
46 * (`patch()` is a last-writer-wins assignment / Set add-remove).
47 *
48 * @returns counts of ops applied to live records vs. skipped (not materialized).
49 */
50export const applyRestoredPatches = async (
51    ops: Array<AnyPatch>,
52    creatorUsername = 'mine',
53): Promise<{ applied: number; skipped: number }> => {
54    let applied = 0;
55    let skipped = 0;
56
57    Patches.setReplay(true);
58    try {
59        // Last-writer-wins consolidation before apply (collapses redundant edits).
60        const cleaned = Patches.cleanData(ops as Array<QueueItem>);
61
62        const createdInBatch = new Set<string>();
63        for (const op of cleaned) {
64            if ((op as any).operation === PatchOperation.CreateRecord) {
65                createdInBatch.add(`${(op as any).objectType}:${(op as any).objectId}`);
66            }
67        }
68
69        const loadPromises: Promise<any>[] = [];
70        for (const op of cleaned) {
71            if (!isEvmPatch(op)) continue;
72            const objectType = (op as any).objectType;
73            const objectId = (op as any).objectId ?? (op as any).fromUid;
74            if (objectType !== undefined && objectId !== undefined) {
75                if (createdInBatch.has(`${objectType}:${objectId}`)) continue;
76                if (!hasRecord(objectType, objectId, creatorUsername)) {
77                    const modelClass = ModelTypes[objectType];
78                    if (modelClass) {
79                        loadPromises.push(modelClass.LoadAsync(objectId, creatorUsername));
80                    }
81                }
82            }
83        }
84        await Promise.all(loadPromises);
85
86        for (let i = 0; i < cleaned.length; i++) {
87            const op = cleaned[i];
88            if (!isEvmPatch(op)) continue; // analytics ops → stats store, not ORM
89
90            const objectType = (op as any).objectType;
91            // SetAlias keys on `fromUid`; all other EVM ops key on `objectId`.
92            const objectId = (op as any).objectId ?? (op as any).fromUid;
93            if (typeof objectId === "undefined") {
94                skipped++;
95                continue;
96            }
97
98            const ctx: PatchContext = {
99                localRecordTable,
100                creatorUsername,
101                blockTimestamp: Date.now(), // Fallback if restoring
102                get batchRemainder() { return cleaned.slice(i + 1) as AnyPatch[]; },
103                instanceRemapStore: localRecordTable.instanceRemaps,
104            };
105
106            await applyPatchWithContext(op as AnyPatch, ctx);
107            applied++;
108        }
109    } finally {
110        Patches.setReplay(false);
111    }
112
113    return { applied, skipped };
114};