📄 src/models/indexdb/patch_storage.ts
D-OPEN SOVEREIGN

Documentation & Insights

V2 patches carry no timestamp — ordering is the insertion order (`pk`),
and duplicate detection compares the full patch signature.
Storage metadata (`pk`, `username`) must be stripped before signing,
otherwise stored rows can never match incoming patches.
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 { proResolver } from "../promises.ts";
16import type { QueueItem } from "../interfaces.ts";
17import { Db, patchStoreName } from "./common.ts";
18
19export type StoredPatch = QueueItem & { pk: number, username: string };
20
21/**
22 * V2 patches carry no timestamp — ordering is the insertion order (`pk`),
23 * and duplicate detection compares the full patch signature.
24 * Storage metadata (`pk`, `username`) must be stripped before signing,
25 * otherwise stored rows can never match incoming patches.
26 */
27const signature = (op: QueueItem): string => JSON.stringify(op, Object.keys(op).sort());
28
29export const bulkAddPatches = (username: string, collection: Array<QueueItem>): Promise<boolean> => {
30    const deferred = proResolver<boolean>()
31    Db().then(db => {
32        if (collection.length === 0) return deferred.resolve(true)
33
34        getAllPatchesByUsername(username).then(existingPatches => {
35            // Derived from the highest surviving pk, not a row count — deletePatches()
36            // removes rows by pk, so a count can fall below the highest surviving pk
37            // and a count-based nextPk would collide with it on the composite
38            // ["username","pk"] key (existingPatches is pk-ascending, see above).
39            let nextPk = (existingPatches.length ? existingPatches[existingPatches.length - 1].pk : 0) + 1;
40            const tx = db.transaction(patchStoreName, "readwrite");
41            const store = tx.objectStore(patchStoreName);
42            const existing = new Set(existingPatches.map(({ pk, username, ...op }) => signature(op as QueueItem)));
43
44            collection.forEach(op => {
45                if (!existing.has(signature(op))) {
46                    const add = store.add(Object.assign({}, { username, pk: nextPk }, op));
47                    nextPk++;
48                    add.onerror = function (event) {
49                        console.error(`error inserting [${patchStoreName}]`, op, event.target)
50                        deferred.reject(new Error(`error inserting patch`))
51                    }
52                }
53            })
54
55            tx.oncomplete = function () {
56                deferred.resolve(true)
57            }
58            tx.onerror = function (event) {
59                console.error(`tx.onerror inserting [${patchStoreName}]: `, event)
60                deferred.reject(event.target)
61            }
62        }).catch(err => {
63            console.error(err)
64            deferred.reject(err);
65        })
66    }).catch(e => {
67        console.error(`[indexdb error:] bulkAddPatches ${e.message}`)
68        deferred.reject(e)
69    });
70    return deferred.pro
71}
72
73export const getNbPatches = (username: string): Promise<number> => {
74    const deferred = proResolver<number>()
75    Db().then(async (db) => {
76        const tx = db.transaction(patchStoreName, "readonly");
77        tx.objectStore(patchStoreName).index("username").count(IDBKeyRange.only(username)).onsuccess = (event) => {
78            // @ts-ignore
79            deferred.resolve(event.target.result || 0);
80        }
81        tx.onerror = function (event) {
82            console.log("worker: error counting patches", event.target)
83            deferred.reject(event.target)
84        };
85    })
86    return deferred.pro
87}
88
89export const getAllPatchesSince = (username: string, pk: number): Promise<Array<StoredPatch>> => {
90    const deferred = proResolver<Array<StoredPatch>>()
91    Db().then(async (db) => {
92        const tx = db.transaction(patchStoreName, "readonly");
93        const store = tx.objectStore(patchStoreName);
94
95        store.index("username").getAll(IDBKeyRange.only(username)).onsuccess = (event) => {
96            // @ts-ignore
97            const all = event.target.result || [];
98            const results = all.filter((row: StoredPatch) => row.pk >= pk);
99            results.sort((a: StoredPatch, b: StoredPatch) => a.pk - b.pk);
100            deferred.resolve(results);
101        };
102
103        tx.onerror = function (event) {
104            console.error("worker: error loading patches since", pk, event.target)
105            deferred.reject(event.target)
106        };
107    }).catch(deferred.reject)
108
109    return deferred.pro
110}
111
112export const getAllPatchesByUsername = (username: string): Promise<Array<StoredPatch>> => {
113    const deferred = proResolver<Array<StoredPatch>>()
114    Db().then(async (db) => {
115        const tx = db.transaction(patchStoreName, "readonly");
116
117        tx.objectStore(patchStoreName).index("username").getAll(IDBKeyRange.only(username)).onsuccess = (event) => {
118            // @ts-ignore
119            const results = event.target.result || [];
120            results.sort((a: StoredPatch, b: StoredPatch) => a.pk - b.pk);
121            deferred.resolve(results);
122        }
123        tx.onerror = function (event) {
124            console.error("worker: error loading all patches", event.target)
125            deferred.reject(event.target)
126        };
127    }).catch(deferred.reject)
128
129    return deferred.pro
130}
131
132export const getAllPatches = (): Promise<{ [user: string]: Array<StoredPatch> }> => {
133    const deferred = proResolver<{ [user: string]: Array<StoredPatch> }>()
134    Db().then(async (db) => {
135        const tx = db.transaction(patchStoreName, "readonly");
136
137        tx.objectStore(patchStoreName).getAll().onsuccess = (event) => {
138            // @ts-ignore
139            if (event.target.result) {
140                // @ts-ignore
141                const groupedByUsername = event.target.result.reduce((acc, patch) => {
142                    const { username } = patch
143                    if (typeof acc[username] === 'undefined') {
144                        acc[username] = []
145                    }
146                    acc[username].push(patch)
147                    return acc
148                }, {});
149                deferred.resolve(groupedByUsername);
150            } else {
151                deferred.resolve({});
152            }
153        };
154        tx.onerror = function (event) {
155            console.log("worker: error loading all patches", event.target)
156            deferred.reject(event.target)
157        };
158    }).catch(deferred.reject)
159
160    return deferred.pro
161}
162
163export const deletePatches = async (username: string, pks: Array<number>) => {
164    const deferred = proResolver<true>()
165    Db().then(async (db) => {
166        const tx = db.transaction(patchStoreName, "readwrite");
167        const store = tx.objectStore(patchStoreName)
168        for (const pk of pks) {
169            store.delete([username, pk])
170        }
171        tx.oncomplete = function () {
172            deferred.resolve(true)
173        };
174        tx.onerror = function (event) {
175            console.log("worker: error deleting patches", event.target)
176            deferred.reject(event.target)
177        };
178    }).catch(deferred.reject)
179    return deferred.pro
180}
181
182export const clearAllPatches = (): Promise<true> => {
183    const deferred = proResolver<true>()
184    Db().then(async (db) => {
185        const tx = db.transaction(patchStoreName, "readwrite");
186        tx.objectStore(patchStoreName).clear()
187        tx.oncomplete = () => {
188            deferred.resolve(true)
189        }
190        tx.onerror = (event) => {
191            deferred.reject(event.target)
192        }
193    });
194    return deferred.pro
195}
196