📄 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