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);

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc