2026-01-17 00:55:52 +01:00

210 lines
5.5 KiB
JavaScript

// public/assets/sse.js
/* English comments: shared SSE client with lease + cache */
(function () {
const SSE_LEASE_KEY = 'scmedia_sse_lease';
const SSE_LAST = 'scmedia_sse_last';
const SSE_LAST_TYPE = 'scmedia_sse_last_type';
const SSE_CONNECTED = 'scmedia_sse_connected';
const SSE_RECONNECTS = 'scmedia_sse_reconnects';
const SSE_KEY_EXP = 'scmedia_sse_key_exp';
let client = null;
let retryMs = 5000;
let tabId = '';
let leaseTimer = null;
let opening = false;
const listeners = {};
function emit(type, payload) {
const list = listeners[type] || [];
list.forEach((cb) => {
try {
cb(payload);
} catch (e) {
// ignore
}
});
}
function on(type, cb) {
if (!type || typeof cb !== 'function') return;
listeners[type] = listeners[type] || [];
listeners[type].push(cb);
}
function loadKeyExp() {
return Number(sessionStorage.getItem(SSE_KEY_EXP) || 0);
}
function cacheKeyExp(ttlSeconds) {
const exp = Date.now() + Math.max(10, ttlSeconds) * 1000;
sessionStorage.setItem(SSE_KEY_EXP, String(exp));
return exp;
}
function isKeyFresh() {
const exp = loadKeyExp();
return exp > Date.now() + 1000;
}
function setConnected(ok) {
localStorage.setItem(SSE_CONNECTED, ok ? '1' : '0');
if (ok) {
localStorage.setItem(SSE_LAST, String(Date.now()));
}
}
function noteEvent(type) {
setConnected(true);
localStorage.setItem(SSE_LAST, String(Date.now()));
if (type) {
localStorage.setItem(SSE_LAST_TYPE, String(type));
}
refreshLease();
}
function claimLease() {
if (!tabId) {
const existing = sessionStorage.getItem('scmedia_sse_tab_id');
tabId = existing || Math.random().toString(36).slice(2);
sessionStorage.setItem('scmedia_sse_tab_id', tabId);
}
const now = Date.now();
const ttlMs = 15000;
const raw = localStorage.getItem(SSE_LEASE_KEY) || '';
let lease = null;
try {
lease = raw ? JSON.parse(raw) : null;
} catch (e) {
lease = null;
}
if (lease && lease.ts && (now - lease.ts) < ttlMs && lease.id !== tabId) {
return false;
}
localStorage.setItem(SSE_LEASE_KEY, JSON.stringify({ ts: now, id: tabId }));
return true;
}
function refreshLease() {
localStorage.setItem(SSE_LEASE_KEY, JSON.stringify({ ts: Date.now(), id: tabId }));
}
function releaseLease() {
const raw = localStorage.getItem(SSE_LEASE_KEY) || '';
try {
const lease = raw ? JSON.parse(raw) : null;
if (lease && lease.id && lease.id !== tabId) return;
} catch (e) {
// ignore
}
localStorage.removeItem(SSE_LEASE_KEY);
}
async function open() {
if (opening) return;
opening = true;
if (!claimLease()) {
opening = false;
return;
}
if (window.Auth?.isAccessExpired?.() && window.Auth?.refreshTokens) {
await window.Auth.refreshTokens();
}
const token = window.Auth?.getAccessToken?.();
if (!token) {
opening = false;
return;
}
if (!isKeyFresh()) {
const res = await fetch('/api/auth/sse-key', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json().catch(() => null);
const key = data?.data?.key || '';
const ttl = Number(data?.data?.expires_in || 60);
if (!data?.ok || !key) {
releaseLease();
opening = false;
return;
}
cacheKeyExp(ttl);
await new Promise((r) => setTimeout(r, 50));
}
const es = new EventSource('/api/events', { withCredentials: true });
client = es;
es.addEventListener('open', () => {
retryMs = 5000;
setConnected(true);
refreshLease();
if (leaseTimer) clearInterval(leaseTimer);
leaseTimer = setInterval(refreshLease, 5000);
emit('open');
});
const handle = (type) => {
noteEvent(type);
emit(type);
};
es.addEventListener('message', () => handle('message'));
es.addEventListener('tick', () => handle('tick'));
es.addEventListener('sources', (e) => {
noteEvent('sources');
let payload = null;
try { payload = JSON.parse(e.data); } catch (e2) { payload = null; }
emit('sources', payload);
});
es.addEventListener('jobs', (e) => {
noteEvent('jobs');
let payload = null;
try { payload = JSON.parse(e.data); } catch (e2) { payload = null; }
emit('jobs', payload);
});
es.addEventListener('error', () => {
if (client) {
client.close();
client = null;
}
if (leaseTimer) {
clearInterval(leaseTimer);
leaseTimer = null;
}
const prev = Number(localStorage.getItem(SSE_RECONNECTS) || 0);
localStorage.setItem(SSE_RECONNECTS, String(prev + 1));
releaseLease();
opening = false;
const delay = retryMs;
retryMs = Math.min(retryMs * 2, 60000);
setTimeout(open, delay);
});
opening = false;
}
function start() {
if (!window.EventSource) return;
if (client) return;
open().catch(() => {});
}
function stop() {
if (client) {
client.close();
client = null;
}
if (leaseTimer) {
clearInterval(leaseTimer);
leaseTimer = null;
}
localStorage.removeItem(SSE_CONNECTED);
localStorage.removeItem(SSE_LAST);
localStorage.removeItem(SSE_LAST_TYPE);
localStorage.removeItem(SSE_LEASE_KEY);
sessionStorage.removeItem(SSE_KEY_EXP);
document.cookie = 'scmedia_sse_key=; path=/; max-age=0';
}
window.Sse = { start, stop, on };
})();