From cef8d3db28f729b18c1b8f7c81afe6cb2f64c0c1 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Fri, 16 Oct 2026 05:33:54 -0400 Subject: [PATCH] Attempt to resolve web socket issues --- helpers/websocketHelper.js | 29 +++++++---- scripts/cardBuilder.js | 103 +++++++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 38 deletions(-) diff --git a/helpers/websocketHelper.js b/helpers/websocketHelper.js index 40a2c25..32844fb 100644 --- a/helpers/websocketHelper.js +++ b/helpers/websocketHelper.js @@ -62,6 +62,8 @@ subscribeAttached[messageType] = true; subscribeUnsubs[messageType] = typeof unsub === 'function' ? unsub : null; deliveryMode = 'subscribe'; + // Drop any legacy onmessage hook immediately — do not wait for the poll tick + unhookSocket(); LOG(`Subscribed via ApiClient.subscribe for ${messageType}`); return true; } catch (err) { @@ -95,8 +97,11 @@ /** * Legacy path: wrap socket.onmessage, re-bind when the socket instance changes. + * Only used when ApiClient.subscribe is unavailable. */ function hookSocketIfNeeded() { + if (canSubscribe()) return false; + const socket = getSocket(); if (!socket) return false; @@ -143,17 +148,21 @@ } function tryInitializeDelivery() { - // Prefer JF-web style subscribe (reconnect-safe) + // Prefer JF-web style subscribe (reconnect-safe). Never wrap onmessage when subscribe exists. if (canSubscribe()) { - if (ensureSubscribeForAllRegisteredTypes()) { - return true; - } + return ensureSubscribeForAllRegisteredTypes(); } // Fallback: raw socket onmessage with rebind support return hookSocketIfNeeded(); } + function stopPolling() { + if (pollingIntervalId == null) return; + clearInterval(pollingIntervalId); + pollingIntervalId = null; + } + function startPolling() { if (pollingIntervalId != null) return; if (tryInitializeDelivery()) { @@ -163,15 +172,14 @@ pollingIntervalId = setInterval(() => { if (canSubscribe()) { - ensureSubscribeForAllRegisteredTypes(); - if (deliveryMode === 'subscribe') { - // Subscribe path owns delivery; drop legacy hook if any + if (ensureSubscribeForAllRegisteredTypes() && deliveryMode === 'subscribe') { unhookSocket(); - clearInterval(pollingIntervalId); - pollingIntervalId = null; + stopPolling(); LOG('Delivery mode: ApiClient.subscribe'); return; } + // subscribe exists but types not attached yet — keep polling, never hook onmessage + return; } const socket = getSocket(); @@ -252,5 +260,6 @@ }, }; - startPolling(); + // Do not startPolling() here — wait until the first listen() so we never + // wrap JF's socket before any consumer needs delivery. })(); diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index d55fd35..947901c 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -2141,11 +2141,30 @@ let cardLibraryChangedListenerStarted = false; let pendingLibraryUpdatedIds = new Set(); let libraryUpdatedFlushTimer = null; - const LIBRARY_UPDATED_DEBOUNCE_MS = 400; + let libraryUpdatedFlushRunning = false; + const LIBRARY_UPDATED_DEBOUNCE_MS = 750; + const LIVE_SYNC_REBUILD_BATCH_SIZE = 8; // Match apiHelper DEFAULT_ITEM_FIELDS so rebuilt cards keep aspect/overlays/indicators. const LIVE_SYNC_ITEM_FIELDS = 'PrimaryImageAspectRatio,DateCreated,Overview,Taglines,ProductionYear,RecursiveItemCount,ChildCount,UserData,ProviderIds,MediaSourceCount'; + function scheduleLibraryUpdatedFlush() { + if (libraryUpdatedFlushTimer) clearTimeout(libraryUpdatedFlushTimer); + libraryUpdatedFlushTimer = setTimeout(() => { + void flushVisibleCardLibraryUpdates(); + }, LIBRARY_UPDATED_DEBOUNCE_MS); + } + + function yieldToMainThread() { + return new Promise((resolve) => { + if (typeof requestAnimationFrame === 'function') { + requestAnimationFrame(() => resolve()); + } else { + setTimeout(resolve, 0); + } + }); + } + function escapeCssIdentForCardSync(value) { return typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(String(value)) @@ -2447,33 +2466,45 @@ async function flushVisibleCardLibraryUpdates() { libraryUpdatedFlushTimer = null; + if (libraryUpdatedFlushRunning) { + // Keep pending ids; schedule one follow-up after the in-flight flush + if (pendingLibraryUpdatedIds.size) { + liveSyncLog('Flush already running — deferring follow-up', { + pendingSize: pendingLibraryUpdatedIds.size, + }); + scheduleLibraryUpdatedFlush(); + } + return; + } + const ids = Array.from(pendingLibraryUpdatedIds); pendingLibraryUpdatedIds.clear(); liveSyncLog('Flush start', { pendingIds: ids }); if (!ids.length) return; - const matchCounts = {}; - const idsToFetch = ids.filter((id) => { - const count = findVisibleCardsForItemId(id).length; - matchCounts[id] = count; - return count > 0; - }); - liveSyncLog('Visible card matches', matchCounts); - - if (!idsToFetch.length) { - liveSyncLog('No on-page cards for updated ids — skipping fetch'); - return; - } - - if (typeof ApiClient === 'undefined' || typeof ApiClient.getItems !== 'function') { - liveSyncWarn('ApiClient.getItems unavailable — cannot refresh cards'); - return; - } - - const userId = ApiClient.getCurrentUserId?.(); - liveSyncLog('Fetching items', { userId, idsToFetch, fields: LIVE_SYNC_ITEM_FIELDS }); - + libraryUpdatedFlushRunning = true; try { + const matchCounts = {}; + const idsToFetch = ids.filter((id) => { + const count = findVisibleCardsForItemId(id).length; + matchCounts[id] = count; + return count > 0; + }); + liveSyncLog('Visible card matches', matchCounts); + + if (!idsToFetch.length) { + liveSyncLog('No on-page cards for updated ids — skipping fetch'); + return; + } + + if (typeof ApiClient === 'undefined' || typeof ApiClient.getItems !== 'function') { + liveSyncWarn('ApiClient.getItems unavailable — cannot refresh cards'); + return; + } + + const userId = ApiClient.getCurrentUserId?.(); + liveSyncLog('Fetching items', { userId, idsToFetch, fields: LIVE_SYNC_ITEM_FIELDS }); + const response = await ApiClient.getItems(userId, { Ids: idsToFetch.join(','), Recursive: false, @@ -2494,8 +2525,8 @@ })), }); - let rebuilt = 0; - let skipped = 0; + // Flatten id → cards into a rebuild work list, then process in rAF batches + const work = []; idsToFetch.forEach((id) => { const freshItem = itemsById.get(normalizeItemIdKey(id)); if (!freshItem) { @@ -2505,13 +2536,29 @@ const cards = findVisibleCardsForItemId(id); liveSyncLog('Replacing cards for item', { id, cardCount: cards.length }); cards.forEach((card) => { + work.push({ card, freshItem }); + }); + }); + + let rebuilt = 0; + let skipped = 0; + for (let i = 0; i < work.length; i += LIVE_SYNC_REBUILD_BATCH_SIZE) { + if (i > 0) await yieldToMainThread(); + const batch = work.slice(i, i + LIVE_SYNC_REBUILD_BATCH_SIZE); + batch.forEach(({ card, freshItem }) => { if (replaceVisibleCardWithFreshItem(card, freshItem)) rebuilt += 1; else skipped += 1; }); - }); - liveSyncLog('Flush complete', { rebuilt, skipped }); + } + liveSyncLog('Flush complete', { rebuilt, skipped, workItems: work.length }); } catch (err) { console.error('[KefinTweaks CardBuilder][LiveSync] Visible card live sync failed:', err); + } finally { + libraryUpdatedFlushRunning = false; + // Ids queued during this flush — schedule another pass + if (pendingLibraryUpdatedIds.size) { + scheduleLibraryUpdatedFlush(); + } } } @@ -2528,9 +2575,9 @@ pendingSizeBefore: before, pendingSizeAfter: pendingLibraryUpdatedIds.size, debounceMs: LIBRARY_UPDATED_DEBOUNCE_MS, + flushRunning: libraryUpdatedFlushRunning, }); - if (libraryUpdatedFlushTimer) clearTimeout(libraryUpdatedFlushTimer); - libraryUpdatedFlushTimer = setTimeout(flushVisibleCardLibraryUpdates, LIBRARY_UPDATED_DEBOUNCE_MS); + scheduleLibraryUpdatedFlush(); } function handleLibraryChangedMessage(msg) {