fix(gallery): clear only Prompt Manager cache entries (#222)
The legacy gallery's Clear cache deleted every localStorage key starting pm_ or promptManager_ (the workspace theme and the dismissed update notice among them) and every CacheStorage cache whose name held gallery or thumbnail, which on ComfyUI's shared origin can belong to another tool. web/js/cache-ownership.js names what a clear may remove from an inventory of the real writers: every key Prompt Manager writes is a preference and no page opens CacheStorage or IndexedDB, so nothing is removed and the toast says so. A test fails when a new storage writer is not classified. The server thumbnail clear stays its own request; a refused or unreachable server changes nothing and is reported, and refused browser storage is reported as such.
This commit is contained in:
@@ -99,6 +99,34 @@ for (const leave of ['beforeunload', 'pagehide']) {
|
||||
});
|
||||
}
|
||||
|
||||
// #222: the gallery's Clear cache clears the server's thumbnails by its own request and keeps this browser's
|
||||
// preferences and other tools' storage; nothing of Prompt Manager's is cached in a browser, and the toast says so.
|
||||
test('the gallery cache clear keeps preferences and other tools caches', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
if (sessionStorage.getItem('seeded')) return;
|
||||
sessionStorage.setItem('seeded', '1');
|
||||
localStorage.setItem('pm_workspace_theme', 'light');
|
||||
localStorage.setItem('other-tool-setting', 'keep');
|
||||
localStorage.setItem('pm_other_tool', 'keep');
|
||||
});
|
||||
await page.goto('/prompt_manager/gallery');
|
||||
await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2);
|
||||
await page.evaluate(async () => { await (await caches.open('other-tool-gallery-thumbnails')).put('/x', new Response('x')); });
|
||||
const clears = [];
|
||||
page.on('request', req => { if (req.url().includes('/clear-thumbnails')) clears.push(req.method()); });
|
||||
page.once('dialog', dialog => dialog.accept());
|
||||
await page.locator('#settingsBtn').click();
|
||||
await page.locator('#clearCacheBtn').click();
|
||||
await expect(page.getByText("Thumbnails cleared; original images untouched. Nothing of Prompt Manager's is cached in this browser."))
|
||||
.toBeVisible();
|
||||
expect(clears).toEqual(['POST']);
|
||||
expect(await page.evaluate(async () => ({
|
||||
theme: localStorage.getItem('pm_workspace_theme'), other: localStorage.getItem('other-tool-setting'),
|
||||
prefixed: localStorage.getItem('pm_other_tool'), notice: localStorage.getItem('pm_update_notice_dismissed'),
|
||||
caches: await caches.keys(),
|
||||
}))).toEqual({ theme: 'light', other: 'keep', prefixed: 'keep', notice: '3.2.0', caches: ['other-tool-gallery-thumbnails'] });
|
||||
});
|
||||
|
||||
test('actual gallery review generates and persists tags for the selected root', async ({ page, request }) => {
|
||||
await page.goto('/prompt_manager/gallery');
|
||||
await expect(page.locator('#galleryGrid .image-item')).toHaveCount(2);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// #222: what "clear the cache" may remove from this browser. Only entries Prompt Manager itself writes as a cache count,
|
||||
// from an inventory of the real writers (never a guessed prefix): preferences (the workspace theme, the update notice,
|
||||
// the display defaults) and other tools' storage and caches on the shared ComfyUI origin always stay.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const CacheOwnership = require('../../web/js/cache-ownership.js');
|
||||
|
||||
const WEB = path.join(__dirname, '../../web');
|
||||
|
||||
function memoryStorage(entries) {
|
||||
const map = new Map(Object.entries(entries));
|
||||
return {
|
||||
map,
|
||||
get length() { return map.size; },
|
||||
key: index => [...map.keys()][index] ?? null,
|
||||
getItem: key => (map.has(key) ? map.get(key) : null),
|
||||
setItem: (key, value) => map.set(key, String(value)),
|
||||
removeItem: key => map.delete(key),
|
||||
};
|
||||
}
|
||||
|
||||
function memoryCaches(names, { failDelete = new Set() } = {}) {
|
||||
const set = new Set(names);
|
||||
return {
|
||||
set,
|
||||
keys: async () => [...set],
|
||||
delete: async name => {
|
||||
if (failDelete.has(name)) throw new Error('denied');
|
||||
return set.delete(name);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const PREFERENCES = { gallerySettings: '{"viewMode":"grid"}', pm_workspace_theme: 'light', pm_update_notice_dismissed: '3.3.0' };
|
||||
const OWNED = Object.freeze({ storageKeys: ['pm-owned-test-entry'], cacheNames: ['pm-owned-test-cache'] });
|
||||
|
||||
test('keeps pm_workspace_theme, pm_update_notice_dismissed and gallerySettings', async () => {
|
||||
const storage = memoryStorage({ ...PREFERENCES, 'pm-owned-test-entry': 'x', pm_cached: '1', promptManager_x: '1' });
|
||||
const result = await CacheOwnership.clearOwned({ storage, cacheStorage: null, owned: OWNED });
|
||||
assert.deepEqual(result.removed, ['pm-owned-test-entry']);
|
||||
for (const [key, value] of Object.entries(PREFERENCES)) assert.equal(storage.getItem(key), value, key);
|
||||
// The old prefix rule's matches are not Prompt Manager caches either: nothing writes them.
|
||||
assert.equal(storage.getItem('pm_cached'), '1');
|
||||
assert.equal(storage.getItem('promptManager_x'), '1');
|
||||
for (const key of Object.keys(PREFERENCES)) assert.ok(CacheOwnership.PREFERENCE_KEYS.includes(key), key);
|
||||
});
|
||||
|
||||
test("keeps other tools' CacheStorage named gallery/thumbnail", async () => {
|
||||
const caches = memoryCaches(['other-gallery', 'thumbnail-cache', 'my-gallery-thumbnails', 'pm-owned-test-cache']);
|
||||
const result = await CacheOwnership.clearOwned({ storage: memoryStorage({}), cacheStorage: caches, owned: OWNED });
|
||||
assert.deepEqual([...caches.set].sort(), ['my-gallery-thumbnails', 'other-gallery', 'thumbnail-cache']);
|
||||
assert.deepEqual(result.removed, ['pm-owned-test-cache']);
|
||||
});
|
||||
|
||||
test('removes only inventoried owned keys', async () => {
|
||||
// The shipped inventory: no Prompt Manager writer stores a cache entry in this browser.
|
||||
assert.deepEqual(CacheOwnership.OWNED, { storageKeys: [], cacheNames: [] });
|
||||
assert.ok(Object.isFrozen(CacheOwnership.OWNED.storageKeys) && Object.isFrozen(CacheOwnership.OWNED.cacheNames));
|
||||
const storage = memoryStorage({ ...PREFERENCES, gallery_cache_1: 'a', metadata_cache_2: 'b', other: 'c' });
|
||||
const caches = memoryCaches(['gallery', 'thumbnail']);
|
||||
const result = await CacheOwnership.clearOwned({ storage, cacheStorage: caches });
|
||||
assert.deepEqual(result, { removed: [], failed: [], denied: false, partial: false });
|
||||
assert.equal(storage.map.size, 6);
|
||||
assert.equal(caches.set.size, 2);
|
||||
assert.equal(CacheOwnership.clearMessage(result), CacheOwnership.NOTHING_CACHED);
|
||||
});
|
||||
|
||||
test('the inventory covers every browser storage writer in web/', () => {
|
||||
// Every key a Prompt Manager page writes is a preference or an inventoried cache entry; a new writer must be
|
||||
// classified here before a cache clear may remove it.
|
||||
const files = [];
|
||||
const walk = dir => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) { if (!['lib', 'toolkit'].includes(entry.name)) walk(full); }
|
||||
else if (/\.(m?js|html)$/.test(entry.name)) files.push(full);
|
||||
}
|
||||
};
|
||||
walk(WEB);
|
||||
const written = new Set();
|
||||
const constants = {};
|
||||
for (const file of files) {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
for (const match of text.matchAll(/(?:const|export const)\s+([A-Z_]+_KEY)\s*=\s*'([^']+)'/g)) constants[match[1]] = match[2];
|
||||
assert.equal(/caches\.open\(/.test(text), false, `${path.relative(WEB, file)} opens a CacheStorage cache`);
|
||||
assert.equal(/indexedDB\.open\(/.test(text), false, `${path.relative(WEB, file)} opens IndexedDB`);
|
||||
for (const match of text.matchAll(/\.setItem\(\s*([^,)]+)/g)) {
|
||||
const literal = /^(?:'([^']+)'|"([^"]+)")$/.exec(match[1].trim());
|
||||
const constant = /^[A-Z_]+_KEY$/.test(match[1].trim()) ? `$${match[1].trim()}` : null;
|
||||
// A key computed at run time could be anything: it must be a literal or a named key constant.
|
||||
assert.ok(literal || constant, `${path.relative(WEB, file)} writes a computed key: ${match[1]}`);
|
||||
written.add(literal ? literal[1] ?? literal[2] : constant);
|
||||
}
|
||||
}
|
||||
const keys = [...written].map(key => (key.startsWith('$') ? constants[key.slice(1)] : key));
|
||||
assert.ok(keys.length >= 3, 'the writers were found');
|
||||
for (const key of keys) {
|
||||
assert.ok(key, 'every written key resolves');
|
||||
assert.ok(CacheOwnership.PREFERENCE_KEYS.includes(key) || CacheOwnership.OWNED.storageKeys.includes(key), key);
|
||||
}
|
||||
});
|
||||
|
||||
test('storage denial reports partial', async () => {
|
||||
const denied = { get length() { throw new Error('SecurityError'); }, key() { throw new Error('SecurityError'); },
|
||||
getItem() { throw new Error('SecurityError'); }, removeItem() { throw new Error('SecurityError'); } };
|
||||
const result = await CacheOwnership.clearOwned({ storage: denied, cacheStorage: null, owned: OWNED });
|
||||
assert.equal(result.denied, true);
|
||||
assert.equal(result.partial, true);
|
||||
assert.equal(CacheOwnership.clearMessage(result), CacheOwnership.STORAGE_DENIED);
|
||||
|
||||
const caches = memoryCaches(['pm-owned-test-cache'], { failDelete: new Set(['pm-owned-test-cache']) });
|
||||
const storage = memoryStorage({ 'pm-owned-test-entry': 'x' });
|
||||
const partial = await CacheOwnership.clearOwned({ storage, cacheStorage: caches, owned: OWNED });
|
||||
assert.deepEqual(partial.removed, ['pm-owned-test-entry']);
|
||||
assert.deepEqual(partial.failed, ['pm-owned-test-cache']);
|
||||
assert.equal(partial.partial, true);
|
||||
assert.equal(CacheOwnership.clearMessage(partial), 'Cleared 1 of 2 cached entries; this browser refused the rest.');
|
||||
|
||||
const keysDenied = { keys: async () => { throw new Error('denied'); } };
|
||||
const unreadable = await CacheOwnership.clearOwned({ storage: memoryStorage({}), cacheStorage: keysDenied, owned: OWNED });
|
||||
assert.equal(unreadable.denied, true);
|
||||
});
|
||||
|
||||
test('server thumbnail clear stays a separate explicit call', async () => {
|
||||
const calls = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = async (...args) => { calls.push(args); return { ok: true }; };
|
||||
try {
|
||||
const storage = memoryStorage({ 'pm-owned-test-entry': 'x' });
|
||||
const result = await CacheOwnership.clearOwned({ storage, cacheStorage: memoryCaches([]), owned: OWNED });
|
||||
assert.deepEqual(result.removed, ['pm-owned-test-entry']);
|
||||
assert.equal(CacheOwnership.clearMessage(result), 'Cleared 1 cached entry from this browser.');
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
assert.deepEqual(calls, []);
|
||||
assert.equal(/fetch|clear-thumbnails|ApiClient/.test(fs.readFileSync(path.join(WEB, 'js/cache-ownership.js'), 'utf8')), false);
|
||||
});
|
||||
|
||||
test('ownedEntries lists what a clear would remove, reading nothing it cannot', async () => {
|
||||
const storage = memoryStorage({ ...PREFERENCES, 'pm-owned-test-entry': 'x' });
|
||||
const caches = memoryCaches(['pm-owned-test-cache', 'gallery']);
|
||||
assert.deepEqual(await CacheOwnership.ownedEntries({ storage, cacheStorage: caches, owned: OWNED }),
|
||||
{ keys: ['pm-owned-test-entry'], caches: ['pm-owned-test-cache'], denied: false });
|
||||
assert.deepEqual(await CacheOwnership.ownedEntries({ storage, cacheStorage: caches }),
|
||||
{ keys: [], caches: [], denied: false });
|
||||
assert.deepEqual(await CacheOwnership.ownedEntries({ storage: null, cacheStorage: undefined, owned: OWNED }),
|
||||
{ keys: [], caches: [], denied: false });
|
||||
assert.equal(CacheOwnership.clearMessage({ removed: ['a', 'b'], failed: [], denied: false, partial: false }),
|
||||
'Cleared 2 cached entries from this browser.');
|
||||
});
|
||||
@@ -32,8 +32,11 @@ function sseResponse(frames) {
|
||||
function load(file, className, fetchUrl, extra = {}, dom = elements()) {
|
||||
const timers = [];
|
||||
const notifications = [];
|
||||
const storage = new Map([['pm_cached', '1'], ['userData', 'keep']]);
|
||||
const storage = new Map([['pm_cached', '1'], ['userData', 'keep'], ['pm_workspace_theme', 'light'],
|
||||
['pm_update_notice_dismissed', '3.3.0']]);
|
||||
const localStorage = {
|
||||
get length() { return storage.size; },
|
||||
key: index => [...storage.keys()][index] ?? null,
|
||||
getItem: key => storage.get(key) ?? null,
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
removeItem: key => storage.delete(key),
|
||||
@@ -45,11 +48,12 @@ function load(file, className, fetchUrl, extra = {}, dom = elements()) {
|
||||
setTimeout: callback => timers.push(callback),
|
||||
confirm: () => true,
|
||||
localStorage,
|
||||
Object: { ...Object, keys: target => (target === localStorage ? [...storage.keys()] : Object.keys(target)) },
|
||||
TextDecoder, TextEncoder, JSON,
|
||||
ApiClient: { fetchUrl },
|
||||
...extra,
|
||||
});
|
||||
// gallery.html loads cache-ownership.js before gallery.js (#222).
|
||||
vm.runInContext(fs.readFileSync(path.join(__dirname, '../../web/js/cache-ownership.js'), 'utf8'), context);
|
||||
const source = fs.readFileSync(path.join(__dirname, `../../web/js/${file}`), 'utf8');
|
||||
const Page = vm.runInContext(`${source}\n${className};`, context, { filename: path.join(__dirname, `../../web/js/${file}`) });
|
||||
const page = Object.create(Page.prototype);
|
||||
@@ -78,18 +82,68 @@ test('gallery cache clear reports a refused server clear and never claims succes
|
||||
assert.equal(storage.get('gallerySettings'), undefined); // thumbnailsGenerated is not reset
|
||||
});
|
||||
|
||||
test('gallery cache clear still says so when the server clears', async () => {
|
||||
const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager',
|
||||
async () => jsonResponse(200, { success: true, cleared_count: 3 }));
|
||||
test('gallery cache clear says what it cleared and keeps preferences and other data (#222)', async () => {
|
||||
const calls = [];
|
||||
const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager', async url => {
|
||||
calls.push(url);
|
||||
return jsonResponse(200, { success: true, cleared_count: 3 });
|
||||
});
|
||||
page.getSettings = () => ({ thumbnailsGenerated: true });
|
||||
|
||||
await page.clearCache();
|
||||
|
||||
assert.deepEqual(notifications, [['success', 'PromptManager cache cleared successfully! Original images untouched.']]);
|
||||
// The server's thumbnails are cleared by their own explicit call; nothing Prompt Manager owns is cached in this
|
||||
// browser, so no browser entry is removed and the toast says exactly that.
|
||||
assert.deepEqual(calls, ['/prompt_manager/images/clear-thumbnails']);
|
||||
assert.deepEqual(notifications, [['success',
|
||||
"Thumbnails cleared; original images untouched. Nothing of Prompt Manager's is cached in this browser."]]);
|
||||
assert.equal(dom.nodes.thumbnailStatus.textContent, 'Not generated');
|
||||
assert.equal(JSON.parse(storage.get('gallerySettings')).thumbnailsGenerated, false);
|
||||
assert.equal(storage.get('userData'), 'keep');
|
||||
assert.equal(storage.has('pm_cached'), false);
|
||||
assert.equal(storage.get('pm_cached'), '1');
|
||||
assert.equal(storage.get('pm_workspace_theme'), 'light');
|
||||
assert.equal(storage.get('pm_update_notice_dismissed'), '3.3.0');
|
||||
});
|
||||
|
||||
test("gallery cache clear never deletes another tool's gallery or thumbnail caches (#222)", async () => {
|
||||
const names = new Set(['comfy-gallery', 'thumbnail-cache']);
|
||||
const caches = { keys: async () => [...names], delete: async name => names.delete(name) };
|
||||
const { page, notifications } = load('gallery.js', 'GalleryManager',
|
||||
async () => jsonResponse(200, { success: true }), { caches, window: { caches } });
|
||||
page.getSettings = () => ({});
|
||||
|
||||
await page.clearCache();
|
||||
|
||||
assert.deepEqual([...names], ['comfy-gallery', 'thumbnail-cache']);
|
||||
assert.equal(notifications.at(-1)[0], 'success');
|
||||
});
|
||||
|
||||
test('gallery cache clear reports a lost server clear instead of success', async () => {
|
||||
const { page, dom, notifications, storage } = load('gallery.js', 'GalleryManager', async () => {
|
||||
throw new TypeError('Failed to fetch');
|
||||
});
|
||||
page.getSettings = () => ({ thumbnailsGenerated: true });
|
||||
dom.getElementById('thumbnailStatus').textContent = 'Generated';
|
||||
|
||||
await page.clearCache();
|
||||
|
||||
assert.deepEqual(notifications, [['error', 'Thumbnails were not cleared: the server could not be reached.']]);
|
||||
assert.equal(dom.nodes.thumbnailStatus.textContent, 'Generated');
|
||||
assert.equal(storage.get('gallerySettings'), undefined);
|
||||
});
|
||||
|
||||
test('gallery cache clear says so when browser storage is denied', async () => {
|
||||
const denied = { get length() { throw new Error('SecurityError'); }, key() { throw new Error('SecurityError'); },
|
||||
getItem() { throw new Error('SecurityError'); }, setItem() { throw new Error('SecurityError'); },
|
||||
removeItem() { throw new Error('SecurityError'); } };
|
||||
const { page, notifications } = load('gallery.js', 'GalleryManager',
|
||||
async () => jsonResponse(200, { success: true }), { localStorage: denied });
|
||||
page.getSettings = () => ({});
|
||||
|
||||
await page.clearCache();
|
||||
|
||||
assert.deepEqual(notifications, [['warning',
|
||||
'Thumbnails cleared; original images untouched. This browser refused access to its storage, so nothing there was cleared.']]);
|
||||
});
|
||||
|
||||
test('admin LoRA import shows a refusal frame and closes its modal', async () => {
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
"functions": 98,
|
||||
"statements": 98
|
||||
},
|
||||
"cache-ownership.js": {
|
||||
"lines": 80,
|
||||
"branches": 80,
|
||||
"functions": 80,
|
||||
"statements": 80
|
||||
},
|
||||
"classic-node.js": {
|
||||
"lines": 98,
|
||||
"branches": 79,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<script src="js/html-escape.js"></script>
|
||||
<script src="js/image-helpers.js"></script>
|
||||
<script src="js/gallery-settings.js"></script>
|
||||
<script src="js/cache-ownership.js"></script>
|
||||
<script src="js/scan-progress.js"></script>
|
||||
<script src="js/api-client.js"></script>
|
||||
<script src="js/png-metadata.js"></script>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* What "clear the cache" may remove from this browser (#222).
|
||||
*
|
||||
* ComfyUI serves every custom node from one origin, so a browser's localStorage and CacheStorage are shared with other
|
||||
* tools. A cache clear removes only the entries Prompt Manager itself writes as a cache, listed by name in `OWNED` from
|
||||
* an inventory of the real writers, never by a guessed prefix. The inventory (2026-10-24, enforced by
|
||||
* tests/js/cache-ownership.test.js): every key a Prompt Manager page writes is a preference (`PREFERENCE_KEYS`: the
|
||||
* display defaults, the workspace theme, the dismissed update notice), and no page opens a CacheStorage cache or
|
||||
* IndexedDB. So `OWNED` is empty: nothing of Prompt Manager's is cached in this browser, and a clear says so instead of
|
||||
* claiming it removed something.
|
||||
*
|
||||
* Server thumbnails are not browser data: clearing them is the caller's own, separate request. This module never makes
|
||||
* a request.
|
||||
*
|
||||
* Loadable in the browser (window.CacheOwnership) and in Node for tests.
|
||||
*/
|
||||
(function (root) {
|
||||
"use strict";
|
||||
|
||||
const PREFERENCE_KEYS = Object.freeze(["gallerySettings", "pm_workspace_theme", "pm_update_notice_dismissed"]);
|
||||
const OWNED = Object.freeze({ storageKeys: Object.freeze([]), cacheNames: Object.freeze([]) });
|
||||
const NOTHING_CACHED = "Nothing of Prompt Manager's is cached in this browser.";
|
||||
const STORAGE_DENIED = "This browser refused access to its storage, so nothing there was cleared.";
|
||||
|
||||
// What a storage getter that threw (the browser refused storage) resolves to.
|
||||
const REFUSED = Object.freeze({});
|
||||
|
||||
/** `storage`, or what its getter returns; `REFUSED` when the getter throws. */
|
||||
function resolveStorage(storage) {
|
||||
if (typeof storage !== "function") return storage;
|
||||
try {
|
||||
return storage() ?? null;
|
||||
} catch {
|
||||
return REFUSED;
|
||||
}
|
||||
}
|
||||
|
||||
/** The keys `storage` holds, or null when the browser refuses to say. */
|
||||
function storageKeys(storage) {
|
||||
if (storage === REFUSED) return null;
|
||||
if (!storage) return [];
|
||||
try {
|
||||
const keys = [];
|
||||
for (let index = 0; index < storage.length; index += 1) keys.push(storage.key(index));
|
||||
return keys;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheNames(cacheStorage) {
|
||||
if (!cacheStorage || typeof cacheStorage.keys !== "function") return [];
|
||||
try {
|
||||
return await cacheStorage.keys();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The owned entries present now: `{keys, caches, denied}`; `denied` when the browser refused to list its storage or
|
||||
* caches.
|
||||
*/
|
||||
async function ownedEntries({ storage, cacheStorage, owned = OWNED } = {}) {
|
||||
const present = storageKeys(resolveStorage(storage));
|
||||
const names = await cacheNames(cacheStorage);
|
||||
return {
|
||||
keys: (present ?? []).filter(key => owned.storageKeys.includes(key)),
|
||||
caches: (names ?? []).filter(name => owned.cacheNames.includes(name)),
|
||||
denied: present === null || names === null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the owned entries present now. `storage` is a Storage or a getter for one (`() => localStorage`, which a
|
||||
* browser refusing storage makes throw). Returns `{removed, failed, denied, partial}`: what was removed and what the
|
||||
* browser refused to remove, by name; `partial` when anything was refused, including listing the storage.
|
||||
*/
|
||||
async function clearOwned({ storage, cacheStorage, owned = OWNED } = {}) {
|
||||
storage = resolveStorage(storage);
|
||||
const { keys, caches, denied } = await ownedEntries({ storage, cacheStorage, owned });
|
||||
const removed = [];
|
||||
const failed = [];
|
||||
for (const key of keys) {
|
||||
try {
|
||||
storage.removeItem(key);
|
||||
removed.push(key);
|
||||
} catch {
|
||||
failed.push(key);
|
||||
}
|
||||
}
|
||||
for (const name of caches) {
|
||||
try {
|
||||
await cacheStorage.delete(name);
|
||||
removed.push(name);
|
||||
} catch {
|
||||
failed.push(name);
|
||||
}
|
||||
}
|
||||
return { removed, failed, denied, partial: denied || failed.length > 0 };
|
||||
}
|
||||
|
||||
const entries = count => `${count.toLocaleString("en-US")} cached ${count === 1 ? "entry" : "entries"}`;
|
||||
|
||||
/** What a clear's result says, truthfully. */
|
||||
function clearMessage(result) {
|
||||
const total = result.removed.length + result.failed.length;
|
||||
if (result.failed.length) {
|
||||
return `Cleared ${result.removed.length.toLocaleString("en-US")} of ${entries(total)}; this browser refused the rest.`;
|
||||
}
|
||||
if (result.denied) return STORAGE_DENIED;
|
||||
if (!total) return NOTHING_CACHED;
|
||||
return `Cleared ${entries(total)} from this browser.`;
|
||||
}
|
||||
|
||||
const api = { PREFERENCE_KEYS, OWNED, NOTHING_CACHED, STORAGE_DENIED, ownedEntries, clearOwned, clearMessage };
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
else root.CacheOwnership = api;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
+36
-53
@@ -2166,61 +2166,44 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear cache (#222): the server's thumbnails by their own request, then only the browser entries Prompt
|
||||
* Manager owns as a cache (cache-ownership.js; preferences and other tools' storage stay). Nothing is
|
||||
* claimed that did not happen: a refused or lost server clear changes nothing here, and the toast says what
|
||||
* the browser part removed, or that nothing of Prompt Manager's is cached, or that storage was refused.
|
||||
*/
|
||||
async clearCache() {
|
||||
if (confirm('Clear PromptManager cache and thumbnails? This will NOT affect your original images.')) {
|
||||
try {
|
||||
// Clear only PromptManager-related browser caches (never touch user images)
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
// Only clear caches that are specifically ours
|
||||
const ourCaches = cacheNames.filter(name =>
|
||||
name.includes('prompt-manager') ||
|
||||
name.includes('gallery') ||
|
||||
name.includes('thumbnail')
|
||||
);
|
||||
await Promise.all(ourCaches.map(name => caches.delete(name)));
|
||||
}
|
||||
|
||||
// Clear only our localStorage entries (never touch user data)
|
||||
Object.keys(localStorage).forEach(key => {
|
||||
if (key.startsWith('gallery_cache_') ||
|
||||
key.startsWith('metadata_cache_') ||
|
||||
key.startsWith('promptManager_') ||
|
||||
key.startsWith('pm_')) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Call backend to clear server-side thumbnails (only our generated thumbnails)
|
||||
try {
|
||||
const response = await ApiClient.fetchUrl('/prompt_manager/images/clear-thumbnails', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Refused (a running job holds the thumbnails): say why and claim nothing.
|
||||
const data = await response.json().catch(() => ({}));
|
||||
this.showNotification(`Thumbnails were not cleared: ${data.error || `HTTP ${response.status}`}`, 'error');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not clear server thumbnails:', e);
|
||||
// Not a critical error - continue with local cleanup
|
||||
}
|
||||
|
||||
// Update thumbnail status
|
||||
const settings = this.getSettings();
|
||||
settings.thumbnailsGenerated = false;
|
||||
localStorage.setItem('gallerySettings', JSON.stringify(settings));
|
||||
document.getElementById('thumbnailStatus').textContent = 'Not generated';
|
||||
|
||||
this.showNotification('PromptManager cache cleared successfully! Original images untouched.', 'success');
|
||||
} catch (error) {
|
||||
console.error('Clear cache error:', error);
|
||||
this.showNotification('Failed to clear cache', 'error');
|
||||
}
|
||||
if (!confirm('Clear PromptManager cache and thumbnails? This will NOT affect your original images.')) return;
|
||||
let response;
|
||||
try {
|
||||
response = await ApiClient.fetchUrl('/prompt_manager/images/clear-thumbnails', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Could not clear server thumbnails:', error);
|
||||
this.showNotification('Thumbnails were not cleared: the server could not be reached.', 'error');
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
// Refused (a running job holds the thumbnails): say why and claim nothing.
|
||||
const data = await response.json().catch(() => ({}));
|
||||
this.showNotification(`Thumbnails were not cleared: ${data.error || `HTTP ${response.status}`}`, 'error');
|
||||
return;
|
||||
}
|
||||
// Read through a getter: a browser that refuses storage throws on `localStorage` itself.
|
||||
const browser = await window.CacheOwnership.clearOwned({ storage: () => localStorage,
|
||||
cacheStorage: 'caches' in window ? window.caches : null });
|
||||
try {
|
||||
const settings = this.getSettings();
|
||||
settings.thumbnailsGenerated = false;
|
||||
localStorage.setItem('gallerySettings', JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
console.warn('Could not record the cleared thumbnails:', error);
|
||||
}
|
||||
document.getElementById('thumbnailStatus').textContent = 'Not generated';
|
||||
this.showNotification(`Thumbnails cleared; original images untouched. ${window.CacheOwnership.clearMessage(browser)}`,
|
||||
browser.partial ? 'warning' : 'success');
|
||||
}
|
||||
|
||||
async rescanFolder() {
|
||||
|
||||
Reference in New Issue
Block a user