๐Ÿ“„ src/models/patcher.ts
D-OPEN SOVEREIGN

Documentation & Insights

Creates a debounced function that delays invoking the provided function
until wait milliseconds have elapsed since the last time this debounced
function was invoked. Each invocation returns a promise that resolves
with the result of the ultimate single execution.
Drops all queued (not yet persisted) patches. Test/reset helper.
Last-Writer-Wins consolidation. Walks the queue from the newest entry
backwards and keeps only the latest edit per logical slot.

The sub-key for property edits incorporates the language code
(`${propertyId}:${langId ?? 0}`) so a French string edit never erases
a previous English edit (SPEC ยง2.2). Instance field edits additionally
key on the instance index.
Syncs the queued patches to the worker (IndexedDB persistence).
Before initThread, patches are retained in the queue (never dropped);
initThread flushes them as soon as the worker is available.
Restores patches received from the blockchain into IndexedDB.
Decoding is layout-driven: the payload's `version` selects the codec
layout from versions.json (delta-merged via resolveLayout).
Serializes locally stored EVM-bound patches into the BackupStorage.sol
payload format. Local analytics ops (ActionTypes) are skipped.
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 {
16    PatchOperation,
17    patchToEvm, evmToPatch,
18    type AnyPatch,
19    type EditPropertyStringObj,
20    type EditInstanceFieldStringObj,
21    type EvmPayload,
22} from "./binary.ts";
23import {
24    isEvmPatch,
25    type QueueItem,
26    type LocalOp,
27    type RatePartOp,
28    type SharePartOp,
29    type PartOp,
30    type IThread,
31} from "./interfaces.ts";
32import { ActionTypes, WorkerCommands } from "./enums.ts";
33import { bulkAddPatches, getAllPatchesByUsername, getAllPatchesSince } from "./indexdb/patch_storage.ts";
34import { sanitizeText } from "./sanitizer.ts";
35import { currentLayout, CURRENT_VERSION, versionsFile } from "./versions.ts";
36
37function debounce<T extends (...args: any[]) => any>(
38    func: T,
39    delay: number
40): (...args: Parameters<T>) => void {
41    let timeout: ReturnType<typeof setTimeout> | undefined;
42
43    return function (...args: Parameters<T>): void {
44        clearTimeout(timeout);
45        timeout = setTimeout(() => func(...args), delay);
46    };
47}
48
49/**
50 * Creates a debounced function that delays invoking the provided function
51 * until wait milliseconds have elapsed since the last time this debounced
52 * function was invoked. Each invocation returns a promise that resolves
53 * with the result of the ultimate single execution.
54 */
55export function debouncePromise<T extends (...args: any[]) => any>(
56    func: T,
57    wait: number
58): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
59    let timeout: ReturnType<typeof setTimeout> | null = null;
60    let resolves: Array<{
61        resolve: (value: Awaited<ReturnType<T>>) => void;
62        reject: (reason?: any) => void;
63    }> = [];
64
65    return (...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> => {
66        if (timeout !== null) {
67            clearTimeout(timeout);
68        }
69
70        const p = new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
71            resolves.push({ resolve, reject });
72        });
73
74        timeout = setTimeout(async () => {
75            timeout = null;
76            const currentResolves = [...resolves];
77            resolves = [];
78
79            try {
80                const result = await func(...args);
81                currentResolves.forEach(r => r.resolve(result));
82            } catch (error) {
83                currentResolves.forEach(r => r.reject(error));
84            }
85        }, wait);
86
87        return p;
88    };
89}
90
91export class Patches {
92    protected _patches: Array<QueueItem> = [];
93    protected thread!: IThread;
94
95    static _replaying = false
96
97    get patches(): Array<QueueItem> {
98        return this._patches;
99    }
100
101    initThread(thread: IThread, debounceMs = 2000) {
102        console.log('InitPatchInstance with thread', thread)
103        this.thread = thread;
104        // Wrap the prototype method (not this.sync) so repeated initThread
105        // calls never nest debounce wrappers.
106        this.sync = debouncePromise(Patches.prototype.sync.bind(this), debounceMs)
107        // Flush anything queued before the worker was ready.
108        if (this._patches.length > 0) {
109            this.sync().catch(console.error)
110        }
111    }
112
113    get threadInstance(): IThread {
114        return this.thread;
115    }
116
117    constructor(public username: string) { }
118
119    static onSyncListeners: Array<(patches: QueueItem[]) => void> = [];
120
121    static onSync(callback: (patches: QueueItem[]) => void) {
122        this.onSyncListeners.push(callback);
123    }
124
125    static setReplay(replay: boolean) {
126        this._replaying = replay
127    }
128
129    /** Drops all queued (not yet persisted) patches. Test/reset helper. */
130    clearQueue() {
131        this._patches = []
132    }
133
134    addPatch(op: QueueItem) {
135        if (Patches._replaying) {
136            return
137        }
138        if (isEvmPatch(op)) {
139            if (op.operation === PatchOperation.EditPropertyString) {
140                (op as EditPropertyStringObj).value = sanitizeText((op as EditPropertyStringObj).value);
141            } else if (op.operation === PatchOperation.EditInstanceFieldString) {
142                (op as EditInstanceFieldStringObj).value = sanitizeText((op as EditInstanceFieldStringObj).value);
143            }
144        }
145        this._patches.push(op);
146        this.sync().catch(console.error);
147    }
148
149    /**
150     * Last-Writer-Wins consolidation. Walks the queue from the newest entry
151     * backwards and keeps only the latest edit per logical slot.
152     *
153     * The sub-key for property edits incorporates the language code
154     * (`${propertyId}:${langId ?? 0}`) so a French string edit never erases
155     * a previous English edit (SPEC ยง2.2). Instance field edits additionally
156     * key on the instance index.
157     */
158    static cleanData(data: Array<QueueItem>): Array<QueueItem> {
159        const dataIndex: Record<string, Record<string, boolean | number>> = {};
160        const isUndefined = (obj: any) => typeof obj === "undefined";
161        const filteredData: QueueItem[] = [];
162
163        const keep = (key: string, subKey: string, item: QueueItem) => {
164            if (isUndefined(dataIndex[key])) {
165                dataIndex[key] = {};
166            }
167            if (isUndefined(dataIndex[key][subKey])) {
168                dataIndex[key][subKey] = true;
169                filteredData.push(item);
170            }
171        }
172
173        const keepOldest = (key: string, subKey: string, item: QueueItem) => {
174            if (isUndefined(dataIndex[key])) {
175                dataIndex[key] = {};
176            }
177            if (isUndefined(dataIndex[key][subKey])) {
178                dataIndex[key][subKey] = filteredData.length;
179                filteredData.push(item);
180            } else {
181                filteredData[dataIndex[key][subKey] as number] = item;
182            }
183        }
184
185        for (let i = data.length - 1; i >= 0; i--) {
186            const item = data[i];
187
188            if (isEvmPatch(item)) {
189                const key = `evm-${item.objectType}-${(item as any).objectId ?? (item as any).fromUid}-${item.operation}`
190
191                switch (item.operation) {
192                    case PatchOperation.EditPropertyString:
193                    case PatchOperation.EditPropertyNumber: {
194                        const d = item as any;
195                        keep(key, `${d.propertyId}:${d.langId ?? 0}`, item);
196                        break;
197                    }
198                    case PatchOperation.EditInstanceFieldString:
199                    case PatchOperation.EditInstanceFieldNumber: {
200                        const d = item as any;
201                        keep(key, `${d.instanceIndex}:${d.propertyId}:${d.langId ?? 0}`, item);
202                        break;
203                    }
204                    case PatchOperation.EditRelation: {
205                        // keep latest add/remove per (relation, target)
206                        const d = item as any;
207                        keep(key, `${d.relationId}:${d.targetId}`, item);
208                        break;
209                    }
210                    case PatchOperation.SetAlias: {
211                        const d = item as any;
212                        keep(key, `${d.fromUid}`, item);
213                        break;
214                    }
215                    case PatchOperation.CreateRecord: {
216                        keep(key, 'create', item);
217                        break;
218                    }
219                    case PatchOperation.AddInstance: {
220                        const d = item as any;
221                        keepOldest(key, `${d.langId ?? 0}:${d.instanceIndex}`, item);
222                        break;
223                    }
224                    default:
225                        throw new Error(`unknown patch operation ${(item as AnyPatch).operation}`)
226                }
227                continue;
228            }
229
230            // Local analytics ops
231            const op = item as LocalOp;
232            const key = `local-${op.objectType}-${op.objectId}-${op.action}`
233            switch (op.action) {
234                case ActionTypes.RatePart:
235                    keep(key, `${(op as RatePartOp).part}`, op);
236                    break;
237                case ActionTypes.SharePart:
238                    keep(key, `${(op as SharePartOp).part}:${(op as SharePartOp).socialMediaId}`, op);
239                    break;
240                case ActionTypes.Rate:
241                    keep(key, 'seen', op);
242                    break;
243                case ActionTypes.Visit:
244                case ActionTypes.Download:
245                case ActionTypes.Share:
246                case ActionTypes.VisitPart:
247                    filteredData.push(op);
248                    break;
249                default:
250                    throw new Error(`unknown local action ${(op as LocalOp).action}`)
251            }
252        }
253
254        filteredData.reverse()
255        return filteredData
256    }
257
258    /**
259     * Syncs the queued patches to the worker (IndexedDB persistence).
260     * Before initThread, patches are retained in the queue (never dropped);
261     * initThread flushes them as soon as the worker is available.
262     */
263    async sync(): Promise<any> {
264        if (typeof this.thread === 'undefined') {
265            return null;
266        }
267        const data = Patches.cleanData([...this._patches]);
268        if (data.length === 0) {
269            this._patches = []
270            return true;
271        }
272        this._patches = []
273
274        Patches.onSyncListeners.forEach(cb => {
275            try { cb(data); } catch (e) { console.error(e); }
276        });
277
278        let savePatchOperation;
279        try {
280            savePatchOperation = await this.thread.sendRequest({
281                cmd: WorkerCommands.SavePatches,
282                payload: {
283                    username: this.username,
284                    patches: data
285                }
286            })
287        } catch (err) {
288            // Persistence failed (worker crashed, IDB quota, tab teardown) โ€” restore
289            // the batch so it isn't silently lost, instead of leaving it cleared.
290            this._patches.unshift(...data)
291            throw err
292        }
293
294        const level = await Patches.calculateLevel(this.username)
295        console.log('level after saving patches', level)
296
297        return savePatchOperation
298    }
299
300    static async calculateLevel(username: string): Promise<number> {
301        const patches = await getAllPatchesByUsername(username)
302        const cleaned = this.cleanData(patches)
303        let score = 0;
304        for (const p of cleaned) {
305            if (isEvmPatch(p)) {
306                switch (p.operation) {
307                    case PatchOperation.EditPropertyString:
308                    case PatchOperation.EditInstanceFieldString:
309                        score += 4; break;
310                    case PatchOperation.EditPropertyNumber:
311                    case PatchOperation.EditInstanceFieldNumber:
312                        score += 2; break;
313                    case PatchOperation.EditRelation:
314                        score += 3; break;
315                    case PatchOperation.SetAlias:
316                        score += 5; break;
317                }
318                continue;
319            }
320            switch ((p as LocalOp).action) {
321                case ActionTypes.VisitPart: score += 0.5; break;
322                case ActionTypes.SharePart: score += 1; break;
323                case ActionTypes.RatePart: score += 1; break;
324                case ActionTypes.Rate: score += 1; break;
325                case ActionTypes.Visit: score += 1; break;
326                case ActionTypes.Share: score += 2; break;
327                case ActionTypes.Download: score += 3; break;
328            }
329        }
330        const level = score / 7;
331
332        if (typeof window !== 'undefined' && typeof CustomEvent !== 'undefined') {
333            const evt = new CustomEvent("LevelUpdate", {
334                detail: { level, username },
335                bubbles: true,
336            })
337            window.dispatchEvent(evt);
338        }
339
340        return level
341    }
342}
343
344const patchesMap = new Map<string, Patches>();
345
346const PatchInstance = new Patches("mine");
347
348patchesMap.set("mine", PatchInstance);
349
350/**
351 * Restores patches received from the blockchain into IndexedDB.
352 * Decoding is layout-driven: the payload's `version` selects the codec
353 * layout from versions.json (delta-merged via resolveLayout).
354 */
355export const deserializePatches = async (username: string, payload: EvmPayload) => {
356    const operations = evmToPatch(payload, versionsFile);
357
358    // Bulk-add directly to IndexedDB โ€” bypassing addPatch's sync queue prevents
359    // restored patches from being re-uploaded to the blockchain.
360    console.log(`[Patches] deserializePatches: writing ${operations.length} restored patches for ${username}`);
361    await bulkAddPatches(username, operations);
362}
363
364/**
365 * Serializes locally stored EVM-bound patches into the BackupStorage.sol
366 * payload format. Local analytics ops (ActionTypes) are skipped.
367 */
368export const serializePatches = async (fromIndex: number, toIndex: number): Promise<EvmPayload> => {
369    const stored = await getAllPatchesSince("mine", fromIndex)
370    const evmPatches = stored
371        .slice(0, toIndex - fromIndex)
372        .filter(isEvmPatch)
373        .map(({ pk, username, ...op }: any) => op as AnyPatch)
374        .map(op => {
375            if (op.operation === PatchOperation.EditPropertyString) {
376                (op as EditPropertyStringObj).value = sanitizeText((op as EditPropertyStringObj).value);
377            } else if (op.operation === PatchOperation.EditInstanceFieldString) {
378                (op as EditInstanceFieldStringObj).value = sanitizeText((op as EditInstanceFieldStringObj).value);
379            }
380            return op;
381        });
382    return patchToEvm(evmPatches, currentLayout(), CURRENT_VERSION)
383}
384
385export const getPatchInstanceByAccount = (account: string): Patches => {
386    if (!patchesMap.has(account)) {
387        console.warn(`Account ${account} does not exist in patchesInstances โ€” creating a new one`);
388        const p = new Patches(account)
389        p.initThread(PatchInstance.threadInstance);
390        patchesMap.set(account, p);
391    }
392    return patchesMap.get(account) as Patches;
393}
394
395export { PatchInstance }
396