photo_server_json_con_aves22/public/js/sync.js.ok
2026-04-18 20:14:42 +02:00

567 lines
No EOL
16 KiB
Text
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ===============================
// 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:"..." }
// ===============================
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" (utile se server manda per-file sotto soglia)
const BATCH_SIZE = 200;
const FLUSH_DEBOUNCE_MS = 250;
const TOO_MANY_THRESHOLD = 1200;
// ------------------------------------
// RECOVERY DONE (server patched behavior)
// ------------------------------------
let needRecoveryDoneAck = false; // ✅ set true quando auth_ok.need_full_sync=true
function _maybeSendRecoveryDone(ws) {
// invia una sola volta dopo una recovery richiesta dal server
if (!needRecoveryDoneAck) return;
if (!ws || ws.readyState !== WebSocket.OPEN) return;
console.log("✅ [WS] Invio recovery_done al server");
_send(ws, { type: "recovery_done" });
needRecoveryDoneAck = false;
}
// -------------------------------
// 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(); // array di foto normalizzate
}
async function getDeletedHard(since) {
const res = await fetch(`/photos/deleted_hard?since=${encodeURIComponent(since)}`, {
headers: _authHeaders(),
});
const json = await res.json();
return json.deleted || []; // [{id, deleted_at}]
}
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(); // array di foto
}
// -------------------------------
// 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 (snapshot completo)
// -------------------------------
async function fullLoad() {
console.log("🟦 FULL LOAD → caricamento completo");
const photos = await getAllPhotos();
console.log(`📥 FULL LOAD → ricevute ${photos.length} foto`);
// 🔥 NON filtriamo i soft delete
setLocalPhotos(photos);
saveLocalState();
refreshGallery();
const now = _nowIso();
setLastSync(now);
console.log(`🕒 FULL LOAD → lastSync = ${now}`);
}
// -------------------------------
// PROGRESSIVE SYNC (entro 30gg: changes + deleted_hard)
// -------------------------------
async function progressiveSync() {
console.log("==============================================");
console.log("🚀 progressiveSync() START");
const lastSync = getLastSync();
const localArr = getLocalPhotos() || [];
console.log(`🕒 lastSync: ${lastSync}`);
console.log(`📸 Foto locali (cache): ${localArr.length}`);
if (!lastSync || localArr.length === 0) {
await fullLoad();
console.log("🏁 progressiveSync() COMPLETATO (fullLoad: no lastSync o cache vuota)");
console.log("==============================================");
return;
}
if (_isTooOldForDelta(lastSync)) {
console.warn(`🟥 lastSync > ${RETENTION_DAYS}gg → FULL LOAD richiesto`);
await fullLoad();
console.log("🏁 progressiveSync() COMPLETATO (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("==============================================");
}
// -------------------------------
// PROGRESSIVE SYNC “MIRATO” usando since del server WS bulk
// (usato per add_dir/del_dir bulk)
// -------------------------------
async function progressiveSyncFrom(sinceIso) {
if (!sinceIso) return progressiveSync();
console.log(`🟦 progressiveSyncFrom(${sinceIso})`);
// se troppo vecchio, fai full
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();
// aggiorna lastSync a "now"
setLastSync(_nowIso());
}
// ===============================================
// PROCESSED EVENTS — Ring Buffer (max 2000)
// ===============================================
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 (queue + batch byIds)
// ===============================================
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);
// se burst troppo grande -> fallback a progressive sync
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(() => {
// se la recovery era richiesta dal server, invia recovery_done
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 (auth + ping/pong + ack)
// ===============================================
let wsInstance = null;
function _ensureSessionId() {
let session_id = localStorage.getItem("ws_session_id");
if (!session_id) {
session_id = crypto.randomUUID();
localStorage.setItem("ws_session_id", session_id);
}
return session_id;
}
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 });
}
function startWebSocket() {
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();
ws.onopen = () => {
console.log("🟢 [WS] Connesso → invio token JWT + session_id");
_send(ws, { type: "auth", token, session_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);
if (msg.type === "auth_ok") {
console.log("🔐 WS autenticato come:", msg.user, "session:", msg.session_id);
if (msg.need_full_sync) {
console.warn("⚠️ [WS] Server richiede recovery → progressiveSync() ritardato");
needRecoveryDoneAck = true;
setTimeout(async () => {
try {
await progressiveSync();
} finally {
_maybeSendRecoveryDone(ws);
}
}, WS_NEED_FULLSYNC_DELAY_MS);
}
return;
}
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: progressive sync da since
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: progressive sync da since
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 (per-file): enqueue e batch byIds
if (msg.type === "added") {
enqueueAdded(msg.id);
finalize();
return;
}
// HARD DELETE (per-file)
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 (soft delete / restore)
if (msg.type === "updated") {
updateLocalPhoto(msg.id, { deleted_at: msg.deleted_at });
saveLocalState();
refreshGallery();
finalize();
return;
}
// add_dir_done / del_dir_done (opzionali)
if (msg.type === "add_dir_done" || msg.type === "del_dir_done") {
console.log(`✅ [WS] ${msg.type} folder=${msg.folder} count=${msg.count}`);
finalize();
return;
}
// fallback
if (event_id) {
console.log(" [WS] Evento non gestito, ACK comunque:", event_id);
finalize();
}
};
ws.onclose = () => {
console.warn("❌ [WS] Connessione chiusa");
if (wsInstance === ws) wsInstance = null;
const now = Date.now();
const lastSeen = _getLastSeen();
if (now - lastSeen < WS_DORMANT_MS) {
console.log("🔄 [WS] reconnect immediato");
setTimeout(startWebSocket, WS_RECONNECT_DELAY_MS);
} else {
console.log("🟦 [WS] sessione dormiente → progressiveSync/full al prossimo avvio");
localStorage.removeItem("ws_session_id");
// opzionale: anche last_seen
// localStorage.removeItem("ws_last_seen");
}
};
ws.onerror = (err) => {
console.error("⚠️ [WS] Errore WebSocket:", err);
};
}
// ===============================
// INIT
// ===============================
document.addEventListener("DOMContentLoaded", () => {
if (AppAuth.isLoggedIn()) {
progressiveSync();
startWebSocket();
}
});