Doppel-Uploads (Prod-Log 17.09.: reihenweise duplicate:true): Seite
(syncQueue) und Service Worker (Background Sync) arbeiteten dieselbe
IndexedDB-Queue ab, ohne voneinander zu wissen. Dank md5-Abgleich
folgenlos, aber doppeltes Datenvolumen. Mit eingestelltem Verkleinern
sogar zwei verschiedene Dateien im Auftrag: der Worker lud das
Original, die Seite danach die kleine Fassung.
- idb.js: queueKeys/queuePatch/queueClaim/queueRelease - Lesen-Aendern-
Schreiben in einer readwrite-Transaktion, ueber Kontexte atomar
- syncQueue (Seite) und drainQueue (Worker) belegen jedes Foto vor dem
Upload (Lease 90 s > laengster Upload-Timeout); belegt = ueberspringen,
abgelaufen = uebernehmen. Nur noch ein Foto gleichzeitig im Speicher
- enqueuePhoto({hold:true}) + releaseHold: waehrend des Verkleinerns
bleibt das Foto belegt
- Versuchszaehler, retryFailed und replaceQueuedBlob per Patch statt
ganzes Item zurueckschreiben
Reload nach App-Update: controllerchange lud sofort neu - offene Kamera,
getippte Notiz, halbe Unterschrift weg; mit Fotos in der Warteschlange
kam dazu der Browserdialog "Seite verlassen?".
- window.appBusy(): Modal, Auswahl, laufender Upload, Eingabe im Fokus,
laufende Sprachnotiz. PIN-Sperre zaehlt bewusst nicht
- index.php holt den Reload nach, sobald nichts mehr offen ist; eigener
Reload setzt __selfReload, beforeunload warnt dann nicht
Verifiziert lokal in Chromium: echte Kamera-Oberflaeche mit Canvas-
Stream offline, paralleler Zugriff Seite+Worker (3 Fotos = 3 POSTs im
Serverlog, kein duplicate), Verkleinern (eine Datei, kleine Fassung),
fremde Lease frisch/abgelaufen, aufgeschobener Reload ohne Dialog.
Nicht getestet: Kamera-Hardware und Background Sync bei Display aus am
Handy. Kein Deploy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
174 lines
6.6 KiB
JavaScript
174 lines
6.6 KiB
JavaScript
/* Mini IndexedDB Wrapper für die PWA.
|
|
* Speichert: Auth-Token, Offline-Queue für Uploads.
|
|
*/
|
|
(function () {
|
|
const DB_NAME = 'baustelle-pwa-v1';
|
|
const DB_VERSION = 1;
|
|
let dbPromise = null;
|
|
|
|
function open() {
|
|
if (dbPromise) return dbPromise;
|
|
dbPromise = new Promise((resolve, reject) => {
|
|
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
req.onupgradeneeded = () => {
|
|
const db = req.result;
|
|
if (!db.objectStoreNames.contains('kv')) {
|
|
db.createObjectStore('kv');
|
|
}
|
|
if (!db.objectStoreNames.contains('queue')) {
|
|
db.createObjectStore('queue', { keyPath: 'id', autoIncrement: true });
|
|
}
|
|
};
|
|
req.onsuccess = () => resolve(req.result);
|
|
req.onerror = () => reject(req.error);
|
|
});
|
|
return dbPromise;
|
|
}
|
|
|
|
async function get(key) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('kv', 'readonly');
|
|
const r = tx.objectStore('kv').get(key);
|
|
r.onsuccess = () => res(r.result);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
|
|
async function set(key, val) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('kv', 'readwrite');
|
|
tx.objectStore('kv').put(val, key);
|
|
tx.oncomplete = () => res();
|
|
tx.onerror = () => rej(tx.error);
|
|
});
|
|
}
|
|
|
|
async function del(key) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('kv', 'readwrite');
|
|
tx.objectStore('kv').delete(key);
|
|
tx.oncomplete = () => res();
|
|
tx.onerror = () => rej(tx.error);
|
|
});
|
|
}
|
|
|
|
// Alle Schluessel im kv-Store — gebraucht, um den Offline-Spiegel aufzuraeumen
|
|
async function keys() {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('kv', 'readonly');
|
|
const r = tx.objectStore('kv').getAllKeys();
|
|
r.onsuccess = () => res(r.result || []);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
|
|
async function queuePush(item) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('queue', 'readwrite');
|
|
const r = tx.objectStore('queue').add(item);
|
|
r.onsuccess = () => res(r.result);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
|
|
async function queueAll() {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('queue', 'readonly');
|
|
const r = tx.objectStore('queue').getAll();
|
|
r.onsuccess = () => res(r.result || []);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
|
|
async function queueDelete(id) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('queue', 'readwrite');
|
|
tx.objectStore('queue').delete(id);
|
|
tx.oncomplete = () => res();
|
|
tx.onerror = () => rej(tx.error);
|
|
});
|
|
}
|
|
|
|
// Item aktualisieren (z.B. Versuchszähler / Fehler-Markierung) — put upsert-t per keyPath id
|
|
async function queueUpdate(item) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('queue', 'readwrite');
|
|
tx.objectStore('queue').put(item);
|
|
tx.oncomplete = () => res();
|
|
tx.onerror = () => rej(tx.error);
|
|
});
|
|
}
|
|
|
|
// Nur die Ids — wer die Queue abarbeitet, holt die Eintraege danach einzeln. queueAll()
|
|
// zieht dagegen jedes Foto komplett in den Speicher.
|
|
async function queueKeys() {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('queue', 'readonly');
|
|
const r = tx.objectStore('queue').getAllKeys();
|
|
r.onsuccess = () => res(r.result || []);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
|
|
/* Lesen-Aendern-Schreiben in EINER Transaktion. IndexedDB reiht readwrite-Transaktionen
|
|
* auf demselben Store ueber alle Kontexte hinweg hintereinander — Seite und Service
|
|
* Worker koennen sich hier also nicht ins Wort fallen. `fn(item)` aendert den Eintrag an
|
|
* Ort und Stelle; gibt sie `false` zurueck, wird NICHT geschrieben.
|
|
* Rueckgabe: der Eintrag nach der Aenderung, oder null (weg bzw. nicht geschrieben). */
|
|
async function queuePatch(id, fn) {
|
|
const db = await open();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('queue', 'readwrite');
|
|
const store = tx.objectStore('queue');
|
|
let out = null;
|
|
const r = store.get(id);
|
|
r.onsuccess = () => {
|
|
const it = r.result;
|
|
if (!it) return; // inzwischen hochgeladen und geloescht
|
|
if (fn(it) === false) return;
|
|
store.put(it);
|
|
out = it;
|
|
};
|
|
tx.oncomplete = () => res(out);
|
|
tx.onerror = () => rej(tx.error);
|
|
tx.onabort = () => rej(tx.error);
|
|
});
|
|
}
|
|
|
|
/* Einen Eintrag fuer den Upload belegen (Lease). Seite und Service Worker arbeiten
|
|
* dieselbe Queue ab; ohne Absprache laden beide dasselbe Foto hoch (Prod-Log 17.09.2026:
|
|
* reihenweise duplicate:true). Belegt wird nur, was frei ist oder dessen Lease abgelaufen
|
|
* ist — eine eingefrorene oder abgeschossene Seite blockiert ihr Foto also nicht ewig.
|
|
* DIESELBE LOGIK STEHT IN sw.js (queueClaim) — beide Stellen zusammen aendern.
|
|
* Rueckgabe: der belegte Eintrag, oder null (weg, fehlgeschlagen-markiert oder belegt). */
|
|
function queueClaim(id, owner, leaseMs) {
|
|
const now = Date.now();
|
|
return queuePatch(id, (it) => {
|
|
if (it.failed) return false;
|
|
if (it.uploading_since && it.uploading_by !== owner
|
|
&& (now - it.uploading_since) < leaseMs) return false;
|
|
it.uploading_since = now;
|
|
it.uploading_by = owner;
|
|
});
|
|
}
|
|
|
|
function queueRelease(id, extra) {
|
|
return queuePatch(id, (it) => {
|
|
delete it.uploading_since;
|
|
delete it.uploading_by;
|
|
if (extra) extra(it);
|
|
});
|
|
}
|
|
|
|
window.idb = { get, set, del, keys, queuePush, queueAll, queueDelete, queueUpdate,
|
|
queueKeys, queuePatch, queueClaim, queueRelease };
|
|
})();
|