The remaining runtime fetches retry, except the one where a miss is the plan
The follow-up dx-shepherd queued: wire retryFetch into the library fetches with no
failure handling, and skip any that handle failure deliberately. Four qualified, one did
not, and the distinction is the work.
WIRED, because each already handles an HTTP error and NOT a transport one:
usePosterPrints the poster HTML — checks .ok, so a 404 is deliberate; a network throw
went uncaught and the poster simply failed
useSharedMapToken the token service — its .catch degrades to '' by design, but the
result is MEMOISED one-fetch-per-URL-per-page, so a single blip meant
no token for the whole page and a tokenless Mapbox map
useCsvExport the bulk list — .ok checked, transport throw blanks the export
PeopleGroupDetail the detail API — .ok checked, transport throw blanks the panel
In every one the deliberate degradation is unchanged; it now simply survives a blip
first. retryFetch returns a non-ok RESPONSE untouched, so no .ok path moves.
NOT WIRED, and named in the gate rather than silently skipped: usePosterPrints' probe for
an OPTIONAL pre-rendered .webp. A miss is the EXPECTED path there — it falls through to
generating the poster — so retrying would delay the fallback three times over for an
asset that is legitimately absent. That is failure handling that means something, which
is exactly what the instruction said to leave alone.
The gate now covers all eight sites and carries the exemption as a COUNT for that one
file, so a second bare fetch appearing there still fails. Negative-tested: reverting any
wired site to bare fetch turns it red.
This commit is contained in:
parent
838df76c14
commit
6c0f1f3040
5 changed files with 22 additions and 7 deletions
|
|
@ -67,16 +67,27 @@ const realFetch = globalThis.fetch
|
|||
globalThis.fetch = realFetch
|
||||
|
||||
// ── and the shared data paths actually use it ────────────────────────────────────────────────────
|
||||
// ★ ONE SITE IS DELIBERATELY EXEMPT AND IS NAMED, not silently skipped. usePosterPrints probes for an
|
||||
// OPTIONAL pre-rendered .webp and falls through to generating the poster when it is absent — a miss is
|
||||
// the expected path, so retrying would delay the fallback for an asset that is legitimately not there.
|
||||
// Everything else in that file, and every other runtime request, must retry.
|
||||
const EXEMPT = new Map([['library/composables/usePosterPrints.js', 1]])
|
||||
for (const rel of [
|
||||
'library/composables/useMapLayers.js',
|
||||
'library/composables/useCountryOutline.js',
|
||||
'library/lists/peopleGroupList.js',
|
||||
'library/composables/usePrayerStatistics.js',
|
||||
'library/composables/usePosterPrints.js',
|
||||
'library/composables/useSharedMapToken.js',
|
||||
'library/composables/useCsvExport.js',
|
||||
'library/components/PeopleGroupDetail.vue',
|
||||
]) {
|
||||
const src = readFileSync(join(ROOT, rel), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
||||
if (/(?<![A-Za-z.])fetch\s*\(/.test(src)) {
|
||||
fails.push(`${rel} still calls bare fetch() for a runtime request — a transport blip there leaves the\n`
|
||||
+ ' feature silently absent. Use retryFetch().')
|
||||
const bare = (src.match(/(?<![A-Za-z.])fetch\s*\(/g) || []).length
|
||||
const allowed = EXEMPT.get(rel) || 0
|
||||
if (bare > allowed) {
|
||||
fails.push(`${rel} has ${bare} bare fetch() call(s) for a runtime request, ${allowed} allowed — a transport\n`
|
||||
+ ' blip there leaves the feature silently absent. Use retryFetch().')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { retryFetch } from '@map/composables/retryFetch.js'
|
||||
import { computed, ref, watch, inject } from 'vue';
|
||||
import { markPrayedLocally } from '../composables/usePrayedLocally.js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
|
@ -239,7 +240,7 @@ async function fetchDetail(slugVal) {
|
|||
// click and routed to the slow WP origin, making images load 10+ seconds.
|
||||
// Live counts (prayer / committed / adopted) come from /list which IS
|
||||
// cache-busted in DataSourceManager — so freshness stays intact.
|
||||
const res = await fetch(`${baseUrl}/api/people-groups/detail/${encodeURIComponent(slugVal)}?lang=${encodeURIComponent(locale.value)}`);
|
||||
const res = await retryFetch(`${baseUrl}/api/people-groups/detail/${encodeURIComponent(slugVal)}?lang=${encodeURIComponent(locale.value)}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
detailData.value = await res.json();
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
* The result is cached for the session; if the fetch fails we still export the loaded record
|
||||
* (24 fields) rather than producing nothing — degraded, never broken.
|
||||
*/
|
||||
import { retryFetch } from './retryFetch.js'
|
||||
import { getApiBaseUrl } from '@map/api/apiBaseUrl.js'
|
||||
import { inject } from 'vue'
|
||||
import sourcesConfig from '@map/api/sources.json'
|
||||
|
|
@ -145,7 +146,7 @@ export function useCsvExport({ rowStore, rows, selectionSet, filterSet, FACET_AC
|
|||
const bulk = src.endpoints?.bulk || '/api/people-groups/list'
|
||||
const lang = src.queryParams?.lang || 'en'
|
||||
const url = `${getApiBaseUrl()}${bulk}?fields=${fields.join(',')}&lang=${lang}`
|
||||
const res = await fetch(url, { cache: 'no-store' })
|
||||
const res = await retryFetch(url, { cache: 'no-store' })
|
||||
if (!res.ok) throw new Error(`export list HTTP ${res.status}`)
|
||||
const json = await res.json()
|
||||
// Only the groups of this page's people-group LIST (library/lists/) are exported.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
// is the mechanism the Driver asked for by name: "not supposed to open up a new
|
||||
// browser, but should open up the print dialog directly in the browser".
|
||||
|
||||
import { retryFetch } from './retryFetch.js'
|
||||
import { ref } from 'vue';
|
||||
import { openPrintFrame } from './printInPlace.js';
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ export function usePosterPrints(opts = {}) {
|
|||
// NO PRE-RENDERED IMAGE? GENERATE IT. The .webp posters are ~41MB across a tree and are
|
||||
// not shipped by default; the HTML is the SOURCE and is smaller than the image it makes.
|
||||
set(item, kind, 'render', 'generating the poster', 45);
|
||||
const htmlRes = await fetch(`${base}/posters/${id}.html`);
|
||||
const htmlRes = await retryFetch(`${base}/posters/${id}.html`);
|
||||
if (!htmlRes.ok) throw new Error(`poster not found (${htmlRes.status})`);
|
||||
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js');
|
||||
const blob = await rasterPoster(await htmlRes.text(), { scale: 2 });
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
* every tile request. It does not widen exposure — it stops a second element on the SAME page from
|
||||
* being unable to use what that page was already given. Nothing is persisted or sent anywhere.
|
||||
*/
|
||||
import { retryFetch } from './retryFetch.js'
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
|
||||
const KEY = '__doxaMapToken' // the value, on window, so every bundle sees one copy
|
||||
|
|
@ -131,7 +132,7 @@ export function fetchMapToken(url) {
|
|||
if (typeof window === 'undefined' || !/^https?:\/\//i.test(String(url || ''))) return Promise.resolve('')
|
||||
const cache = (window.__doxaMapTokenFetches = window.__doxaMapTokenFetches || {})
|
||||
if (cache[url]) return cache[url]
|
||||
const p = fetch(url)
|
||||
const p = retryFetch(url)
|
||||
.then((r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return (r.headers.get('content-type') || '').includes('json') ? r.json() : r.text() })
|
||||
.then((d) => {
|
||||
const tk = String((typeof d === 'string' ? d : d && (d.token || d.tk || d.accessToken)) || '').trim()
|
||||
|
|
|
|||
Loading…
Reference in a new issue