Merge remote-tracking branch 'origin/development' into regions/card-action
This commit is contained in:
commit
85c27e94f7
6 changed files with 74 additions and 25 deletions
|
|
@ -660,6 +660,26 @@ function assertNoDuplicateTabKeys(navModel) {
|
|||
return dupes
|
||||
}
|
||||
|
||||
// ★ FRESH BUNDLES AFTER A DEPLOY, AT THE SAME URLs (2026-10-02). A host that serves the tree with Last-Modified and no
|
||||
// Cache-Control (the DX Life landing does) lets a browser run its cached copies for hours: measured, a typed-URL revisit
|
||||
// after a deploy requested NONE of the gallery, the manifest or the bundles, so a returning reader ran the old files, or
|
||||
// an old manifest against a new bundle. A version query cannot fix it here: loadScriptOnce keys its registry by the full
|
||||
// href, so X.js?v= beside a composite's plain X.js would run the bundle twice. So the manifest is always revalidated, and
|
||||
// when its fingerprint (every bundle's integrity) differs from this reader's last visit to this tree, every listed bundle
|
||||
// gets ONE conditional request before any tab mounts: a 304 refreshes the cached copy, and a 200 is cancelled at its
|
||||
// headers, which drops the stale entry. The plain <script> that follows (ours, or a composite's for its children) then
|
||||
// runs the new file. An ordinary visit costs nothing extra.
|
||||
const TREE_FP_KEY = 'dxm.treeFingerprint@'
|
||||
function treeFingerprint(manifest) {
|
||||
const bundles = (manifest && Array.isArray(manifest.bundles)) ? manifest.bundles : []
|
||||
const s = bundles.map(b => (b && b.bundle) + ':' + ((b && b.integrity) || '')).join('|')
|
||||
let h = 2166136261 // FNV-1a: a short, stable key, not a security hash
|
||||
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) }
|
||||
return (h >>> 0).toString(36) + '.' + s.length
|
||||
}
|
||||
function readTreeFp(root) { try { return localStorage.getItem(TREE_FP_KEY + root) } catch { return null } }
|
||||
function writeTreeFp(root, fp) { try { localStorage.setItem(TREE_FP_KEY + root, fp) } catch { /* private mode: revalidate next time */ } }
|
||||
|
||||
// Turn a bundle id into a human label: "all-people-groups" → "All People Groups". Small, local and
|
||||
// dependency-free; a bundle may override it by shipping a profile `title` in the manifest.
|
||||
function titleize(id) {
|
||||
|
|
@ -1324,17 +1344,24 @@ export default defineComponent({
|
|||
const devFallback = '/public/' + __DOXA_TREE_NAME__ + '/manifest.json'
|
||||
if (!candidates.some(c => c.url === devFallback)) candidates.push({ url: devFallback, root: dirOf(devFallback) })
|
||||
const tried = []
|
||||
// Every mount waits for this gate (see FRESH BUNDLES above): it opens once the manifest's bundles are known
|
||||
// to be current, when no manifest can be read (fail-soft, as before), or after 8 s whatever happens.
|
||||
let open
|
||||
this._freshGate = new Promise(res => { open = res })
|
||||
setTimeout(open, 8000)
|
||||
const attempt = (i) => {
|
||||
if (i >= candidates.length) {
|
||||
// Every candidate failed. Record it on the tabs so the PANE says so — see mountTab().
|
||||
this.manifestError = 'manifest.json not found (tried ' + tried.join(', ') + ')'
|
||||
console.error('[doxa-maps-spa] ' + this.manifestError +
|
||||
' — every tab except Big Picture is composed from it, so their panes cannot mount.')
|
||||
open()
|
||||
return
|
||||
}
|
||||
const { url, root: treeRoot } = candidates[i]
|
||||
tried.push(url)
|
||||
return fetch(url, { credentials: 'same-origin' })
|
||||
// no-cache: always revalidated (a 304 when unchanged), so the manifest is never a stale copy.
|
||||
return fetch(url, { credentials: 'same-origin', cache: 'no-cache' })
|
||||
.then(r => { if (!r || !r.ok) throw new Error(url + ' → HTTP ' + (r ? r.status : '?')); return r.json() })
|
||||
.then(m => {
|
||||
if (!m) throw new Error(url + ' → empty')
|
||||
|
|
@ -1344,11 +1371,32 @@ export default defineComponent({
|
|||
// resolves '../' and rooted forms alike, so a caller-supplied treeBase, a built-tree
|
||||
// path and the dev fallback all normalise here.
|
||||
this.manifestBase = treeRoot || ''
|
||||
this.revalidateBundles(m, treeRoot || '').then(open, open)
|
||||
this.applyManifest(m)
|
||||
})
|
||||
.catch(() => attempt(i + 1))
|
||||
}
|
||||
try { attempt(0) } catch { /* fetch unavailable (non-browser): same fail-soft path */ }
|
||||
try { attempt(0) } catch { open() /* fetch unavailable (non-browser): same fail-soft path */ }
|
||||
},
|
||||
// After a deploy (the manifest's fingerprint changed since this reader's last visit to this tree), ONE conditional
|
||||
// request per listed bundle, so no stale cached copy is run (see FRESH BUNDLES). Never rejects.
|
||||
revalidateBundles(m, root) {
|
||||
const fp = treeFingerprint(m)
|
||||
const prev = readTreeFp(root)
|
||||
if (prev === fp) return Promise.resolve()
|
||||
const urls = [...new Set(((m && m.bundles) || []).map(b => (b && b.scriptUrl) ? this.resolve(b.scriptUrl) : '').filter(Boolean))]
|
||||
// Only a copy this browser already HOLDS can be stale, so the cache is asked first (only-if-cached never touches
|
||||
// the network) and only those get the conditional request: a first visit with an empty cache costs nothing.
|
||||
// only-if-cached needs a same-origin request; across origins, only a reader who has been here before is checked.
|
||||
const sameOrigin = (u) => { try { return new URL(u, location.href).origin === location.origin } catch { return false } }
|
||||
const held = (u) => (sameOrigin(u)
|
||||
? fetch(u, { mode: 'same-origin', credentials: 'same-origin', cache: 'only-if-cached' })
|
||||
.then(r => { if (r && r.body && r.body.cancel) r.body.cancel(); return !!(r && r.ok) }, () => false)
|
||||
: Promise.resolve(prev !== null))
|
||||
const revalidate = (u) => fetch(u, { credentials: 'same-origin', cache: 'no-cache' })
|
||||
.then(r => { if (r && r.status === 200 && r.body && r.body.cancel) return r.body.cancel() })
|
||||
const one = (u) => held(u).then(h => (h ? revalidate(u) : null)).catch(() => {})
|
||||
return Promise.all(urls.map(one)).then(() => writeTreeFp(root, fp))
|
||||
},
|
||||
// Complete the curated tabs from the manifest, drop any whose bundle this tree does not ship,
|
||||
// then append the discovered Experimental tabs — and finally (re)drive the active tab's mount,
|
||||
|
|
@ -1592,7 +1640,8 @@ export default defineComponent({
|
|||
// Gate the mount on the TAG being registered, NOT on the script's onload — onload fires even
|
||||
// when a dev ES module then fails to execute (see loadScriptOnce), which would hand us an inert
|
||||
// HTMLUnknownElement and an empty pane cheerfully reporting 'ready'.
|
||||
loadScriptOnce(jsUrl).then(() => whenTagDefined(t.tag)).then(() => {
|
||||
// …and on the freshness gate first, so a deploy's new bundle is the copy that runs (see FRESH BUNDLES).
|
||||
Promise.resolve(this._freshGate).then(() => loadScriptOnce(jsUrl)).then(() => whenTagDefined(t.tag)).then(() => {
|
||||
// Everything below is per-nested-plugin machinery; a throw here (a nested element that blows
|
||||
// up in its connectedCallback, a serialise failure) must NOT crash the SPA — it lands in THIS
|
||||
// tab's error pane. Hence the try/catch on top of the .catch() that handles a failed load.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-6LiR6vjB5GrQ6TfcAVuCU0pon7+MTANx5bWgB0a9RiWqYqBvFZ1DS9XX7/hg07iO" crossorigin="anonymous"></script>
|
||||
<script src="../../doxa-maps-spa.js" integrity="sha384-D/MCvAZhvdZ4CaVcpxKTxFLdT9GEuHidcKwBImnHSQfvUJg6mkAae/Y5JeUQYUjc" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <doxa-maps-spa> custom element. -->
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-6LiR6vjB5GrQ6TfcAVuCU0pon7+MTANx5bWgB0a9RiWqYqBvFZ1DS9XX7/hg07iO" crossorigin="anonymous"></script>
|
||||
<script src="../doxa-maps-spa.js" integrity="sha384-D/MCvAZhvdZ4CaVcpxKTxFLdT9GEuHidcKwBImnHSQfvUJg6mkAae/Y5JeUQYUjc" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@
|
|||
"bundle": "doxa-maps-spa",
|
||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
||||
"integrity": "sha384-6LiR6vjB5GrQ6TfcAVuCU0pon7+MTANx5bWgB0a9RiWqYqBvFZ1DS9XX7/hg07iO",
|
||||
"integrity": "sha384-D/MCvAZhvdZ4CaVcpxKTxFLdT9GEuHidcKwBImnHSQfvUJg6mkAae/Y5JeUQYUjc",
|
||||
"elements": [
|
||||
"doxa-maps-spa"
|
||||
],
|
||||
|
|
@ -439,7 +439,7 @@
|
|||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-6LiR6vjB5GrQ6TfcAVuCU0pon7+MTANx5bWgB0a9RiWqYqBvFZ1DS9XX7/hg07iO\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-D/MCvAZhvdZ4CaVcpxKTxFLdT9GEuHidcKwBImnHSQfvUJg6mkAae/Y5JeUQYUjc\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -409,7 +409,7 @@
|
|||
"bundle": "doxa-maps-spa",
|
||||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"output": "dx-maps/doxa-maps-spa/doxa-maps-spa.js",
|
||||
"integrity": "sha384-6LiR6vjB5GrQ6TfcAVuCU0pon7+MTANx5bWgB0a9RiWqYqBvFZ1DS9XX7/hg07iO",
|
||||
"integrity": "sha384-D/MCvAZhvdZ4CaVcpxKTxFLdT9GEuHidcKwBImnHSQfvUJg6mkAae/Y5JeUQYUjc",
|
||||
"elements": [
|
||||
"doxa-maps-spa"
|
||||
],
|
||||
|
|
@ -439,7 +439,7 @@
|
|||
"scriptUrl": "./doxa-maps-spa/doxa-maps-spa.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./doxa-maps-spa/doxa-maps-spa/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-6LiR6vjB5GrQ6TfcAVuCU0pon7+MTANx5bWgB0a9RiWqYqBvFZ1DS9XX7/hg07iO\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./doxa-maps-spa/doxa-maps-spa.js\" integrity=\"sha384-D/MCvAZhvdZ4CaVcpxKTxFLdT9GEuHidcKwBImnHSQfvUJg6mkAae/Y5JeUQYUjc\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<doxa-maps-spa profile-config='{\"profile\":\"doxa-maps-spa\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></doxa-maps-spa>"
|
||||
},
|
||||
"instances": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue