wse-1.0.x-dev/modules/wse_task_monitor/js/task-monitor.js
modules/wse_task_monitor/js/task-monitor.js
/**
* @file
* Task monitor with minimal JavaScript for Drupal integration.
*/
(function (Drupal, once) {
/**
* Task Monitor behavior - handles Drupal-specific integrations.
*/
Drupal.behaviors.wseTaskMonitor = {
attach(context, settings) {
// Initialize manager once using the once() pattern.
once('wse-task-monitor-init', 'body', context).forEach(function () {
// Create WSE namespace if it doesn't exist.
Drupal.wse = Drupal.wse || {};
if (!Drupal.wse.taskMonitor) {
// eslint-disable-next-line no-use-before-define
Drupal.wse.taskMonitor = new TaskMonitorManager();
}
});
// Attach form submit handlers.
const taskForms = once(
'wse-task-form',
'[data-wse-task-monitor="true"]',
context,
);
taskForms.forEach(function (form) {
form.addEventListener('submit', function (event) {
// Activate monitoring immediately - task is created on submit.
if (Drupal.wse && Drupal.wse.taskMonitor) {
Drupal.wse.taskMonitor.activateTaskMonitoring();
}
});
});
},
};
/**
* Task Monitor Manager - handles Drupal-specific task monitoring.
*/
class TaskMonitorManager {
constructor() {
this.messageAPI = new Drupal.Message();
this.tasks = new Map(); // Map task ID to task data.
this.progressBars = new Map(); // Map task ID to ProgressBar instance.
this.isActive = false;
this.monitorElement = null;
}
/**
* Activate task monitoring.
*/
activateTaskMonitoring() {
if (this.isActive) {
return;
}
this.isActive = true;
this.setupEventListeners();
this.startSSEConnection();
}
/**
* Start SSE connection.
*/
startSSEConnection() {
// Find or create the monitor element.
let monitorElement = document.querySelector('#wse-task-monitor');
if (!monitorElement) {
monitorElement = document.createElement('div');
monitorElement.id = 'wse-task-monitor';
monitorElement.setAttribute('hx-ext', 'sse');
monitorElement.style.display = 'none'; // Hidden container.
document.body.appendChild(monitorElement);
// Add child elements for each event type we want to listen to.
const eventTypes = [
'connected',
'new-task',
'task-progress',
'task-complete',
'all-complete',
'idle-timeout',
'connection-timeout',
'error',
'heartbeat',
];
eventTypes.forEach((eventType) => {
const eventElement = document.createElement('div');
eventElement.setAttribute('sse-swap', eventType);
eventElement.style.display = 'none';
monitorElement.appendChild(eventElement);
});
}
this.monitorElement = monitorElement;
// Wait 1 second before connecting to ensure page is fully loaded.
setTimeout(() => {
monitorElement.setAttribute(
'sse-connect',
Drupal.url('wse-task-monitor/stream'),
);
htmx.process(monitorElement);
}, 1000);
}
/**
* Setup SSE event listeners.
*/
setupEventListeners() {
const self = this;
document.addEventListener('htmx:sseBeforeMessage', function (event) {
// Check if this event is from our monitor (either the main element
// or its children).
const monitorElement = document.querySelector('#wse-task-monitor');
if (
!monitorElement ||
(!monitorElement.contains(event.target) &&
event.target !== monitorElement)
) {
return;
}
try {
// The event.detail is the MessageEvent from SSE.
const messageEvent = event.detail;
const eventType = messageEvent.type;
const data = JSON.parse(messageEvent.data);
self.handleSSEEvent(eventType, data);
} catch (error) {
// Silently ignore parsing errors.
}
});
document.addEventListener('htmx:sseClose', function (event) {
const monitorElement = document.querySelector('#wse-task-monitor');
if (
!monitorElement ||
(!monitorElement.contains(event.target) &&
event.target !== monitorElement)
) {
return;
}
self.isActive = false;
});
}
/**
* Handle SSE events.
*/
handleSSEEvent(eventType, data) {
switch (eventType) {
case 'new-task':
this.handleNewTask(data);
break;
case 'task-progress':
this.handleTaskProgress(data);
break;
case 'task-complete':
this.handleTaskComplete(data);
break;
case 'all-complete':
this.handleAllComplete(data);
break;
case 'idle-timeout':
this.handleIdleTimeout(data);
break;
case 'connection-timeout':
this.handleConnectionTimeout(data);
break;
case 'error':
this.handleError(data);
break;
}
}
/**
* Handle new task event.
*/
handleNewTask(task) {
this.tasks.set(task.id, task);
// Create new task display using Drupal's Message API.
const html = `<div class="wse-task-monitor__progress"></div>`;
this.messageAPI.add(html, { type: 'status', id: task.id });
// Create and attach progress bar.
const messageElement = document.querySelector(
`[data-drupal-message-id="${task.id}"]`,
);
const progressContainer = messageElement?.querySelector(
'.wse-task-monitor__progress',
);
if (progressContainer) {
const progressBar = new Drupal.ProgressBar(
`task-progress-${task.id}`,
null,
'GET',
null,
);
const progressBarElement =
progressBar.element[0] || progressBar.element;
progressContainer.appendChild(progressBarElement);
this.progressBars.set(task.id, progressBar);
// Set initial progress.
progressBar.setProgress(
task.progress || 0,
task.message || 'Initializing...',
task.label || 'Processing...',
);
}
}
/**
* Handle task progress update.
*/
handleTaskProgress(data) {
const task = this.tasks.get(data.taskId);
const progressBar = this.progressBars.get(data.taskId);
if (task && progressBar) {
task.progress = data.percentage;
task.message = data.message;
progressBar.setProgress(
data.percentage || 0,
data.message || 'Processing...',
task.label || 'Processing...',
);
}
}
/**
* Handle task completion.
*/
handleTaskComplete(data) {
const task = this.tasks.get(data.taskId);
const progressBar = this.progressBars.get(data.taskId);
if (task && progressBar) {
// Mark as completed.
progressBar.setProgress(
100,
data.message || 'Task completed',
task.label || 'Completed',
);
// Remove after delay.
setTimeout(() => {
this.removeTask(data.taskId);
}, 1000);
}
}
/**
* Handle all tasks complete.
*/
handleAllComplete(data) {
this.stopTaskMonitoring();
}
/**
* Handle idle timeout.
*/
handleIdleTimeout(data) {
// Only stop monitoring if no active tasks exist.
if (this.tasks.size === 0) {
this.stopTaskMonitoring();
}
}
/**
* Handle connection timeout.
*/
// eslint-disable-next-line class-methods-use-this
handleConnectionTimeout(data) {
// Don't stop monitoring, the SSE extension will handle reconnection.
// The connection will close naturally and HTMX will reconnect with
// exponential backoff (500ms, 1s, 2s, 4s, ..., max 64s).
}
/**
* Handle error event.
*/
handleError(data) {
this.messageAPI.add(data.error, { type: 'error' });
}
/**
* Remove a task from the display.
*/
removeTask(taskId) {
// Remove the Drupal message (task ID is the message ID).
this.messageAPI.remove(taskId);
// Clean up stored data.
this.tasks.delete(taskId);
this.progressBars.delete(taskId);
}
/**
* Stop task monitoring.
*/
stopTaskMonitoring() {
this.isActive = false;
if (this.monitorElement) {
this.monitorElement.removeAttribute('sse-connect');
}
}
/**
* Clean up all resources.
*/
destroy() {
this.stopTaskMonitoring();
// Remove monitor element.
if (this.monitorElement && this.monitorElement.parentNode) {
this.monitorElement.parentNode.removeChild(this.monitorElement);
this.monitorElement = null;
}
// Remove all task messages.
this.tasks.forEach((task, taskId) => {
this.messageAPI.remove(taskId);
});
// Clear all stored data.
this.tasks.clear();
this.progressBars.clear();
}
}
})(Drupal, once);
