All files / apps/chatbot/src/hooks usePendingActionCountdown.ts

100% Statements 24/24
95% Branches 38/40
100% Functions 5/5
100% Lines 20/20

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        2x             13x 13x   13x 13x 13x 13x 13x 4x 13x 13x 13x 13x 9x 92x 3x   13x   13x       13x     13x                
import { useEffect, useRef, useState } from "react";
 
import { ChatbotPendingAction } from "@/types/chat";
 
export const usePendingActionCountdown = (
  pendingAction:
    | (Pick<ChatbotPendingAction, "id"> & Partial<ChatbotPendingAction>)
    | null
    | undefined,
) => {
  const expiresAtMs =
    typeof pendingAction?.expiresAt === "string" ? Date.parse(pendingAction.expiresAt) : null;
  const hasExpiry = expiresAtMs !== null && !Number.isNaN(expiresAtMs);
  const createdAtMs =
    typeof pendingAction?.createdAt === "string" ? Date.parse(pendingAction.createdAt) : null;
  const [nowMs, setNowMs] = useState(() => Date.now());
  const windowKey = hasExpiry ? `${pendingAction?.id ?? ""}:${expiresAtMs}` : null;
  const anchorRef = useRef<{ key: string; atMs: number } | null>(null);
  if (windowKey !== null && anchorRef.current?.key !== windowKey)
    anchorRef.current = { key: windowKey, atMs: Date.now() };
  const anchorMs = windowKey !== null ? (anchorRef.current?.atMs ?? null) : null;
  const effectiveNowMs = anchorMs !== null ? Math.max(nowMs, anchorMs) : nowMs;
  const isExpired = hasExpiry && expiresAtMs <= effectiveNowMs;
  useEffect(() => {
    if (!hasExpiry || isExpired) return undefined;
    const interval = setInterval(() => setNowMs(Date.now()), 1000);
    return () => clearInterval(interval);
  }, [hasExpiry, isExpired]);
  const rawRemainingMs = hasExpiry ? expiresAtMs - effectiveNowMs : null;
  const ttlBoundMs =
    createdAtMs !== null && !Number.isNaN(createdAtMs) && hasExpiry && anchorMs !== null
      ? expiresAtMs - createdAtMs - (effectiveNowMs - anchorMs)
      : null;
  const remainingSec =
    rawRemainingMs !== null
      ? Math.max(0, Math.ceil(Math.min(rawRemainingMs, ttlBoundMs ?? rawRemainingMs) / 1000))
      : null;
  return {
    isExpired,
    countdownLabel:
      remainingSec === null
        ? null
        : `Expires in ${Math.floor(remainingSec / 60)}:${String(remainingSec % 60).padStart(2, "0")}`,
  };
};