feat: move offline work into IndexedDB (Closes #525)
This commit is contained in:
1 parent
7cadb36b41
commit
0a96dfb173
20 files changed
+451
-69
No files matched your search
+34
-27
@@ -1,4 +1,4 @@
|
||||
(function(){
|
||||
(async function(){
|
||||
const qs = (s, el=document) => el.querySelector(s);
|
||||
const fmt = (d) => new Date(d).toLocaleString();
|
||||
const cardPlanning = createCardPlanning(document);
|
||||
@@ -399,7 +399,8 @@
|
||||
const pullController = createPullSheet({ fetchJson: fetchReviewJson, storage: localStorage });
|
||||
const draftInbox = createDraftInbox({ storage: localStorage, getCurrentLogin: () => activeFlushLogin });
|
||||
outboxCoordinator.subscribe(() => refreshMyWorkView());
|
||||
const offlineWorkStore = createOfflineWorkStore({ storage: localStorage });
|
||||
const offlineWorkStore = createOfflineWorkStore({ storage: localStorage, indexedDB:window.indexedDB });
|
||||
await offlineWorkStore.ready();
|
||||
function renderOfflineTodayStatus(status) {
|
||||
const container = qs('#offline-today-readiness');
|
||||
const label = qs('#offline-today-status');
|
||||
@@ -656,7 +657,7 @@
|
||||
setOfflineUpdateControls(false);
|
||||
qs('#keep-update-unread').focus();
|
||||
},
|
||||
onDetail: detail => {
|
||||
onDetail: async detail => {
|
||||
qs('#update-sheet-title').textContent = detail.title || 'Unread update';
|
||||
qs('#update-subject-type').textContent = detail.subject_type || 'Update';
|
||||
qs('#update-subject-state').textContent = detail.state || '';
|
||||
@@ -667,7 +668,7 @@
|
||||
} else {
|
||||
updateOwnership.open(detail, selectedUpdate);
|
||||
if (offlineWorkStore.enabled() && confirmedOwnerLogin && selectedUpdate) {
|
||||
offlineWorkStore.saveDetail(confirmedOwnerLogin, selectedUpdate, detail);
|
||||
await offlineWorkStore.saveDetail(confirmedOwnerLogin, selectedUpdate, detail);
|
||||
}
|
||||
}
|
||||
qs('#retry-update-load').hidden = true;
|
||||
@@ -1549,11 +1550,11 @@
|
||||
setClock();
|
||||
}
|
||||
|
||||
function handleContextError(e) {
|
||||
async function handleContextError(e) {
|
||||
console.error('context failed', e);
|
||||
liveMode = false;
|
||||
activeFlushLogin = '';
|
||||
if (!hasContextSnapshot && hydrateOfflineWork('outage')) return;
|
||||
if (!hasContextSnapshot && await hydrateOfflineWork('outage')) return;
|
||||
const timeoutStatus = e.name === 'TimeoutError' ?
|
||||
'Update delayed · showing last snapshot' : 'Update failed · showing last snapshot';
|
||||
setStatus(hasContextSnapshot ? timeoutStatus : 'Unavailable');
|
||||
@@ -2268,7 +2269,7 @@
|
||||
qs('#issue-sheet-status').textContent = 'Offline copy · saved ' + fmt(detail.saved_at) +
|
||||
' · comments queue for sync';
|
||||
} else if (offlineWorkStore.enabled() && confirmedOwnerLogin && todayWork.contains(item)) {
|
||||
offlineWorkStore.saveDetail(confirmedOwnerLogin, item, detail);
|
||||
await offlineWorkStore.saveDetail(confirmedOwnerLogin, item, detail);
|
||||
}
|
||||
} catch (error) {
|
||||
if (selectedIssue !== item) return;
|
||||
@@ -2445,7 +2446,7 @@
|
||||
qs('#pull-sheet-status').textContent = 'Offline copy · saved ' + fmt(detail.saved_at) +
|
||||
' · comments queue for sync';
|
||||
} else if (offlineWorkStore.enabled() && confirmedOwnerLogin && todayWork.contains(item)) {
|
||||
offlineWorkStore.saveDetail(confirmedOwnerLogin, item, detail);
|
||||
await offlineWorkStore.saveDetail(confirmedOwnerLogin, item, detail);
|
||||
}
|
||||
} catch (error) {
|
||||
if (selectedPull !== item) return;
|
||||
@@ -3117,7 +3118,7 @@
|
||||
qs('#gitea-events-status').textContent = message;
|
||||
}
|
||||
|
||||
function renderLiveSnapshot(snapshot, changedSections = ['context', 'events', 'notifications']) {
|
||||
async function renderLiveSnapshot(snapshot, changedSections = ['context', 'events', 'notifications']) {
|
||||
setOfflineWorkMode(false);
|
||||
offlineStatus.hidden = true;
|
||||
const contextFreshness = snapshot.freshness?.sections?.context;
|
||||
@@ -3171,12 +3172,13 @@
|
||||
if (contextFreshness?.stale) markMyWorkStale();
|
||||
else if (!notificationsFresh) markNotificationsStale();
|
||||
if (!contextFreshness?.stale && notificationsFresh) {
|
||||
offlineWorkStore.save({
|
||||
const admitted = await offlineWorkStore.save({
|
||||
...snapshot.context,
|
||||
notifications: snapshot.notifications,
|
||||
notification_pagination: snapshot.notification_pagination,
|
||||
});
|
||||
updateOfflineWorkControls();
|
||||
await updateOfflineWorkControls(admitted === false ?
|
||||
'Offline saving unavailable · retry after reconnect.' : undefined);
|
||||
warmTodayOffline();
|
||||
}
|
||||
flushIssueOutbox();
|
||||
@@ -4690,8 +4692,8 @@
|
||||
const contextPoller = createContextPoller({
|
||||
fetchContext: fetchLiveSnapshot,
|
||||
onSnapshot: renderLiveSnapshot,
|
||||
onError: error => {
|
||||
handleContextError(error);
|
||||
onError: async error => {
|
||||
await handleContextError(error);
|
||||
setEventStreamStatus('Update failed · showing last activity');
|
||||
},
|
||||
isHidden: () => document.hidden,
|
||||
@@ -4704,9 +4706,9 @@
|
||||
const offlineWorkStatus = qs('#offline-work-status');
|
||||
const deliveryReceipts = qs('#delivery-receipts');
|
||||
const deliveryReceiptStatus = qs('#delivery-receipt-status');
|
||||
function updateOfflineWorkControls(message) {
|
||||
async function updateOfflineWorkControls(message) {
|
||||
keepWorkOffline.checked = offlineWorkStore.enabled();
|
||||
const saved = offlineWorkStore.load();
|
||||
const saved = await offlineWorkStore.load();
|
||||
offlineWorkStatus.textContent = message || (saved ? 'Saved ' + fmt(saved.saved_at) + ' · expires after 7 days.' :
|
||||
(keepWorkOffline.checked ? 'Waiting for a healthy live refresh.' : 'Off · no work data is stored.'));
|
||||
}
|
||||
@@ -4738,8 +4740,8 @@
|
||||
.forEach(button => { button.disabled = true; });
|
||||
}
|
||||
}
|
||||
function hydrateOfflineWork(mode = 'offline') {
|
||||
const saved = offlineWorkStore.load();
|
||||
async function hydrateOfflineWork(mode = 'offline') {
|
||||
const saved = await offlineWorkStore.load();
|
||||
if (!saved) return false;
|
||||
const outage = mode === 'outage';
|
||||
confirmedOwnerLogin = String(saved.user?.login || '').trim();
|
||||
@@ -4768,11 +4770,11 @@
|
||||
offlineStatus.hidden = false;
|
||||
return true;
|
||||
}
|
||||
function showOfflineStatus() {
|
||||
async function showOfflineStatus() {
|
||||
activeFlushLogin = '';
|
||||
offlineStatus.hidden = false;
|
||||
setStatus('Offline');
|
||||
if (!hasContextSnapshot) hydrateOfflineWork();
|
||||
if (!hasContextSnapshot) await hydrateOfflineWork();
|
||||
}
|
||||
function reconnectLiveData() {
|
||||
offlineStatus.hidden = true;
|
||||
@@ -4782,13 +4784,18 @@
|
||||
if (selectedReview && offlineReview) openReviewSheet(selectedReview, reviewTrigger);
|
||||
});
|
||||
}
|
||||
keepWorkOffline.addEventListener('change', () => {
|
||||
keepWorkOffline.addEventListener('change', async () => {
|
||||
offlineWorkStore.setEnabled(keepWorkOffline.checked);
|
||||
if (keepWorkOffline.checked && liveMode && lastContextSnapshot) {
|
||||
offlineWorkStore.save({ ...lastContextSnapshot, notifications:lastNotifications,
|
||||
const admitted = await offlineWorkStore.save({ ...lastContextSnapshot, notifications:lastNotifications,
|
||||
notification_pagination:notificationPagination });
|
||||
if (admitted === false) {
|
||||
await updateOfflineWorkControls('Offline saving unavailable · retry after reconnect.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
updateOfflineWorkControls(keepWorkOffline.checked ? 'Offline saving enabled.' : 'Offline work data cleared.');
|
||||
if (!keepWorkOffline.checked) await offlineWorkStore.clear();
|
||||
await updateOfflineWorkControls(keepWorkOffline.checked ? 'Offline saving enabled.' : 'Offline work data cleared.');
|
||||
if (keepWorkOffline.checked) warmTodayOffline();
|
||||
else offlineToday.cancel();
|
||||
});
|
||||
@@ -4804,18 +4811,18 @@
|
||||
await updateDeliveryReceiptControls(enabled ? 'Background delivery receipts enabled.' :
|
||||
(Notification.permission === 'denied' ? 'Notifications are blocked in browser settings.' : 'Background delivery receipts disabled.'));
|
||||
});
|
||||
qs('#clear-offline-work').addEventListener('click', () => {
|
||||
offlineWorkStore.clear();
|
||||
qs('#clear-offline-work').addEventListener('click', async () => {
|
||||
await offlineWorkStore.clear();
|
||||
offlineToday.cancel();
|
||||
renderOfflineTodayStatus({ total:0, ready:0, failed:0, pending:0 });
|
||||
updateOfflineWorkControls('Offline work data cleared.');
|
||||
await updateOfflineWorkControls('Offline work data cleared.');
|
||||
});
|
||||
qs('#retry-offline-today').addEventListener('click', () =>
|
||||
offlineToday.retry(confirmedOwnerLogin, todayMyWork)
|
||||
);
|
||||
updateOfflineWorkControls();
|
||||
await updateOfflineWorkControls();
|
||||
updateDeliveryReceiptControls();
|
||||
if (!navigator.onLine) showOfflineStatus();
|
||||
if (!navigator.onLine) await showOfflineStatus();
|
||||
window.addEventListener('offline', showOfflineStatus);
|
||||
window.addEventListener('online', reconnectLiveData);
|
||||
|
||||
|
||||
@@ -31,14 +31,18 @@
|
||||
}
|
||||
|
||||
const previousFailed = failed;
|
||||
const readyKeys = new Set(items.filter(item => loadSavedDetail(login, item)).map(itemKey));
|
||||
const savedDetails = new Map();
|
||||
await Promise.all(items.map(async item => {
|
||||
savedDetails.set(itemKey(item), await loadSavedDetail(login, item));
|
||||
}));
|
||||
const readyKeys = new Set(items.filter(item => savedDetails.get(itemKey(item))).map(itemKey));
|
||||
const snapshot = pending => ({
|
||||
total: items.length, ready: readyKeys.size, failed: failed.size, pending,
|
||||
});
|
||||
const candidates = items.filter(item => {
|
||||
const key = itemKey(item);
|
||||
if (onlyFailed && !previousFailed.has(key)) return false;
|
||||
const saved = loadSavedDetail(login, item);
|
||||
const saved = savedDetails.get(key);
|
||||
return onlyFailed || !saved || saved.source_updated_at !== item.updated_at;
|
||||
});
|
||||
failed = new Set();
|
||||
@@ -51,8 +55,9 @@
|
||||
try {
|
||||
const detail = await loadDetail(item);
|
||||
if (runGeneration !== generation) return;
|
||||
const saved = saveDetail(login, item, { ...detail, source_updated_at: item.updated_at });
|
||||
if (saved !== false) readyKeys.add(itemKey(item));
|
||||
const saved = await saveDetail(login, item, { ...detail, source_updated_at: item.updated_at });
|
||||
if (saved === false) throw new Error('Offline detail was not durably admitted.');
|
||||
readyKeys.add(itemKey(item));
|
||||
} catch (_error) {
|
||||
if (runGeneration === generation) failed.add(itemKey(item));
|
||||
}
|
||||
|
||||
+184
-2
@@ -27,6 +27,54 @@
|
||||
];
|
||||
const REVIEW_FIELDS = ['id', 'state', 'body', 'submitted_at'];
|
||||
|
||||
function createIndexedDbTransaction(indexedDB, dbName = 'stackchain-offline-work-v2') {
|
||||
if (!indexedDB) return null;
|
||||
let databasePromise;
|
||||
function database() {
|
||||
if (!databasePromise) databasePromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(dbName, 1);
|
||||
request.onupgradeneeded = () => {
|
||||
if (!request.result.objectStoreNames.contains('work')) {
|
||||
request.result.createObjectStore('work', { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
db.onversionchange = () => { db.close(); databasePromise = undefined; };
|
||||
resolve(db);
|
||||
};
|
||||
request.onerror = () => reject(request.error || new Error('Offline work database failed to open.'));
|
||||
});
|
||||
return databasePromise;
|
||||
}
|
||||
const requested = request => new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
return async work => {
|
||||
const db = await database();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction('work', 'readwrite');
|
||||
const store = transaction.objectStore('work');
|
||||
let result;
|
||||
let workError;
|
||||
transaction.oncomplete = () => workError ? undefined : resolve(result);
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
transaction.onabort = () => reject(workError || transaction.error || new Error('Offline work transaction aborted.'));
|
||||
Promise.resolve(work({
|
||||
get: key => requested(store.get(key)),
|
||||
getAll: () => requested(store.getAll()),
|
||||
put: value => requested(store.put(value)),
|
||||
delete: key => requested(store.delete(key)),
|
||||
clear: () => requested(store.clear()),
|
||||
})).then(value => { result = value; }).catch(error => {
|
||||
workError = error;
|
||||
try { transaction.abort(); } catch (_) { reject(error); }
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function pick(source, fields) {
|
||||
const output = {};
|
||||
fields.forEach(field => {
|
||||
@@ -36,14 +84,94 @@
|
||||
}
|
||||
|
||||
function createOfflineWorkStore({
|
||||
storage, now = () => new Date(), maxAgeMs = DEFAULT_MAX_AGE_MS, maxDetails = 10,
|
||||
storage, indexedDB, transaction, now = () => new Date(), maxAgeMs = DEFAULT_MAX_AGE_MS,
|
||||
maxDetails = 10,
|
||||
}) {
|
||||
const transact = transaction || createIndexedDbTransaction(indexedDB);
|
||||
let migrationPromise;
|
||||
let recordCache = null;
|
||||
function migrateLegacy() {
|
||||
if (!transact) return Promise.resolve(true);
|
||||
if (!migrationPromise) migrationPromise = (async () => {
|
||||
let snapshot = null;
|
||||
let details = [];
|
||||
try {
|
||||
snapshot = JSON.parse(storage.getItem(SNAPSHOT_KEY) || 'null');
|
||||
const parsedDetails = JSON.parse(storage.getItem(DETAILS_KEY) || '[]');
|
||||
details = Array.isArray(parsedDetails) ? parsedDetails : [];
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
if (!snapshot && !details.length) return true;
|
||||
await transact(async records => {
|
||||
if (snapshot && !(await records.get('snapshot'))) {
|
||||
await records.put({ ...snapshot, id:'snapshot' });
|
||||
}
|
||||
for (const record of details.slice(-Math.max(1, maxDetails))) {
|
||||
if (!record?.key || !record?.user_login || !record?.data) continue;
|
||||
const id = 'detail:' + record.user_login + ':' + record.key;
|
||||
if (!(await records.get(id))) await records.put({ ...record, id });
|
||||
}
|
||||
});
|
||||
storage.removeItem(SNAPSHOT_KEY);
|
||||
storage.removeItem(DETAILS_KEY);
|
||||
return true;
|
||||
})().catch(() => false);
|
||||
return migrationPromise;
|
||||
}
|
||||
function ready() {
|
||||
if (!transact) return Promise.resolve(true);
|
||||
if (recordCache) return Promise.resolve(true);
|
||||
return migrateLegacy().then(migrated => {
|
||||
if (!migrated) {
|
||||
recordCache = new Map();
|
||||
try {
|
||||
const snapshot = JSON.parse(storage.getItem(SNAPSHOT_KEY) || 'null');
|
||||
if (snapshot) recordCache.set('snapshot', { ...snapshot, id:'snapshot' });
|
||||
const details = JSON.parse(storage.getItem(DETAILS_KEY) || '[]');
|
||||
if (Array.isArray(details)) details.forEach(record => {
|
||||
if (!record?.key || !record?.user_login || !record?.data) return;
|
||||
const id = 'detail:' + record.user_login + ':' + record.key;
|
||||
recordCache.set(id, { ...record, id });
|
||||
});
|
||||
} catch (_) { recordCache.clear(); }
|
||||
return false;
|
||||
}
|
||||
return transact(async records => {
|
||||
const currentTime = now().getTime();
|
||||
const valid = [];
|
||||
for (const record of await records.getAll()) {
|
||||
const savedAt = Date.parse(record?.saved_at || '');
|
||||
const validShape = record?.id === 'snapshot' ?
|
||||
record.version === VERSION && record.user_login && record.data :
|
||||
record?.id?.startsWith('detail:') && record.user_login && record.key && record.data;
|
||||
if (!validShape || !Number.isFinite(savedAt) || currentTime - savedAt > maxAgeMs) {
|
||||
if (record?.id) await records.delete(record.id);
|
||||
} else valid.push(record);
|
||||
}
|
||||
recordCache = new Map(valid.map(record => [record.id, record]));
|
||||
return true;
|
||||
});
|
||||
}).catch(() => false);
|
||||
}
|
||||
function enabled() {
|
||||
try { return storage.getItem(ENABLED_KEY) === 'true'; }
|
||||
catch (_) { return false; }
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (transact) {
|
||||
try {
|
||||
storage.removeItem(SNAPSHOT_KEY);
|
||||
storage.removeItem(DETAILS_KEY);
|
||||
} catch (_) { /* IndexedDB remains the source of truth. */ }
|
||||
migrationPromise = Promise.resolve(true);
|
||||
return transact(async records => {
|
||||
await records.clear();
|
||||
recordCache = new Map();
|
||||
return true;
|
||||
}).catch(() => false);
|
||||
}
|
||||
try {
|
||||
storage.removeItem(SNAPSHOT_KEY);
|
||||
storage.removeItem(DETAILS_KEY);
|
||||
@@ -104,6 +232,22 @@
|
||||
saved_at: now().toISOString(),
|
||||
data,
|
||||
};
|
||||
if (transact) {
|
||||
record.id = 'detail:' + login + ':' + key;
|
||||
return ready().then(() => transact(async records => {
|
||||
const existing = (await records.getAll()).filter(candidate =>
|
||||
candidate?.id?.startsWith('detail:') && candidate.id !== record.id
|
||||
);
|
||||
await records.put(record);
|
||||
const overflow = existing.concat(record).sort((a, b) =>
|
||||
String(a.saved_at).localeCompare(String(b.saved_at))
|
||||
).slice(0, -Math.max(1, maxDetails));
|
||||
for (const candidate of overflow) await records.delete(candidate.id);
|
||||
overflow.forEach(candidate => recordCache.delete(candidate.id));
|
||||
recordCache.set(record.id, record);
|
||||
return true;
|
||||
})).catch(() => false);
|
||||
}
|
||||
const records = readDetails().filter(candidate =>
|
||||
!(candidate?.key === key && candidate?.user_login === login)
|
||||
);
|
||||
@@ -117,6 +261,19 @@
|
||||
const key = detailKey(item);
|
||||
login = String(login || '').trim();
|
||||
if (!login || !key) return null;
|
||||
if (transact) {
|
||||
const id = 'detail:' + login + ':' + key;
|
||||
if (!recordCache) return ready().then(() => loadDetail(login, item));
|
||||
const record = recordCache.get(id);
|
||||
if (!record) return null;
|
||||
const savedAt = Date.parse(record.saved_at || '');
|
||||
if (!record.data || !Number.isFinite(savedAt) || now().getTime() - savedAt > maxAgeMs) {
|
||||
recordCache.delete(id);
|
||||
transact(async records => { await records.delete(id); }).catch(() => {});
|
||||
return null;
|
||||
}
|
||||
return { ...record.data, saved_at: record.saved_at };
|
||||
}
|
||||
const records = readDetails();
|
||||
const currentTime = now().getTime();
|
||||
const valid = records.filter(record => {
|
||||
@@ -142,6 +299,7 @@
|
||||
function save(snapshot) {
|
||||
if (!enabled() || !snapshot?.user?.login) return false;
|
||||
const record = {
|
||||
id: 'snapshot',
|
||||
version: VERSION,
|
||||
user_login: String(snapshot.user.login),
|
||||
saved_at: now().toISOString(),
|
||||
@@ -154,12 +312,36 @@
|
||||
notification_pagination: snapshot.notification_pagination || {},
|
||||
},
|
||||
};
|
||||
if (transact) {
|
||||
return ready().then(() => transact(async records => {
|
||||
await records.put(record);
|
||||
recordCache.set(record.id, record);
|
||||
return true;
|
||||
})).catch(() => false);
|
||||
}
|
||||
try { storage.setItem(SNAPSHOT_KEY, JSON.stringify(record)); }
|
||||
catch (_) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
function load(expectedLogin) {
|
||||
if (transact) {
|
||||
if (!recordCache) return ready().then(() => load(expectedLogin));
|
||||
const record = recordCache.get('snapshot');
|
||||
const savedAt = Date.parse(record?.saved_at || '');
|
||||
const invalid = record?.version !== VERSION || !record?.user_login || !record?.data ||
|
||||
!Number.isFinite(savedAt);
|
||||
const expired = Number.isFinite(savedAt) && now().getTime() - savedAt > maxAgeMs;
|
||||
if (invalid || expired) {
|
||||
if (record) {
|
||||
recordCache.delete('snapshot');
|
||||
transact(async records => { await records.delete('snapshot'); }).catch(() => {});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (expectedLogin && record.user_login !== expectedLogin) return null;
|
||||
return { ...record.data, saved_at: record.saved_at };
|
||||
}
|
||||
let record;
|
||||
try { record = JSON.parse(storage.getItem(SNAPSHOT_KEY) || 'null'); }
|
||||
catch (_) { clear(); return null; }
|
||||
@@ -175,7 +357,7 @@
|
||||
return { ...record.data, saved_at: record.saved_at };
|
||||
}
|
||||
|
||||
return { enabled, setEnabled, save, load, saveDetail, loadDetail, clear };
|
||||
return { enabled, setEnabled, ready, save, load, saveDetail, loadDetail, clear };
|
||||
}
|
||||
|
||||
return createOfflineWorkStore;
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
function deletePrivateOutbox() {
|
||||
function deletePrivateDatabase(name) {
|
||||
if (!indexedDB) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
let request;
|
||||
try { request = indexedDB.deleteDatabase('stackchain-background-outbox-v1'); }
|
||||
try { request = indexedDB.deleteDatabase(name); }
|
||||
catch (error) { reject(error); return; }
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error || new Error('IndexedDB deletion failed.'));
|
||||
@@ -52,7 +52,8 @@
|
||||
removeOwnedStorage(localStorage);
|
||||
if (sessionStorage !== localStorage) removeOwnedStorage(sessionStorage);
|
||||
await stopWorkerOutbox();
|
||||
await deletePrivateOutbox();
|
||||
await deletePrivateDatabase('stackchain-background-outbox-v1');
|
||||
await deletePrivateDatabase('stackchain-offline-work-v2');
|
||||
const keys = await caches?.keys?.() || [];
|
||||
await Promise.all(
|
||||
keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v85';
|
||||
const CACHE = 'stackchain-dashboard-shell-v86';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
||||
+4
-3
@@ -572,11 +572,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
function deletePrivateOutbox() {
|
||||
function deletePrivateDatabase(name) {
|
||||
if (!indexedDB) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
let request;
|
||||
try { request = indexedDB.deleteDatabase('stackchain-background-outbox-v1'); }
|
||||
try { request = indexedDB.deleteDatabase(name); }
|
||||
catch (error) { reject(error); return; }
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error || new Error('IndexedDB deletion failed.'));
|
||||
@@ -589,7 +589,8 @@
|
||||
if (sessionStorage !== localStorage) removeDashboardStorage(sessionStorage);
|
||||
try {
|
||||
await stopWorkerOutbox();
|
||||
await deletePrivateOutbox();
|
||||
await deletePrivateDatabase('stackchain-background-outbox-v1');
|
||||
await deletePrivateDatabase('stackchain-offline-work-v2');
|
||||
} catch (_error) {
|
||||
const error = new Error('Could not clear private queued work from this device.');
|
||||
onClearError(error);
|
||||
|
||||
Reference in new issue
Block a user