Feat: Improve history ordering and incremental updates

This commit is contained in:
Simon Cordt
2025-09-25 06:00:42 +02:00
parent d77e3546e6
commit 32f8e97c73
+117 -20
View File
@@ -412,6 +412,44 @@
}
},
// Compute a robust numeric key for history ordering
historyKey(row) {
try {
const ts = row && (row.completed_at || row.created_at);
// Numeric timestamp (seconds or ms)
if (typeof ts === 'number' && Number.isFinite(ts)) {
return ts < 1e12 ? Math.floor(ts * 1000) : Math.floor(ts);
}
if (typeof ts === 'string') {
const s = ts.trim();
// Pure digits
if (/^\d+$/.test(s)) {
const n = parseInt(s, 10);
if (Number.isFinite(n)) return n < 1e12 ? n * 1000 : n;
}
// Try ISO-like
let ms = Date.parse(s);
if (!Number.isFinite(ms)) {
// Try replace space with T (e.g. 'YYYY-MM-DD HH:mm:ss')
ms = Date.parse(s.replace(' ', 'T'));
}
if (Number.isFinite(ms)) return ms;
}
const id = Number(row?.id);
return Number.isFinite(id) ? id : 0;
} catch (err) {
const id = Number(row?.id);
return Number.isFinite(id) ? id : 0;
}
},
// Composite key: [ms, id] for strict total ordering
historyKeyParts(row) {
const ms = UI.historyKey(row) || 0;
const id = Number(row?.id) || 0;
return [ms, id];
},
// Find and cache the real scroll container (ComfyUI sidebar)
getScrollContainer() {
try {
@@ -896,17 +934,25 @@
if (!id || existingIds.has(id)) continue;
const card = UI.historyCard(row);
const key = Number(card.getAttribute('data-key') || 0);
const key2 = Number(card.getAttribute('data-key2') || 0);
const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card'));
let inserted = false;
for (const existing of existingCards) {
const otherKey = Number(existing.getAttribute('data-key') || 0);
if (dir === 'desc') {
if (key >= otherKey) { grid.insertBefore(card, existing); inserted = true; break; }
} else {
if (key <= otherKey) { grid.insertBefore(card, existing); inserted = true; break; }
if (dir === 'desc') {
for (const existing of existingCards) {
const otherKey = Number(existing.getAttribute('data-key') || 0);
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing); inserted = true; break; }
}
if (!inserted) grid.insertBefore(card, sentinel);
} else {
for (let i = existingCards.length - 1; i >= 0; i--) {
const existing = existingCards[i];
const otherKey = Number(existing.getAttribute('data-key') || 0);
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing.nextSibling || sentinel); inserted = true; break; }
}
if (!inserted) grid.insertBefore(card, sentinel);
}
if (!inserted) grid.insertBefore(card, sentinel);
existingIds.add(id);
}
@@ -1042,9 +1088,10 @@
try {
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
list = list.slice().sort((a, b) => {
const ak = Date.parse(a?.completed_at || a?.created_at || 0) || (a?.id || 0);
const bk = Date.parse(b?.completed_at || b?.created_at || 0) || (b?.id || 0);
return dir === 'desc' ? (bk - ak) : (ak - bk);
const [ams, aid] = UI.historyKeyParts(a);
const [bms, bid] = UI.historyKeyParts(b);
if (ams !== bms) return dir === 'desc' ? (bms - ams) : (ams - bms);
return dir === 'desc' ? (bid - aid) : (aid - bid);
});
} catch (err) { /* noop */ }
if (!state.historyIds) state.historyIds = new Set();
@@ -1057,17 +1104,27 @@
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
// Determine insertion point by timestamp or id, matching current sort
const key = Number(card.getAttribute('data-key') || 0);
const key2 = Number(card.getAttribute('data-key2') || 0);
const existingCards = Array.from(grid.querySelectorAll('.pqueue-history-card'));
let inserted = false;
for (const existing of existingCards) {
const otherKey = Number(existing.getAttribute('data-key') || 0);
if (dir === 'desc') {
if (key >= otherKey) { grid.insertBefore(card, existing); inserted = true; break; }
} else {
if (key <= otherKey) { grid.insertBefore(card, existing); inserted = true; break; }
if (dir === 'desc') {
// Newest-first: insert before first card with smaller key; break ties by id
for (const existing of existingCards) {
const otherKey = Number(existing.getAttribute('data-key') || 0);
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing); inserted = true; break; }
}
if (!inserted) grid.insertBefore(card, sentinel);
} else {
// Oldest-first: insert after last card with smaller/equal key; break ties by id
for (let i = existingCards.length - 1; i >= 0; i--) {
const existing = existingCards[i];
const otherKey = Number(existing.getAttribute('data-key') || 0);
const otherKey2 = Number(existing.getAttribute('data-key2') || 0);
if (key > otherKey || (key === otherKey && key2 >= otherKey2)) { grid.insertBefore(card, existing.nextSibling || sentinel); inserted = true; break; }
}
if (!inserted) grid.insertBefore(card, sentinel);
}
if (!inserted) grid.insertBefore(card, sentinel);
// Maintain in-memory list order similarly
if (Array.isArray(state.history)) {
if (dir === 'desc') state.history.unshift(row);
@@ -1201,9 +1258,10 @@
const attrs = { class: "pqueue-history-card", title: Format.tooltip(row) };
if (row && row.id != null) attrs["data-id"] = String(row.id);
const ts = row && (row.completed_at || row.created_at);
if (ts) attrs["data-ts"] = String(ts);
const key = ts ? Date.parse(ts) : (Number(row?.id) || 0);
attrs["data-key"] = String(Number.isFinite(key) ? key : 0);
if (ts != null) attrs["data-ts"] = String(ts);
const [ms, idPart] = UI.historyKeyParts(row);
attrs["data-key"] = String(ms);
attrs["data-key2"] = String(idPart);
const card = UI.el("article", attrs);
const header = UI.el("div", { class: "pqueue-history-card__header" }, [UI.statusBadge(row.status || "success"), UI.el("span", { class: "pqueue-history-card__time", text: row.completed_at ? Format.relative(row.completed_at) : Format.relative(row.created_at) })]);
const fileLabel = UI.historyPrimaryFilename(row);
@@ -1821,6 +1879,40 @@
} catch (err) { /* noop */ }
},
async syncLatestForAsc() {
try {
const paging = state.historyPaging;
const params = paging?.params || {};
if ((params.sort_dir || 'desc') !== 'asc') return;
const latestParams = { ...params, sort_dir: 'desc', limit: Math.min(10, params.limit || 10) };
delete latestParams.cursor_id;
delete latestParams.cursor_value;
const result = await API.getHistoryPaginated(latestParams);
let list = Array.isArray(result?.history) ? result.history : [];
if (!list.length) return;
// Normalize to current ordering key so reconcile inserts correctly
try {
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
list = list.slice().sort((a, b) => {
const ak = UI.historyKey(a);
const bk = UI.historyKey(b);
return dir === 'desc' ? (bk - ak) : (ak - bk);
});
} catch (err) { /* noop */ }
if (!state.historyIds) state.historyIds = new Set();
let added = 0;
for (const row of list) {
const id = row?.id;
if (id == null || state.historyIds.has(id)) continue;
state.historyIds.add(id);
// Keep in-memory list (first page) extended; reconciliation will order
state.history.push(row);
added += 1;
}
if (added) UI.reconcileHistoryFromState();
} catch (err) { /* noop */ }
},
async togglePause() {
try {
if (state.paused) await API.resume();
@@ -2213,6 +2305,9 @@
state.historyPaging.hasMore = true;
if (sentinel) sentinel.style.display = '';
await Events.loadMoreHistory();
// If sorting asc, proactively fetch a small page of latest items and reconcile,
// so newly finished rows appear at the bottom without toggling sort
await Events.syncLatestForAsc();
UI.updateHistorySubtitle();
UI.updateSortToggle();
if (typeof restore === 'function') requestAnimationFrame(restore);
@@ -2425,6 +2520,8 @@
}
// After state updated, attempt incremental UI update to prevent flicker
try { UI.updateAfterRefresh(paged); } catch (err) { /* noop */ }
// In asc mode, also proactively fetch a small latest page and reconcile
try { await Events.syncLatestForAsc(); } catch (err) { /* noop */ }
} catch (err) {
console.error("pqueue: refresh failed", err);
state.error = err?.message || "Failed to load persistent queue";