From 7a9054bb3f033c98067b3196378311dae54c5fbf Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 20 Jan 2026 01:31:53 +0000
Subject: [PATCH] =OperationQueue refactor to the JVBase/managers/queue namespace
---
assets/js/concise/Queue.js | 212 ++++++++++++++++++++++++++++++++++++----------------
1 files changed, 147 insertions(+), 65 deletions(-)
diff --git a/assets/js/concise/Queue.js b/assets/js/concise/Queue.js
index 478be92..4c9adbc 100644
--- a/assets/js/concise/Queue.js
+++ b/assets/js/concise/Queue.js
@@ -5,6 +5,7 @@
this.user = window.auth.getUser();
+
this.canUpdateUI = true;
this.isProcessing = false;
this.isPolling = false;
@@ -15,6 +16,8 @@
this.api = jvbSettings.api;
this.endpoint = 'queue';
+ this.queueItems = new Map();
+
this.init();
}
init() {
@@ -24,13 +27,14 @@
this.initElements();
this.initListeners();
this.initStore();
- if (this.canUpdateUI) {
+ if (this.canUpdateUI && this.ui.panel) {
this.popup = new window.jvbPopup({
popup: this.ui.panel,
toggle: this.ui.toggle.button,
name: 'Queue Panel',
});
}
+ this.defineTemplates();
}
initElements() {
@@ -138,6 +142,17 @@
if (!this.ui.panel) this.canUpdateUI = false;
}
+ defineTemplates() {
+ const T = window.jvbTemplates;
+
+ T.define('emptyState');
+ T.define('queueItem', {
+ setup({el, refs, manyRefs, data}) {
+ el.dataset.id = data.id;
+ }
+ });
+ }
+
initListeners() {
this.activityListeners = null;
@@ -152,7 +167,7 @@
window.addEventListener('beforeunload', this.unloadHandler);
}
handleOnline() {
- this.updatePanel();
+ this.updatePanel('synced');
if (this.getQueueByStatus(this.pendingStatuses).length > 0) {
this.processQueue();
}
@@ -161,6 +176,7 @@
this.updatePanel('offline');
}
handleBeforeUnload(e) {
+ if (!this.ui.panel) return;
const total = this.getQueueByStatus(this.pendingStatuses).length;
if (total > 0) {
// Modern browsers ignore custom messages, but this triggers the native dialog
@@ -173,8 +189,12 @@
if (!window.targetCheck(e, this.selectors.panel+', '+this.selectors.toggle.button)) return;
const refresh = window.targetCheck(e, this.selectors.refresh.button);
if (refresh) {
+ this.ui.refresh.button.classList.add('fetching');
this.store.clearCache();
- this.store.fetch();
+ this.store.clearFilters();
+ this.store.fetch().finally(() => {
+ this.ui.refresh.button.classList.remove('fetching');
+ });
return;
}
@@ -265,6 +285,9 @@
{name: 'status', keyPath: 'status'},
{name: 'type', keyPath: 'type'},
],
+ filters: {
+ user: window.auth.getUser()
+ },
showLoading: false,
}
)
@@ -304,6 +327,7 @@
title: 'Operation',
status: 'queued',
timestamp: Date.now(),
+ created_at: new Date().toISOString(),
retries: 0,
user: this.user,
... operation
@@ -364,9 +388,12 @@
}
if (statusOrId.length ===0) return;
if (!['cancel', 'dismiss', 'retry'].includes(action)) return;
+
const shouldRemove = ['cancel', 'dismiss'].includes(action);
if (shouldRemove) {
- statusOrId.forEach(id => this.removeOperationUI(id));
+ statusOrId.forEach(id => {
+ this.removeOperationUI(id)
+ });
}
try {
@@ -394,7 +421,10 @@
}
statusOrId.forEach(id => {
let item = this.getQueue(id);
- this.notify(`${action}-operation`, item);
+ if (item) {
+ this.notify(`${action}-operation`, item);
+ }
+
if (shouldRemove) {
this.clearQueue(id);
} else {
@@ -441,8 +471,7 @@
this.setProcessing(false);
this.stopActivityTracking();
- // this.toggleQueue(this.maybeStartPolling());
-
+ this.toggleQueue(this.maybeStartPolling());
}
async processOperation(operation) {
@@ -462,13 +491,13 @@
let requestBody;
if (operation.data instanceof FormData) {
operation.data.append('id', operation.id);
- operation.data.append('user', this.user);
+ operation.data.append('user', window.auth.getUser());
requestBody = operation.data;
} else {
requestBody = JSON.stringify({
...operation.data,
id: operation.id,
- user: this.user
+ user: window.auth.getUser()
});
operation.headers['Content-Type'] = 'application/json';
}
@@ -545,13 +574,37 @@
});
}
+ sortOperations(ops) {
+ const statusPriority = {
+ 'processing': 0,
+ 'uploading': 1,
+ 'pending': 2,
+ 'queued': 3,
+ 'localProcessing': 4,
+ 'failed': 5,
+ 'completed': 6,
+ 'failed_permanent': 7
+ };
+
+ return ops.sort((a, b) => {
+ // First by status priority
+ const priorityDiff = (statusPriority[a.status] ?? 99) - (statusPriority[b.status] ?? 99);
+ if (priorityDiff !== 0) return priorityDiff;
+
+ // Then by updated_at (most recent first)
+ const aTime = a.updated_at ?? a.timestamp ?? 0;
+ const bTime = b.updated_at ?? b.timestamp ?? 0;
+ return new Date(bTime) - new Date(aTime);
+ });
+ }
+
getAllQueue() {
let ops = [... new Set([
...Array.from(this.store.data.values()),
... Array.from(this.queue.values())
])];
//Sort operations by operation updated_at
- return this.sortByDate(ops);
+ return this.sortOperations(ops);
}
getQueueByStatus(status) {
@@ -563,7 +616,7 @@
...Array.from(this.store.filterByIndex({status: status})),
...Array.from(this.queue.values()).filter(op => status.includes(op.status))
])];
- return this.sortByDate(ops);
+ return this.sortOperations(ops);
}
@@ -589,32 +642,59 @@
POLLING
****************************************************************************/
maybeStartPolling() {
- const incomplete = this.getQueueByStatus(this.pendingStatuses);
+ const incomplete = this.getQueueByStatus([...this.pendingStatuses, ...this.workingStatuses]);
if (incomplete.length > 0) {
this.startPolling();
return true;
}
+ this.updatePanel('synced');
return false;
}
startPolling() {
if (this.isPolling) return;
this.isPolling = true;
this.updatePanel('pending');
+ this.runPollCycle();
+ }
- this.pollTimer = setInterval(async () => {
- try {
- this.store.clearCache();
- await this.store.fetch();
- if (!this.maybeStartPolling()) {
- this.stopPolling();
- this.updatePanel('synced');
- }
- } catch (error) {
- console.error('Polling error:', error);
+ async runPollCycle() {
+ if (!this.isPolling) return;
+
+ try {
+ this.ui.refresh.button.classList.add('fetching');
+ this.store.clearCache();
+ await this.store.fetch();
+ this.ui.refresh.button.classList.remove('fetching');
+ if (!this.maybeStartPolling()) {
+ this.stopPolling();
+ this.updatePanel('synced');
+ return;
}
- },
- 5000);
- this.startCountdown();
+ } catch (error) {
+ console.error('Polling error:', error);
+ }
+
+ // Schedule next poll with countdown
+ this.startCountdown(5, () => this.runPollCycle());
+ }
+
+ startCountdown(count, onComplete) {
+ if (!this.ui.refresh.countdown) {
+ console.warn('Countdown element not found');
+ return;
+ }
+ this.ui.refresh.countdown.classList.add('counting');
+ this.ui.refresh.countdown.textContent = count;
+
+ this.countdownTimer = setInterval(() => {
+ count--;
+ if (count > 0) {
+ this.ui.refresh.countdown.textContent = count;
+ } else {
+ this.stopCountdown();
+ if (onComplete) onComplete();
+ }
+ }, 1000);
}
stopPolling() {
@@ -626,24 +706,14 @@
}
this.stopCountdown();
}
- startCountdown(count = 5) {
- if (!this.isPolling) return;
- this.ui.refresh.countdown.textContent = count;
- this.countdownTimer = setInterval(async() => {
- count--;
- if (count >= 0) {
- this.ui.refresh.countdown.textContent = count;
- }else {
- this.ui.refresh.countdown.textContent = '';
- this.stopCountdown();
- }
- },1000);
- }
+
stopCountdown() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
+ this.ui.refresh.countdown.classList.remove('counting');
+ this.ui.refresh.countdown.textContent = '';
}
/****************************************************************************
UI
@@ -661,6 +731,12 @@
this.ui.actions.retry.disabled = operations.filter(op => op.status === 'failed').length === 0;
this.ui.actions.clear.disabled = operations.filter(op => op.status === 'completed').length ===0;
+ const activeCount = operations.filter(op =>
+ [...this.pendingStatuses, ...this.workingStatuses].includes(op.status)
+ ).length;
+ this.ui.toggle.count.hidden = activeCount === 0;
+ this.ui.toggle.count.textContent = activeCount;
+
for (let status of this.statuses) {
if (status === 'failed_permanent') continue;
let total = operations.filter(op => op.status === status).length;
@@ -681,35 +757,45 @@
const status = this.store.filters?.status ?? 'all';
const operations = (status === 'all') ? this.getAllQueue() : this.getQueueByStatus(status);
+ const sortedOps = this.sortOperations(operations);
- window.removeChildren(this.ui.items.container);
-
- if (operations.length === 0) {
- const empty = window.getTemplate('emptyQueue');
+ if (sortedOps.length === 0) {
+ window.removeChildren(this.ui.items.container);
+ const empty = window.jvbTemplates.create('emptyQueue');
this.ui.items.container.append(empty);
this.a11y.announce('No items in queue');
return;
+ } else {
+ this.ui.items.container.querySelector('.empty-group')?.remove();
}
- operations.forEach(op => {
- let item = this.items.get(op.id);
+ // Track which items should exist
+ const expectedIds = new Set(sortedOps.map(op => op.id));
+ // Remove items that shouldn't exist
+ this.items.forEach((item, id) => {
+ if (!expectedIds.has(id)) {
+ item.element?.remove();
+ this.items.delete(id);
+ }
+ });
+
+ // Update/add items in order
+ sortedOps.forEach((op, index) => {
+ let item = this.items.get(op.id);
if (!item) {
- // Create new element and reference
item = this.createOperationElement(op);
}
-
if (item?.element) {
this.updateOperationUI(op.id);
+ // Reorder by re-appending (moves to end in correct order)
this.ui.items.container.append(item.element);
}
});
}
createOperationElement(op) {
- const el = window.getTemplate('queueItem');
- el.dataset.id = op.id;
-
+ const el = window.jvbTemplates.create('queueItem', op);
const item = {
element: el,
ui: window.uiFromSelectors(this.selectors.item, el)
@@ -742,20 +828,15 @@
item.ui.startedAt.textContent = window.formatTimeAgo(op.created_at);
}
let text = op.status === 'completed' ? 'Completed: ' : 'Last updated: ';
- let shouldShow =Object.hasOwn(op, 'updated_at') || Object.hasOwn(op, 'completed_at');
- item.ui.completed.wrap.hidden = !shouldShow;
- if (shouldShow && item.ui.completed.label && item.ui.completed.time) {
- let time;
- if (Object.hasOwn(op, 'completed_at')) {
- time = op.completed_at;
- } else {
- time = op.updated_at;
- }
-
- item.ui.completed.label.textContent = text;
- item.ui.completed.time.setAttribute('datetime', time);
- item.ui.completed.time.textContent = window.formatTimeAgo(time);
+ const shouldShowCompleted = op.status === 'completed' && (op.completed_at || op.updated_at);
+ item.ui.completed.wrap.hidden = !shouldShowCompleted;
+ if (shouldShowCompleted) {
+ const completedTime = op.completed_at ?? op.updated_at;
+ item.ui.completed.label.textContent = 'Completed: ';
+ item.ui.completed.time.setAttribute('datetime', completedTime);
+ item.ui.completed.time.textContent = window.formatTimeAgo(completedTime);
}
+
window.showProgress(item.ui.progress, progress, 100, this.statusLabel(op.status));
if (item.ui.actions.cancel) item.ui.actions.cancel.hidden = this.completedStatuses.includes(op.status);
if (item.ui.actions['retry']) {
@@ -785,8 +866,8 @@
}
updatePanel(status = 'syncing') {
- if (!this.panelStatuses.includes(status)) return;
- this.ui.panel.classList.remove(this.panelStatuses);
+ if (!this.ui.panel || !this.panelStatuses.includes(status)) return;
+ this.ui.panel.classList.remove(...this.panelStatuses);
this.ui.panel.classList.add(status);
}
/****************************************************************************
@@ -822,7 +903,7 @@
case 'completed':
return 'Successfully completed';
case 'failed':
- return `Failed: ${item.lastError || 'Unknown error'} (Retry ${item.retries}/${this.config.maxRetries})`;
+ return `Failed: ${item.lastError || 'Unknown error'} (Retry ${item.retries}/${2})`;
case 'failed_permanent':
return `Failed: ${item.lastError || 'Unknown error'}`;
default:
@@ -830,6 +911,7 @@
}
}
toggleQueue(on = true) {
+ if (!this.ui.panel) return;
this.ui.panel.hidden = !on;
this.ui.toggle.button.hidden = !on;
}
--
Gitblit v1.10.0