UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

112 lines (102 loc) 4.45 kB
// adds vite-ignore to avoid warning logs in vite: export const dynamicImport = (path: string) => { if (process.env.TAMAGUI_TARGET === 'native') { // import causes an "invalid expression" bug in react native // TODO make this work, probably needs to fetch + eval? console.info(`dynamicImport TODO`, path) // return require(path) return Promise.resolve(undefined) } // retry a transient chunk-fetch failure in place before giving up, then fall // back to one's debounced reload. the in-place retry matters because a single // rejection permanently poisons callers that memoize the loader result — most // notably React.lazy, which caches the FIRST settled value (rejection // included) and re-throws it forever without re-invoking the loader. the retry // is always cheap; only the reload honors the ONE_SKEW_PROTECTION opt-out. return loadWithRetry( () => import( /* @vite-ignore */ path ), { onChunkErrorExhausted: () => process.env.ONE_SKEW_PROTECTION !== 'false' ? handleSkewError() : false, } ) } const CHUNK_ERROR_PATTERNS = [ 'Failed to fetch dynamically imported module', // chrome 'error loading dynamically imported module', // firefox 'Importing a module script failed', // safari ] export function isChunkLoadError(err: unknown): boolean { const msg = err instanceof Error ? err.message : String(err) return CHUNK_ERROR_PATTERNS.some((p) => msg.includes(p)) } // debounced one-time reload to recover from a deploy skew or a poisoned dev // module graph. returns whether it actually scheduled a reload (false when // debounced away within the cooldown, or on the server) so callers like // loadWithRetry can decide whether to keep waiting for the teardown or surface // the underlying error. export function handleSkewError(): boolean { if (typeof window === 'undefined') return false const key = '__one_skew_reload' const last = sessionStorage.getItem(key) if (!last || Date.now() - Number(last) > 10_000) { sessionStorage.setItem(key, String(Date.now())) window.location.reload() return true } return false } // extra attempts after the first before giving up. a transient chunk fetch // (ECONNRESET / dev-server 5xx / timeout under load) usually recovers on the // next try when the failure is the top-level chunk. export const CHUNK_RETRY_ATTEMPTS = 3 // linear backoff between chunk retries: ride out a brief hiccup, not a long // outage. export const CHUNK_RETRY_DELAY_MS = 500 const realDelay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)) // wraps a dynamic-import loader so a transient rejection is retried with backoff // before it settles, instead of permanently poisoning a caller that memoizes the // result (e.g. React.lazy — see dynamicImport above, and lazyWithRetry). when // retries are exhausted on a chunk-load error, recover via the injected reload // (defaults to one's debounced handleSkewError); the reload tears the page down, // so the returned promise is left pending rather than rejecting into a surviving // tree. `delay` + `onChunkErrorExhausted` are injectable so this is unit-testable // without real timers or a real location.reload. export async function loadWithRetry<T>( loader: () => Promise<T>, options: { attempts?: number delayMs?: number delay?: (ms: number) => Promise<void> onChunkErrorExhausted?: () => boolean } = {} ): Promise<T> { const attempts = options.attempts ?? CHUNK_RETRY_ATTEMPTS const delayMs = options.delayMs ?? CHUNK_RETRY_DELAY_MS const delay = options.delay ?? realDelay const recover = options.onChunkErrorExhausted ?? handleSkewError const attempt = async (retriesLeft: number): Promise<T> => { try { return await loader() } catch (err) { if (retriesLeft > 0) { await delay(delayMs) return attempt(retriesLeft - 1) } // out of retries: a chunk-load failure is a dev module-graph poison or a // prod deploy skew — recover via the debounced reload. the reload tears the // page down, so leave this promise pending (resolving/rejecting would // flash a broken tree). if the reload was debounced away, surface the real // error so it isn't swallowed. if (isChunkLoadError(err) && recover()) { return new Promise<T>(() => {}) } throw err } } return attempt(attempts) }