Merge pull request 'Make PWA updates resilient with progressive feature caching' (#620) from timmy/619-progressive-feature-caching into main
All checks were successful
CI / lint (push) Successful in 1m30s
CI / build-release (push) Successful in 6s
CI / release-candidate (push) Successful in 6s

This commit is contained in:
timmy 2026-08-12 04:26:38 +00:00
commit 1f0e9fb59d
4 changed files with 83 additions and 7 deletions

View File

@ -73,6 +73,8 @@ const SHELL = [
BASE + 'static/push-notifications.js',
BASE + 'static/background-issue-sync.js',
];
const OPTIONAL_FEATURES = [
];
async function fetchNavigation(request) {
const controller = new AbortController();
@ -262,10 +264,16 @@ self.addEventListener('install', event => {
});
self.addEventListener('activate', event => {
event.waitUntil(caches.keys().then(keys => Promise.all(
keys.filter(key => key.startsWith('stackchain-dashboard-') && key !== CACHE)
.map(key => caches.delete(key))
)).then(() => self.clients.claim()));
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys.filter(key => key.startsWith('stackchain-dashboard-') && key !== CACHE)
.map(key => caches.delete(key))
);
const cache = await caches.open(CACHE);
await Promise.allSettled(OPTIONAL_FEATURES.map(asset => cache.add(asset)));
await self.clients.claim();
})());
});
self.addEventListener('sync', event => {
@ -433,5 +441,9 @@ self.addEventListener('fetch', event => {
}
if (url.origin === self.location.origin && SHELL.includes(url.pathname)) {
event.respondWith(caches.match(request).then(cached => cached || fetch(request)));
return;
}
if (url.origin === self.location.origin && OPTIONAL_FEATURES.includes(url.pathname)) {
event.respondWith(caches.match(request).then(cached => cached || fetch(request)));
}
});

View File

@ -114,11 +114,20 @@ def build_frontend(frontend_dir: Path) -> FrontendBuild:
for source in sources:
if source != WORKER_RUNTIME_SOURCE:
worker = worker.replace(f" BASE + '{source}',\n", "")
eager_feature = feature_bundles["today-timer"]
optional_features = {
name: bundle for name, bundle in feature_bundles.items() if name != "today-timer"
}
worker = worker.replace(
" BASE + 'static/dashboard.css',\n",
" BASE + 'static/dashboard.css',\n"
+ f" BASE + '{core.runtime_name}',\n"
+ "".join(f" BASE + '{bundle.runtime_name}',\n" for bundle in feature_bundles.values()),
+ f" BASE + '{eager_feature.runtime_name}',\n",
)
worker = worker.replace(
"const OPTIONAL_FEATURES = [\n",
"const OPTIONAL_FEATURES = [\n"
+ "".join(f" BASE + '{bundle.runtime_name}',\n" for bundle in optional_features.values()),
)
worker = CACHE_DECLARATION.sub(
"const CACHE = 'stackchain-dashboard-shell-BUILD';", worker, count=1

View File

@ -77,6 +77,18 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
assert f'name="stackchain-feature-security-center" content="{security_center.runtime_name}"' in first.dashboard_html
assert f"BASE + '{security_center.runtime_name}'" in first.service_worker_source
shell_block, optional_block = first.service_worker_source.split(
"const OPTIONAL_FEATURES = [", 1
)
optional_block = optional_block.split("];", 1)[0]
assert f"BASE + '{first.feature_bundles['today-timer'].runtime_name}'" in shell_block
for name, bundle in first.feature_bundles.items():
if name == "today-timer":
assert f"BASE + '{bundle.runtime_name}'" not in optional_block
else:
assert f"BASE + '{bundle.runtime_name}'" not in shell_block
assert f"BASE + '{bundle.runtime_name}'" in optional_block
changed_frontend = tmp_path / "frontend"
shutil.copytree(FRONTEND, changed_frontend)
capture_source = changed_frontend / "create-issue-sheet.js"

View File

@ -13,7 +13,7 @@ def run_worker_scenario(scenario: str) -> dict:
const fs = require('fs');
const vm = require('vm');
const listeners = {{}};
const state = {{ added: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
const state = {{ added: [], individuallyAdded: [], failedAdds: [], deleted: [], deletedDatabases: [], claimed: false, skipped: false, fetches: [], puts: [], backgroundFlushes: 0, backgroundResumes: 0, outboxPurges: 0, outboxLifecycle: [], notifications: [], focused: [], opened: [], failFetch: false, stallFetch: false, lateFetch: false, fetchAborted: false, fetchStatus: 200, fetchRedirected: false, cachedBody: null }};
const storedResponses = new Map();
storedResponses.set(
'https://forge.example/dashboard/__offline-session-lease',
@ -24,6 +24,10 @@ storedResponses.set(
);
const cache = {{
addAll: async urls => {{ state.added = urls; }},
add: async url => {{
if (state.failedAdds.includes(url)) throw new Error('optional asset unavailable');
state.individuallyAdded.push(url);
}},
match: async request => {{
const key = String(request.url || request);
if (storedResponses.has(key)) return storedResponses.get(key).clone();
@ -89,7 +93,7 @@ const context = {{
}},
}};
vm.createContext(context);
vm.runInContext(fs.readFileSync({json.dumps(str(WORKER))}, 'utf8') + '\\nself.__testFetchJson = fetchJson; self.__testWithSessionCsrf = withSessionCsrf;', context);
vm.runInContext(fs.readFileSync({json.dumps(str(WORKER))}, 'utf8') + '\\nself.__testFetchJson = fetchJson; self.__testWithSessionCsrf = withSessionCsrf; self.__testOptionalFeatures = OPTIONAL_FEATURES;', context);
async function dispatch(name, request) {{
let pending;
let response;
@ -746,6 +750,45 @@ def test_activate_deletes_only_stale_stackchain_caches():
assert result["deleted"] == ["stackchain-dashboard-old"]
def test_activate_warms_optional_features_without_blocking_siblings_or_claim():
result = run_worker_scenario(
"""
state.failedAdds = ['/dashboard/feature-security-center-test.js'];
context.self.__testOptionalFeatures.push(
'/dashboard/feature-issue-capture-test.js',
'/dashboard/feature-security-center-test.js',
'/dashboard/feature-pull-workflow-test.js',
);
await dispatch('activate');
process.stdout.write(JSON.stringify(state));
"""
)
assert result["claimed"] is True
assert result["individuallyAdded"] == [
"/dashboard/feature-issue-capture-test.js",
"/dashboard/feature-pull-workflow-test.js",
]
def test_warmed_optional_feature_is_served_from_cache_while_offline():
result = run_worker_scenario(
"""
context.self.__testOptionalFeatures.push('/dashboard/feature-issue-capture-test.js');
state.cachedBody = 'cached feature';
state.failFetch = true;
const response = await dispatch('fetch', {
method: 'GET', mode: 'cors',
url: 'https://forge.example/dashboard/feature-issue-capture-test.js',
});
process.stdout.write(JSON.stringify({ body: await response.text(), state }));
"""
)
assert result["body"] == "cached feature"
assert result["state"]["fetches"] == []
def test_offline_navigation_returns_cached_shell_for_share_target_url():
result = run_worker_scenario(
"""