mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
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:
98
src/base/common/async.ts
Normal file
98
src/base/common/async.ts
Normal 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)
|
||||||
|
})()
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user