fix: add requestIdleCallback polyfill at app entry for iOS WebKit (Fixes #9231)#9358
fix: add requestIdleCallback polyfill at app entry for iOS WebKit (Fixes #9231)#9358rohanmaan07 wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughAdds a conditional polyfill for ChangesrequestIdleCallback Polyfill
Estimated code review effort: 1 (Trivial) | ~5 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR prevents iOS WebKit clients from crashing when code calls window.requestIdleCallback by installing a requestIdleCallback / cancelIdleCallback fallback at each app’s client entry point, addressing issue #9231 (error boundary triggered by a missing API on iOS).
Changes:
- Add a
requestIdleCallbackpolyfill (viasetTimeout) at app startup to avoid runtimeTypeErroron iOS WebKit. - Add a matching
cancelIdleCallbackfallback so scheduled idle work can be cancelled safely. - Apply the fix consistently across
web,admin, andspaceclient entry points.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| apps/web/app/entry.client.tsx | Installs a requestIdleCallback/cancelIdleCallback fallback before hydration to prevent iOS WebKit crashes. |
| apps/admin/app/entry.client.tsx | Adds the same startup polyfill in the admin client entry point. |
| apps/space/app/entry.client.tsx | Adds the same startup polyfill in the space client entry point. |
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | ||
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; | ||
| }; | ||
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | ||
| } |
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | ||
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; | ||
| }; | ||
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | ||
| } |
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | ||
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; | ||
| }; | ||
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | ||
| } |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/admin/app/entry.client.tsx`:
- Around line 11-23: The idle-callback fallback in the entry client is
duplicated and uses loose `any` types, so replace it with the shared typed
polyfill used by installIdleCallbackPolyfill in apps/web/core/lib/idle-task.ts.
Extract or reuse a common module for the requestIdleCallback/cancelIdleCallback
setup, and import it from the admin/space/web entry points so there is one
source of truth. Make sure the shared helper uses the existing
IdleRequestCallback and IdleRequestOptions types instead of any, and keep the
entry file limited to invoking that helper.
In `@apps/space/app/entry.client.tsx`:
- Around line 12-19: The requestIdleCallback fallback in entry.client.tsx
ignores the optional timeout argument, so callers passing options lose the
intended behavior. Update the fallback signature to accept both the callback and
options, and use the provided timeout when scheduling the setTimeout fallback,
matching the behavior in the sibling idle-task implementation. Keep the existing
cb wrapper and timeRemaining logic, but ensure options?.timeout is honored
instead of always using 1ms.
- Around line 11-22: The requestIdleCallback polyfill in entry.client.tsx is
duplicated across entry files and also overlaps with the existing
installIdleCallbackPolyfill utility in apps/web/core/lib/idle-task.ts. Replace
the inline block with a shared import so all entry points reuse the same
implementation, including the options.timeout behavior already supported there.
While updating the shared polyfill, remove the any casts by typing the callback
and cancel handle with IdleRequestCallback, IdleRequestOptions, and number to
keep strict typing intact.
In `@apps/web/app/entry.client.tsx`:
- Around line 16-23: The requestIdleCallback polyfill in entry.client.tsx is
using any-typed parameters and a hardcoded fallback that ignores the native
options timeout. Update the window.requestIdleCallback shim to use proper
requestIdleCallback-style types instead of cb: any/id: any/as any, and accept
the optional options parameter so a caller-provided timeout is preserved rather
than always forcing a 1ms delay.
- Around line 15-26: `entry.client.tsx` is duplicating the idle callback
polyfill that already exists in `installIdleCallbackPolyfill`. Remove the inline
`requestIdleCallback`/`cancelIdleCallback` shim and reuse the shared installer
from `idle-task.ts` (through the existing polyfills entrypoint), so there is a
single implementation that preserves `options.timeout` and the
`globalThis`-based behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ea4d3b15-03af-43d6-abbf-48be99ed702f
📒 Files selected for processing (3)
apps/admin/app/entry.client.tsxapps/space/app/entry.client.tsxapps/web/app/entry.client.tsx
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | ||
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; | ||
| }; | ||
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | ||
| } | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicated polyfill logic + loose typing (any).
This block is copy-pasted verbatim across apps/admin, apps/space, and apps/web entry files, and duplicates the already-existing installIdleCallbackPolyfill in apps/web/core/lib/idle-task.ts. Three independent copies of the same fallback logic will drift over time (e.g., one uses window, the other globalThis; timeout/timeRemaining constants could diverge). Also, cb: any and id: any violate the repo's strict-typing guideline.
Consider extracting this into a shared, typed polyfill module (e.g. a workspace:* package) and importing it from all three entry points, reusing the IdleRequestCallback/IdleRequestOptions types already established in apps/web/core/lib/idle-task.ts.
♻️ Suggested typed version
-if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") {
- window.requestIdleCallback = (cb: any) => {
- const start = Date.now();
- return setTimeout(() => {
- cb({
- didTimeout: false,
- timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
- });
- }, 1) as any;
- };
- window.cancelIdleCallback = (id: any) => clearTimeout(id);
-}
+if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") {
+ window.requestIdleCallback = (cb: IdleRequestCallback, options?: IdleRequestOptions): number => {
+ const start = Date.now();
+ return window.setTimeout(() => {
+ cb({
+ didTimeout: false,
+ timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
+ });
+ }, options?.timeout ?? 1);
+ };
+ window.cancelIdleCallback = (id: number) => window.clearTimeout(id);
+}As per coding guidelines, **/*.{ts,tsx}: "TypeScript strict mode enabled; all files must be typed."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | |
| window.requestIdleCallback = (cb: any) => { | |
| const start = Date.now(); | |
| return setTimeout(() => { | |
| cb({ | |
| didTimeout: false, | |
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | |
| }); | |
| }, 1) as any; | |
| }; | |
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | |
| } | |
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | |
| window.requestIdleCallback = (cb: IdleRequestCallback, options?: IdleRequestOptions): number => { | |
| const start = Date.now(); | |
| return window.setTimeout(() => { | |
| cb({ | |
| didTimeout: false, | |
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | |
| }); | |
| }, options?.timeout ?? 1); | |
| }; | |
| window.cancelIdleCallback = (id: number) => window.clearTimeout(id); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/admin/app/entry.client.tsx` around lines 11 - 23, The idle-callback
fallback in the entry client is duplicated and uses loose `any` types, so
replace it with the shared typed polyfill used by installIdleCallbackPolyfill in
apps/web/core/lib/idle-task.ts. Extract or reuse a common module for the
requestIdleCallback/cancelIdleCallback setup, and import it from the
admin/space/web entry points so there is one source of truth. Make sure the
shared helper uses the existing IdleRequestCallback and IdleRequestOptions types
instead of any, and keep the entry file limited to invoking that helper.
Source: Coding guidelines
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | ||
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; | ||
| }; | ||
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicated polyfill; consider a shared utility and stricter typing.
This same block is copy-pasted verbatim across admin/space/web entry files, and apps/web/core/lib/idle-task.ts already implements an equivalent installIdleCallbackPolyfill (with options.timeout support). Extracting a single shared implementation (e.g., a workspace package) avoids drift between the three copies and reuses the existing, more complete implementation. Also, cb: any / id: any / as any bypass strict typing — use IdleRequestCallback/IdleRequestOptions/number instead.
♻️ Suggested typed fix
-if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") {
- window.requestIdleCallback = (cb: any) => {
- const start = Date.now();
- return setTimeout(() => {
- cb({
- didTimeout: false,
- timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
- });
- }, 1) as any;
- };
- window.cancelIdleCallback = (id: any) => clearTimeout(id);
-}
+if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") {
+ window.requestIdleCallback = (cb: IdleRequestCallback, options?: IdleRequestOptions) => {
+ const start = Date.now();
+ return setTimeout(() => {
+ cb({
+ didTimeout: false,
+ timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
+ });
+ }, options?.timeout ?? 1) as unknown as number;
+ };
+ window.cancelIdleCallback = (id: number) => clearTimeout(id);
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | |
| window.requestIdleCallback = (cb: any) => { | |
| const start = Date.now(); | |
| return setTimeout(() => { | |
| cb({ | |
| didTimeout: false, | |
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | |
| }); | |
| }, 1) as any; | |
| }; | |
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | |
| } | |
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | |
| window.requestIdleCallback = (cb: IdleRequestCallback, options?: IdleRequestOptions) => { | |
| const start = Date.now(); | |
| return setTimeout(() => { | |
| cb({ | |
| didTimeout: false, | |
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | |
| }); | |
| }, options?.timeout ?? 1) as unknown as number; | |
| }; | |
| window.cancelIdleCallback = (id: number) => clearTimeout(id); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/space/app/entry.client.tsx` around lines 11 - 22, The
requestIdleCallback polyfill in entry.client.tsx is duplicated across entry
files and also overlaps with the existing installIdleCallbackPolyfill utility in
apps/web/core/lib/idle-task.ts. Replace the inline block with a shared import so
all entry points reuse the same implementation, including the options.timeout
behavior already supported there. While updating the shared polyfill, remove the
any casts by typing the callback and cancel handle with IdleRequestCallback,
IdleRequestOptions, and number to keep strict typing intact.
Source: Coding guidelines
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fallback ignores the options.timeout param.
Native requestIdleCallback accepts (callback, options); this fallback only accepts cb, so any caller passing { timeout: N } silently loses that hint and always fires after 1ms. This diverges from the sibling implementation in apps/web/core/lib/idle-task.ts, which honors options?.timeout.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/space/app/entry.client.tsx` around lines 12 - 19, The
requestIdleCallback fallback in entry.client.tsx ignores the optional timeout
argument, so callers passing options lose the intended behavior. Update the
fallback signature to accept both the callback and options, and use the provided
timeout when scheduling the setTimeout fallback, matching the behavior in the
sibling idle-task implementation. Keep the existing cb wrapper and timeRemaining
logic, but ensure options?.timeout is honored instead of always using 1ms.
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | ||
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; | ||
| }; | ||
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Redundant with existing installIdleCallbackPolyfill; prefer reusing it.
apps/web/core/lib/idle-task.ts already exports installIdleCallbackPolyfill, invoked via apps/web/core/lib/polyfills/index.ts on import, and it already supports options.timeout via requestIdleFallback. Adding a second, slightly different inline implementation here (using window instead of globalThis, dropping options, and typed with any) creates two competing polyfill paths in the same app. Prefer importing and calling the existing installer instead of duplicating the logic.
♻️ Suggested fix
-if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") {
- window.requestIdleCallback = (cb: any) => {
- const start = Date.now();
- return setTimeout(() => {
- cb({
- didTimeout: false,
- timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
- });
- }, 1) as any;
- };
- window.cancelIdleCallback = (id: any) => clearTimeout(id);
-}
+import { installIdleCallbackPolyfill } from "`@/lib/idle-task`";
+
+installIdleCallbackPolyfill();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (typeof window !== "undefined" && typeof window.requestIdleCallback !== "function") { | |
| window.requestIdleCallback = (cb: any) => { | |
| const start = Date.now(); | |
| return setTimeout(() => { | |
| cb({ | |
| didTimeout: false, | |
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | |
| }); | |
| }, 1) as any; | |
| }; | |
| window.cancelIdleCallback = (id: any) => clearTimeout(id); | |
| } | |
| import { installIdleCallbackPolyfill } from "`@/lib/idle-task`"; | |
| installIdleCallbackPolyfill(); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/app/entry.client.tsx` around lines 15 - 26, `entry.client.tsx` is
duplicating the idle callback polyfill that already exists in
`installIdleCallbackPolyfill`. Remove the inline
`requestIdleCallback`/`cancelIdleCallback` shim and reuse the shared installer
from `idle-task.ts` (through the existing polyfills entrypoint), so there is a
single implementation that preserves `options.timeout` and the
`globalThis`-based behavior.
| window.requestIdleCallback = (cb: any) => { | ||
| const start = Date.now(); | ||
| return setTimeout(() => { | ||
| cb({ | ||
| didTimeout: false, | ||
| timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), | ||
| }); | ||
| }, 1) as any; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Same any-typing / dropped options.timeout concerns as the space app entry.
cb: any/id: any/as any bypass strict typing, and the fallback signature drops the options parameter that native requestIdleCallback supports, silently overriding any requested timeout with a hardcoded 1ms delay.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/app/entry.client.tsx` around lines 16 - 23, The requestIdleCallback
polyfill in entry.client.tsx is using any-typed parameters and a hardcoded
fallback that ignores the native options timeout. Update the
window.requestIdleCallback shim to use proper requestIdleCallback-style types
instead of cb: any/id: any/as any, and accept the optional options parameter so
a caller-provided timeout is preserved rather than always forcing a 1ms delay.
Description
This PR fixes a bug where the
gantt-layout-loaderchunk throws an error on iOS WebKit devices (Safari, iOS Chrome, iOS Firefox) due to the absence of the nativewindow.requestIdleCallbackAPI.The missing API caused the React Error Boundary to catch the thrown error, resulting in a generic "Looks like something went wrong" page whenever users navigated to project views containing the layout loader on iPad/iOS devices.
Changes Made
window.requestIdleCallbackandwindow.cancelIdleCallbackdirectly at the app entry points (apps/web/app/entry.client.tsx,apps/admin/app/entry.client.tsx,apps/space/app/entry.client.tsx).setTimeoutto mimic idle time deferral for layout measurement, completely preventing the crash on iOS WebKit environments.Fixes #9231
Summary by CodeRabbit