"use strict"; var notification_data = JSON.parse( document.getElementById("notification_data").textContent, ); /** Boolean meaning 'some tab have stream' */ const STORAGE_KEY_STREAM = "stream"; /** Number of notifications. May be increased or reset */ const STORAGE_KEY_NOTIF_COUNT = "notification_count"; var notifications, delivered; var notifications_mock = { close: function () {} }; async function get_subscriptions_call() { return new Promise((resolve) => { helpers.xhr( "GET", "/api/v1/auth/subscriptions", { retries: 5, entity_name: "subscriptions", }, { on200: function (subscriptions) { create_notification_stream(subscriptions); resolve(subscriptions); }, }, ); }); } // Start the retry mechanism const get_subscriptions = exponential_backoff(get_subscriptions_call, 100, 1000); function create_notification_stream(subscriptions) { // sse.js can't be replaced to EventSource in place as it lack support of payload and headers // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource notifications = new SSE("/api/v1/auth/notifications", { withCredentials: true, payload: "topics=" + subscriptions .map(function (subscription) { return subscription.authorId; }) .join(","), headers: { "Content-Type": "application/x-www-form-urlencoded" }, }); delivered = []; var start_time = Math.round(new Date() / 1000); notifications.onmessage = function (event) { if (!event.id) return; var notification = JSON.parse(event.data); console.info("Got notification:", notification); // Ignore not actual and delivered notifications if ( start_time > notification.published || delivered.includes(notification.videoId) ) return; delivered.push(notification.videoId); let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0; notification_count++; helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); update_ticker_count(); // permission for notifications handled on settings page. JS handler is in handlers.js if (window.Notification && Notification.permission === "granted") { var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text; notification_text = notification_text.replace("`x`", notification.author); var system_notification = new Notification(notification_text, { body: notification.title, icon: "/ggpht" + new URL(notification.authorThumbnails[2].url).pathname, img: "/ggpht" + new URL(notification.authorThumbnails[4].url).pathname, }); system_notification.onclick = function (e) { open("/watch?v=" + notification.videoId, "_blank"); }; } }; notifications.addEventListener("error", function (e) { console.warn( "Something went wrong with notifications, trying to reconnect...", ); notifications = notifications_mock; }); notifications.stream(); } function update_ticker_count() { var notification_ticker = document.getElementById("notification_ticker"); const notification_count = helpers.storage.get(STORAGE_KEY_STREAM); if (notification_count > 0) { notification_ticker.innerHTML = '' + notification_count + ' '; } else { notification_ticker.innerHTML = ''; } } function start_stream_if_needed() { // random wait for other tabs set 'stream' flag setTimeout( function () { if (!helpers.storage.get(STORAGE_KEY_STREAM)) { // if no one set 'stream', set it by yourself and start stream helpers.storage.set(STORAGE_KEY_STREAM, true); notifications = notifications_mock; get_subscriptions(); } }, Math.random() * 1000 + 50, ); // [0.050 .. 1.050) second } addEventListener("storage", function (e) { if (e.key === STORAGE_KEY_NOTIF_COUNT) update_ticker_count(); // if 'stream' key was removed if ( e.key === STORAGE_KEY_STREAM && !helpers.storage.get(STORAGE_KEY_STREAM) ) { if (notifications) { // restore it if we have active stream helpers.storage.set(STORAGE_KEY_STREAM, true); } else { start_stream_if_needed(); } } }); addEventListener("load", function () { var notification_count_el = document.getElementById("notification_count"); var notification_count = notification_count_el ? parseInt(notification_count_el.textContent) : 0; helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); if (helpers.storage.get(STORAGE_KEY_STREAM)) helpers.storage.remove(STORAGE_KEY_STREAM); start_stream_if_needed(); }); addEventListener("unload", function () { // let chance to other tabs to be a streamer via firing 'storage' event if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM); }); function exponential_backoff( fn, maxRetries = 5, initialDelay = 1000, randomnessFactor = 0.5, ) { let attempt = 0; return function tryFunction() { fn() .then((response) => { attempt = 0; }) .catch((error) => { if (attempt < maxRetries) { attempt++; let delay = initialDelay * Math.pow(2, attempt); // Exponential backoff let randomMultiplier = 1 + Math.random() * randomnessFactor; delay = delay * randomMultiplier; console.log( `Attempt ${attempt} failed. Retrying in ${(delay / 1000).toPrecision(2)} seconds...`, ); setTimeout(tryFunction, delay); // Retry after delay } else { console.log("Max retries reached. Operation failed:", error); } }); } }