diff --git a/.changeset/lazy-load-timeout.md b/.changeset/lazy-load-timeout.md new file mode 100644 index 0000000..b64384c --- /dev/null +++ b/.changeset/lazy-load-timeout.md @@ -0,0 +1,7 @@ +--- +"@geajs/core": patch +--- + +### @geajs/core (patch) + +- **Lazy component load timeout**: `resolveLazy` now accepts a `timeout` parameter (default `10000`ms). Each load attempt is raced against the timeout, preventing the router from hanging indefinitely on a stalled network request. A timeout counts as a failure and triggers the existing retry logic with exponential backoff. diff --git a/packages/gea/src/lib/router/lazy.ts b/packages/gea/src/lib/router/lazy.ts index da67b28..e82c0e5 100644 --- a/packages/gea/src/lib/router/lazy.ts +++ b/packages/gea/src/lib/router/lazy.ts @@ -1,15 +1,29 @@ -export async function resolveLazy(loader: () => Promise, retries = 3, delay = 1000): Promise { +export async function resolveLazy( + loader: () => Promise, + retries = 3, + delay = 1000, + timeout = 10000, +): Promise { let lastError: unknown for (let attempt = 0; attempt <= retries; attempt++) { + let timeoutId: ReturnType | undefined try { - const mod = await loader() + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout( + () => reject(new Error(`[gea] Lazy component load timed out after ${timeout}ms`)), + timeout, + ) + }) + const mod = await Promise.race([Promise.resolve().then(loader), timeoutPromise]) return mod && typeof mod === 'object' && 'default' in mod ? mod.default : mod } catch (err) { lastError = err if (attempt < retries) { await new Promise((resolve) => setTimeout(resolve, delay * 2 ** attempt)) } + } finally { + clearTimeout(timeoutId) } }