Untitled

Anonymous
plain_text
02/27/2026 4:26 AM
13.5 KB
18
Indexable
import {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosError,
  AxiosResponse,
} from 'axios';
import Router from 'next/router';
import { dispatchGlobal } from '@otten-ecommerce/store';

interface InterceptorModel {
  axios: AxiosInstance;
  handleRTO?: boolean;
  isErrorNoRemoveToken?: boolean;
  hideBlocker?: boolean;
  noRedirect?: boolean;
  cardLoader?: boolean;
  loaderClaim?: boolean;
}

// Request batch tracking for simple majority logic
const BATCH_WINDOW_MS = 2000; // 2 seconds window for a batch
const BATCH_IDLE_MS = 500; // 500ms idle time to consider batch complete
const FAILURE_THRESHOLD = 0.5; // Show modal if >50% of requests failed

interface RequestBatch {
  successCount: number;
  failedCount: number;
  pendingCount: number;
  lastActivityTime: number;
  hasEvaluated: boolean;
  failedMethods: string[]; // Track methods of failed requests
}

let requestBatch: RequestBatch = {
  successCount: 0,
  failedCount: 0,
  pendingCount: 0,
  lastActivityTime: 0,
  hasEvaluated: false,
  failedMethods: [],
};

let batchEvaluateTimer: ReturnType<typeof setTimeout> | null = null;

/**
 * Reset batch counters for a new batch of requests
 */
const resetBatch = () => {
  requestBatch = {
    successCount: 0,
    failedCount: 0,
    pendingCount: 0,
    lastActivityTime: Date.now(),
    hasEvaluated: false,
    failedMethods: [],
  };
};

/**
 * Evaluate the batch and show modal if failure rate exceeds threshold
 */
const evaluateBatch = () => {
  if (requestBatch.hasEvaluated) {
    return;
  }

  const total = requestBatch.successCount + requestBatch.failedCount;

  // Only evaluate if we have some requests completed
  if (total === 0) {
    return;
  }

  const failureRate = requestBatch.failedCount / total;

  // Show modal only if majority failed
  if (failureRate > FAILURE_THRESHOLD) {
    requestBatch.hasEvaluated = true;

    // Determine error type from the most recent failure context
    // For now, we'll defer to the error handler to dispatch with actual type
    return true; // Signal that modal should be shown
  }

  return false;
};

/**
 * Schedule batch evaluation after idle time
 */
const scheduleBatchEvaluation = (
  callback: (shouldShow: boolean) => void
) => {
  if (batchEvaluateTimer) {
    clearTimeout(batchEvaluateTimer);
  }

  batchEvaluateTimer = setTimeout(() => {
    batchEvaluateTimer = null;

    // Check if still active (no new requests in IDLE time)
    const idleTime = Date.now() - requestBatch.lastActivityTime;
    if (idleTime >= BATCH_IDLE_MS && requestBatch.pendingCount === 0) {
      const shouldShow = evaluateBatch();
      if (!shouldShow && !requestBatch.hasEvaluated) {
        // Batch passed threshold, prepare for new batch
        resetBatch();
      }
      callback(shouldShow || false);
    }
  }, BATCH_IDLE_MS);
};

/**
 * Track request start
 */
const trackRequestStart = () => {
  const now = Date.now();

  // Start new batch if window expired
  if (
    requestBatch.lastActivityTime > 0 &&
    now - requestBatch.lastActivityTime > BATCH_WINDOW_MS
  ) {
    resetBatch();
  }

  requestBatch.pendingCount++;
  requestBatch.lastActivityTime = now;
};

/**
 * Track request success
 */
const trackRequestSuccess = () => {
  requestBatch.successCount++;
  requestBatch.pendingCount--;
  requestBatch.lastActivityTime = Date.now();

  scheduleBatchEvaluation(() => {});
};

/**
 * Track request error - returns error type for batch evaluation
 */
const trackRequestError = (
  errorType: 'no-internet' | 'server-error' | null,
  method: string
): { shouldShow: boolean; batchErrorType: 'no-internet' | 'server-error' | null } => {
  requestBatch.pendingCount--;
  requestBatch.lastActivityTime = Date.now();

  if (!errorType) {
    return { shouldShow: false, batchErrorType: null };
  }

  requestBatch.failedCount++;
  requestBatch.failedMethods.push(method);

  // Both no-internet and server-error defer to batch evaluation
  scheduleBatchEvaluation((shouldShow) => {
    if (shouldShow && errorType) {
      // Use the most common failed method, or GET as default
      const methodCounts = requestBatch.failedMethods.reduce(
        (acc, m) => {
          acc[m] = (acc[m] || 0) + 1;
          return acc;
        },
        {} as Record<string, number>
      );
      const mostCommonMethod = Object.entries(methodCounts).sort(
        (a, b) => b[1] - a[1]
      )[0]?.[0] || 'GET';

      dispatchGlobal({
        type: 'setModalError',
        payload: {
          isShow: true,
          type: errorType,
          method: mostCommonMethod,
        },
      });
    }
  });

  return { shouldShow: false, batchErrorType: errorType };
};

