From f2d11bd477fe81892122abf81f4da832c392ce61 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Sun, 2 Aug 2026 13:26:21 +0200 Subject: [PATCH] refactor(frontend): extract queue storage methods --- tests/test_queue_storage_methods.mjs | 92 ++++++++ web/resolver/actions/queue_methods.js | 220 ----------------- web/resolver/actions/queue_storage_methods.js | 221 ++++++++++++++++++ web/resolver/resolver_dialog.js | 2 + 4 files changed, 315 insertions(+), 220 deletions(-) create mode 100644 tests/test_queue_storage_methods.mjs create mode 100644 web/resolver/actions/queue_storage_methods.js diff --git a/tests/test_queue_storage_methods.mjs b/tests/test_queue_storage_methods.mjs new file mode 100644 index 0000000..9ad4f86 --- /dev/null +++ b/tests/test_queue_storage_methods.mjs @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { queueStorageMethods } from '../web/resolver/actions/queue_storage_methods.js'; + +function createStorage() { + const values = new Map(); + return { + getItem(key) { + return values.has(key) ? values.get(key) : null; + }, + setItem(key, value) { + values.set(key, String(value)); + }, + removeItem(key) { + values.delete(key); + }, + }; +} + +function installStorage(storage) { + const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'localStorage'); + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + enumerable: true, + value: storage, + writable: true, + }); + return () => { + if (previousDescriptor) { + Object.defineProperty(globalThis, 'localStorage', previousDescriptor); + } else { + delete globalThis.localStorage; + } + }; +} + +test('queue storage persists collapsed state and splitter width immediately', () => { + const storage = createStorage(); + const restoreStorage = installStorage(storage); + + try { + const context = { ...queueStorageMethods }; + context.persistQueueCollapsedState(true, 0); + context.persistQueueSplitWidth(421, 0); + + assert.equal(storage.getItem('model_resolver_queue_collapsed'), '1'); + assert.equal(storage.getItem('model_resolver_split_w'), '421'); + } finally { + restoreStorage(); + } +}); + +test('download history replaces duplicate identities and keeps newest entries first', () => { + const storage = createStorage(); + const restoreStorage = installStorage(storage); + + try { + const context = { + ...queueStorageMethods, + downloadHistoryLimit: 10, + downloadHistoryStorageKey: 'test-download-history', + updateQueuePanel() {}, + }; + + const first = context.addDownloadHistoryEntry({ + filename: 'model.safetensors', + path: 'models/model.safetensors', + category: 'checkpoints', + sourceUrl: 'https://example.test/model', + completedAt: '2026-01-01T00:00:00.000Z', + }); + const second = context.addDownloadHistoryEntry({ + filename: 'model.safetensors', + path: 'models/model.safetensors', + category: 'checkpoints', + sourceUrl: 'https://example.test/model', + completedAt: '2026-01-02T00:00:00.000Z', + }); + + assert.notEqual(first.id, second.id); + assert.deepEqual(context.getDownloadHistory().map(entry => entry.completedAt), [ + '2026-01-02T00:00:00.000Z', + ]); + + const persisted = JSON.parse(storage.getItem('test-download-history')); + assert.equal(persisted.length, 1); + assert.equal(persisted[0].id, second.id); + } finally { + restoreStorage(); + } +}); diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 285a7cb..b7f2ae8 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -205,46 +205,6 @@ export const queueMethods = { this.contentElement.style.removeProperty('flex-shrink'); }, - persistQueueCollapsedState(collapsed, delayMs = 250) { - if (this._queueCollapsedPersistTimer) { - clearTimeout(this._queueCollapsedPersistTimer); - this._queueCollapsedPersistTimer = null; - } - const writeState = () => { - this._queueCollapsedPersistTimer = null; - try { - localStorage.setItem('model_resolver_queue_collapsed', collapsed ? '1' : '0'); - } catch (_e) { } - }; - const delay = Math.max(0, Number(delayMs) || 0); - if (delay > 0) { - this._queueCollapsedPersistTimer = setTimeout(writeState, delay); - } else { - writeState(); - } - }, - - persistQueueSplitWidth(width, delayMs = 250) { - const nextWidth = Math.round(Number(width)); - if (!Number.isFinite(nextWidth) || nextWidth <= 0) return; - if (this._queueSplitPersistTimer) { - clearTimeout(this._queueSplitPersistTimer); - this._queueSplitPersistTimer = null; - } - const writeWidth = () => { - this._queueSplitPersistTimer = null; - try { - localStorage.setItem('model_resolver_split_w', String(nextWidth)); - } catch (_e) { } - }; - const delay = Math.max(0, Number(delayMs) || 0); - if (delay > 0) { - this._queueSplitPersistTimer = setTimeout(writeWidth, delay); - } else { - writeWidth(); - } - }, - updateQueuePanelTabs(queueCount, downloadCount, activeTab) { const queuedTab = this.queueQueuedTabButton || this.queueTabs?.querySelector?.('.mr-queue-tab[data-tab="queued"]'); const downloadsTab = this.queueDownloadsTabButton || this.queueTabs?.querySelector?.('.mr-queue-tab[data-tab="downloads"]'); @@ -1006,160 +966,6 @@ export const queueMethods = { return this.getActiveQueuePanelDownloads().map(({ downloadId }) => downloadId); }, - loadDownloadHistory() { - if (this._downloadHistoryLoaded) return this.downloadHistory || []; - this._downloadHistoryLoaded = true; - try { - const raw = localStorage.getItem(this.downloadHistoryStorageKey || 'model_resolver_download_history'); - const parsed = raw ? JSON.parse(raw) : []; - this.downloadHistory = Array.isArray(parsed) - ? parsed.filter(item => item && typeof item === 'object') - : []; - } catch (error) { - console.warn('Model Resolver: failed to load download history', error); - this.downloadHistory = []; - } - return this.downloadHistory; - }, - - getDownloadHistory() { - if (!this._downloadHistoryLoaded) { - this.loadDownloadHistory(); - } - return Array.isArray(this.downloadHistory) ? this.downloadHistory : []; - }, - - saveDownloadHistory() { - const history = this.getDownloadHistory().slice(0, this.downloadHistoryLimit || 200); - this.downloadHistory = history; - try { - localStorage.setItem( - this.downloadHistoryStorageKey || 'model_resolver_download_history', - JSON.stringify(history) - ); - } catch (error) { - console.warn('Model Resolver: failed to save download history', error); - } - }, - - getDownloadHistoryIdentity(entry = {}) { - return [ - entry.path || '', - entry.filename || '', - entry.category || '', - entry.sourceUrl || '' - ].map(value => String(value || '').trim().toLowerCase()).join('::'); - }, - - addDownloadHistoryEntry(entry = {}) { - if (!entry || !entry.filename) return null; - - const history = this.getDownloadHistory(); - const identity = this.getDownloadHistoryIdentity(entry); - const filtered = identity - ? history.filter(item => this.getDownloadHistoryIdentity(item) !== identity) - : history; - const nextEntry = { - ...entry, - id: entry.id || `${Date.now()}-${Math.random().toString(36).slice(2)}`, - completedAt: entry.completedAt || new Date().toISOString() - }; - this.downloadHistory = [nextEntry, ...filtered].slice(0, this.downloadHistoryLimit || 200); - this.saveDownloadHistory(); - this.updateQueuePanel?.(); - return nextEntry; - }, - - rememberCompletedDownloadHistory(downloadId, info = {}, progress = {}) { - const missing = info?.missing || {}; - const filename = progress.filename - || info.filename - || missing.download_source?.filename - || missing.original_path?.split(/[/\\]/).pop() - || ''; - if (!filename) return null; - - const directory = progress.directory || info.downloadDirectory || ''; - const path = progress.path || info.downloadPath || ''; - const category = info.category || missing.category || progress.category || ''; - const workflowLabel = this.getDownloadWorkflowLabel?.(info) || info.workflowLabel || ''; - const nodeLabel = missing.subgraph_name || missing.node_type || (missing.subgraph_id ? 'Subgraph' : 'Node'); - const status = progress.already_exists ? 'already_exists' : 'completed'; - return this.addDownloadHistoryEntry({ - downloadId, - filename, - category, - categoryLabel: this.getCategoryDisplayName?.(category) || category, - nodeLabel, - nodeId: missing.node_id ?? '', - widgetIndex: missing.widget_index ?? '', - workflowLabel, - workflowId: info.workflowId || info.workflow_id || this.getWorkflowContextId?.(info) || '', - workflowKey: info.workflowKey || '', - workflowRouteKey: info.workflowRouteKey || '', - workflowSignature: info.workflowSignature || '', - workflowTabId: info.workflowTabId || '', - workflowTabName: info.workflowTabName || '', - workflowTabAriaControls: info.workflowTabAriaControls || '', - workflowTabText: info.workflowTabText || '', - path, - directory, - sourceUrl: info.sourceUrl || missing.download_source?.url || '', - totalSize: progress.total_size || progress.size || 0, - status, - statusLabel: status === 'already_exists' ? 'Already downloaded' : 'Downloaded', - message: progress.message || '', - completedAt: new Date().toISOString() - }); - }, - - formatDownloadHistoryTime(value = '') { - if (!value) return ''; - try { - const date = new Date(value); - if (Number.isNaN(date.getTime())) return ''; - return date.toLocaleString(undefined, { - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }); - } catch (_error) { - return ''; - } - }, - - getDownloadHistoryFolderContext(entry = {}) { - const filePath = entry.path || ''; - const directory = entry.directory || ''; - const targetPath = filePath || directory; - const context = { - context_scope: 'download_history', - open_folder_label: targetPath ? 'Open Download Folder' : '', - name: entry.filename || 'Download', - path: targetPath, - resolved_path: targetPath, - open_path: filePath || directory, - folder_path: directory || targetPath, - download_directory: directory, - download_path: filePath, - category: entry.category || '', - workflow_id: entry.workflowId || entry.workflow_id || (this.isLikelyWorkflowId?.(entry.workflowLabel) ? entry.workflowLabel : ''), - workflow_label: entry.workflowLabel || '', - workflow_key: entry.workflowKey || '', - workflow_route_key: entry.workflowRouteKey || '', - workflow_signature: entry.workflowSignature || '', - workflow_tab_id: entry.workflowTabId || '', - workflow_tab_name: entry.workflowTabName || '', - workflow_tab_aria_controls: entry.workflowTabAriaControls || '', - workflow_tab_text: entry.workflowTabText || '', - node_id: entry.nodeId ?? '', - widget_index: entry.widgetIndex ?? '' - }; - - return (targetPath || this.canSwitchToDownloadWorkflow?.(context)) ? context : null; - }, - renderDownloadsPanel(downloads = [], history = this.getDownloadHistory()) { const activeSubTab = this.queueDownloadsActiveTab === 'history' ? 'history' : 'active'; const activeSelected = activeSubTab === 'active'; @@ -1465,32 +1271,6 @@ export const queueMethods = { return html; }, - clearDownloadHistory() { - this.downloadHistory = []; - this.saveDownloadHistory(); - this.updateQueuePanel(); - }, - - removeDownloadHistoryEntry(historyId = '', fallbackIndex = -1) { - const history = this.getDownloadHistory(); - if (!history.length) return; - - const normalizedId = String(historyId || ''); - let nextHistory = history; - if (normalizedId) { - nextHistory = history.filter(entry => String(entry?.id || '') !== normalizedId); - } - - if (nextHistory.length === history.length && Number.isInteger(fallbackIndex) && fallbackIndex >= 0 && fallbackIndex < history.length) { - nextHistory = history.filter((_, index) => index !== fallbackIndex); - } - - if (nextHistory.length === history.length) return; - this.downloadHistory = nextHistory; - this.saveDownloadHistory(); - this.updateQueuePanel(); - }, - // Remove queued by index removeQueuedByIndex(i) { const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : []; diff --git a/web/resolver/actions/queue_storage_methods.js b/web/resolver/actions/queue_storage_methods.js new file mode 100644 index 0000000..fad38e1 --- /dev/null +++ b/web/resolver/actions/queue_storage_methods.js @@ -0,0 +1,221 @@ +export const queueStorageMethods = { + persistQueueCollapsedState(collapsed, delayMs = 250) { + if (this._queueCollapsedPersistTimer) { + clearTimeout(this._queueCollapsedPersistTimer); + this._queueCollapsedPersistTimer = null; + } + const writeState = () => { + this._queueCollapsedPersistTimer = null; + try { + localStorage.setItem('model_resolver_queue_collapsed', collapsed ? '1' : '0'); + } catch (_e) { } + }; + const delay = Math.max(0, Number(delayMs) || 0); + if (delay > 0) { + this._queueCollapsedPersistTimer = setTimeout(writeState, delay); + } else { + writeState(); + } + }, + + persistQueueSplitWidth(width, delayMs = 250) { + const nextWidth = Math.round(Number(width)); + if (!Number.isFinite(nextWidth) || nextWidth <= 0) return; + if (this._queueSplitPersistTimer) { + clearTimeout(this._queueSplitPersistTimer); + this._queueSplitPersistTimer = null; + } + const writeWidth = () => { + this._queueSplitPersistTimer = null; + try { + localStorage.setItem('model_resolver_split_w', String(nextWidth)); + } catch (_e) { } + }; + const delay = Math.max(0, Number(delayMs) || 0); + if (delay > 0) { + this._queueSplitPersistTimer = setTimeout(writeWidth, delay); + } else { + writeWidth(); + } + }, + + loadDownloadHistory() { + if (this._downloadHistoryLoaded) return this.downloadHistory || []; + this._downloadHistoryLoaded = true; + try { + const raw = localStorage.getItem(this.downloadHistoryStorageKey || 'model_resolver_download_history'); + const parsed = raw ? JSON.parse(raw) : []; + this.downloadHistory = Array.isArray(parsed) + ? parsed.filter(item => item && typeof item === 'object') + : []; + } catch (error) { + console.warn('Model Resolver: failed to load download history', error); + this.downloadHistory = []; + } + return this.downloadHistory; + }, + + getDownloadHistory() { + if (!this._downloadHistoryLoaded) { + this.loadDownloadHistory(); + } + return Array.isArray(this.downloadHistory) ? this.downloadHistory : []; + }, + + saveDownloadHistory() { + const history = this.getDownloadHistory().slice(0, this.downloadHistoryLimit || 200); + this.downloadHistory = history; + try { + localStorage.setItem( + this.downloadHistoryStorageKey || 'model_resolver_download_history', + JSON.stringify(history) + ); + } catch (error) { + console.warn('Model Resolver: failed to save download history', error); + } + }, + + getDownloadHistoryIdentity(entry = {}) { + return [ + entry.path || '', + entry.filename || '', + entry.category || '', + entry.sourceUrl || '' + ].map(value => String(value || '').trim().toLowerCase()).join('::'); + }, + + addDownloadHistoryEntry(entry = {}) { + if (!entry || !entry.filename) return null; + + const history = this.getDownloadHistory(); + const identity = this.getDownloadHistoryIdentity(entry); + const filtered = identity + ? history.filter(item => this.getDownloadHistoryIdentity(item) !== identity) + : history; + const nextEntry = { + ...entry, + id: entry.id || `${Date.now()}-${Math.random().toString(36).slice(2)}`, + completedAt: entry.completedAt || new Date().toISOString() + }; + this.downloadHistory = [nextEntry, ...filtered].slice(0, this.downloadHistoryLimit || 200); + this.saveDownloadHistory(); + this.updateQueuePanel?.(); + return nextEntry; + }, + + rememberCompletedDownloadHistory(downloadId, info = {}, progress = {}) { + const missing = info?.missing || {}; + const filename = progress.filename + || info.filename + || missing.download_source?.filename + || missing.original_path?.split(/[/\\]/).pop() + || ''; + if (!filename) return null; + + const directory = progress.directory || info.downloadDirectory || ''; + const path = progress.path || info.downloadPath || ''; + const category = info.category || missing.category || progress.category || ''; + const workflowLabel = this.getDownloadWorkflowLabel?.(info) || info.workflowLabel || ''; + const nodeLabel = missing.subgraph_name || missing.node_type || (missing.subgraph_id ? 'Subgraph' : 'Node'); + const status = progress.already_exists ? 'already_exists' : 'completed'; + return this.addDownloadHistoryEntry({ + downloadId, + filename, + category, + categoryLabel: this.getCategoryDisplayName?.(category) || category, + nodeLabel, + nodeId: missing.node_id ?? '', + widgetIndex: missing.widget_index ?? '', + workflowLabel, + workflowId: info.workflowId || info.workflow_id || this.getWorkflowContextId?.(info) || '', + workflowKey: info.workflowKey || '', + workflowRouteKey: info.workflowRouteKey || '', + workflowSignature: info.workflowSignature || '', + workflowTabId: info.workflowTabId || '', + workflowTabName: info.workflowTabName || '', + workflowTabAriaControls: info.workflowTabAriaControls || '', + workflowTabText: info.workflowTabText || '', + path, + directory, + sourceUrl: info.sourceUrl || missing.download_source?.url || '', + totalSize: progress.total_size || progress.size || 0, + status, + statusLabel: status === 'already_exists' ? 'Already downloaded' : 'Downloaded', + message: progress.message || '', + completedAt: new Date().toISOString() + }); + }, + + formatDownloadHistoryTime(value = '') { + if (!value) return ''; + try { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ''; + return date.toLocaleString(undefined, { + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch (_error) { + return ''; + } + }, + + getDownloadHistoryFolderContext(entry = {}) { + const filePath = entry.path || ''; + const directory = entry.directory || ''; + const targetPath = filePath || directory; + const context = { + context_scope: 'download_history', + open_folder_label: targetPath ? 'Open Download Folder' : '', + name: entry.filename || 'Download', + path: targetPath, + resolved_path: targetPath, + open_path: filePath || directory, + folder_path: directory || targetPath, + download_directory: directory, + download_path: filePath, + category: entry.category || '', + workflow_id: entry.workflowId || entry.workflow_id || (this.isLikelyWorkflowId?.(entry.workflowLabel) ? entry.workflowLabel : ''), + workflow_label: entry.workflowLabel || '', + workflow_key: entry.workflowKey || '', + workflow_route_key: entry.workflowRouteKey || '', + workflow_signature: entry.workflowSignature || '', + workflow_tab_id: entry.workflowTabId || '', + workflow_tab_name: entry.workflowTabName || '', + workflow_tab_aria_controls: entry.workflowTabAriaControls || '', + workflow_tab_text: entry.workflowTabText || '', + node_id: entry.nodeId ?? '', + widget_index: entry.widgetIndex ?? '' + }; + + return (targetPath || this.canSwitchToDownloadWorkflow?.(context)) ? context : null; + }, + + clearDownloadHistory() { + this.downloadHistory = []; + this.saveDownloadHistory(); + this.updateQueuePanel(); + }, + + removeDownloadHistoryEntry(historyId = '', fallbackIndex = -1) { + const history = this.getDownloadHistory(); + if (!history.length) return; + + const normalizedId = String(historyId || ''); + let nextHistory = history; + if (normalizedId) { + nextHistory = history.filter(entry => String(entry?.id || '') !== normalizedId); + } + + if (nextHistory.length === history.length && Number.isInteger(fallbackIndex) && fallbackIndex >= 0 && fallbackIndex < history.length) { + nextHistory = history.filter((_, index) => index !== fallbackIndex); + } + + if (nextHistory.length === history.length) return; + this.downloadHistory = nextHistory; + this.saveDownloadHistory(); + this.updateQueuePanel(); + }, +}; diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index 379440e..d9aa2da 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -13,6 +13,7 @@ import { tabsLoadedMethods } from "./views/tabs_loaded_methods.js"; import { downloadTargetMethods } from "./search/download_target_methods.js"; import { searchPanelMethods } from "./search/search_panel.js"; import { queueMethods } from "./actions/queue_methods.js"; +import { queueStorageMethods } from "./actions/queue_storage_methods.js"; import { resolveDownloadMethods } from "./actions/resolve_download_methods.js"; import { selectionMethods } from "./actions/selection_methods.js"; import { renderFormatMethods } from "./utils/render_format_methods.js"; @@ -341,6 +342,7 @@ applyDialogMethods( optionsMethods, tabsLoadedMethods, queueMethods, + queueStorageMethods, lifecycleGraphMethods, missingBrowserMethods, resolveDownloadMethods,