fix: add Safari requestIdleCallback polyfill (#5664)

## Summary

Implemented cross-browser requestIdleCallback polyfill to fix Safari
crashes during graph initialization.

## Changes

- **What**: Added
[requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
polyfill following [VS Code's
pattern](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts)
with setTimeout fallback for Safari
- **Breaking**: None - maintains existing GraphView behavior

## Review Focus

Safari compatibility testing and timeout handling in the 15ms fallback
window. Verify that initialization tasks (keybindings, server config,
model loading) still execute properly on Safari iOS.

## References

- [VS Code async.ts
implementation](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts)
- Source pattern for our polyfill
- [MDN
requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)
- Browser API documentation
- [Safari requestIdleCallback
support](https://caniuse.com/requestidlecallback) - Browser
compatibility table

Fixes CLOUD-FRONTEND-STAGING-N

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5664-fix-add-Safari-requestIdleCallback-polyfill-2736d73d365081cdbcf1fb816fe098d6)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-09-19 23:34:15 -07:00
committed by GitHub
parent 0801778f60
commit b3c939ff15
2 changed files with 119 additions and 23 deletions

98
src/base/common/async.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Cross-browser async utilities for scheduling tasks during browser idle time
* with proper fallbacks for browsers that don't support requestIdleCallback.
*
* Implementation based on:
* https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts
*/
interface IdleDeadline {
didTimeout: boolean
timeRemaining(): number
}
interface IDisposable {
dispose(): void
}
/**
* Internal implementation function that handles the actual scheduling logic.
* Uses feature detection to determine whether to use native requestIdleCallback
* or fall back to setTimeout-based implementation.
*/
let _runWhenIdle: (
targetWindow: any,
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
/**
* Execute the callback during the next browser idle period.
* Falls back to setTimeout-based scheduling in browsers without native support.
*/
export let runWhenGlobalIdle: (
callback: (idle: IdleDeadline) => void,
timeout?: number
) => IDisposable
// Self-invoking function to set up the idle callback implementation
;(function () {
const safeGlobal: any = globalThis
if (
typeof safeGlobal.requestIdleCallback !== 'function' ||
typeof safeGlobal.cancelIdleCallback !== 'function'
) {
// Fallback implementation for browsers without native support (e.g., Safari)
_runWhenIdle = (_targetWindow, runner, _timeout?) => {
setTimeout(() => {
if (disposed) {
return
}
// Simulate IdleDeadline - give 15ms window (one frame at ~64fps)
const end = Date.now() + 15
const deadline: IdleDeadline = {
didTimeout: true,
timeRemaining() {
return Math.max(0, end - Date.now())
}
}
runner(Object.freeze(deadline))
})
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
}
}
}
} else {
// Native requestIdleCallback implementation
_runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => {
const handle: number = targetWindow.requestIdleCallback(
runner,
typeof timeout === 'number' ? { timeout } : undefined
)
let disposed = false
return {
dispose() {
if (disposed) {
return
}
disposed = true
targetWindow.cancelIdleCallback(handle)
}
}
}
}
runWhenGlobalIdle = (runner, timeout) =>
_runWhenIdle(globalThis, runner, timeout)
})()

View File

@@ -33,6 +33,7 @@ import {
} from 'vue' } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { runWhenGlobalIdle } from '@/base/common/async'
import MenuHamburger from '@/components/MenuHamburger.vue' import MenuHamburger from '@/components/MenuHamburger.vue'
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue' import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
import GraphCanvas from '@/components/graph/GraphCanvas.vue' import GraphCanvas from '@/components/graph/GraphCanvas.vue'
@@ -253,33 +254,30 @@ void nextTick(() => {
}) })
const onGraphReady = () => { const onGraphReady = () => {
requestIdleCallback( runWhenGlobalIdle(() => {
() => { // Setting values now available after comfyApp.setup.
// Setting values now available after comfyApp.setup. // Load keybindings.
// Load keybindings. wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()
// Load server config // Load server config
wrapWithErrorHandling(useServerConfigStore().loadServerConfig)( wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(
SERVER_CONFIG_ITEMS, SERVER_CONFIG_ITEMS,
settingStore.get('Comfy.Server.ServerConfigValues') settingStore.get('Comfy.Server.ServerConfigValues')
) )
// Load model folders // Load model folders
void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)() void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()
// Non-blocking load of node frequencies // Non-blocking load of node frequencies
void wrapWithErrorHandlingAsync( void wrapWithErrorHandlingAsync(
useNodeFrequencyStore().loadNodeFrequencies useNodeFrequencyStore().loadNodeFrequencies
)() )()
// Node defs now available after comfyApp.setup. // Node defs now available after comfyApp.setup.
// Explicitly initialize nodeSearchService to avoid indexing delay when // Explicitly initialize nodeSearchService to avoid indexing delay when
// node search is triggered // node search is triggered
useNodeDefStore().nodeSearchService.searchNode('') useNodeDefStore().nodeSearchService.searchNode('')
}, }, 1000)
{ timeout: 1000 }
)
} }
</script> </script>