From 5ce8fdbc37730d221c1bfbf0e9f9cbc44ff9302f Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 4 Aug 2022 19:20:54 +0800
Subject: [PATCH] Fix the JS error "EventSource is not defined" caused by some
 non-standard browsers (#20584) (#20663)

---
 .../js/features/eventsource.sharedworker.js   |  7 ++
 web_src/js/features/notification.js           | 37 +++++----
 web_src/js/features/stopwatch.js              | 77 ++++++++++---------
 3 files changed, 66 insertions(+), 55 deletions(-)

diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js
index 824ccfea79..2ac7d93cc1 100644
--- a/web_src/js/features/eventsource.sharedworker.js
+++ b/web_src/js/features/eventsource.sharedworker.js
@@ -70,6 +70,13 @@ class Source {
 self.addEventListener('connect', (e) => {
   for (const port of e.ports) {
     port.addEventListener('message', (event) => {
+      if (!self.EventSource) {
+        // some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope.
+        // this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller,
+        // in case the caller would like to use a fallback method to do its work.
+        port.postMessage({type: 'no-event-source'});
+        return;
+      }
       if (event.data.type === 'start') {
         const url = event.data.url;
         if (sourcesByUrl[url]) {
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index d184db0ddd..551121dfc0 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -47,14 +47,24 @@ export function initNotificationCount() {
     return;
   }
 
-  if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) {
+  let usingPeriodicPoller = false;
+  const startPeriodicPoller = (timeout, lastCount) => {
+    if (timeout <= 0 || !Number.isFinite(timeout)) return;
+    usingPeriodicPoller = true;
+    lastCount = lastCount ?? notificationCount.text();
+    setTimeout(async () => {
+      await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
+    }, timeout);
+  };
+
+  if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
     // Try to connect to the event source via the shared worker first
     const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
     worker.addEventListener('error', (event) => {
-      console.error(event);
+      console.error('worker error', event);
     });
     worker.port.addEventListener('messageerror', () => {
-      console.error('Unable to deserialize message');
+      console.error('unable to deserialize message');
     });
     worker.port.postMessage({
       type: 'start',
@@ -62,13 +72,16 @@ export function initNotificationCount() {
     });
     worker.port.addEventListener('message', (event) => {
       if (!event.data || !event.data.type) {
-        console.error(event);
+        console.error('unknown worker message event', event);
         return;
       }
       if (event.data.type === 'notification-count') {
         const _promise = receiveUpdateCount(event.data);
+      } else if (event.data.type === 'no-event-source') {
+        // browser doesn't support EventSource, falling back to periodic poller
+        if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
       } else if (event.data.type === 'error') {
-        console.error(event.data);
+        console.error('worker port event error', event.data);
       } else if (event.data.type === 'logout') {
         if (event.data.data !== 'here') {
           return;
@@ -86,7 +99,7 @@ export function initNotificationCount() {
       }
     });
     worker.port.addEventListener('error', (e) => {
-      console.error(e);
+      console.error('worker port error', e);
     });
     worker.port.start();
     window.addEventListener('beforeunload', () => {
@@ -99,17 +112,7 @@ export function initNotificationCount() {
     return;
   }
 
-  if (notificationSettings.MinTimeout <= 0) {
-    return;
-  }
-
-  const fn = (timeout, lastCount) => {
-    setTimeout(() => {
-      const _promise = updateNotificationCountWithCallback(fn, timeout, lastCount);
-    }, timeout);
-  };
-
-  fn(notificationSettings.MinTimeout, notificationCount.text());
+  startPeriodicPoller(notificationSettings.MinTimeout);
 }
 
 async function updateNotificationCountWithCallback(callback, timeout, lastCount) {
diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js
index c47ba22124..c3aa79b767 100644
--- a/web_src/js/features/stopwatch.js
+++ b/web_src/js/features/stopwatch.js
@@ -2,7 +2,6 @@ import $ from 'jquery';
 import prettyMilliseconds from 'pretty-ms';
 
 const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking} = window.config;
-let updateTimeInterval = null; // holds setInterval id when active
 
 export function initStopwatch() {
   if (!enableTimeTracking) {
@@ -26,14 +25,28 @@ export function initStopwatch() {
     $(this).parent().trigger('submit');
   });
 
-  if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) {
+  // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
+  const currSeconds = $('.stopwatch-time').attr('data-seconds');
+  if (currSeconds) {
+    updateStopwatchTime(currSeconds);
+  }
+
+  let usingPeriodicPoller = false;
+  const startPeriodicPoller = (timeout) => {
+    if (timeout <= 0 || !Number.isFinite(timeout)) return;
+    usingPeriodicPoller = true;
+    setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
+  };
+
+  // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller
+  if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
     // Try to connect to the event source via the shared worker first
     const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
     worker.addEventListener('error', (event) => {
-      console.error(event);
+      console.error('worker error', event);
     });
     worker.port.addEventListener('messageerror', () => {
-      console.error('Unable to deserialize message');
+      console.error('unable to deserialize message');
     });
     worker.port.postMessage({
       type: 'start',
@@ -41,13 +54,16 @@ export function initStopwatch() {
     });
     worker.port.addEventListener('message', (event) => {
       if (!event.data || !event.data.type) {
-        console.error(event);
+        console.error('unknown worker message event', event);
         return;
       }
       if (event.data.type === 'stopwatches') {
         updateStopwatchData(JSON.parse(event.data.data));
+      } else if (event.data.type === 'no-event-source') {
+        // browser doesn't support EventSource, falling back to periodic poller
+        if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
       } else if (event.data.type === 'error') {
-        console.error(event.data);
+        console.error('worker port event error', event.data);
       } else if (event.data.type === 'logout') {
         if (event.data.data !== 'here') {
           return;
@@ -65,7 +81,7 @@ export function initStopwatch() {
       }
     });
     worker.port.addEventListener('error', (e) => {
-      console.error(e);
+      console.error('worker port error', e);
     });
     worker.port.start();
     window.addEventListener('beforeunload', () => {
@@ -78,22 +94,7 @@ export function initStopwatch() {
     return;
   }
 
-  if (notificationSettings.MinTimeout <= 0) {
-    return;
-  }
-
-  const fn = (timeout) => {
-    setTimeout(() => {
-      const _promise = updateStopwatchWithCallback(fn, timeout);
-    }, timeout);
-  };
-
-  fn(notificationSettings.MinTimeout);
-
-  const currSeconds = $('.stopwatch-time').data('seconds');
-  if (currSeconds) {
-    updateTimeInterval = updateStopwatchTime(currSeconds);
-  }
+  startPeriodicPoller(notificationSettings.MinTimeout);
 }
 
 async function updateStopwatchWithCallback(callback, timeout) {
@@ -114,12 +115,6 @@ async function updateStopwatch() {
     url: `${appSubUrl}/user/stopwatches`,
     headers: {'X-Csrf-Token': csrfToken},
   });
-
-  if (updateTimeInterval) {
-    clearInterval(updateTimeInterval);
-    updateTimeInterval = null;
-  }
-
   return updateStopwatchData(data);
 }
 
@@ -127,10 +122,7 @@ function updateStopwatchData(data) {
   const watch = data[0];
   const btnEl = $('.active-stopwatch-trigger');
   if (!watch) {
-    if (updateTimeInterval) {
-      clearInterval(updateTimeInterval);
-      updateTimeInterval = null;
-    }
+    clearStopwatchTimer();
     btnEl.addClass('hidden');
   } else {
     const {repo_owner_name, repo_name, issue_index, seconds} = watch;
@@ -139,22 +131,31 @@ function updateStopwatchData(data) {
     $('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`);
     $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
     $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
-    $('.stopwatch-time').text(prettyMilliseconds(seconds * 1000));
     updateStopwatchTime(seconds);
     btnEl.removeClass('hidden');
   }
-
   return !!data.length;
 }
 
+let updateTimeIntervalId = null; // holds setInterval id when active
+function clearStopwatchTimer() {
+  if (updateTimeIntervalId !== null) {
+    clearInterval(updateTimeIntervalId);
+    updateTimeIntervalId = null;
+  }
+}
 function updateStopwatchTime(seconds) {
   const secs = parseInt(seconds);
   if (!Number.isFinite(secs)) return;
 
+  clearStopwatchTimer();
+  const $stopwatch = $('.stopwatch-time');
   const start = Date.now();
-  updateTimeInterval = setInterval(() => {
+  const updateUi = () => {
     const delta = Date.now() - start;
     const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
-    $('.stopwatch-time').text(dur);
-  }, 1000);
+    $stopwatch.text(dur);
+  };
+  updateUi();
+  updateTimeIntervalId = setInterval(updateUi, 1000);
 }