mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-29 18:12:07 +00:00
Keep notification counts and stopwatches in sync when tabs open or reconnect, closing the gap behind [this flake](https://github.com/go-gitea/gitea/actions/runs/31312525323/job/93242429394). 1. Send current notification counts and stopwatch state on each WebSocket connection, replacing the separate state fetches tabs made on every connect. 2. Tabs joining an already open connection fetch current state once, since its snapshot went out before they attached. 3. Send events queued during the handshake before the snapshot, so older events can't roll it back. 4. Wait for the Redis SUBSCRIBE acknowledgement, so the snapshot read after subscribing can't miss an event.
110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import {GET} from '../modules/fetch.ts';
|
|
import {toggleElem, createElementFromHTML} from '../utils/dom.ts';
|
|
import {onUserEvent} from '../modules/worker.ts';
|
|
|
|
const {appSubUrl, notificationSettings} = window.config;
|
|
let notificationSequenceNumber = 0;
|
|
|
|
async function receiveUpdateCount(count: number) {
|
|
toggleElem('.notification_count', count !== 0);
|
|
for (const el of document.querySelectorAll('.notification_count')) {
|
|
el.textContent = String(count);
|
|
}
|
|
await updateNotificationTable();
|
|
}
|
|
|
|
export function initNotificationCount() {
|
|
if (!document.querySelector('.notification_count')) return;
|
|
|
|
const startPeriodicPoller = (timeout: number, lastCount?: number) => {
|
|
if (timeout <= 0 || !Number.isFinite(timeout)) return;
|
|
lastCount = lastCount ?? getCurrentCount();
|
|
setTimeout(async () => {
|
|
await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
|
|
}, timeout);
|
|
};
|
|
|
|
let pollerStarted = false;
|
|
onUserEvent('notification-count', (msg) => { receiveUpdateCount(msg.eventData.count) }); // no await
|
|
onUserEvent('worker-attached', () => { updateNotificationCount(); updateNotificationTable() }); // no await
|
|
onUserEvent('worker-unavailable', () => {
|
|
if (pollerStarted) return;
|
|
pollerStarted = true;
|
|
startPeriodicPoller(notificationSettings.MinTimeout);
|
|
});
|
|
}
|
|
|
|
function getCurrentCount() {
|
|
return Number(document.querySelector('.notification_count')!.textContent ?? '0');
|
|
}
|
|
|
|
async function updateNotificationCountWithCallback(callback: (timeout: number, newCount: number) => void, timeout: number, lastCount: number) {
|
|
const currentCount = getCurrentCount();
|
|
if (lastCount !== currentCount) {
|
|
callback(notificationSettings.MinTimeout, currentCount);
|
|
return;
|
|
}
|
|
|
|
const newCount = await updateNotificationCount();
|
|
let needsUpdate = false;
|
|
|
|
if (lastCount !== newCount) {
|
|
needsUpdate = true;
|
|
timeout = notificationSettings.MinTimeout;
|
|
} else if (timeout < notificationSettings.MaxTimeout) {
|
|
timeout += notificationSettings.TimeoutStep;
|
|
}
|
|
|
|
callback(timeout, newCount);
|
|
if (needsUpdate) {
|
|
await updateNotificationTable();
|
|
}
|
|
}
|
|
|
|
async function updateNotificationTable() {
|
|
const notificationDiv = document.querySelector('#notification_div');
|
|
if (!notificationDiv) return;
|
|
|
|
try {
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.set('div-only', 'true');
|
|
params.set('sequence-number', String(++notificationSequenceNumber));
|
|
const response = await GET(`${appSubUrl}/notifications?${params.toString()}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch notification table');
|
|
}
|
|
|
|
const data = await response.text();
|
|
const el = createElementFromHTML(data);
|
|
if (parseInt(el.getAttribute('data-sequence-number')!) === notificationSequenceNumber) {
|
|
notificationDiv.outerHTML = data;
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
|
|
async function updateNotificationCount(): Promise<number> {
|
|
try {
|
|
const response = await GET(`${appSubUrl}/notifications/new`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch notification count');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
toggleElem('.notification_count', data.new !== 0);
|
|
|
|
for (const el of document.querySelectorAll('.notification_count')) {
|
|
el.textContent = String(data.new);
|
|
}
|
|
|
|
return data.new as number;
|
|
} catch (error) {
|
|
console.error(error);
|
|
return 0;
|
|
}
|
|
}
|