refactor(frontend): extract queue storage methods

This commit is contained in:
Dariusz L
2026-08-02 13:26:21 +02:00
parent 922a30fdd4
commit f2d11bd477
4 changed files with 315 additions and 220 deletions
+92
View File
@@ -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();
}
});
-220
View File
@@ -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 : [];
@@ -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();
},
};
+2
View File
@@ -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,