๐ 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