export default (props: InterceptorModel) => {
  const {
    axios,
    handleRTO,
    isErrorNoRemoveToken,
    hideBlocker,
    noRedirect,
    cardLoader,
    loaderClaim
  } = props;
  const blocker = document.getElementById('blocker') as HTMLElement;
  axios.interceptors.request.use(
    (configAxios: AxiosRequestConfig) => {
      if (cardLoader) {
        window.dispatchEvent(new CustomEvent('show-card-loader'));
      }

      if (loaderClaim) {
        window.dispatchEvent(new CustomEvent('show-claim-loader'));
      }

      if (!hideBlocker) {
        blocker?.classList.add('active');
        document.body.classList.add('hidden');
      }

      // Track request start for batch evaluation
      trackRequestStart();

      return configAxios;
  },
  (errorAxios: AxiosError) => {

    if (cardLoader) {
      window.dispatchEvent(new CustomEvent('hide-card-loader'));
    }

    if (loaderClaim) {
      window.dispatchEvent(new CustomEvent('hide-claim-loader'));
    }

    return Promise.reject(errorAxios);
  }
);

  axios.interceptors.response.use(
    (responseAxios: any) => {
      if (cardLoader) {
        window.dispatchEvent(new CustomEvent('hide-card-loader'));
      }

      if (loaderClaim) {
        window.dispatchEvent(new CustomEvent('hide-claim-loader'));
      }

      if (!hideBlocker) {
        blocker?.classList.remove('active');
        document.body.classList.remove('hidden');
      }

      // Track request success
      trackRequestSuccess();

      return responseAxios;
    },
    (errorAxios: AxiosError) => {
      if (cardLoader) {
        window.dispatchEvent(new CustomEvent('hide-card-loader'));
      }

      if (loaderClaim) {
        window.dispatchEvent(new CustomEvent('hide-claim-loader'));
      }

      if (!hideBlocker) {
        blocker?.classList.remove('active');
        document.body.classList.remove('hidden');
      }
      const errorCode = errorAxios?.response?.status;
      const pathname = location.pathname;
      const fullUrl = `${pathname}${location.search}`;
      const errorType = resolveModalErrorType(errorAxios);
      const requestMethod = errorAxios?.config?.method?.toUpperCase() || 'GET';

      if (errorType) {
        // Track error - modal will be shown by batch evaluator if threshold exceeded
        trackRequestError(errorType, requestMethod);
      }
      // (disabled) needed updated flow
      // if (handleRTO && errorAxios.code === "ECONNABORTED") {
      //   Router.push("/server-busy");
      // }
      if (errorCode === 401 && window.location.pathname !== '/login') {
        if (!isErrorNoRemoveToken) localStorage.removeItem('access_token');
        if (!noRedirect) {
          if (
            localStorage.getItem('history_path') === '/' ||
            localStorage.getItem('history_path') === ''
          ) {
            localStorage.removeItem('history_path');
            Router.push('/');
          } else {
            Router.push({ pathname: '/login', query: { next: fullUrl } });
          }
        }
      }
      return Promise.reject(errorAxios);
    }
  );
};

const resolveModalErrorType = (
  errorAxios: AxiosError
): 'no-internet' | 'server-error' | null => {
  const errorCode = errorAxios?.code;
  const errorMessage = String(errorAxios?.message || '').toLowerCase();
  const statusCode = errorAxios?.response?.status;
  const requestStatus = Number((errorAxios as any)?.request?.status || 0);
  const resolvedStatusCode =
    typeof statusCode === 'number'
      ? statusCode
      : requestStatus > 0
      ? requestStatus
      : undefined;
  const isOnline = typeof navigator !== 'undefined' ? navigator.onLine : true;
  const hasNoResponse = !errorAxios?.response;

  if (typeof resolvedStatusCode === 'number' && resolvedStatusCode >= 500) {
    return 'server-error';
  }

  if (errorCode === 'ERR_INTERNET_DISCONNECTED') {
    return 'no-internet';
  }

  if (
    isLikelyCorsError({
      errorAxios,
      isOnline,
      hasNoResponse,
      errorCode,
      errorMessage,
      requestStatus,
    })
  ) {
    return null;
  }

  if (errorCode === 'ERR_NETWORK') {
    return isOnline ? 'server-error' : 'no-internet';
  }

  if (hasNoResponse && !isOnline) {
    return 'no-internet';
  }

  if (errorCode === 'ECONNABORTED') {
    return isOnline ? 'server-error' : 'no-internet';
  }

  if (hasNoResponse && errorMessage.includes('network error')) {
    return isOnline ? 'server-error' : 'no-internet';
  }

  return null;
};

const isLikelyCorsError = ({
  errorAxios,
  isOnline,
  hasNoResponse,
  errorCode,
  errorMessage,
  requestStatus,
}: {
  errorAxios: AxiosError;
  isOnline: boolean;
  hasNoResponse: boolean;
  errorCode?: string;
  errorMessage: string;
  requestStatus: number;
}): boolean => {
  if (!isOnline || !hasNoResponse) {
    return false;
  }

  const isNetworkLikeError =
    errorCode === 'ERR_NETWORK' || errorMessage.includes('network error');

  if (!isNetworkLikeError || requestStatus !== 0) {
    return false;
  }

  if (typeof window === 'undefined') {
    return false;
  }

  const requestUrl = resolveRequestUrl(errorAxios?.config);

  if (!requestUrl) {
    return false;
  }

  try {
    return new URL(requestUrl).origin !== window.location.origin;
  } catch {
    return false;
  }
};

const resolveRequestUrl = (config?: AxiosRequestConfig): string | null => {
  const rawUrl = config?.url;

  if (!rawUrl || typeof rawUrl !== 'string') {
    return null;
  }

  try {
    const base =
      config?.baseURL ||
      (typeof window !== 'undefined' ? window.location.origin : undefined);

    return base ? new URL(rawUrl, base).toString() : rawUrl;
  } catch {
    return rawUrl;
  }
};
Editor is loading...
Leave a Comment