diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 0c5bf3e..1a4e850 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -7388,8 +7388,9 @@ updateDeliveryReceiptControls(); if (!navigator.onLine) await showOfflineStatus(); window.addEventListener('offline', showOfflineStatus); - window.addEventListener('online', reconnectOutboxes); - workspaceLifecycle.replayOnline(reconnectOutboxes); + const reconnectAfterOnline = () => setTimeout(reconnectOutboxes, 500); + window.addEventListener('online', reconnectAfterOnline); + workspaceLifecycle.replayOnline(reconnectAfterOnline); qs('#refresh').addEventListener('click', load); qs('#plan-today').addEventListener('click', event => openPlanToday(event.currentTarget)); diff --git a/frontend/service-worker.js b/frontend/service-worker.js index 69bdb74..bbdeb68 100644 --- a/frontend/service-worker.js +++ b/frontend/service-worker.js @@ -383,6 +383,17 @@ async function warmOptionalFeature(cache, staleCacheNames, asset) { await cache.add(asset); } +async function cachedOptionalFeature(request) { + const cached = await caches.match(request); + if (cached) return cached; + const response = await fetch(request); + if (response.ok) { + const cache = await caches.open(CACHE); + await cache.put(request, response.clone()); + } + return response; +} + self.addEventListener('activate', event => { event.waitUntil((async () => { const keys = await caches.keys(); @@ -635,6 +646,6 @@ self.addEventListener('fetch', event => { return; } if (url.origin === self.location.origin && OPTIONAL_FEATURES.includes(url.pathname)) { - event.respondWith(caches.match(request).then(cached => cached || fetch(request))); + event.respondWith(cachedOptionalFeature(request)); } }); diff --git a/tests/test_reconnect_outboxes.py b/tests/test_reconnect_outboxes.py index 28b91ab..01485dc 100644 --- a/tests/test_reconnect_outboxes.py +++ b/tests/test_reconnect_outboxes.py @@ -48,4 +48,5 @@ def test_dashboard_uses_reconnect_flush_after_live_identity_refresh(): assert '' in index assert "const reconnectOutboxes = createReconnectOutboxes({" in dashboard assert "restoreIdentity: login => { activeFlushLogin = login; confirmedOwnerLogin = login; }" in dashboard - assert "window.addEventListener('online', reconnectOutboxes);" in dashboard + assert "window.addEventListener('online', reconnectAfterOnline);" in dashboard + assert "setTimeout(reconnectOutboxes, 500)" in dashboard diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py index 628e45d..0d22430 100644 --- a/tests/test_service_worker.py +++ b/tests/test_service_worker.py @@ -1108,6 +1108,25 @@ def test_offline_activation_migrates_cached_optional_feature_before_deleting_old assert state["claimed"] is True +def test_successful_optional_feature_fetch_is_cached_for_offline_reopen(): + result = run_worker_scenario( + """ + const asset = '/dashboard/feature-today-timer-test.js'; + context.self.__testOptionalFeatures.push(asset); + const response = await dispatch('fetch', { + method: 'GET', mode: 'cors', + url: 'https://forge.example/dashboard/feature-today-timer-test.js', + }); + process.stdout.write(JSON.stringify({ body: await response.text(), state })); +""" + ) + + assert result["body"] == "network" + assert result["state"]["puts"] == [ + "https://forge.example/dashboard/feature-today-timer-test.js" + ] + + def test_activate_warms_optional_features_without_blocking_siblings_or_claim(): result = run_worker_scenario( """