114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import { withStore } from './idb';
|
|
import { getDeviceId } from '../lib/device';
|
|
import { createUpload } from './xhr';
|
|
import { notify } from './notify';
|
|
|
|
export type QueueItem = {
|
|
id?: number;
|
|
slug: string;
|
|
fileName: string;
|
|
blob: Blob;
|
|
emotion_id?: number | null;
|
|
task_id?: number | null;
|
|
status: 'pending' | 'uploading' | 'done' | 'error';
|
|
retries: number;
|
|
nextAttemptAt?: number | null;
|
|
createdAt: number;
|
|
photoId?: number;
|
|
};
|
|
|
|
let processing = false;
|
|
|
|
export async function enqueue(item: Omit<QueueItem, 'id' | 'status' | 'retries' | 'createdAt'>) {
|
|
const now = Date.now();
|
|
await withStore('readwrite', (store) => {
|
|
store.add({ ...item, status: 'pending', retries: 0, createdAt: now });
|
|
});
|
|
// Register background sync if available
|
|
if ('serviceWorker' in navigator && 'SyncManager' in window) {
|
|
try { const reg = await navigator.serviceWorker.ready; await reg.sync.register('upload-queue'); } catch {}
|
|
}
|
|
}
|
|
|
|
export async function list(): Promise<QueueItem[]> {
|
|
return withStore('readonly', (store) => new Promise((resolve) => {
|
|
const req = store.getAll();
|
|
req.onsuccess = () => resolve(req.result as QueueItem[]);
|
|
}));
|
|
}
|
|
|
|
export async function clearDone() {
|
|
const items = await list();
|
|
await withStore('readwrite', (store) => {
|
|
for (const it of items) {
|
|
if (it.status === 'done') store.delete(it.id!);
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function processQueue() {
|
|
if (processing) return; processing = true;
|
|
try {
|
|
const now = Date.now();
|
|
let items = await list();
|
|
for (const it of items) {
|
|
if (it.status === 'done') continue;
|
|
if (it.nextAttemptAt && it.nextAttemptAt > now) continue;
|
|
await markStatus(it.id!, 'uploading');
|
|
const ok = await attemptUpload(it);
|
|
if (ok) {
|
|
await markStatus(it.id!, 'done');
|
|
} else {
|
|
const retries = (it.retries ?? 0) + 1;
|
|
const backoffSec = Math.min(60, Math.pow(2, Math.min(retries, 5))); // 2,4,8,16,32,60
|
|
await update(it.id!, { status: 'error', retries, nextAttemptAt: Date.now() + backoffSec * 1000 });
|
|
}
|
|
}
|
|
} finally {
|
|
processing = false;
|
|
}
|
|
}
|
|
|
|
async function attemptUpload(it: QueueItem): Promise<boolean> {
|
|
if (!navigator.onLine) return false;
|
|
try {
|
|
const json = await createUpload(
|
|
`/api/v1/events/${encodeURIComponent(it.slug)}/photos`,
|
|
it,
|
|
getDeviceId(),
|
|
(pct) => {
|
|
try {
|
|
window.dispatchEvent(new CustomEvent('queue-progress', { detail: { id: it.id, progress: pct } }));
|
|
} catch {}
|
|
}
|
|
);
|
|
// mark my-photo-ids for "Meine"
|
|
try {
|
|
const raw = localStorage.getItem('my-photo-ids');
|
|
const arr: number[] = raw ? JSON.parse(raw) : [];
|
|
if (json.id && !arr.includes(json.id)) localStorage.setItem('my-photo-ids', JSON.stringify([json.id, ...arr]));
|
|
} catch {}
|
|
notify('Upload erfolgreich', 'success');
|
|
return true;
|
|
} catch {
|
|
notify('Upload fehlgeschlagen', 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function markStatus(id: number, status: QueueItem['status']) {
|
|
await update(id, { status });
|
|
}
|
|
|
|
async function update(id: number, patch: Partial<QueueItem>) {
|
|
await withStore('readwrite', (store) => new Promise<void>((resolve, reject) => {
|
|
const getReq = store.get(id);
|
|
getReq.onsuccess = () => {
|
|
const val = getReq.result as QueueItem;
|
|
store.put({ ...val, ...patch, id });
|
|
resolve();
|
|
};
|
|
getReq.onerror = () => reject(getReq.error);
|
|
}));
|
|
}
|