marwanhawari/LeetRooms

bug: fix timer inconsistencies

Opened this issue · 2 comments

There are currently 2 issues with the timer:

  1. Timer drift causes everybody's timer to be out of sync.

  2. The timer will pause if you go to a different tab for too long. When you come back it will resume, but the time will be completely wrong. Only after you refresh the page will the timer display the proper time remaining.

These fixes should be implemented purely in the React app. I don't expect any chrome extension or server changes to be necessary.

#24 fixes point number 2 above

@marwanhawari Here's a solution that should fix the timer bug inconsistencies. I can submit a PR, but before I do, I'd recommend that you validate this code locally within the extension. Unfortunately, I don't use Chrome, so I don't have any way to validate myself.

In summary, regardless of when the tab becomes active, the state will become in sync because it recalculates the time left by using the end date minus the current date instead of the previous date in state.

Bugfix code
import { useState, useEffect, useRef } from "react";
import StopwatchIcon from "../icons/StopwatchIcon";

function calculateTimeLeft(eT: Date) {
  const tL = eT.getTime() - new Date().getTime();
  const exp = tL <= 0;

  let h = Math.floor((tL % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  let m = Math.floor((tL % (1000 * 60 * 60)) / (1000 * 60));
  let s = Math.floor((tL % (1000 * 60)) / 1000);

  if (exp) {
    h = 0;
    m = 0;
    s = 0;
  }

  return {
    expired: exp,
    hours: h.toString().padStart(2, "0"),
    minutes: m.toString().padStart(2, "0"),
    seconds: s.toString().padStart(2, "0")
  };
}

type TimeLeft = {
  expired: boolean;
  hours: string;
  minutes: string;
  seconds: string;
};

/*
  When endTime is created, it should be static and the same for all users

  const endTime = new Date();
  endTime.setHours(endTime.getHours() + 1);
*/

export default function Timer({ endTime }: { endTime: Date }) {
  const [timeLeft, setTimeLeft] = useState<TimeLeft>(calculateTimeLeft(endTime));
  const timer = useRef<NodeJS.Timer | null>(null);

  useEffect(() => {
    function updateTime() {
      const newTime = calculateTimeLeft(endTime);

      if (newTime.expired && timer.current) {
        clearInterval(timer.current);
        timer.current = null;
      }

      setTimeLeft(newTime);
    }

    timer.current = setInterval(updateTime, 1000);

    return () => {
      if (timer.current) clearInterval(timer.current);
    };
  }, [endTime]);

  return (
    <div
      className="mt-2 flex flex-col items-center justify-center rounded-md px-2 py-[8px] text-xs transition-all dark:bg-lc-fg ${
        expired ? "text-lc-hd-fg-light dark:text-lc-hd-fg" : ""
      }`}
    >
      <div className="flex items-center gap-2">
        <StopwatchIcon />
        <time className="font-mono">{timeLeft.hours}:{timeLeft.minutes}:{timeLeft.seconds}</time>
      </div>
    </div>
  );
}

Working Demo

or you can edit the demo's code by clicking the button below:

Edit Bug Fix - Leet Code Rooms Issue #20

📝 Just note that this solution still relies upon the client machine to have their time in-sync with actual time. Otherwise, users will still see a different countdown timer. An alternative solution for this would be to generate and recalculate the time left on a server via a websocket. The main draw back with this would be that you'll incur more bandwidth usage.

On an unrelated note, ideally you'll want to use semantic HTML for text -- like utilizing a time or p element. Otherwise, TTS readers won't be able to read the text within a div element. If you're not interested in making the extension web-accessible, then please disregard this note.