/**
* Refactor statuses:
* 1) queued - local queue
* 1) localProcessing - any local processing that can be done in the browser
* 2) uploading - sending to server
* 3) pending; x in line - sent to server, waiting
* 4) processing - server actively working on it
* 5) completed - with optional refresh
*/
class QueueManagerBackup {
constructor() {
// localStorage.clear();
//Core components
this.a11y = window.jvbA11y;
this.errors = window.jvbError;
this.cache = window.jvbCache;
//Config
this.STORAGE_KEY = 'jvb_queue';
this.API = `${jvbSettings.api}queue`;
this.maxRetries = 3;
//Initialize State
this.queue = new Map();
this.hasChanges = false;
this.processing = false;
this.lastPollTime = null;
//Status Tracking
this.statuses = [
'queued', // Local, waiting to be sent
'localProcessing', // Being processed client-side
'uploading', // Being sent to server
'pending', // On server, waiting to be processed
'processing', // Server is actively processing
'completed', // Successfully completed
'failed', // Failed, can be retried
'failed_permanent' // Failed too many times
];
//Initialize UI
this.loadTemplates();
this.initUI();
// Operation type handlers
this.processors = {
'handle_vote': this.processVote.bind(this),
'invite_artist': this.processArtistInvite.bind(this),
'new_news': this.processNewNews.bind(this),
'new_response': this.processNewResponse.bind(this),
'bio_update': this.processBioUpdate.bind(this),
'favourite_toggle': this.processFavourite.bind(this),
'favourite_notes': this.processFavouriteNotes.bind(this),
'favourite_list_create': this.processFavouriteListCreate.bind(this),
'favourite_list_add': this.processFavouriteListAddItems.bind(this),
'favourite_list_remove': this.processFavouriteListRemoveItems.bind(this),
'favourite_list_delete': this.processFavouriteListDelete.bind(this),
'favourite_list_share': this.processFavouriteListShare.bind(this),
'favourite_list_unshare': this.processFavouriteListUnshare.bind(this),
'user_settings': this.processSettingsUpdate.bind(this),
'image_upload': this.processFileUpload.bind(this),
'content_create': this.processContentCreation.bind(this),
'batch_creation': this.processBatchCreation.bind(this),
'content_update': this.processContentUpdate.bind(this),
};
// Cache types that need to be cleared after operations
this.cacheTypesToClear = {
'handle_vote': ['karma'],
'new_news': ['news'],
'new_response': ['responses', 'news'],
'bio_update': ['artist'],
'favourite_toggle': ['favourites', 'favouritesManager'],
'favourite_notes': ['favouritesManager'],
'favourite_list_create': ['list-item', 'favourite-lists'],
'favourite_list_add': ['list-item', 'favourite-lists'],
'favourite_list_remove': ['list-item', 'favourite-lists'],
'favourite_list_delete': ['list-item', 'favourite-lists'],
'favourite_list_share': ['list-item', 'favourite-lists'],
'favourite_list_unshare': ['list-item', 'favourite-lists'],
};
// Human-readable operation names
this.operationNames = {
'handle_vote': 'Adding your voice',
'new_news': 'New News',
'invite_artist': 'Sending Invites...',
'new_response': 'Sending in your response',
'image_upload': 'Image Upload',
'content_update': 'Content Update',
'content_create': 'New Content',
'user_settings': 'Settings Update',
'favourite_toggle': 'Favourite Update',
'bio_update': 'Profile Update',
'batch_creation': 'Batch Content Creation',
'favourite_notes': 'Favourite Notes',
'favourite_list_create': 'List Creation',
'favourite_list_add': 'List Update',
'favourite_list_remove': 'List Update',
'favourite_list_delete': 'List Deletion',
'favourite_list_share': 'List Sharing',
'favourite_list_unshare': 'List Share Removal'
};
// Status messages for UI
this.statusMessages = {
'queued': 'Waiting to send to server...',
'localProcessing': 'Processing locally...',
'uploading': 'Sending to server...',
'pending': 'In line for processing...',
'processing': 'Server is working on it...',
'completed': 'All done!',
'failed': 'There was an error',
'failed_permanent': 'Failed permanently'
};
// Skip if not logged in
if (!jvbSettings.currentUser) {
return;
}
// Load queue from localStorage
this.loadQueue();
// Set up polling configuration
this.pollingConfig = {
enabled: true,
interval: 10000,
minInterval: 5000,
maxInterval: 60000,
countdown: true,
backoffFactor: 1.5, // Increase interval by this factor when no changes
consecutiveNoChanges: 0, // Track consecutive polls with no changes
maxNoChangesCount: 8, // Cap on increasing interval
lastETag: null,
lastUserActivity: Date.now(),
inactiveThreshold: 5 * 60 * 1000
};
// Track user activity
this.setupActivityTracking();
// Set up event listeners
this.initEventListeners();
// Initial fetch of server operations
this.fetchOperations(true).then(() => {
this.startPolling();
});
// Process queue periodically if there are changes
setInterval(() => {
if (this.hasChanges) {
this.processQueue();
}
}, 3000);
}
/**
* Track user activity to adjust polling
*/
setupActivityTracking() {
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
events.forEach(event => {
document.addEventListener(event, () => {
this.pollingConfig.lastUserActivity = Date.now();
}, { passive: true });
});
}
/**
* Initialize UI elements
*/
initUI() {
if (!jvbSettings.currentUser) return;
// Main panel elements
this.panel = document.querySelector('#queue-status-panel');
if (!this.panel) return;
this.panelList = this.panel.querySelector('.queue-list');
this.togglePanel = this.panel.querySelector('.queue-status-toggle');
this.countdown = this.panel.querySelector('.refresh-countdown');
this.refreshButton = this.panel.querySelector('.manual-refresh');
this.popup = this.panel.querySelector('.popup');
// Filter buttons
this.filterButtonsContainer = this.panel.querySelector('.queue-filters');
this.filterButtons = this.panel.querySelectorAll('.queue-filters .filter');
// Status-specific filter buttons
this.statusButtons = {};
this.statuses.forEach(status => {
this.statusButtons[status] = this.panel.querySelector(`.filter[data-filter="${status}"]`);
});
// Action buttons
this.retryButton = this.panel.querySelector('.retry-failed');
this.clearButton = this.panel.querySelector('.dismiss-completed');
// Initialize status panel
this.initStatusPanel();
}
/**
* Load HTML templates
*/
loadTemplates() {
this.templates = new Map();
// Default templates
this.templates.set('replyButton', this.createReplyButtonTemplate());
this.templates.set('commentsButton', this.createCommentsButtonTemplate());
this.templates.set('voteButton', this.createVoteButtonsTemplate());
// Load templates from DOM
document.querySelectorAll('template').forEach(template => {
const classes = Array.from(template.classList);
if (classes.length > 0) {
const item = template.content.cloneNode(true).firstElementChild;
classes.forEach(key => {
if (!this.templates.has(key)) {
this.templates.set(key, item);
}
});
}
});
}
/**
* Create reply button template
*/
createReplyButtonTemplate() {
const button = document.createElement('button');
button.type = 'button';
button.className = 'reply';
button.dataset.action = 'make-response';
button.innerHTML = jvbSettings.icons.reply + 'Reply';
return button;
}
/**
* Create comments button template
*/
createCommentsButtonTemplate() {
const button = document.createElement('a');
button.className = 'button';
button.innerHTML = jvbSettings.icons.response + '{ }';
return button;
}
/**
* Create vote buttons template
*/
createVoteButtonsTemplate() {
const vote = document.createElement('div');
vote.className = 'vote';
vote.innerHTML = `
`;
return vote;
}
/**
* Initialize status panel
*/
initStatusPanel() {
if (!this.panel) return;
// Toggle panel visibility
if (this.togglePanel) {
this.togglePanel.addEventListener('click', () => {
this.panel.classList.toggle('expanded');
this.togglePanel.title = this.panel.classList.contains('expanded')
? 'Hide Queue' : 'Show Queue';
const message = this.panel.classList.contains('expanded')
? 'Opened Queue Panel' : 'Closed Queue Panel';
this.a11y.announce(message);
this.maybeAddEmptyState();
});
}
// Close panel when clicking outside
document.addEventListener('click', (e) => {
if (this.panel.classList.contains('expanded') &&
!this.panel.contains(e.target) &&
e.target !== this.togglePanel) {
this.panel.classList.remove('expanded');
}
});
// Handle Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.panel.classList.contains('expanded')) {
this.panel.classList.remove('expanded');
}
});
// Retry failed operations
if (this.retryButton) {
this.retryButton.addEventListener('click', () => this.retryFailedOperations());
}
// Clear completed operations
if (this.clearButton) {
this.clearButton.addEventListener('click', () => this.clearCompletedOperations());
}
// Filter buttons
if (this.filterButtons) {
this.filterButtons.forEach(button => {
button.addEventListener('click', () => {
this.filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
const filter = button.dataset.filter || 'all';
this.fetchOperations(true, {status: filter});
});
});
}
// Refresh button
if (this.refreshButton) {
this.refreshButton.addEventListener('click', () => {
this.refreshButton.classList.add('refreshing');
this.fetchOperations(true, {force: true}).finally(() => {
this.refreshButton.classList.remove('refreshing');
});
});
}
// Item-specific actions
this.panelList.addEventListener('click', (e) => {
// Retry operation
if (e.target.classList.contains('retry-operation')) {
const operationItem = e.target.closest('.queue-item');
if (operationItem) {
this.retryOperation(operationItem.dataset.id);
}
}
// Dismiss operation
else if (e.target.classList.contains('dismiss-operation')) {
const operationItem = e.target.closest('.queue-item');
if (operationItem) {
this.dismissOperations([operationItem.dataset.id]);
}
}
});
// Initialize empty state
this.maybeAddEmptyState();
}
/**
* Set up event listeners
*/
initEventListeners() {
// Network status change
window.addEventListener('online', () => {
this.updateNetworkIndicator();
if (this.hasChanges) {
this.processQueue();
}
});
window.addEventListener('offline', () => {
this.updateNetworkIndicator();
this.updateStatusPanel('offline');
});
// Before unload warning for unsaved changes
window.addEventListener('beforeunload', (e) => {
const hasPendingLocalChanges = [...this.queue.values()].some(item =>
['queued', 'localProcessing', 'uploading'].includes(item.status)
);
if (hasPendingLocalChanges) {
e.preventDefault();
return e.returnValue = 'You have unsaved changes that haven\'t been sent to the server. Are you sure you want to leave?';
}
});
}
/**
* Maybe add empty state message
*/
maybeAddEmptyState() {
if (this.panelList.children.length === 0) {
this.panelList.innerHTML = '
Everything up to date
';
this.a11y.announce('Nothing queued.');
} else if (this.panelList.children.length > 1 && this.panelList.querySelector('.no-operations')) {
this.panelList.querySelector('.no-operations').remove();
}
}
/**
* Add an operation to the queue
*
* @param {Object} operation Operation data
* @returns {string} Operation ID
*/
async addToQueue(operation) {
// Try to merge with similar operations
const mergedOperation = this.mergeOperations(operation);
operation = mergedOperation || operation;
// Generate unique ID
const id = this.generateId();
// Create queue item
const item = {
id: id,
type: operation.type,
data: operation.data,
user: jvbSettings.currentUser,
started_at: Date.now(),
status: 'queued',
retries: 0,
dependencies: operation.dependencies || [],
onUpdate: operation.onUpdate??false
};
// Add to queue
this.queue.set(id, item);
this.saveQueue();
// Update UI
this.updateStatusPanel('pending');
this.addPopup(this.getPopupMessage(item));
this.hasChanges = true;
return item.id;
}
/**
* Generate a unique operation ID
*
* @returns {string} Unique ID
*/
generateId() {
// Create a timestamp-based prefix
const timestamp = new Date().getTime().toString(36);
// Add random component
const randomPart = Math.random().toString(36).substring(2, 8);
// Add counter to ensure uniqueness within same millisecond
this.idCounter = (this.idCounter || 0) + 1;
const counter = this.idCounter.toString(36);
return `u${jvbSettings.currentUser}-${timestamp}-${randomPart}-${counter}`;
}
/**
* Load queue from localStorage
*/
loadQueue() {
try {
const storedData = localStorage.getItem(this.STORAGE_KEY);
if (!storedData) return;
const queueData = JSON.parse(storedData);
if (!queueData || !Array.isArray(queueData)) return;
this.queue = new Map();
queueData.forEach(item => {
if (item && item.id) {
this.queue.set(item.id, item);
}
});
// Update UI for loaded items
[...this.queue.values()].forEach(item => {
this.updatePanelItem(item);
});
// Set hasChanges if there are queued items
this.hasChanges = [...this.queue.values()].some(item =>
['queued', 'localProcessing', 'uploading'].includes(item.status)
);
} catch (error) {
console.error('Error loading queue:', error);
this.queue = new Map();
}
}
/**
* Save queue to localStorage
*/
saveQueue() {
try {
// Filter items for storage
const queueToSave = [...this.queue.values()].filter(item => {
// Keep local items
if (['queued', 'localProcessing', 'uploading'].includes(item.status)) {
return true;
}
// Keep completed items for a short while
if (item.status === 'completed' && item.completed_at) {
const hourAgo = Date.now() - (60 * 60 * 1000);
return new Date(item.completed_at).getTime() > hourAgo;
}
// Keep failed items that might be retried
return (item.status === 'failed' || item.status === 'failed_permanent') &&
item.retries < this.maxRetries;
});
// Serialize and store
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(queueToSave));
// Also save stats for quick access
localStorage.setItem(
this.STORAGE_KEY + '_stats',
JSON.stringify(this.getQueueStats())
);
} catch (error) {
console.error('Error saving queue:', error);
// If we hit storage limits, clean up
this.emergencyQueueCleanup();
}
}
/**
* Emergency cleanup when storage is full
*/
emergencyQueueCleanup() {
console.warn('Emergency queue cleanup triggered');
// Keep only essential pending items
const tempQueue = this.queue;
this.queue = new Map();
for (const [key, item] of tempQueue.entries()) {
if (item && item.status === 'pending') {
this.queue.set(key, item);
}
}
// Save the reduced queue
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify([...this.queue.values()]));
localStorage.removeItem(this.STORAGE_KEY + '_stats');
} catch (error) {
console.error('Failed even after cleanup:', error);
// Last resort - clear everything
localStorage.removeItem(this.STORAGE_KEY);
localStorage.removeItem(this.STORAGE_KEY + '_stats');
}
}
/**
* Fetch operations from the server
*
* @param {boolean} initial Whether this is the initial fetch
* @param {Object} options Filter options
* @returns {Promise