Skip to content

fix(app): polyfill requestIdleCallback for iOS Safari#5816

Merged
MarkusNeusinger merged 2 commits into
mainfrom
fix/ios-request-idle-callback
May 6, 2026
Merged

fix(app): polyfill requestIdleCallback for iOS Safari#5816
MarkusNeusinger merged 2 commits into
mainfrom
fix/ios-request-idle-callback

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

  • iPhone users could not use the app: every iOS Safari (and CriOS, which uses WebKit) instance threw TypeError: window.requestIdleCallback is not a function at AppDataProvider mount, which ErrorBoundary caught — leaving the user stuck on the error fallback. Surfaced by the on-device debug console added in feat(debug): on-device console + richer ErrorBoundary for mobile crashes #5808.
  • requestIdleCallback is documented as unsupported on Safari/iOS by caniuse, MDN, and WebKit bug 164193. It exists only behind the Experimental Features toggle, which is off by default — so this is a hard regression for every iOS visitor, not a one-off device bug.
  • Fix is an inline feature-detect in the existing useEffect: use requestIdleCallback if present, otherwise fall back to setTimeout(cb, 1). Cleanup picks the matching cancel function via captured boolean. No new npm dependency, no global mutation.
  • Adds a regression test that stubs both idle APIs as undefined and asserts the three initial fetches still fire and children still render.

Original iPhone error report

Message: window.requestIdleCallback is not a function. (In 'window.requestIdleCallback(async()=>{try{let[e,t,n]=await Promise.all([fetch(`${T}/specs`),fetch(`${T}/libraries`),fetch(`${T}/stats`)]);...},{timeout:2e3})')
URL: https://anyplot.ai/?debug=1
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 26_4_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/148.0.7778.100 Mobile/15E148 Safari/604.1

Test plan

  • cd app && yarn test --run — 467/467 pass, including new falls back to setTimeout when requestIdleCallback is unavailable (iOS Safari) case in Layout.test.tsx.
  • cd app && yarn build — TypeScript / Vite build green.
  • Pre-existing lint findings on main unchanged; no new findings introduced by this PR.
  • Production verification on the reporter's iPhone after deploy (visit https://anyplot.ai/?debug=1, confirm home page loads and the Eruda console is clean).

Why this approach (not a polyfill package)

  • Two well-known polyfills exist (aFarkas/requestIdleCallback, pladaria/requestidlecallback-polyfill) but both internally just wrap setTimeout — there is no way to truly polyfill real browser idle time. Adding a dependency would ship bytes to every modern-browser user for the same effect we get inline.
  • Single call site in the entire repo — per CLAUDE.md "no abstractions beyond what the task requires", a per-file inline fix is the right shape. If a second call site appears later, that is the moment to extract a utils/idleCallback.ts.

🤖 Generated with Claude Code

Safari/iOS doesn't ship requestIdleCallback by default (caniuse / WebKit
bug 164193 — only available behind the experimental-features flag). Calling
window.requestIdleCallback on the iPhone therefore threw TypeError at mount
of AppDataProvider, which the ErrorBoundary caught — leaving every iOS user
stuck on the error fallback after PR #5808's debug rollout surfaced it.

Inline feature-detect in the existing useEffect: use requestIdleCallback
when present, else fall back to setTimeout(cb, 1). Cleanup picks the
matching cancel function via captured boolean. No new dependency, no
global mutation.

Adds a regression test that stubs both idle APIs as undefined and asserts
the three initial fetches (/specs, /libraries, /stats) still fire and
children still render — locking in the fix for future refactors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 6, 2026 20:19
@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a hard crash on iOS Safari/WebKit where window.requestIdleCallback is undefined, preventing the app from loading by ensuring AppDataProvider schedules its initial data fetches via a safe fallback.

Changes:

  • Feature-detect window.requestIdleCallback and fall back to setTimeout when unavailable; clean up with the matching cancel API.
  • Add a regression test that simulates iOS Safari by stubbing the idle APIs as unavailable and asserting initial fetches still occur and children render.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
app/src/components/Layout.tsx Adds requestIdleCallback feature-detect with setTimeout fallback for initial shared-data loading.
app/src/components/Layout.test.tsx Adds a regression test covering the fallback behavior when idle callbacks are missing.

Comment on lines +131 to +135
it('falls back to setTimeout when requestIdleCallback is unavailable (iOS Safari)', async () => {
// Simulate Safari/iOS where requestIdleCallback is undefined by default.
vi.stubGlobal('requestIdleCallback', undefined);
vi.stubGlobal('cancelIdleCallback', undefined);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed in 585ba50 — added vi.unstubAllGlobals() at the end of the test so subsequent tests do not see leaked undefined globals.

Addresses Copilot review comment on PR #5816: the new test stubbed
requestIdleCallback/cancelIdleCallback to undefined and never restored
them. The existing beforeEach re-stubs at the start of the next test,
but a future test inserted between this one and another that depends on
the globals would see undefined and break silently. Adding
vi.unstubAllGlobals() at the end of the test makes the leak impossible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MarkusNeusinger MarkusNeusinger merged commit b2fb3fc into main May 6, 2026
9 checks passed
@MarkusNeusinger MarkusNeusinger deleted the fix/ios-request-idle-callback branch May 6, 2026 20:29
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.

2 participants