994 lines
25 KiB
JavaScript
994 lines
25 KiB
JavaScript
// public/js/sync.js
|
||
// ===============================
|
||
// sync.js — Full load + Progressive Sync + WS
|
||
// Photos + Activities
|
||
// ===============================
|
||
|
||
// WS config invariato
|
||
const WS_URL = "wss://prova-ws.patachina.it";
|
||
|
||
const RETENTION_DAYS = 30;
|
||
const RETENTION_MS = RETENTION_DAYS * 86400000;
|
||
|
||
const WS_DORMANT_MS = 120000;
|
||
const WS_RECONNECT_DELAY_MS = 1500;
|
||
const WS_NEED_FULLSYNC_DELAY_MS = 800;
|
||
|
||
const MAX_PROCESSED_EVENTS = 2000;
|
||
|
||
const BATCH_SIZE = 200;
|
||
const FLUSH_DEBOUNCE_MS = 250;
|
||
const TOO_MANY_THRESHOLD = 1200;
|
||
|
||
// ===============================================
|
||
// MODE / CACHE HELPERS — IMPORTANTI
|
||
// ===============================================
|
||
|
||
function _normalizeMode(mode) {
|
||
return mode === "activities" ? "activities" : "photos";
|
||
}
|
||
|
||
function _cacheKeyForMode(mode) {
|
||
return _normalizeMode(mode) === "photos"
|
||
? "photosCache"
|
||
: "activitiesCache";
|
||
}
|
||
|
||
function _lastSyncKeyForMode(mode) {
|
||
return _normalizeMode(mode) === "photos"
|
||
? "lastSyncPhotos"
|
||
: "lastSyncActivities";
|
||
}
|
||
|
||
function getLocalItemsForMode(mode) {
|
||
const safeMode = _normalizeMode(mode);
|
||
|
||
try {
|
||
const raw = localStorage.getItem(_cacheKeyForMode(safeMode));
|
||
if (!raw) return [];
|
||
|
||
const arr = JSON.parse(raw);
|
||
return Array.isArray(arr) ? arr : [];
|
||
} catch (e) {
|
||
console.warn("[getLocalItemsForMode] error:", e);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function setLocalItemsForMode(mode, arr) {
|
||
const safeMode = _normalizeMode(mode);
|
||
const safeArr = Array.isArray(arr) ? arr : [];
|
||
|
||
try {
|
||
localStorage.setItem(_cacheKeyForMode(safeMode), JSON.stringify(safeArr));
|
||
|
||
// Aggiorna state.items solo se sto salvando la modalità attualmente visibile
|
||
if (window.state && state.mode === safeMode) {
|
||
state.items = safeArr;
|
||
}
|
||
} catch (e) {
|
||
console.warn("[setLocalItemsForMode] error:", e);
|
||
}
|
||
}
|
||
|
||
function setLocalPhotos(arr) {
|
||
setLocalItemsForMode("photos", arr);
|
||
}
|
||
|
||
function setLocalActivities(arr) {
|
||
setLocalItemsForMode("activities", arr);
|
||
}
|
||
|
||
function getLastSyncForMode(mode) {
|
||
return localStorage.getItem(_lastSyncKeyForMode(mode));
|
||
}
|
||
|
||
function setLastSyncForMode(mode, ts) {
|
||
if (!ts) return;
|
||
localStorage.setItem(_lastSyncKeyForMode(mode), ts);
|
||
}
|
||
|
||
function removeLocalItemForMode(mode, id) {
|
||
if (!id) return;
|
||
|
||
const safeMode = _normalizeMode(mode);
|
||
const arr = getLocalItemsForMode(safeMode);
|
||
const before = arr.length;
|
||
|
||
const next = arr.filter(x => String(x.id) !== String(id));
|
||
|
||
setLocalItemsForMode(safeMode, next);
|
||
|
||
console.log("[removeLocalItemForMode]", {
|
||
mode: safeMode,
|
||
id,
|
||
before,
|
||
after: next.length
|
||
});
|
||
}
|
||
|
||
function addLocalItemForMode(mode, item) {
|
||
if (!item || item.id == null) return;
|
||
|
||
const safeMode = _normalizeMode(mode);
|
||
const arr = getLocalItemsForMode(safeMode);
|
||
|
||
const next = arr.filter(x => String(x.id) !== String(item.id));
|
||
next.push(item);
|
||
|
||
setLocalItemsForMode(safeMode, next);
|
||
}
|
||
|
||
function refreshIfVisible(mode) {
|
||
const safeMode = _normalizeMode(mode);
|
||
|
||
if (window.state && state.mode === safeMode) {
|
||
if (typeof loadLocalState === "function") {
|
||
loadLocalState();
|
||
}
|
||
|
||
if (typeof refreshGallery === "function") {
|
||
refreshGallery();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===============================================
|
||
// 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, mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
try {
|
||
console.log(">>> CALLING sync_done", {
|
||
session_id,
|
||
device_id: getDeviceId(),
|
||
mode: syncMode
|
||
});
|
||
|
||
const endpoint =
|
||
syncMode === "photos"
|
||
? "/photos/sync_done"
|
||
: "/activities/sync_done";
|
||
|
||
await fetch(endpoint, {
|
||
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", syncMode);
|
||
} catch (e) {
|
||
console.error("❌ [WS] Errore sync_done:", e);
|
||
}
|
||
}
|
||
|
||
// ===============================================
|
||
// AUTH HEADERS
|
||
// ===============================================
|
||
|
||
function _authHeaders() {
|
||
const token = localStorage.getItem("token");
|
||
return { Authorization: "Bearer " + token };
|
||
}
|
||
|
||
// ===============================================
|
||
// API HELPERS — PER MODALITÀ
|
||
// ===============================================
|
||
|
||
function _endpointBase(mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
return syncMode === "photos" ? "/photos" : "/activities";
|
||
}
|
||
|
||
async function getAllItems(mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
const res = await fetch(_endpointBase(syncMode), {
|
||
headers: _authHeaders(),
|
||
});
|
||
|
||
return await res.json();
|
||
}
|
||
|
||
async function getChanges(since, mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
const res = await fetch(
|
||
`${_endpointBase(syncMode)}/changes?since=${encodeURIComponent(since)}`,
|
||
{ headers: _authHeaders() }
|
||
);
|
||
|
||
return await res.json();
|
||
}
|
||
|
||
async function getDeletedHard(since, mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
const endpoint =
|
||
syncMode === "photos"
|
||
? "/photos/deleted_hard"
|
||
: "/activities/deleted_hard";
|
||
|
||
const res = await fetch(
|
||
`${endpoint}?since=${encodeURIComponent(since)}`,
|
||
{ headers: _authHeaders() }
|
||
);
|
||
|
||
const json = await res.json();
|
||
|
||
return json.deleted || [];
|
||
}
|
||
|
||
async function fetchItemsByIds(ids, mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
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 = `${_endpointBase(syncMode)}/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 — SICURO PER MODALITÀ
|
||
// ===============================================
|
||
|
||
async function fullLoad(mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
console.log("🟦 FULL LOAD → caricamento completo", syncMode);
|
||
|
||
const items = await getAllItems(syncMode);
|
||
|
||
console.log(`📥 FULL LOAD → ricevuti ${items.length} elementi (${syncMode})`);
|
||
|
||
if (syncMode === "photos") {
|
||
setLocalPhotos(items);
|
||
} else {
|
||
setLocalActivities(items);
|
||
}
|
||
|
||
refreshIfVisible(syncMode);
|
||
|
||
const now = _nowIso();
|
||
setLastSyncForMode(syncMode, now);
|
||
|
||
console.log(`🕒 FULL LOAD → lastSync(${syncMode}) = ${now}`);
|
||
}
|
||
|
||
// ===============================================
|
||
// PROGRESSIVE SYNC — SICURO PER MODALITÀ
|
||
// ===============================================
|
||
|
||
async function progressiveSync(mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
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 — mode:", syncMode);
|
||
|
||
const lastSync = getLastSyncForMode(syncMode);
|
||
const localArr = getLocalItemsForMode(syncMode) || [];
|
||
|
||
console.log(`🕒 lastSync(${syncMode}): ${lastSync}`);
|
||
console.log(`📦 Cache locale (${syncMode}): ${localArr.length}`);
|
||
|
||
// Primo avvio / cache vuota → fullLoad
|
||
if (!lastSync || localArr.length === 0) {
|
||
console.warn("🟦 Cache locale vuota o lastSync mancante → fullLoad()");
|
||
await fullLoad(syncMode);
|
||
console.log("🏁 progressiveSync() COMPLETATO con fullLoad");
|
||
console.log("==============================================");
|
||
return;
|
||
}
|
||
|
||
// lastSync troppo vecchio → fullLoad
|
||
if (_isTooOldForDelta(lastSync)) {
|
||
console.warn(`🟥 lastSync > ${RETENTION_DAYS}gg → FULL LOAD richiesto`);
|
||
await fullLoad(syncMode);
|
||
console.log("🏁 progressiveSync() COMPLETATO con fullLoad: lastSync troppo vecchio");
|
||
console.log("==============================================");
|
||
return;
|
||
}
|
||
|
||
console.log("🟩 PROGRESSIVE SYNC → changes + deleted_hard", syncMode);
|
||
|
||
const changed = await getChanges(lastSync, syncMode);
|
||
console.log(`🟨 changes (${syncMode}): ${Array.isArray(changed) ? changed.length : 0}`);
|
||
|
||
const hardDeleted = await getDeletedHard(lastSync, syncMode);
|
||
|
||
console.log(`🟥 deleted_hard (${syncMode}): ${hardDeleted.length}`);
|
||
|
||
const localMap = _toMapById(localArr);
|
||
|
||
// Merge changed
|
||
if (Array.isArray(changed)) {
|
||
for (const p of changed) {
|
||
if (!p || p.id == null) continue;
|
||
localMap.set(String(p.id), p);
|
||
}
|
||
}
|
||
|
||
// Hard delete solo per le foto
|
||
for (const d of hardDeleted) {
|
||
if (!d || d.id == null) continue;
|
||
localMap.delete(String(d.id));
|
||
}
|
||
|
||
const merged = Array.from(localMap.values());
|
||
|
||
if (syncMode === "photos") {
|
||
setLocalPhotos(merged);
|
||
} else {
|
||
setLocalActivities(merged);
|
||
}
|
||
|
||
refreshIfVisible(syncMode);
|
||
|
||
const now = _nowIso();
|
||
setLastSyncForMode(syncMode, now);
|
||
|
||
console.log(`🕒 Aggiorno lastSync(${syncMode}) → ${now}`);
|
||
console.log("🏁 progressiveSync() COMPLETATO");
|
||
console.log("==============================================");
|
||
} finally {
|
||
window._syncInProgress = false;
|
||
}
|
||
}
|
||
|
||
// ===============================================
|
||
// PROGRESSIVE SYNC MIRATO DA since WS
|
||
// ===============================================
|
||
|
||
async function progressiveSyncFrom(sinceIso, mode = state.mode) {
|
||
const syncMode = _normalizeMode(mode);
|
||
|
||
if (!sinceIso) return progressiveSync(syncMode);
|
||
|
||
console.log(`🟦 progressiveSyncFrom(${sinceIso}) — mode: ${syncMode}`);
|
||
|
||
if (_isTooOldForDelta(sinceIso)) {
|
||
console.warn("🟥 since troppo vecchio → fullLoad()");
|
||
await fullLoad(syncMode);
|
||
return;
|
||
}
|
||
|
||
const localArr = getLocalItemsForMode(syncMode) || [];
|
||
|
||
if (!localArr.length) {
|
||
await fullLoad(syncMode);
|
||
return;
|
||
}
|
||
|
||
const changed = await getChanges(sinceIso, syncMode);
|
||
|
||
const hardDeleted = await getDeletedHard(sinceIso, syncMode);
|
||
|
||
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));
|
||
}
|
||
|
||
const merged = Array.from(localMap.values());
|
||
|
||
if (syncMode === "photos") {
|
||
setLocalPhotos(merged);
|
||
} else {
|
||
setLocalActivities(merged);
|
||
}
|
||
|
||
refreshIfVisible(syncMode);
|
||
|
||
setLastSyncForMode(syncMode, _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 — PER MODALITÀ
|
||
// ===============================================
|
||
|
||
let addedQueue = [];
|
||
let addedSet = new Set();
|
||
let flushTimer = null;
|
||
let flushing = false;
|
||
|
||
function enqueueAdded(id, mode = state.mode) {
|
||
if (!id) return;
|
||
|
||
const syncMode = _normalizeMode(mode);
|
||
const key = `${syncMode}:${String(id)}`;
|
||
|
||
if (addedSet.has(key)) return;
|
||
|
||
addedSet.add(key);
|
||
addedQueue.push({
|
||
id: String(id),
|
||
mode: syncMode
|
||
});
|
||
|
||
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(syncMode).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);
|
||
|
||
for (const q of chunk) {
|
||
addedSet.delete(`${q.mode}:${q.id}`);
|
||
}
|
||
|
||
// Raggruppo per modalità, così non mischio mai foto e attività
|
||
const byMode = {
|
||
photos: [],
|
||
activities: []
|
||
};
|
||
|
||
for (const q of chunk) {
|
||
byMode[q.mode].push(q.id);
|
||
}
|
||
|
||
for (const mode of ["photos", "activities"]) {
|
||
const ids = byMode[mode];
|
||
if (!ids.length) continue;
|
||
|
||
const items = await fetchItemsByIds(ids, mode);
|
||
|
||
if (Array.isArray(items) && items.length) {
|
||
for (const item of items) {
|
||
addLocalItemForMode(mode, item);
|
||
}
|
||
|
||
refreshIfVisible(mode);
|
||
}
|
||
}
|
||
|
||
if (addedQueue.length > 0) {
|
||
setTimeout(flushAddedQueue, 0);
|
||
}
|
||
} catch (e) {
|
||
console.error("❌ [WS] flushAddedQueue error:", e);
|
||
|
||
const fallbackMode =
|
||
addedQueue[0]?.mode ||
|
||
state.mode ||
|
||
"photos";
|
||
|
||
addedQueue = [];
|
||
addedSet.clear();
|
||
|
||
await progressiveSync(fallbackMode);
|
||
|
||
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 {}
|
||
}
|
||
|
||
const wsMode = _normalizeMode(state.mode);
|
||
|
||
console.log("🔌 [WS] Creo nuova connessione WebSocket...", wsMode);
|
||
|
||
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);
|
||
console.log("[WS] mode:", wsMode);
|
||
|
||
_send(ws, {
|
||
type: "auth",
|
||
token,
|
||
session_id,
|
||
device_id,
|
||
mode: wsMode,
|
||
});
|
||
};
|
||
|
||
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);
|
||
|
||
const eventMode = _normalizeMode(msg.mode || wsMode || state.mode);
|
||
|
||
|
||
// -----------------------------------------------
|
||
// 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 = getLastSyncForMode(eventMode);
|
||
const localArr = getLocalItemsForMode(eventMode) || [];
|
||
const localCacheMissing = !lastSync || localArr.length === 0;
|
||
|
||
console.log("🧪 [WS AUTH OK DECISION]", {
|
||
mode: eventMode,
|
||
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()", eventMode);
|
||
await progressiveSync(eventMode);
|
||
} catch (e) {
|
||
console.error("❌ [WS] Errore progressiveSync:", e);
|
||
} finally {
|
||
if (needRecovery) {
|
||
_maybeSendRecoveryDone(ws);
|
||
await sendSyncDone(msg.session_id, eventMode);
|
||
}
|
||
|
||
_send(ws, { type: "client_recovered" });
|
||
}
|
||
}, needFullSync ? WS_NEED_FULLSYNC_DELAY_MS : 0);
|
||
|
||
return;
|
||
}
|
||
|
||
console.log("🟢 [WS] Nessuna recovery richiesta → 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(eventMode);
|
||
} catch (e) {
|
||
console.error("❌ [WS] Errore progressiveSync su need_recovery:", e);
|
||
} finally {
|
||
_maybeSendRecoveryDone(ws);
|
||
await sendSyncDone(session_id, eventMode);
|
||
_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.bulk === true) {
|
||
console.log(
|
||
`📦 [WS] add_dir bulk folder=${msg.folder} count=${msg.count} → progressiveSyncFrom(since)`
|
||
);
|
||
|
||
await progressiveSyncFrom(msg.since, eventMode);
|
||
} 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, eventMode);
|
||
} else {
|
||
console.log(`📁 [WS] del_dir folder=${msg.folder}`);
|
||
}
|
||
|
||
finalize();
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// ADDED
|
||
// -----------------------------------------------
|
||
if (msg.type === "added") {
|
||
enqueueAdded(msg.id, eventMode);
|
||
finalize();
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// HARD DELETE / REMOVED
|
||
// -----------------------------------------------
|
||
if (msg.type === "del" || msg.type === "removed") {
|
||
removeLocalItemForMode(eventMode, msg.id);
|
||
refreshIfVisible(eventMode);
|
||
finalize();
|
||
return;
|
||
}
|
||
|
||
// -----------------------------------------------
|
||
// UPDATED
|
||
// -----------------------------------------------
|
||
if (msg.type === "updated") {
|
||
const arr = getLocalItemsForMode(eventMode);
|
||
const item = arr.find(p => String(p.id) === String(msg.id));
|
||
|
||
if (item) {
|
||
item.deleted_at = msg.deleted_at;
|
||
addLocalItemForMode(eventMode, item);
|
||
} else {
|
||
// Se non ho l'item locale, provo a recuperarlo
|
||
const fetched = await fetchItemsByIds([msg.id], eventMode);
|
||
if (Array.isArray(fetched) && fetched[0]) {
|
||
addLocalItemForMode(eventMode, fetched[0]);
|
||
}
|
||
}
|
||
|
||
refreshIfVisible(eventMode);
|
||
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);
|
||
};
|
||
}
|
||
|
||
// ===============================================
|
||
// EXPORT UTILI
|
||
// ===============================================
|
||
|
||
window.fullLoad = fullLoad;
|
||
window.progressiveSync = progressiveSync;
|
||
window.progressiveSyncFrom = progressiveSyncFrom;
|
||
window.startWebSocket = startWebSocket;
|
||
|
||
window.getLocalItemsForMode = getLocalItemsForMode;
|
||
window.setLocalItemsForMode = setLocalItemsForMode;
|
||
window.setLocalPhotos = setLocalPhotos;
|
||
window.setLocalActivities = setLocalActivities;
|
||
|
||
// ===============================================
|
||
// INIT
|
||
// ===============================================
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
if (AppAuth.isLoggedIn()) {
|
||
startWebSocket();
|
||
}
|
||
});
|