fix: purge capture Draft screenshots (Closes #617)
All checks were successful
CI / lint (pull_request) Successful in 1m31s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-12 03:58:10 +00:00
parent f43f776479
commit ae07dd185c
6 changed files with 44 additions and 6 deletions

View File

@ -54,6 +54,7 @@
await stopWorkerOutbox(); await stopWorkerOutbox();
await deletePrivateDatabase('stackchain-background-outbox-v1'); await deletePrivateDatabase('stackchain-background-outbox-v1');
await deletePrivateDatabase('stackchain-offline-work-v2'); await deletePrivateDatabase('stackchain-offline-work-v2');
await deletePrivateDatabase('stackchain-unfiled-captures-v1');
const keys = await caches?.keys?.() || []; const keys = await caches?.keys?.() || [];
await Promise.all( await Promise.all(
keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key))

View File

@ -117,8 +117,21 @@ async function withSessionCsrf(work) {
} }
} }
async function deletePrivateDatabase(name) {
if (!indexedDB) return;
await new Promise((resolve, reject) => {
let request;
try { request = indexedDB.deleteDatabase(name); }
catch (error) { reject(error); return; }
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error || new Error('IndexedDB deletion failed.'));
request.onblocked = () => reject(new Error('IndexedDB deletion was blocked.'));
});
}
async function purgeRevokedSessionData() { async function purgeRevokedSessionData() {
await issueSync.purge(); await issueSync.purge();
await deletePrivateDatabase('stackchain-unfiled-captures-v1');
const keys = await caches.keys(); const keys = await caches.keys();
await Promise.all( await Promise.all(
keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key))
@ -155,6 +168,7 @@ async function offlineLeaseState(cache) {
async function expiredOfflineResponse() { async function expiredOfflineResponse() {
await issueSync.purge(); await issueSync.purge();
await deletePrivateDatabase('stackchain-unfiled-captures-v1');
const keys = await caches.keys(); const keys = await caches.keys();
await Promise.all( await Promise.all(
keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key)) keys.filter(key => key.startsWith('stackchain-dashboard-')).map(key => caches.delete(key))
@ -269,6 +283,7 @@ self.addEventListener('message', event => {
if (event.data?.type === 'stackchain-purge-outbox') event.waitUntil((async () => { if (event.data?.type === 'stackchain-purge-outbox') event.waitUntil((async () => {
try { try {
await issueSync.purge(); await issueSync.purge();
await deletePrivateDatabase('stackchain-unfiled-captures-v1');
event.ports?.[0]?.postMessage({ ok: true }); event.ports?.[0]?.postMessage({ ok: true });
} catch (error) { } catch (error) {
event.ports?.[0]?.postMessage({ ok: false, error: String(error?.message || 'Outbox purge failed.') }); event.ports?.[0]?.postMessage({ ok: false, error: String(error?.message || 'Outbox purge failed.') });

View File

@ -456,6 +456,7 @@
await stopWorkerOutbox(); await stopWorkerOutbox();
await deletePrivateDatabase('stackchain-background-outbox-v1'); await deletePrivateDatabase('stackchain-background-outbox-v1');
await deletePrivateDatabase('stackchain-offline-work-v2'); await deletePrivateDatabase('stackchain-offline-work-v2');
await deletePrivateDatabase('stackchain-unfiled-captures-v1');
} catch (_error) { } catch (_error) {
const error = new Error('Could not clear private queued work from this device.'); const error = new Error('Could not clear private queued work from this device.');
onClearError(error); onClearError(error);

View File

@ -348,7 +348,8 @@ process.stdout.write(JSON.stringify(state));
assert result["deletedAtIdle"] == [] assert result["deletedAtIdle"] == []
assert result["deletedDatabases"] == [ assert result["deletedDatabases"] == [
"stackchain-background-outbox-v1", "stackchain-offline-work-v2" "stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1",
] ]
assert result["remaining"] == ["gitea.preference"] assert result["remaining"] == ["gitea.preference"]
assert result["replaced"] == [ assert result["replaced"] == [
@ -373,7 +374,8 @@ process.stdout.write(JSON.stringify(state));
assert result["valid"] is False assert result["valid"] is False
assert result["remaining"] == ["gitea.preference"] assert result["remaining"] == ["gitea.preference"]
assert result["deletedDatabases"] == [ assert result["deletedDatabases"] == [
"stackchain-background-outbox-v1", "stackchain-offline-work-v2" "stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1",
] ]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["replaced"] == ["/dashboard/login?reason=session-expired"] assert result["replaced"] == ["/dashboard/login?reason=session-expired"]
@ -503,7 +505,8 @@ process.stdout.write(JSON.stringify(state));
] ]
assert result["remaining"] == ["gitea.preference"] assert result["remaining"] == ["gitea.preference"]
assert result["deletedDatabases"] == [ assert result["deletedDatabases"] == [
"stackchain-background-outbox-v1", "stackchain-offline-work-v2" "stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1",
] ]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}] assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
@ -525,7 +528,8 @@ process.stdout.write(JSON.stringify(state));
assert result["remaining"] == ["gitea.preference"] assert result["remaining"] == ["gitea.preference"]
assert result["deletedDatabases"] == [ assert result["deletedDatabases"] == [
"stackchain-background-outbox-v1", "stackchain-offline-work-v2" "stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1",
] ]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}] assert result["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
@ -620,7 +624,8 @@ process.stdout.write(JSON.stringify(state));
assert "stackchain.private" in result["removed"] assert "stackchain.private" in result["removed"]
assert result["remaining"] == ["gitea.preference"] assert result["remaining"] == ["gitea.preference"]
assert result["deletedDatabases"] == [ assert result["deletedDatabases"] == [
"stackchain-background-outbox-v1", "stackchain-offline-work-v2" "stackchain-background-outbox-v1", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1",
] ]
assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"] assert result["deletedCaches"] == ["stackchain-dashboard-shell-v15"]
assert result["assigned"] == "/dashboard/login" assert result["assigned"] == "/dashboard/login"

View File

@ -46,6 +46,7 @@ const clear = createPrivateDeviceDataPurger({{
assert state["databases"] == [ assert state["databases"] == [
"stackchain-background-outbox-v1", "stackchain-background-outbox-v1",
"stackchain-offline-work-v2", "stackchain-offline-work-v2",
"stackchain-unfiled-captures-v1",
] ]
assert state["caches"] == ["stackchain-dashboard-shell-v37"] assert state["caches"] == ["stackchain-dashboard-shell-v37"]
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}] assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]

View File

@ -13,7 +13,7 @@ def run_worker_scenario(scenario: str) -> dict:
const fs = require('fs'); const fs = require('fs');
const vm = require('vm'); const vm = require('vm');
const listeners = {{}}; const listeners = {{}};
const state = {{ added: [], deleted: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }}; const state = {{ added: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
const storedResponses = new Map(); const storedResponses = new Map();
storedResponses.set( storedResponses.set(
'https://forge.example/dashboard/__offline-session-lease', 'https://forge.example/dashboard/__offline-session-lease',
@ -58,6 +58,12 @@ const context = {{
}}, }},
}}, }},
importScripts: () => {{}}, importScripts: () => {{}},
indexedDB: {{deleteDatabase:name=>{{
state.deletedDatabases.push(name);
const request={{}};
queueMicrotask(()=>request.onsuccess?.());
return request;
}}}},
caches: {{ caches: {{
open: async () => cache, open: async () => cache,
keys: async () => ['stackchain-dashboard-old', 'another-app-cache'], keys: async () => ['stackchain-dashboard-old', 'another-app-cache'],
@ -244,6 +250,9 @@ def test_revoked_background_session_purges_worker_data_and_notifies_dashboard_cl
assert result["outcome"] == {"status": 401, "code": "session_revoked"} assert result["outcome"] == {"status": 401, "code": "session_revoked"}
assert result["state"]["outboxPurges"] == 1 assert result["state"]["outboxPurges"] == 1
assert result["state"]["deletedDatabases"] == [
"stackchain-unfiled-captures-v1"
]
assert result["state"]["deleted"] == ["stackchain-dashboard-old"] assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
assert result["state"]["clientMessages"] == [ assert result["state"]["clientMessages"] == [
{"type": "stackchain-session-revoked"} {"type": "stackchain-session-revoked"}
@ -331,6 +340,9 @@ def test_device_purge_message_stops_worker_outbox_and_acknowledges_completion():
) )
assert result["state"]["outboxPurges"] == 1 assert result["state"]["outboxPurges"] == 1
assert result["state"]["deletedDatabases"] == [
"stackchain-unfiled-captures-v1"
]
assert result["replies"] == [{"ok": True}] assert result["replies"] == [{"ok": True}]
@ -773,6 +785,9 @@ def test_expired_offline_lease_purges_private_worker_data_and_refuses_cached_she
assert result["status"] == 401 assert result["status"] == 401
assert result["body"] == "Your Stackchain session expired. Reconnect and sign in." assert result["body"] == "Your Stackchain session expired. Reconnect and sign in."
assert result["state"]["outboxPurges"] == 1 assert result["state"]["outboxPurges"] == 1
assert result["state"]["deletedDatabases"] == [
"stackchain-unfiled-captures-v1"
]
assert result["state"]["deleted"] == ["stackchain-dashboard-old"] assert result["state"]["deleted"] == ["stackchain-dashboard-old"]
assert "private cached dashboard" not in result["body"] assert "private cached dashboard" not in result["body"]