Skip to content

Commit cb92a5b

Browse files
aboseclaude
andcommitted
Add web.phcode.dev trusted origin and cache version purge
Adds https://web.phcode.dev to the trusted origins allow list so that live preview can be embedded from the new phoenix domain. Everything we serve is held in a stale while revalidate service worker cache for offline use. So on the first load after a deploy, an already cached browser serves the old trusted origins list and wrongly rejects a newly added domain, leaving the user with an alert and a dead live preview until they reload. To fix this, index.html now carries an INDEX_CACHE_VERSION number. On a version change we purge all caches and reload before loading anything else, so the whole site is refetched. index.html itself is now served network first(falling back to cache when offline), as serving it stale would mean never seeing the version change. Bump INDEX_CACHE_VERSION on any deploy that must not be served stale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Kr57Ec2cFEkCxXEr4J1x5
1 parent c5c22a2 commit cb92a5b

3 files changed

Lines changed: 87 additions & 3 deletions

File tree

docs/index.html

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,63 @@
33
<head>
44
<meta charset="UTF-8">
55
<title>Live Preview Server Connector</title>
6-
<script src="virtual-server-loader.js" type="module"></script>
6+
<script>
7+
// Bump this on every deploy that must not be served from an old cache- Eg. when a new phoenix
8+
// domain is added to trustedOrigins.js . Everything we serve is held in a stale while revalidate
9+
// service worker cache for offline use, so without this, an already cached browser will serve the
10+
// old files on the first load after a deploy. This page itself is served network first
11+
// (see _isIndexPage in virtual-server-main.js) so that we always see the latest version number here.
12+
const INDEX_CACHE_VERSION = "2.0";
13+
const CACHE_VERSION_KEY = "indexCacheVersion";
14+
15+
function _loadLivePreviewServer() {
16+
const script = document.createElement('script');
17+
script.type = 'module';
18+
script.src = 'virtual-server-loader.js';
19+
document.head.appendChild(script);
20+
}
21+
22+
async function _purgeCaches() {
23+
const cacheNames = await caches.keys();
24+
await Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)));
25+
console.log("live preview server: purged caches on version change", cacheNames);
26+
}
27+
28+
function _boot() {
29+
let storedVersion, isFirstEverLoad;
30+
try {
31+
storedVersion = localStorage.getItem(CACHE_VERSION_KEY);
32+
// `loadedTwice` is set by virtual-server-loader.js after the service worker first takes
33+
// control. Without it, this browser has never loaded us and so has nothing cached to purge.
34+
isFirstEverLoad = !localStorage.getItem("loadedTwice");
35+
if(storedVersion !== INDEX_CACHE_VERSION){
36+
// write the new version before purging so that we never end up in a reload loop
37+
// if the purge or reload below fails for any reason.
38+
localStorage.setItem(CACHE_VERSION_KEY, INDEX_CACHE_VERSION);
39+
}
40+
} catch (e) {
41+
// storage may be unavailable in some privacy modes. carry on with what we have.
42+
console.error("live preview server: could not read cache version.", e);
43+
_loadLivePreviewServer();
44+
return;
45+
}
46+
if(storedVersion === INDEX_CACHE_VERSION || isFirstEverLoad || !window.caches){
47+
_loadLivePreviewServer();
48+
return;
49+
}
50+
console.log(`live preview server: cache version changed ${storedVersion} -> ${INDEX_CACHE_VERSION},`
51+
+ " purging caches and reloading.");
52+
_purgeCaches()
53+
.then(() => {
54+
location.reload();
55+
})
56+
.catch((e) => {
57+
console.error("live preview server: could not purge caches.", e);
58+
_loadLivePreviewServer();
59+
});
60+
}
61+
_boot();
62+
</script>
763
<!--
864
This page should only be loaded once per phcode window and not reloaded for every live preview change.
965
This server iframe should be reused. Note that different phcode window may still load this page
@@ -13,4 +69,4 @@
1369
<body>
1470
Serving Live Preview...
1571
</body>
16-
</html>
72+
</html>

docs/trustedOrigins.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ export const TRUSTED_ORIGINS = {
99
'https://phcode.dev': true,
1010
'https://dev.phcode.dev': true,
1111
'https://staging.phcode.dev': true,
12-
'https://create.phcode.dev': true
12+
'https://create.phcode.dev': true,
13+
'https://web.phcode.dev': true
1314
};

docs/virtual-server-main.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ workbox.setConfig({debug: _debugSWCacheLogs});
4343
const Route = workbox.routing.Route;
4444
// other strategies include CacheFirst, NetworkFirst Etc..
4545
const StaleWhileRevalidate = workbox.strategies.StaleWhileRevalidate;
46+
const NetworkFirst = workbox.strategies.NetworkFirst;
4647
const ExpirationPlugin = workbox.expiration.ExpirationPlugin;
4748
const DAYS_30_IN_SEC = 60 * 60 * 24 * 30;
4849
const CACHE_NAME_EVERYTHING = "everything";
@@ -85,6 +86,11 @@ function _isVirtualServing(url) {
8586
return url.startsWith(virtualServerBaseURL);
8687
}
8788

89+
function _isIndexPage(url) {
90+
const urlWithoutParams = url.split("?")[0].split("#")[0];
91+
return urlWithoutParams === baseURL || urlWithoutParams === `${baseURL}index.html`;
92+
}
93+
8894
function _shouldVirtualServe(request) {
8995
return _isVirtualServing(request.url.href);
9096
}
@@ -163,6 +169,26 @@ function _belongsToEverythingCache(request) {
163169
return false;
164170
}
165171

172+
// index.html holds the cache version number that we use to purge stale caches after a deploy. So it is
173+
// served network first, falling back to cache when offline. If we served it stale like everything else,
174+
// we would never see the version change and would keep serving stale files from cache. See index.html.
175+
const indexPageRoute = new Route(({ request }) => {
176+
return (request.method === 'GET'
177+
&& _isIndexPage(request.url) && _belongsToEverythingCache(request));
178+
}, new NetworkFirst({
179+
cacheName: CACHE_NAME_EVERYTHING,
180+
networkTimeoutSeconds: 5, // on slow networks, fall back to the cached page instead of hanging
181+
plugins: [
182+
new ExpirationPlugin({
183+
maxAgeSeconds: DAYS_30_IN_SEC,
184+
purgeOnQuotaError: true
185+
})
186+
],
187+
matchOptions: {
188+
ignoreSearch: true // ignore query string params in cache so that parentOrigin is not part of the key
189+
}
190+
}));
191+
166192
// handle all document
167193
const allCachedRoutes = new Route(({ request }) => {
168194
return (request.method === 'GET'
@@ -193,6 +219,7 @@ const externalCachedRoutes = new Route(({ request }) => {
193219
]
194220
}));
195221

222+
workbox.routing.registerRoute(indexPageRoute); // this should be before allCachedRoutes to take effect
196223
workbox.routing.registerRoute(allCachedRoutes);
197224
workbox.routing.registerRoute(externalCachedRoutes);
198225

0 commit comments

Comments
 (0)