From ebe85e4ecef65fa55b9a4d6a92e35f3de167de64 Mon Sep 17 00:00:00 2001 From: Serdar Atalay Date: Mon, 30 Mar 2026 17:29:21 +0300 Subject: [PATCH 1/2] fix(core): add per-attempt timeout to resolveLazy Prevents the router from hanging indefinitely when a lazy component chunk stalls on a slow or broken network. Each load attempt is now raced against a configurable timeout (default 10 000 ms); a timeout is treated as a failure and feeds into the existing exponential-backoff retry logic. --- .changeset/lazy-load-timeout.md | 7 +++++++ packages/gea/src/lib/router/lazy.ts | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .changeset/lazy-load-timeout.md 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..52f5a3c 100644 --- a/packages/gea/src/lib/router/lazy.ts +++ b/packages/gea/src/lib/router/lazy.ts @@ -1,9 +1,22 @@ -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++) { try { - const mod = await loader() + let timeoutId: ReturnType + 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([loader(), timeoutPromise]) + clearTimeout(timeoutId!) return mod && typeof mod === 'object' && 'default' in mod ? mod.default : mod } catch (err) { lastError = err From f36e0ccf5ad8aea448082a8e308c63f7cf33bd5c Mon Sep 17 00:00:00 2001 From: Serdar Atalay Date: Mon, 30 Mar 2026 17:42:51 +0300 Subject: [PATCH 2/2] fix(core): clear timeout in finally block, wrap loader in Promise.resolve --- packages/gea/src/lib/router/lazy.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/gea/src/lib/router/lazy.ts b/packages/gea/src/lib/router/lazy.ts index 52f5a3c..e82c0e5 100644 --- a/packages/gea/src/lib/router/lazy.ts +++ b/packages/gea/src/lib/router/lazy.ts @@ -7,22 +7,23 @@ export async function resolveLazy( let lastError: unknown for (let attempt = 0; attempt <= retries; attempt++) { + let timeoutId: ReturnType | undefined try { - let timeoutId: ReturnType 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([loader(), timeoutPromise]) - clearTimeout(timeoutId!) + 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) } }