All files / src/hooks/exports asyncExportCore.ts

38% Statements 19/50
11.11% Branches 2/18
40% Functions 2/5
38% Lines 19/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142                                          539x   539x   539x       3x 3x 3x 3x 3x 3x 3x   3x               3x 3x             539x                                                                                                                                               539x 1x     1x 1x                   539x  
import { QueryClient } from "react-query";
import {
  getBackgroundTaskStatus,
  markBackgroundTasksAsRead,
} from "@services/api/transactions";
import type {
  BackgroundTask,
  TransactionExportResult,
} from "@customTypes/backgroundTasks";
import { getTaskErrorMessage } from "@hooks/exports/asyncExport.helpers";
import { showMessage } from "@common/Toast";
 
export type AsyncExportEntry = {
  merchantId: number;
  taskId: number;
  downloadAttempted: boolean;
  storageKey: string;
  queryKeyPrefix: string;
};
 
// Single global polling interval handles all in-flight async exports regardless of type
let globalPollingInterval: NodeJS.Timeout | null = null;
 
export const activeExports = new Map<string, AsyncExportEntry>();
 
export const handleExportDownload = async (
  taskData: BackgroundTask<TransactionExportResult>,
  merchantId: number,
) => {
  Eif (taskData.result?.downloadURL) {
    const link = document.createElement("a");
    link.href = taskData.result.downloadURL;
    link.download = taskData.result.fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
 
    showMessage(
      "Success",
      `Export completed! ${taskData.result.recordCount.toLocaleString()} records downloaded.`,
      true,
      "Download Started",
      5000,
    );
 
    try {
      await markBackgroundTasksAsRead(merchantId, [taskData.id]);
    } catch (error) {
      console.error("Error marking task as read:", error);
    }
  }
};
 
export const startGlobalPolling = (
  merchantId: number,
  taskId: number,
  queryClient: QueryClient,
  storageKey: string,
  queryKeyPrefix: string,
) => {
  const key = `${merchantId}-${taskId}`;
  activeExports.set(key, {
    merchantId,
    taskId,
    downloadAttempted: false,
    storageKey,
    queryKeyPrefix,
  });
 
  if (globalPollingInterval) {
    clearInterval(globalPollingInterval);
  }
 
  globalPollingInterval = setInterval(async () => {
    const entries = Array.from(activeExports.entries());
    for (const [entryKey, exportInfo] of entries) {
      const {
        merchantId: mid,
        taskId: tid,
        downloadAttempted,
        storageKey: sKey,
        queryKeyPrefix: qPrefix,
      } = exportInfo;
      try {
        const taskData = (await getBackgroundTaskStatus(
          mid,
          tid,
        )) as BackgroundTask<TransactionExportResult>;
 
        queryClient.setQueryData([qPrefix, mid, tid], taskData);
 
        if (taskData.status === "completed" && !downloadAttempted) {
          exportInfo.downloadAttempted = true;
          await handleExportDownload(taskData, mid);
          activeExports.delete(entryKey);
          sessionStorage.removeItem(sKey);
        }
 
        if (taskData.status === "failed") {
          showMessage(
            "Error",
            getTaskErrorMessage(taskData.error),
            true,
            "Export Failed",
          );
          activeExports.delete(entryKey);
          sessionStorage.removeItem(sKey);
        }
 
        if (taskData.status === "cancelled") {
          activeExports.delete(entryKey);
          sessionStorage.removeItem(sKey);
        }
      } catch (error) {
        console.error("Error polling task:", error);
      }
    }
 
    if (activeExports.size === 0 && globalPollingInterval) {
      clearInterval(globalPollingInterval);
      globalPollingInterval = null;
    }
  }, 3000);
};
 
const clearAllExports = () => {
  activeExports.forEach(({ storageKey }) => {
    sessionStorage.removeItem(storageKey);
  });
  activeExports.clear();
  Iif (globalPollingInterval) {
    clearInterval(globalPollingInterval);
    globalPollingInterval = null;
  }
};
 
/**
 * Clear all in-flight async exports and stop polling.
 * Called on logout and account switch.
 */
export const cleanupAllAsyncExports = clearAllExports;