Skip to content

fix: add requestIdleCallback polyfill at app entry for iOS WebKit (Fixes #9231)#9358

Open
rohanmaan07 wants to merge 1 commit into
makeplane:previewfrom
rohanmaan07:fix/request-idle-callback-polyfill
Open

fix: add requestIdleCallback polyfill at app entry for iOS WebKit (Fixes #9231)#9358
rohanmaan07 wants to merge 1 commit into
makeplane:previewfrom
rohanmaan07:fix/request-idle-callback-polyfill

Conversation

@rohanmaan07

@rohanmaan07 rohanmaan07 commented Jul 5, 2026

Copy link
Copy Markdown

Description

This PR fixes a bug where the gantt-layout-loader chunk throws an error on iOS WebKit devices (Safari, iOS Chrome, iOS Firefox) due to the absence of the native window.requestIdleCallback API.

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

  • Added a fallback polyfill for window.requestIdleCallback and window.cancelIdleCallback directly at the app entry points (apps/web/app/entry.client.tsx, apps/admin/app/entry.client.tsx, apps/space/app/entry.client.tsx).
  • The polyfill safely checks for the function's existence and uses setTimeout to mimic idle time deferral for layout measurement, completely preventing the crash on iOS WebKit environments.

Fixes #9231

Summary by CodeRabbit

  • Bug Fixes
    • Improved browser compatibility by adding a fallback for idle callback scheduling when the native browser support is unavailable.
    • This helps pages load and hydrate more reliably in more environments without changing the app experience.

Copilot AI review requested due to automatic review settings July 5, 2026 05:40
@CLAassistant

CLAassistant commented Jul 5, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a conditional polyfill for window.requestIdleCallback and window.cancelIdleCallback in the client entry files of the admin, space, and web apps. When the native API is missing, a setTimeout-based fallback with a timeRemaining() implementation is installed.

Changes

requestIdleCallback Polyfill

Layer / File(s) Summary
Feature-detect and polyfill idle callback APIs
apps/admin/app/entry.client.tsx, apps/space/app/entry.client.tsx, apps/web/app/entry.client.tsx
Each entry file checks whether window.requestIdleCallback exists and, if not, defines a setTimeout-based requestIdleCallback (returning didTimeout: false and an elapsed-time-based timeRemaining()) plus a cancelIdleCallback using clearTimeout.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • makeplane/plane#9094: Both PRs address missing window.requestIdleCallback with equivalent idle-task scheduling/fallback logic.
  • makeplane/plane#9137: Both PRs update apps/web/app/entry.client.tsx to install a requestIdleCallback/cancelIdleCallback fallback.

Suggested reviewers: sriramveeraghanta, codingwolf-at

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the bug, fix, and reference, but it omits required template sections like Type of Change and Test Scenarios. Add the missing template sections, especially Type of Change and Test Scenarios, and include Screenshots/Media if applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a requestIdleCallback polyfill for iOS WebKit.
Linked Issues check ✅ Passed The PR implements the requested app-entry polyfill in all three apps and addresses the linked iOS WebKit crash.
Out of Scope Changes check ✅ Passed The changes stay focused on the requestIdleCallback fallback in app entry files and do not introduce unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 requestIdleCallback polyfill (via setTimeout) at app startup to avoid runtime TypeError on iOS WebKit.
  • Add a matching cancelIdleCallback fallback so scheduled idle work can be cancelled safely.
  • Apply the fix consistently across web, admin, and space client 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.

Comment on lines +15 to +26
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);
}
Comment on lines +11 to +22
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);
}
Comment on lines +11 to +22
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);
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7fbf14a and 786d537.

📒 Files selected for processing (3)
  • apps/admin/app/entry.client.tsx
  • apps/space/app/entry.client.tsx
  • apps/web/app/entry.client.tsx

Comment on lines +11 to +23
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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

Comment on lines +11 to +22
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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

Comment on lines +12 to +19
window.requestIdleCallback = (cb: any) => {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
}, 1) as any;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +15 to +26
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

Comment on lines +16 to +23
window.requestIdleCallback = (cb: any) => {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
}, 1) as any;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS WebKit: gantt-layout-loader throws on missing window.requestIdleCallback — global error boundary on every issues-list view

3 participants