mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(client): five loads that catch their own error recover from a deploy too
The PDF export on desktop and phone, the language switch, the holiday regions and the PDF's map image load a chunk on demand and catch the failure themselves. After a deploy the old page asks for chunk names the new build no longer has, so they showed their own error on every retry (or, for the language, silently stayed in the old one) instead of reloading onto the new build the way an uncaught chunk error does. importChunk reloads once for a missing chunk and still hands the caller its error.
This commit is contained in:
@@ -2,6 +2,7 @@ import { buildRouteOverlay, wrapSvg } from './tripMapSvg'
|
||||
import { hasWebGL } from '../../utils/webgl'
|
||||
import { normalizeStyleForProvider } from '../Map/glProviders'
|
||||
import type { TripOverviewDay } from '../Map/tripRouteGeometry'
|
||||
import { importChunk } from '../../utils/chunkReload'
|
||||
|
||||
/**
|
||||
* The trip's route over a real basemap, for the PDF (#1736).
|
||||
@@ -46,7 +47,7 @@ export async function renderTripMapImage(
|
||||
let container: HTMLDivElement | null = null
|
||||
let map: { remove: () => void } | null = null
|
||||
try {
|
||||
const gl = (await import('../Map/engines/maplibre')).default
|
||||
const gl = (await importChunk(() => import('../Map/engines/maplibre'))).default
|
||||
|
||||
container = document.createElement('div')
|
||||
// Off-screen rather than hidden: MapLibre needs a laid-out box with real
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useRoadtripSettings } from '../../hooks/useRoadtripSettings'
|
||||
import { useAuthStore } from '../../store/authStore'
|
||||
import { hasPersonalPlan } from '../PDF/pdfScope'
|
||||
import { importChunk } from '../../utils/chunkReload'
|
||||
|
||||
/**
|
||||
* What a GPX download can carry. Worded by what someone wants on their device
|
||||
@@ -98,7 +99,7 @@ export function TripExportModal({
|
||||
// sidebar, so every trip used to pay for it whether or not anyone
|
||||
// exported. A missing chunk lands in the catch and shows the same error
|
||||
// the export already had.
|
||||
const { downloadTripPDF } = await import('../PDF/TripPDF')
|
||||
const { downloadTripPDF } = await importChunk(() => import('../PDF/TripPDF'))
|
||||
await downloadTripPDF({ trip, days, places, assignments, categories, dayNotes: flatNotes, reservations, t, locale, timeFormat, distanceUnit, showServiceStops, onlyUserId: mine ? myId : undefined })
|
||||
onClose()
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { schoolHolidayCatalogRepo } from '../../repo/schoolHolidayCatalogRepo'
|
||||
import apiClient from '../../api/client'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { SCHOOL_HOLIDAY_COUNTRY_CONFIG } from '../../vacay/schoolHolidayCountries'
|
||||
import { importChunk } from '../../utils/chunkReload'
|
||||
|
||||
// Loads the subdivision (state/region) options for a holiday-calendar country.
|
||||
//
|
||||
@@ -23,7 +24,7 @@ export async function fetchRegionOptions(country: string): Promise<{ value: stri
|
||||
// that get a region picker at all. Deliberately inside the try — if the chunk
|
||||
// fails, the result is the same empty array as a failed request, rather than an
|
||||
// unhandled rejection in the two callers that only do .then(setRegions).
|
||||
const iso31662 = (await import('iso-3166-2')).default
|
||||
const iso31662 = (await importChunk(() => import('iso-3166-2'))).default
|
||||
|
||||
const opts = new Map<string, string>() // ISO code -> display name
|
||||
const sub = iso31662.country(country)?.sub || {}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sanitizeInlineHtml,
|
||||
} from '@trek/shared'
|
||||
import type { TranslationStrings } from '@trek/shared/i18n'
|
||||
import { importChunk } from '../utils/chunkReload'
|
||||
|
||||
export { SUPPORTED_LANGUAGES }
|
||||
|
||||
@@ -122,7 +123,7 @@ export function TranslationProvider({ children }: TranslationProviderProps) {
|
||||
if (!loader) return
|
||||
|
||||
let cancelled = false
|
||||
loader().then(mod => {
|
||||
importChunk(loader).then(mod => {
|
||||
if (!cancelled) setStrings(mod.default)
|
||||
}).catch(err => {
|
||||
// The locale chunk can be gone after a deploy. Keep the strings we have —
|
||||
|
||||
@@ -11,6 +11,7 @@ import { hasPersonalPlan } from '../../../../components/PDF/pdfScope'
|
||||
import { INNER_CLS, TileHeader } from './MTripSheetUi'
|
||||
import type { MTripSheetsProps } from '../MTripShell'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { importChunk } from '../../../../utils/chunkReload'
|
||||
|
||||
/**
|
||||
* Export sheet ('export', opened from the Mehr sheet): the desktop day-plan
|
||||
@@ -47,7 +48,7 @@ export default function MExportSheet({ planner, shell }: MTripSheetsProps) {
|
||||
setPdfBusy(mine ? 'mine' : 'all')
|
||||
try {
|
||||
// See DayPlanSidebarToolbar: loaded on demand, not with the trip.
|
||||
const { downloadTripPDF } = await import('../../../../components/PDF/TripPDF')
|
||||
const { downloadTripPDF } = await importChunk(() => import('../../../../components/PDF/TripPDF'))
|
||||
await downloadTripPDF({
|
||||
trip: planner.trip,
|
||||
days: planner.days,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isChunkLoadError, reloadOnceForChunk, clearChunkReloadMarker, reloadFresh, FRESH_LOAD_PARAM } from './chunkReload';
|
||||
import { isChunkLoadError, reloadOnceForChunk, clearChunkReloadMarker, reloadFresh, importChunk, FRESH_LOAD_PARAM } from './chunkReload';
|
||||
|
||||
const net = vi.hoisted(() => ({
|
||||
probeNow: vi.fn(async () => 'online' as 'online' | 'offline' | 'proxy-wall'),
|
||||
@@ -83,6 +83,18 @@ describe('reloadFresh', () => {
|
||||
describe('reloadOnceForChunk', () => {
|
||||
const stubReload = () => stubLocation().reload;
|
||||
|
||||
it('FE-UTIL-CHUNK-020: importChunk reloads once for a chunk the deploy removed, and still hands the caller the error', async () => {
|
||||
const reload = stubReload();
|
||||
const gone = new TypeError('Failed to fetch dynamically imported module: /assets/TripPDF-abc123.js');
|
||||
await expect(importChunk(() => Promise.reject(gone))).rejects.toBe(gone);
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Anything else is the caller's error alone.
|
||||
await expect(importChunk(() => Promise.reject(new Error('pdf layout failed')))).rejects.toThrow('pdf layout failed');
|
||||
expect(reload).toHaveBeenCalledTimes(1);
|
||||
await expect(importChunk(() => Promise.resolve({ ok: true }))).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('FE-UTIL-CHUNK-004: reloads the first time and refuses afterwards', () => {
|
||||
const reload = stubReload();
|
||||
expect(reloadOnceForChunk()).toBe(true);
|
||||
|
||||
@@ -116,3 +116,19 @@ export function clearChunkReloadMarker(): void {
|
||||
// Nothing to clear if storage is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dynamic import whose failure the caller catches. After a deploy the old
|
||||
* page asks for chunk names the new build no longer has; a caller that only
|
||||
* shows its own error would show it on every retry, so a missing chunk reloads
|
||||
* onto the current build once, the way an uncaught one already does. The
|
||||
* error is rethrown for the caller either way.
|
||||
*/
|
||||
export async function importChunk<T>(load: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await load()
|
||||
} catch (err) {
|
||||
if (isChunkLoadError(err)) reloadOnceForChunk()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user