851 lines
20 KiB
JavaScript
851 lines
20 KiB
JavaScript
// ===============================
|
||
// sync.js — Full load + Progressive Sync + WS
|
||
// Compatibile con server bulk add_dir / del_dir:
|
||
// { type:"add_dir", mode:"bulk", since:"ISO", count:n, folder:"..." }
|
||
// { type:"del_dir", mode:"bulk", since:"ISO", count:n, folder:"..." }
|
||
//
|
||
// Protocollo WS:
|
||
// auth_ok.need_full_sync = full bootstrap vero, quasi sempre false
|
||
// auth_ok.need_recovery = recovery/progressive sync richiesta
|
||
// need_recovery = recovery richiesta dopo client_recovered
|
||
// recovery_done = inviato dopo recovery completata
|
||
// client_recovered = inviato quando il client è pronto a ricevere pending
|
||
// ===============================
|
||
|
||
const WS_URL = "wss://prova-ws.patachina.it";
|
||
|
||
// retention server-side: 30gg
|
||
const RETENTION_DAYS = 30;
|
||
const RETENTION_MS = RETENTION_DAYS * 86400000;
|
||
|
||
// WS reconnect window: 2 minuti
|
||
const WS_DORMANT_MS = 120000;
|
||
const WS_RECONNECT_DELAY_MS = 1500;
|
||
const WS_NEED_FULLSYNC_DELAY_MS = 800;
|
||
|
||
// processed events ring buffer
|
||
const MAX_PROCESSED_EVENTS = 2000;
|
||
|
||
// gestione burst "added"
|
||
const BATCH_SIZE = 200;
|
||
const FLUSH_DEBOUNCE_MS = 250;
|
||
const TOO_MANY_THRESHOLD = 1200;
|
||
|
||
// ===============================================
|
||
// SESSIONI E DEVICE ID
|
||
// ===============================================
|
||
|
||
function getSessionId() {
|
||
let id = localStorage.getItem("ws_session_id");
|
||
|
||
if (!id) {
|
||
id = crypto.randomUUID();
|
||
localStorage.setItem("ws_session_id", id);
|
||
}
|
||
|
||
return id;
|
||
}
|
||
|
||
function getDeviceId() {
|
||
let id = localStorage.getItem("ws_device_id");
|
||
|
||
if (!id) {
|
||
id = crypto.randomUUID();
|
||
localStorage.setItem("ws_device_id", id);
|
||
}
|
||
|
||
return id;
|
||
}
|
||
|
||
function _ensureSessionId() {
|
||
return getSessionId();
|
||
}
|
||
|
||
// ===============================================
|
||
// RECOVERY DONE / SYNC DONE
|
||
// ===============================================
|
||
|
||
let needRecoveryDoneAck = false;
|
||
|
||
function _maybeSendRecoveryDone(ws) {
|
||
if (!needRecoveryDoneAck) return;
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||
|
||
console.log(">>> SENDING recovery_done");
|
||
|
||
_send(ws, { type: "recovery_done" });
|
||
needRecoveryDoneAck = false;
|
||
}
|
||
|
||
async function sendSyncDone(session_id) {
|
||
try {
|
||
console.log(">>> CALLING sync_done", {
|
||
session_id,
|
||
device_id: getDeviceId(),
|
||
});
|
||
|
||
await fetch("/photos/sync_done", {
|
||
method: "POST",
|
||
headers: {
|
||
..._authHeaders(),
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
session_id,
|
||
device_id: getDeviceId(),
|
||
last_sync: Date.now(),
|
||
}),
|
||
});
|
||
|
||
console.log("📨 [WS] sync_done inviato");
|
||
} catch (e) {
|
||
console.error("❌ [WS] Errore sync_done:", e);
|
||
}
|
||
}
|
||
|
||
// ===============================================
|
||
// AUTH HEADERS
|
||
// ===============================================
|
||
|
||
function _authHeaders() {
|
||
const token = localStorage.getItem("token");
|
||
return { Authorization: "Bearer " + token };
|
||
}
|
||
|
||
// ===============================================
|
||
// API HELPERS
|
||
// ===============================================
|
||
|
||
async function getAllPhotos() {
|
||
const res = await fetch("/photos", {
|
||
headers: _authHeaders(),
|
||
});
|
||
|
||
return await res.json();
|
||
}
|
||
|
||
async function getChanges(since) {
|
||
const res = await fetch(`/photos/changes?since=${encodeURIComponent(since)}`, {
|
||
headers: _authHeaders(),
|
||
});
|
||
|
||
return await res.json();
|
||
}
|
||
|
||
async function getDeletedHard(since) {
|
||
const res = await fetch(`/photos/deleted_hard?since=${encodeURIComponent(since)}`, {
|
||
headers: _authHeaders(),
|
||
});
|
||
|
||
const json = await res.json();
|
||
|
||
return json.deleted || [];
|
||
}
|
||
|
||
async function fetchPhotosByIds(ids) {
|
||
if (!ids || !ids.length) return [];
|
||
|
||
const qs = ids.map((id) => `id=${encodeURIComponent(id)}`).join("&");
|
||
|
||
const payload = parseJwt(localStorage.getItem("token") || "");
|
||
const user = payload?.name || "Common";
|
||
|
||
const url = `/photos/byIds?${qs}&user=${encodeURIComponent(user)}`;
|
||
|
||
const res = await fetch(url, {
|
||
headers: _authHeaders(),
|
||
});
|
||
|
||
return await res.json();
|
||
}
|
||
|
||
// ===============================================
|
||
// TIME HELPERS
|
||
// ===============================================
|
||
|
||
function _nowIso() {
|
||
return new Date().toISOString();
|
||
}
|
||
|
||
function _parseIsoMs(iso) {
|
||
const t = Date.parse(iso);
|
||
return Number.isFinite(t) ? t : 0;
|
||
}
|
||
|
||
function _isTooOldForDelta(lastSyncIso) {
|
||
if (!lastSyncIso) return true;
|
||
|
||
const lastMs = _parseIsoMs(lastSyncIso);
|
||
if (!lastMs) return true;
|
||
|
||
return Date.now() - lastMs > RETENTION_MS;
|
||
}
|
||
|
||
// ===============================================
|
||
// MAP UTILS
|
||
// ===============================================
|
||
|
||
function _toMapById(arr) {
|
||
const m = new Map();
|
||
|
||
for (const p of arr || []) {
|
||
if (p && p.id != null) {
|
||
m.set(String(p.id), p);
|
||
}
|
||
}
|
||
|
||
return m;
|
||
}
|
||
|
||
// ===============================================
|
||
// FULL LOAD
|
||
// ===============================================
|
||
|
||
async function fullLoad() {
|
||
console.log("🟦 FULL LOAD → caricamento completo");
|
||
|
||
const photos = await getAllPhotos();
|
||
|
||
console.log(`📥 FULL LOAD → ricevute ${photos.length} foto`);
|
||
|
||
// Non filtriamo soft delete.
|
||
setLocalPhotos(photos);
|
||
|
||
saveLocalState();
|
||
refreshGallery();
|
||
|
||
const now = _nowIso();
|
||
|
||
setLastSync(now);
|
||
|
||
console.log(`🕒 FULL LOAD → lastSync = ${now}`);
|
||
}
|
||
|
||
// ===============================================
|
||
// PROGRESSIVE SYNC
|
||
// ===============================================
|
||
|
||
async function progressiveSync() {
|
||
if (window._syncInProgress) {
|
||
console.warn("⏳ progressiveSync già in corso");
|
||
return;
|
||
}
|
||
|
||
window._syncInProgress = true;
|
||
|
||
try {
|
||
console.log("==============================================");
|
||
console.log("🧪 [SYNC DEBUG] progressiveSync() chiamato");
|
||
console.log("🚀 progressiveSync() START");
|
||
|
||
const lastSync = getLastSync();
|
||
const localArr = getLocalPhotos() || [];
|
||
|
||
console.log(`🕒 lastSync: ${lastSync}`);
|
||
console.log(`📸 Foto locali cache: ${localArr.length}`);
|
||
|
||
// Primo avvio / cache vuota:
|
||
// progressiveSync fa automaticamente fullLoad.
|
||
if (!lastSync || localArr.length === 0) {
|
||
console.warn("🟦 Cache locale vuota o lastSync mancante → fullLoad()");
|
||
|
||
await fullLoad();
|
||
|
||
console.log("🏁 progressiveSync() COMPLETATO con fullLoad");
|
||
console.log("==============================================");
|
||
|
||
return;
|
||
}
|
||
|
||
if (_isTooOldForDelta(lastSync)) {
|
||
console.warn(`🟥 lastSync > ${RETENTION_DAYS}gg → FULL LOAD richiesto`);
|
||
|
||
await fullLoad();
|
||
|
||
console.log("🏁 progressiveSync() COMPLETATO con fullLoad: lastSync troppo vecchio");
|
||
console.log("==============================================");
|
||
|
||
return;
|
||
}
|
||
|
||
console.log("🟩 PROGRESSIVE SYNC → changes + deleted_hard");
|
||
|
||
const changed = await getChanges(lastSync);
|
||
|
||
console.log(`🟨 changes: ${Array.isArray(changed) ? changed.length : 0}`);
|
||
|
||
const hardDeleted = await getDeletedHard(lastSync);
|
||
|
||
console.log(`🟥 deleted_hard: ${hardDeleted.length}`);
|
||
|
||
const localMap = _toMapById(localArr);
|
||
|
||
if (Array.isArray(changed)) {
|
||
for (const p of changed) {
|
||
if (!p || p.id == null) continue;
|
||
|
||
localMap.set(String(p.id), p);
|
||
}
|
||
}
|
||
|
||
for (const d of hardDeleted) {
|
||
if (!d || d.id == null) continue;
|
||
|
||
localMap.delete(String(d.id));
|
||
}
|
||
|
||
const merged = Array.from(localMap.values());
|
||
|
||
setLocalPhotos(merged);
|
||
saveLocalState();
|
||
refreshGallery();
|
||
|
||
const now = _nowIso();
|
||
|
||
setLastSync(now);
|
||
|
||
console.log(`🕒 Aggiorno lastSync → ${now}`);
|
||
console.log("🏁 progressiveSync() COMPLETATO");
|
||
console.log("==============================================");
|
||
} finally {
|
||
window._syncInProgress = false;
|
||
}
|
||
}
|
||
|
||
// ===============================================
|
||
// PROGRESSIVE SYNC MIRATO DA since WS
|
||
// ===============================================
|
||
|
||
async function progressiveSyncFrom(sinceIso) {
|
||
if (!sinceIso) return progressiveSync();
|
||
|
||
console.log(`🟦 progressiveSyncFrom(${sinceIso})`);
|
||
|
||
if (_isTooOldForDelta(sinceIso)) {
|
||
console.warn("🟥 since troppo vecchio → fullLoad()");
|
||
|
||
await fullLoad();
|
||
|
||
return;
|
||
}
|
||
|
||
const localArr = getLocalPhotos() || [];
|
||
|
||
if (!localArr.length) {
|
||
await fullLoad();
|
||
return;
|
||
}
|
||
|
||
const changed = await getChanges(sinceIso);
|
||
const hardDeleted = await getDeletedHard(sinceIso);
|
||
|
||
const localMap = _toMapById(localArr);
|
||
|
||
for (const p of changed || []) {
|
||
if (!p || p.id == null) continue;
|
||
|
||
localMap.set(String(p.id), p);
|
||
}
|
||
|
||
for (const d of hardDeleted || []) {
|
||
if (!d || d.id == null) continue;
|
||
|
||
localMap.delete(String(d.id));
|
||
}
|
||
|
||
setLocalPhotos(Array.from(localMap.values()));
|
||
saveLocalState();
|
||
refreshGallery();
|
||
|
||
setLastSync(_nowIso());
|
||
}
|
||
|
||
// ===============================================
|
||
// PROCESSED EVENTS — Ring Buffer
|
||
// ===============================================
|
||
|
||
function _loadProcessedRing() {
|
||
try {
|
||
const arr = JSON.parse(localStorage.getItem("processed_events") || "[]");
|
||
|
||
if (!Array.isArray(arr)) return [];
|
||
|
||
return arr.slice(-MAX_PROCESSED_EVENTS);
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function _saveProcessedRing(ring) {
|
||
localStorage.setItem(
|
||
"processed_events",
|
||
JSON.stringify(ring.slice(-MAX_PROCESSED_EVENTS))
|
||
);
|
||
}
|
||
|
||
const processedRing = _loadProcessedRing();
|
||
const processedSet = new Set(processedRing);
|
||
|
||
function isProcessed(eventId) {
|
||
return !!eventId && processedSet.has(eventId);
|
||
}
|
||
|
||
function markProcessed(eventId) {
|
||
if (!eventId) return;
|
||
if (processedSet.has(eventId)) return;
|
||
|
||
processedRing.push(eventId);
|
||
processedSet.add(eventId);
|
||
|
||
while (processedRing.length > MAX_PROCESSED_EVENTS) {
|
||
const old = processedRing.shift();
|
||
|
||
if (old) {
|
||
processedSet.delete(old);
|
||
}
|
||
}
|
||
|
||
_saveProcessedRing(processedRing);
|
||
}
|
||
|
||
// ===============================================
|
||
// WS ADDED BURST HANDLER
|
||
// ===============================================
|
||
|
||
let addedQueue = [];
|
||
let addedSet = new Set();
|
||
let flushTimer = null;
|
||
let flushing = false;
|
||
|
||
function enqueueAdded(id) {
|
||
if (!id) return;
|
||
|
||
const sid = String(id);
|
||
|
||
if (addedSet.has(sid)) return;
|
||
|
||
addedSet.add(sid);
|
||
addedQueue.push(sid);
|
||
|
||
if (addedQueue.length >= TOO_MANY_THRESHOLD) {
|
||
console.warn(
|
||
`🟥 [WS] Burst added (${addedQueue.length}) → fallback progressiveSync()`
|
||
);
|
||
|
||
if (flushTimer) {
|
||
clearTimeout(flushTimer);
|
||
}
|
||
|
||
flushTimer = null;
|
||
addedQueue = [];
|
||
addedSet.clear();
|
||
|
||
progressiveSync().then(() => {
|
||
if (wsInstance && wsInstance.readyState === WebSocket.OPEN) {
|
||
_maybeSendRecoveryDone(wsInstance);
|
||
}
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
if (!flushTimer) {
|
||
flushTimer = setTimeout(() => {
|
||
flushTimer = null;
|
||
flushAddedQueue();
|
||
}, FLUSH_DEBOUNCE_MS);
|
||
}
|
||
}
|
||
|
||
async function flushAddedQueue() {
|
||
if (flushing) return;
|
||
if (addedQueue.length === 0) return;
|
||
|
||
flushing = true;
|
||
|
||
try {
|
||
const chunk = addedQueue.splice(0, BATCH_SIZE);
|
||
|
||
chunk.forEach((id) => addedSet.delete(id));
|
||
|
||
const items = await fetchPhotosByIds(chunk);
|
||
|
||
if (Array.isArray(items) && items.length) {
|
||
for (const p of items) {
|
||
addPhotoLocal(p);
|
||
}
|
||
|
||
saveLocalState();
|
||
refreshGallery();
|
||
}
|
||
|
||
if (addedQueue.length > 0) {
|
||
setTimeout(flushAddedQueue, 0);
|
||
}
|
||
} catch (e) {
|
||
console.error("❌ [WS] flushAddedQueue error:", e);
|
||
|
||
addedQueue = [];
|
||
addedSet.clear();
|
||
|
||
await progressiveSync();
|
||
|
||
if (wsInstance && wsInstance.readyState === WebSocket.OPEN) {
|
||
_maybeSendRecoveryDone(wsInstance);
|
||
}
|
||
} finally {
|
||
flushing = false;
|
||
}
|
||
}
|
||
|
||
// ===============================================
|
||
// WEBSOCKET REAL-TIME
|
||
// ===============================================
|
||
|
||
let wsInstance = null;
|
||
|
||
function _setLastSeenNow() {
|
||
localStorage.setItem("ws_last_seen", String(Date.now()));
|
||
}
|
||
|
||
function _getLastSeen() {
|
||
return parseInt(localStorage.getItem("ws_last_seen") || "0", 10);
|
||
}
|
||
|
||
function _safeJsonParse(raw) {
|
||
try {
|
||
return JSON.parse(raw);
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function _send(ws, obj) {
|
||
try {
|
||
ws.send(JSON.stringify(obj));
|
||
} catch (e) {
|
||
console.warn("⚠️ [WS] send failed:", e);
|
||
}
|
||
}
|
||
|
||
function _ack(ws, event_id) {
|
||
if (!event_id) return;
|
||
|
||
_send(ws, {
|
||
type: "ack",
|
||
event_id,
|
||
});
|
||
}
|
||
|
||
// ===============================================
|
||
// START WEBSOCKET
|
||
// ===============================================
|
||
|
||
function startWebSocket() {
|
||
console.log("🧪 [WS DEBUG] startWebSocket() chiamato");
|
||
|
||
const token = localStorage.getItem("token");
|
||
|
||
if (!token) {
|
||
console.error("❌ [WS] Nessun token JWT trovato");
|
||
return;
|
||
}
|
||
|
||
if (
|
||
wsInstance &&
|
||
(
|
||
wsInstance.readyState === WebSocket.OPEN ||
|
||
wsInstance.readyState === WebSocket.CONNECTING
|
||
)
|
||
) {
|
||
console.log("⚠️ [WS] Connessione già attiva, ignoro startWebSocket()");
|
||
return;
|
||
}
|
||
|
||
if (wsInstance) {
|
||
try {
|
||
wsInstance.close();
|
||
} catch (e) {}
|
||
}
|
||
|
||
console.log("🔌 [WS] Creo nuova connessione WebSocket...");
|
||
|
||
wsInstance = new WebSocket(WS_URL);
|
||
|
||
const ws = wsInstance;
|
||
const session_id = _ensureSessionId();
|
||
const device_id = getDeviceId();
|
||
|
||
ws.onopen = () => {
|
||
console.log(`🟢 [WS OPEN] Connesso. session_id=${session_id}`);
|
||
console.log("[WS] device_id:", device_id);
|
||
|
||
_send(ws, {
|
||
type: "auth",
|
||
token,
|
||
session_id,
|
||
device_id,
|
||
});
|
||
};
|
||
|
||
ws.onmessage = async (ev) => {
|
||
console.log("📩 [WS RAW] Messaggio ricevuto:", ev.data);
|
||
|
||
_setLastSeenNow();
|
||
|
||
const msg = _safeJsonParse(ev.data);
|
||
|
||
if (!msg) {
|
||
console.error("❌ [WS] Errore parsing JSON");
|
||
return;
|
||
}
|
||
|
||
console.log("📩 [WS PARSED]:", msg);
|
||
|
||
// -----------------------------------------------
|
||
// AUTH OK
|
||
// -----------------------------------------------
|
||
if (msg.type === "auth_ok") {
|
||
console.log("🔐 WS autenticato come:", msg.user, "session:", msg.session_id);
|
||
|
||
const needFullSync = msg.need_full_sync === true;
|
||
const needRecovery = needFullSync || msg.need_recovery === true;
|
||
|
||
const lastSync = getLastSync();
|
||
const localArr = getLocalPhotos() || [];
|
||
const localCacheMissing = !lastSync || localArr.length === 0;
|
||
|
||
console.log("🧪 [WS AUTH OK DECISION]", {
|
||
need_full_sync: msg.need_full_sync,
|
||
need_recovery: msg.need_recovery,
|
||
needFullSync,
|
||
needRecovery,
|
||
reason: msg.reason,
|
||
lastSync,
|
||
localCount: localArr.length,
|
||
localCacheMissing,
|
||
});
|
||
|
||
if (needRecovery || localCacheMissing) {
|
||
if (needRecovery) {
|
||
console.warn("🟨 [WS] Server richiede recovery/progressive sync");
|
||
needRecoveryDoneAck = true;
|
||
} else {
|
||
console.warn("🟦 [WS] Primo avvio o cache locale vuota → fullLoad tramite progressiveSync()");
|
||
}
|
||
|
||
setTimeout(async () => {
|
||
try {
|
||
console.log("🔄 [WS] Eseguo progressiveSync()");
|
||
await progressiveSync();
|
||
} catch (e) {
|
||
console.error("❌ [WS] Errore progressiveSync:", e);
|
||
} finally {
|
||
if (needRecovery) {
|
||
_maybeSendRecoveryDone(ws);
|
||
await sendSyncDone(msg.session_id);
|
||
}
|
||
|
||
_send(ws, {
|
||
type: "client_recovered",
|
||
});
|
||
}
|
||
}, needFullSync ? WS_NEED_FULLSYNC_DELAY_MS : 0);
|
||
|
||
return;
|
||
}
|
||
|
||
console.log("🟢 [WS] Nessuna recovery richiesta e cache locale presente → client_recovered");
|
||
|
||
_send(ws, {
|
||
type: "client_recovered",
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// NEED RECOVERY
|
||
// -----------------------------------------------
|
||
if (msg.type === "need_recovery") {
|
||
console.warn("🟨 [WS] need_recovery ricevuto:", msg.reason);
|
||
|
||
needRecoveryDoneAck = true;
|
||
|
||
try {
|
||
await progressiveSync();
|
||
} catch (e) {
|
||
console.error("❌ [WS] Errore progressiveSync su need_recovery:", e);
|
||
} finally {
|
||
_maybeSendRecoveryDone(ws);
|
||
await sendSyncDone(session_id);
|
||
|
||
_send(ws, {
|
||
type: "client_recovered",
|
||
});
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// PING
|
||
// -----------------------------------------------
|
||
if (msg.type === "ping") {
|
||
_send(ws, {
|
||
type: "pong",
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
const event_id = msg.event_id;
|
||
|
||
if (event_id && isProcessed(event_id)) {
|
||
console.log("♻️ [WS] Evento già processato, invio solo ACK:", event_id);
|
||
_ack(ws, event_id);
|
||
return;
|
||
}
|
||
|
||
const finalize = () => {
|
||
if (event_id) {
|
||
markProcessed(event_id);
|
||
_ack(ws, event_id);
|
||
}
|
||
};
|
||
|
||
// -----------------------------------------------
|
||
// BULK ADD_DIR
|
||
// -----------------------------------------------
|
||
if (msg.type === "add_dir") {
|
||
if (msg.mode === "bulk") {
|
||
console.log(
|
||
`📦 [WS] add_dir bulk folder=${msg.folder} count=${msg.count} → progressiveSyncFrom(since)`
|
||
);
|
||
|
||
await progressiveSyncFrom(msg.since);
|
||
} else {
|
||
console.log(`📁 [WS] add_dir folder=${msg.folder}`);
|
||
}
|
||
|
||
finalize();
|
||
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// BULK DEL_DIR
|
||
// -----------------------------------------------
|
||
if (msg.type === "del_dir") {
|
||
if (msg.mode === "bulk") {
|
||
console.log(
|
||
`📦 [WS] del_dir bulk folder=${msg.folder} count=${msg.count} → progressiveSyncFrom(since)`
|
||
);
|
||
|
||
await progressiveSyncFrom(msg.since);
|
||
} else {
|
||
console.log(`📁 [WS] del_dir folder=${msg.folder}`);
|
||
}
|
||
|
||
finalize();
|
||
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// ADDED
|
||
// -----------------------------------------------
|
||
if (msg.type === "added") {
|
||
enqueueAdded(msg.id);
|
||
finalize();
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// HARD DELETE
|
||
// -----------------------------------------------
|
||
if (msg.type === "del") {
|
||
removePhotoLocal(msg.id);
|
||
saveLocalState();
|
||
refreshGallery();
|
||
finalize();
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// REMOVED API
|
||
// -----------------------------------------------
|
||
if (msg.type === "removed") {
|
||
removePhotoLocal(msg.id);
|
||
saveLocalState();
|
||
refreshGallery();
|
||
finalize();
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// UPDATED
|
||
// -----------------------------------------------
|
||
if (msg.type === "updated") {
|
||
updateLocalPhoto(msg.id, {
|
||
deleted_at: msg.deleted_at,
|
||
});
|
||
|
||
saveLocalState();
|
||
refreshGallery();
|
||
finalize();
|
||
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// DONE EVENTS
|
||
// -----------------------------------------------
|
||
if (msg.type === "add_dir_done" || msg.type === "del_dir_done") {
|
||
console.log(`✅ [WS] ${msg.type} folder=${msg.folder} count=${msg.count}`);
|
||
|
||
finalize();
|
||
|
||
return;
|
||
}
|
||
|
||
console.log("ℹ️ [WS] Evento non gestito:", msg);
|
||
|
||
if (event_id) {
|
||
console.log("ℹ️ [WS] Evento non gestito, ACK comunque:", event_id);
|
||
finalize();
|
||
}
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
console.warn(`❌ [WS CLOSE] Connessione chiusa. session_id=${session_id}`);
|
||
|
||
if (wsInstance === ws) {
|
||
wsInstance = null;
|
||
}
|
||
|
||
const now = Date.now();
|
||
const lastSeen = _getLastSeen();
|
||
|
||
if (now - lastSeen < WS_DORMANT_MS) {
|
||
console.log("🔄 [WS] Tentativo di reconnect...");
|
||
setTimeout(startWebSocket, WS_RECONNECT_DELAY_MS);
|
||
} else {
|
||
console.log("🟦 [WS] sessione dormiente → recovery al prossimo avvio");
|
||
}
|
||
};
|
||
|
||
ws.onerror = (err) => {
|
||
console.error("⚠️ [WS ERROR]", err);
|
||
};
|
||
}
|
||
|
||
// ===============================================
|
||
// INIT
|
||
// ===============================================
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
if (AppAuth.isLoggedIn()) {
|
||
startWebSocket();
|
||
}
|
||
});
|