From a0b73920053e23d32c83fd321f4cdc266f11a4f5 Mon Sep 17 00:00:00 2001 From: seo Date: Tue, 21 Jul 2026 18:19:40 +0900 Subject: [PATCH] Improve mobile asset reload compatibility --- README.md | 2 +- public/assets/asset-reload.js | 113 ++++++++++++++++++++++++++++++++-- public/index.php | 2 +- 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 89cc57e..f5f0844 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Control은 라즈베리파이/리눅스 호스트의 팬과 시스템 상태를 - `bin/wifi_observe.php`: 5G WiFi 연결 시간 보정을 위한 시스템단 관측 CLI - `systemd/control-wifi-observe.*`: WiFi 관측 CLI를 주기적으로 실행하는 systemd unit 예시 -`asset-version.php`는 `public`의 PHP/JS/CSS/manifest/icon 파일과 `config`의 PHP 파일을 읽어 크기, 수정 시각, sha256 hash를 묶은 버전을 만듭니다. `asset-reload.js`는 화면 복귀, 포커스 복귀, 30초 간격으로 이 값을 다시 확인하고, 값이 달라지면 브라우저 Cache Storage를 비운 뒤 현재 화면을 다시 엽니다. +`asset-version.php`는 `public`의 PHP/JS/CSS/manifest/icon 파일과 `config`의 PHP 파일을 읽어 크기, 수정 시각, sha256 hash를 묶은 버전을 만듭니다. `asset-reload.js`는 화면 복귀, 포커스 복귀, 온라인 복귀, 모바일 터치/클릭 복귀, 30초 간격으로 이 값을 다시 확인하고, 값이 달라지면 브라우저 Cache Storage를 비운 뒤 현재 화면을 다시 엽니다. 삼성 브라우저 등에서 Web Storage나 `Promise.finally()` 호환성이 부족해도 동작하도록 쿠키 fallback과 timeout 기반 캐시 삭제를 사용합니다. ## 데이터/저장소 diff --git a/public/assets/asset-reload.js b/public/assets/asset-reload.js index 12a6a6a..6008abd 100644 --- a/public/assets/asset-reload.js +++ b/public/assets/asset-reload.js @@ -19,6 +19,82 @@ var storageKey = 'assetVersion:' + service; var reloadKey = 'assetReload:' + service; var running = false; + var memoryStore = {}; + var lastCheckAt = 0; + + function cookieName(key) { + return key.replace(/[^A-Za-z0-9_-]/g, '_'); + } + + function getCookie(key) { + var name = cookieName(key) + '='; + return (document.cookie || '').split(';').map(function (part) { + return part.trim(); + }).reduce(function (found, part) { + if (found || part.indexOf(name) !== 0) return found; + return decodeURIComponent(part.slice(name.length)); + }, ''); + } + + function setCookie(key, value) { + document.cookie = cookieName(key) + '=' + encodeURIComponent(value) + '; Path=/; Max-Age=31536000; SameSite=Lax'; + } + + function storeGet(key) { + try { + return localStorage.getItem(key) || getCookie(key) || memoryStore[key] || ''; + } catch (e) { + return getCookie(key) || memoryStore[key] || ''; + } + } + + function storeSet(key, value) { + memoryStore[key] = value; + try { + localStorage.setItem(key, value); + } catch (e) {} + try { + setCookie(key, value); + } catch (e) {} + } + + function sessionGet(key) { + try { + return sessionStorage.getItem(key) || memoryStore[key] || ''; + } catch (e) { + return memoryStore[key] || ''; + } + } + + function sessionSet(key, value) { + memoryStore[key] = value; + try { + sessionStorage.setItem(key, value); + } catch (e) {} + } + + function withTimeout(promise, timeout) { + return new Promise(function (resolve) { + var done = false; + var timer = setTimeout(function () { + if (done) return; + done = true; + resolve(); + }, timeout); + + Promise.resolve(promise).then(function () { + if (done) return; + done = true; + clearTimeout(timer); + resolve(); + }).catch(function () { + if (done) return; + done = true; + clearTimeout(timer); + resolve(); + }); + }); + } function addQuery(url, key, value) { var parsed = new URL(url, window.location.href); @@ -79,17 +155,26 @@ var token = version.slice(0, 12); var marker = token + ':' + Math.floor(Date.now() / 10000); - if (sessionStorage.getItem(reloadKey) === marker) return; + if (sessionGet(reloadKey) === marker) return; - sessionStorage.setItem(reloadKey, marker); - deleteCaches().then(function () { + sessionSet(reloadKey, marker); + withTimeout(deleteCaches(), 2500).then(function () { + if (navigator.serviceWorker && navigator.serviceWorker.getRegistrations) { + navigator.serviceWorker.getRegistrations().then(function (registrations) { + registrations.forEach(function (registration) { + if (registration && typeof registration.update === 'function') registration.update(); + }); + }).catch(function () {}); + } window.location.replace(addQuery(window.location.href, 'assetReload', token)); }); } function check(force) { if (running || (!force && document.hidden)) return; + if (force && Date.now() - lastCheckAt < 1000) return; + lastCheckAt = Date.now(); running = true; fetch(addQuery(endpoint, '_', String(Date.now())), { cache: 'no-store', @@ -104,23 +189,39 @@ }).then(function (data) { if (!data || data.result !== 'ok' || !data.version) return; - var previous = localStorage.getItem(storageKey); - localStorage.setItem(storageKey, data.version); + var previous = storeGet(storageKey); + storeSet(storageKey, data.version); if (previous && previous !== data.version) { reloadWith(data.version); } - }).catch(function () {}).finally(function () { + }).then(function () { + running = false; + }, function () { running = false; }); } window.addEventListener('pageshow', function () { check(true); + setTimeout(function () { check(true); }, 250); + setTimeout(function () { check(true); }, 1500); }); window.addEventListener('focus', function () { check(true); }); + window.addEventListener('online', function () { + check(true); + }); + window.addEventListener('orientationchange', function () { + setTimeout(function () { check(true); }, 500); + }); + ['pointerdown', 'touchstart', 'click'].forEach(function (eventName) { + window.addEventListener(eventName, function () { + if (document.hidden || Date.now() - lastCheckAt < 5000) return; + check(true); + }, { passive: true }); + }); document.addEventListener('visibilitychange', function () { if (!document.hidden) check(true); }); diff --git a/public/index.php b/public/index.php index 62edd5a..fb064d9 100644 --- a/public/index.php +++ b/public/index.php @@ -76,7 +76,7 @@ $loggedIn = signed_in(); - +