fix: reduce browser storage pressure

This commit is contained in:
Michael Lam
2026-05-16 08:44:04 -07:00
parent e3035b3e40
commit 48b82d57db
4 changed files with 99 additions and 15 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Service worker updates now delete old shell caches before creating the new versioned cache, reducing temporary Cache Storage pressure during frequent releases. Deleting sessions also prunes the localStorage maps that track viewed counts, completion unread state, and observed streaming state so stale per-session entries do not accumulate indefinitely. Closes #2389.
## [v0.51.74] — 2026-05-16 — Release AX (stage-367 — 4-PR safe-lane batch — #2362 table-cell spacing + #2363 run-state-consistency RFC + #2365 custom_providers list-format + #2367 settings sidebar i18n)
### Added
+14
View File
@@ -170,6 +170,14 @@ function _clearSessionCompletionUnread(sid) {
_saveSessionCompletionUnread();
}
function _clearSessionViewedCount(sid) {
if (!sid) return;
const counts = _getSessionViewedCounts();
if (!Object.prototype.hasOwnProperty.call(counts, sid)) return;
delete counts[sid];
_saveSessionViewedCounts();
}
function _hasSessionCompletionUnread(sid) {
if (!sid) return false;
return Object.prototype.hasOwnProperty.call(_getSessionCompletionUnread(), sid);
@@ -810,6 +818,12 @@ function _clearHandoffStorageForSession(sid) {
_setHandoffStorageValue(sid, _HANDOFF_SUFFIX_DISMISSED_AT, null);
_setHandoffStorageValue(sid, _HANDOFF_SUFFIX_SUMMARY_HANDLED_AT, null);
} catch {}
// Session deletion should also prune per-session tracking maps. Otherwise
// heavy users accumulate one localStorage entry per deleted session forever,
// which increases quota pressure and can make future UI persistence fail.
try { _clearSessionViewedCount(sid); } catch {}
try { _clearSessionCompletionUnread(sid); } catch {}
try { _forgetObservedStreamingSession(sid); } catch {}
}
function _getHandoffDismissedAt(sid) {
+22 -15
View File
@@ -39,28 +39,35 @@ const SHELL_ASSETS = [
'./manifest.json',
];
// Install: pre-cache the app shell
function deleteOldShellCaches() {
return caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
)
);
}
// Install: prune old shell caches first, then pre-cache the app shell. Doing
// this before caches.open(CACHE_NAME) avoids a temporary double-cache window on
// quota-sensitive browsers during frequent version bumps.
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(SHELL_ASSETS).catch((err) => {
// Non-fatal: if any asset fails, still activate
console.warn('[sw] Shell pre-cache partial failure:', err);
});
})
deleteOldShellCaches().then(() =>
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(SHELL_ASSETS).catch((err) => {
// Non-fatal: if any asset fails, still activate
console.warn('[sw] Shell pre-cache partial failure:', err);
});
})
)
);
self.skipWaiting();
});
// Activate: clean up old caches
// Activate: keep the old-cache cleanup as a safety net in case install was
// interrupted or an older worker was already waiting.
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
)
)
);
event.waitUntil(deleteOldShellCaches());
self.clients.claim();
});
+59
View File
@@ -0,0 +1,59 @@
"""Regression coverage for storage-pressure cleanup from issue #2389."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SW_SRC = (ROOT / "static" / "sw.js").read_text(encoding="utf-8")
SESSIONS_SRC = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
def _function_block(src: str, name: str, window: int = 1600) -> str:
idx = src.find(f"function {name}(")
assert idx != -1, f"missing function {name}"
return src[idx : idx + window]
def test_service_worker_install_deletes_old_caches_before_opening_new_cache():
install_idx = SW_SRC.find("self.addEventListener('install'")
assert install_idx != -1, "service worker must define an install handler"
install_block = SW_SRC[install_idx : SW_SRC.find("self.addEventListener('activate'", install_idx)]
cleanup_idx = install_block.find("deleteOldShellCaches().then")
open_idx = install_block.find("caches.open(CACHE_NAME)")
assert cleanup_idx != -1, "install must delete stale shell caches before pre-cache"
assert open_idx != -1, "install must still pre-cache the current shell cache"
assert cleanup_idx < open_idx, (
"opening the new shell cache before deleting old ones creates a temporary "
"double-cache window that increases quota pressure"
)
def test_service_worker_keeps_activate_cleanup_safety_net():
activate_idx = SW_SRC.find("self.addEventListener('activate'")
assert activate_idx != -1, "service worker must define an activate handler"
activate_block = SW_SRC[activate_idx : activate_idx + 500]
assert "event.waitUntil(deleteOldShellCaches())" in activate_block
assert "self.clients.claim()" in activate_block
def test_deleted_sessions_prune_all_session_tracking_maps():
assert "const SESSION_VIEWED_COUNTS_KEY = 'hermes-session-viewed-counts';" in SESSIONS_SRC
assert "const SESSION_COMPLETION_UNREAD_KEY = 'hermes-session-completion-unread';" in SESSIONS_SRC
assert "const SESSION_OBSERVED_STREAMING_KEY = 'hermes-session-observed-streaming';" in SESSIONS_SRC
assert "function _clearSessionViewedCount(sid)" in SESSIONS_SRC
clear_block = _function_block(SESSIONS_SRC, "_clearHandoffStorageForSession")
assert "_clearSessionViewedCount(sid)" in clear_block
assert "_clearSessionCompletionUnread(sid)" in clear_block
assert "_forgetObservedStreamingSession(sid)" in clear_block
def test_session_viewed_count_prune_is_best_effort_and_persists_when_changed():
viewed_block = _function_block(SESSIONS_SRC, "_clearSessionViewedCount")
assert "Object.prototype.hasOwnProperty.call(counts, sid)" in viewed_block
assert "delete counts[sid]" in viewed_block
assert "_saveSessionViewedCounts()" in viewed_block
clear_block = _function_block(SESSIONS_SRC, "_clearHandoffStorageForSession")
assert "try { _clearSessionViewedCount(sid); } catch {}" in clear_block
assert "try { _clearSessionCompletionUnread(sid); } catch {}" in clear_block
assert "try { _forgetObservedStreamingSession(sid); } catch {}" in clear_block