hotfix jointlist
This commit is contained in:
@@ -31,10 +31,16 @@ sub vcl_recv {
|
||||
# omijamy cache dla healthchecków / wewnętrznych nagłówków
|
||||
if (req.url == "/healthcheck" || req.http.X-Internal-Check) { return (pass); }
|
||||
|
||||
# Specjalna obsługa WebSocket i socket.io
|
||||
if (req.http.Upgrade ~ "(?i)websocket" || req.url ~ "^/socket.io/") {
|
||||
# WebSocket wymaga surowego tunelu TCP, ale zwykły transport polling
|
||||
# Socket.IO jest normalnym HTTP i nie może trafiać do pipe. Puszczenie całego
|
||||
# /socket.io/ przez pipe może pozostawić połączenie HTTP przypięte do backendu
|
||||
# po uśpieniu mobilnej przeglądarki i utrudniać późniejszy refresh/nawigację.
|
||||
if (req.http.Upgrade ~ "(?i)websocket") {
|
||||
return (pipe);
|
||||
}
|
||||
if (req.url ~ "^/socket.io/") {
|
||||
return (pass);
|
||||
}
|
||||
|
||||
# metody inne niż GET/HEAD bez cache
|
||||
if (req.method != "GET" && req.method != "HEAD") { return (pass); }
|
||||
@@ -103,7 +109,7 @@ sub vcl_recv {
|
||||
sub vcl_pipe {
|
||||
if (req.http.Upgrade) {
|
||||
set bereq.http.Upgrade = req.http.Upgrade;
|
||||
set bereq.http.Connection = req.http.Connection;
|
||||
set bereq.http.Connection = "Upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -221,6 +221,7 @@ def handle_join(data):
|
||||
global active_users
|
||||
room = str(data["room"])
|
||||
username = data.get("username", "Gość")
|
||||
silent = bool(data.get("silent", False))
|
||||
sid = request.sid
|
||||
|
||||
# Trzymamy obecność per SID, a nie tylko per nazwa użytkownika. Dzięki temu
|
||||
@@ -245,10 +246,20 @@ def handle_join(data):
|
||||
|
||||
emit(
|
||||
"joined_confirmation",
|
||||
{"room": room, "list_title": list_title},
|
||||
{
|
||||
"room": room,
|
||||
"list_title": list_title,
|
||||
"users": sorted(set(room_users.values())),
|
||||
"silent": silent,
|
||||
},
|
||||
to=sid,
|
||||
)
|
||||
return {"ok": True, "room": room, "already_joined": already_joined}
|
||||
return {
|
||||
"ok": True,
|
||||
"room": room,
|
||||
"already_joined": already_joined,
|
||||
"silent": silent,
|
||||
}
|
||||
|
||||
|
||||
@socketio.on("disconnect")
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
const socket = io();
|
||||
const socket = io({
|
||||
reconnection: true,
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 500,
|
||||
reconnectionDelayMax: 5000,
|
||||
timeout: 8000
|
||||
});
|
||||
|
||||
/*──────────────── placeholder pustej listy ────────────────*/
|
||||
function toggleEmptyPlaceholder() {
|
||||
@@ -121,7 +127,7 @@ function setupList(listId, username) {
|
||||
}
|
||||
|
||||
if (typeof window.recoverListSocket === 'function') {
|
||||
window.recoverListSocket({ force: true });
|
||||
window.recoverListSocket();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
let didReceiveFirstFullList = false;
|
||||
|
||||
// Karta może zostać zamrożona lub transport może umrzeć bez natychmiastowej
|
||||
// zmiany socket.connected. Po wznowieniu najpierw weryfikujemy połączenie ACK-em.
|
||||
// Reconnect robimy dopiero wtedy, gdy transport faktycznie nie odpowiada.
|
||||
const WAKE_RECONNECT_DEBOUNCE_MS = 200;
|
||||
const WAKE_WATCHDOG_INTERVAL_MS = 10000;
|
||||
const WAKE_WATCHDOG_GAP_MS = 25000;
|
||||
let recoveryTimer = null;
|
||||
let recoveryForceRequested = false;
|
||||
let recoveryInProgress = false;
|
||||
let lastWatchdogTick = Date.now();
|
||||
// Stan dołączenia do aktualnego pokoju. Pierwsze udane wejście ma pokazać
|
||||
// użytkownikowi potwierdzenie i listę obecnych osób. Każdy kolejny join po
|
||||
// reconnectcie jest cichy po stronie tej karty.
|
||||
let joinedSocketId = null;
|
||||
let joinedRoomId = null;
|
||||
let didCompleteInitialJoin = false;
|
||||
let currentRoomUsers = [];
|
||||
|
||||
// Safari i inne mobilne przeglądarki potrafią zamrozić kartę razem z transportem.
|
||||
// Po wznowieniu najpierw robimy krótki request z ACK. Jeżeli stary transport
|
||||
// faktycznie nie odpowiada, zamykamy wyłącznie transport Engine.IO i pozwalamy
|
||||
// wbudowanemu mechanizmowi Socket.IO zestawić nowe połączenie. Nie wywołujemy
|
||||
// socket.disconnect(), bo jest to ręczne rozłączenie i może wejść w konflikt
|
||||
// z automatycznym reconnectem.
|
||||
const WAKE_CHECK_DEBOUNCE_MS = 250;
|
||||
const WAKE_ACK_TIMEOUT_MS = 4000;
|
||||
const WAKE_WATCHDOG_INTERVAL_MS = 5000;
|
||||
const WAKE_WATCHDOG_GAP_MS = 15000;
|
||||
const WAKE_RETRY_CONNECT_MS = 1200;
|
||||
|
||||
let wakeCheckTimer = null;
|
||||
let wakeCheckInProgress = false;
|
||||
let reconnectFallbackTimer = null;
|
||||
let lastWatchdogTick = Date.now();
|
||||
|
||||
function hasActiveListRoom() {
|
||||
return Boolean(window.LIST_ID && window.usernameForReconnect);
|
||||
@@ -20,7 +32,6 @@ function hasActiveListRoom() {
|
||||
function disableCheckboxes(disable) {
|
||||
document.querySelectorAll('#items input[type="checkbox"]').forEach(cb => {
|
||||
if (disable) {
|
||||
// Zapamiętaj stan biznesowy tylko przy pierwszym nałożeniu blokady transportu.
|
||||
if (cb.dataset.socketDisabledBefore === undefined) {
|
||||
cb.dataset.socketDisabledBefore = cb.disabled ? '1' : '0';
|
||||
}
|
||||
@@ -41,99 +52,119 @@ function ensureCurrentListJoined() {
|
||||
const roomId = String(window.LIST_ID);
|
||||
const socketId = socket.id || null;
|
||||
|
||||
// join_list może przyjść zarówno z setupList(), jak i z eventu connect.
|
||||
// Ten klucz sprawia, że dla jednego fizycznego połączenia wysyłamy go tylko raz.
|
||||
if (joinedSocketId === socketId && joinedRoomId === roomId) return false;
|
||||
|
||||
joinedSocketId = socketId;
|
||||
joinedRoomId = roomId;
|
||||
|
||||
socket.emit('join_list', {
|
||||
room: window.LIST_ID,
|
||||
username: window.usernameForReconnect
|
||||
username: window.usernameForReconnect,
|
||||
// Pierwsze wejście jest widoczne. Reconnect tej samej strony jest cichy.
|
||||
silent: didCompleteInitialJoin
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
window.ensureCurrentListJoined = ensureCurrentListJoined;
|
||||
|
||||
function requestCurrentList({ verify = false } = {}) {
|
||||
function requestCurrentList() {
|
||||
if (!socket.connected || !window.LIST_ID) return;
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
}
|
||||
|
||||
if (!verify) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
function forceCloseStaleTransport() {
|
||||
wakeCheckInProgress = false;
|
||||
joinedSocketId = null;
|
||||
joinedRoomId = null;
|
||||
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
|
||||
try {
|
||||
const engine = socket.io && socket.io.engine;
|
||||
const transport = engine && engine.transport;
|
||||
|
||||
// Zamknięcie samego transportu zachowuje automatyczny reconnect Managera.
|
||||
// Transport.close() emituje close od razu, zamiast czekać na opróżnienie
|
||||
// bufora starego, zamrożonego połączenia.
|
||||
if (transport && transport.readyState !== 'closed') {
|
||||
transport.close();
|
||||
} else if (!socket.connected) {
|
||||
socket.connect();
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Nie udało się zamknąć starego transportu Socket.IO', err);
|
||||
if (!socket.connected) socket.connect();
|
||||
}
|
||||
|
||||
clearTimeout(reconnectFallbackTimer);
|
||||
reconnectFallbackTimer = setTimeout(() => {
|
||||
if (document.hidden || !hasActiveListRoom() || socket.connected) return;
|
||||
socket.connect();
|
||||
}, WAKE_RETRY_CONNECT_MS);
|
||||
}
|
||||
|
||||
function runWakeCheck() {
|
||||
if (document.hidden || !hasActiveListRoom()) return;
|
||||
if (typeof navigator !== 'undefined' && navigator.onLine === false) return;
|
||||
|
||||
if (!socket.connected) {
|
||||
disableCheckboxes(true);
|
||||
socket.connect();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.timeout(4000).emit('request_full_list', { list_id: window.LIST_ID }, (err, response) => {
|
||||
if (!err && response && response.ok === true) return;
|
||||
recoverListSocket({ force: true });
|
||||
});
|
||||
const engine = socket.io && socket.io.engine;
|
||||
if (!engine || engine.readyState !== 'open' || !engine.transport || engine.transport.readyState !== 'open') {
|
||||
forceCloseStaleTransport();
|
||||
return;
|
||||
}
|
||||
|
||||
if (wakeCheckInProgress) return;
|
||||
wakeCheckInProgress = true;
|
||||
|
||||
// request_full_list pełni jednocześnie rolę health-checka i synchronizacji.
|
||||
socket.timeout(WAKE_ACK_TIMEOUT_MS).emit(
|
||||
'request_full_list',
|
||||
{ list_id: window.LIST_ID },
|
||||
(err, response) => {
|
||||
wakeCheckInProgress = false;
|
||||
|
||||
if (!err && response && response.ok === true) {
|
||||
// Jeśli połączenie jest zdrowe, nie wykonujemy reconnectu i nie emitujemy
|
||||
// ponownego join_list. Samo przełączenie karty pozostaje więc całkowicie ciche.
|
||||
return;
|
||||
}
|
||||
|
||||
forceCloseStaleTransport();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function recoverListSocket({ force = false } = {}) {
|
||||
recoveryForceRequested = recoveryForceRequested || force;
|
||||
clearTimeout(recoveryTimer);
|
||||
|
||||
recoveryTimer = setTimeout(() => {
|
||||
const shouldForce = recoveryForceRequested;
|
||||
recoveryForceRequested = false;
|
||||
|
||||
if (document.hidden) return;
|
||||
|
||||
if (!socket.connected) {
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
socket.connect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldForce && hasActiveListRoom()) {
|
||||
if (recoveryInProgress) return;
|
||||
recoveryInProgress = true;
|
||||
disableCheckboxes(true);
|
||||
joinedSocketId = null;
|
||||
joinedRoomId = null;
|
||||
|
||||
// Twardy reconnect tylko po realnym błędzie/timeout ACK, nie po samym
|
||||
// przełączeniu karty lub powrocie aplikacji na pierwszy plan.
|
||||
socket.disconnect();
|
||||
setTimeout(() => socket.connect(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasActiveListRoom()) {
|
||||
// Jeśli transport żyje, synchronizacja wystarczy. Timeout sam uruchomi reconnect.
|
||||
requestCurrentList({ verify: true });
|
||||
}
|
||||
}, WAKE_RECONNECT_DEBOUNCE_MS);
|
||||
function recoverListSocket() {
|
||||
clearTimeout(wakeCheckTimer);
|
||||
wakeCheckTimer = setTimeout(runWakeCheck, WAKE_CHECK_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Udostępnione dla akcji w live.js (np. timeout checkboxa).
|
||||
window.recoverListSocket = recoverListSocket;
|
||||
|
||||
// Powrót po zablokowaniu telefonu, zmianie aplikacji lub karty.
|
||||
// Najważniejszy przypadek: powrót po zablokowaniu telefonu / zmianie aplikacji.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) recoverListSocket();
|
||||
});
|
||||
|
||||
// BFCache może przywrócić stronę ze starym transportem Socket.IO.
|
||||
window.addEventListener('pageshow', () => {
|
||||
recoverListSocket();
|
||||
// pageshow bez persisted występuje także przy zwykłym pierwszym ładowaniu strony.
|
||||
// Reconnect jest potrzebny wyłącznie przy rzeczywistym powrocie z BFCache.
|
||||
window.addEventListener('pageshow', event => {
|
||||
if (event.persisted) recoverListSocket();
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
recoverListSocket();
|
||||
});
|
||||
window.addEventListener('online', recoverListSocket);
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
recoverListSocket();
|
||||
});
|
||||
// Dla środowisk emitujących zdarzenie resume (np. webview/PWA wrapper).
|
||||
document.addEventListener('resume', recoverListSocket);
|
||||
|
||||
document.addEventListener('resume', () => {
|
||||
recoverListSocket();
|
||||
});
|
||||
|
||||
// Watchdog łapie także wznowienia, przy których przeglądarka nie wyśle poprawnie
|
||||
// visibilitychange/pageshow (np. po agresywnym uśpieniu procesu lub karty).
|
||||
// Fallback dla przeglądarek, które po wybudzeniu nie wyślą visibilitychange.
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const gap = now - lastWatchdogTick;
|
||||
@@ -145,7 +176,8 @@ setInterval(() => {
|
||||
}, WAKE_WATCHDOG_INTERVAL_MS);
|
||||
|
||||
socket.on('connect', function () {
|
||||
recoveryInProgress = false;
|
||||
wakeCheckInProgress = false;
|
||||
clearTimeout(reconnectFallbackTimer);
|
||||
joinedSocketId = null;
|
||||
joinedRoomId = null;
|
||||
|
||||
@@ -156,19 +188,36 @@ socket.on('connect', function () {
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
wakeCheckInProgress = false;
|
||||
joinedSocketId = null;
|
||||
joinedRoomId = null;
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
});
|
||||
|
||||
socket.on('connect_error', function () {
|
||||
wakeCheckInProgress = false;
|
||||
if (window.LIST_ID) disableCheckboxes(true);
|
||||
});
|
||||
|
||||
socket.off('joined_confirmation');
|
||||
socket.on('joined_confirmation', function () {
|
||||
// Potwierdzenie dołączenia nie jest komunikatem dla użytkownika. Po zwykłym
|
||||
// reconnectcie karta ma wrócić do działania bez sztucznego "ponownie dołączono".
|
||||
socket.on('joined_confirmation', function (data) {
|
||||
const isInitialVisibleJoin = !didCompleteInitialJoin && !data.silent;
|
||||
didCompleteInitialJoin = true;
|
||||
|
||||
if (Array.isArray(data.users)) {
|
||||
currentRoomUsers = data.users;
|
||||
window.currentRoomUsers = data.users;
|
||||
}
|
||||
|
||||
if (isInitialVisibleJoin) {
|
||||
const title = data.list_title ? `: ${data.list_title}` : '';
|
||||
showToast(`Dołączono do listy${title}`, 'success');
|
||||
|
||||
if (currentRoomUsers.length > 0) {
|
||||
showToast(`Obecni: ${currentRoomUsers.join(', ')}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
requestCurrentList();
|
||||
});
|
||||
|
||||
@@ -181,10 +230,11 @@ socket.on('user_left', function (data) {
|
||||
});
|
||||
|
||||
socket.on('user_list', function (data) {
|
||||
if (data.users.length > 0) {
|
||||
const userList = data.users.join(', ');
|
||||
showToast(`Obecni: ${userList}`, 'info');
|
||||
}
|
||||
// Stan obecności przechowujemy, ale nie pokazujemy dodatkowego toasta przy
|
||||
// każdej zmianie. Pierwsze wejście pokazuje pełną listę z joined_confirmation,
|
||||
// a później wystarczają komunikaty user_joined / user_left.
|
||||
currentRoomUsers = Array.isArray(data.users) ? data.users : [];
|
||||
window.currentRoomUsers = currentRoomUsers;
|
||||
});
|
||||
|
||||
socket.on('receipt_added', function (data) {
|
||||
@@ -227,6 +277,7 @@ socket.on("items_reordered", data => {
|
||||
|
||||
socket.on('full_list', function (data) {
|
||||
const itemsContainer = document.getElementById('items');
|
||||
if (!itemsContainer) return;
|
||||
|
||||
const oldItems = Array.from(
|
||||
itemsContainer.querySelectorAll('li[data-name], li[id^="item-"]')
|
||||
@@ -237,7 +288,7 @@ socket.on('full_list', function (data) {
|
||||
window.currentItems = data.items;
|
||||
updateListSmoothly(data.items);
|
||||
disableCheckboxes(false);
|
||||
recoveryInProgress = false;
|
||||
wakeCheckInProgress = false;
|
||||
if (typeof window.syncSortModeUI === 'function') {
|
||||
window.syncSortModeUI();
|
||||
}
|
||||
@@ -255,4 +306,4 @@ socket.on('item_marked_not_purchased', data => {
|
||||
|
||||
socket.on('item_unmarked_not_purchased', data => {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user