From b3c939ff15f06ac9209c28e175dbf0bcd3d6303a Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Fri, 19 Sep 2025 23:34:15 -0700 Subject: [PATCH] fix: add Safari requestIdleCallback polyfill (#5664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- src/base/common/async.ts | 98 ++++++++++++++++++++++++++++++++++++++++ src/views/GraphView.vue | 44 +++++++++--------- 2 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 src/base/common/async.ts diff --git a/src/base/common/async.ts b/src/base/common/async.ts new file mode 100644 index 000000000..a97f6f1bd --- /dev/null +++ b/src/base/common/async.ts @@ -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) +})() diff --git a/src/views/GraphView.vue b/src/views/GraphView.vue index a23d4fecf..bbfca6ea0 100644 --- a/src/views/GraphView.vue +++ b/src/views/GraphView.vue @@ -33,6 +33,7 @@ import { } from 'vue' import { useI18n } from 'vue-i18n' +import { runWhenGlobalIdle } from '@/base/common/async' import MenuHamburger from '@/components/MenuHamburger.vue' import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue' import GraphCanvas from '@/components/graph/GraphCanvas.vue' @@ -253,33 +254,30 @@ void nextTick(() => { }) const onGraphReady = () => { - requestIdleCallback( - () => { - // Setting values now available after comfyApp.setup. - // Load keybindings. - wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)() + runWhenGlobalIdle(() => { + // Setting values now available after comfyApp.setup. + // Load keybindings. + wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)() - // Load server config - wrapWithErrorHandling(useServerConfigStore().loadServerConfig)( - SERVER_CONFIG_ITEMS, - settingStore.get('Comfy.Server.ServerConfigValues') - ) + // Load server config + wrapWithErrorHandling(useServerConfigStore().loadServerConfig)( + SERVER_CONFIG_ITEMS, + settingStore.get('Comfy.Server.ServerConfigValues') + ) - // Load model folders - void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)() + // Load model folders + void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)() - // Non-blocking load of node frequencies - void wrapWithErrorHandlingAsync( - useNodeFrequencyStore().loadNodeFrequencies - )() + // Non-blocking load of node frequencies + void wrapWithErrorHandlingAsync( + useNodeFrequencyStore().loadNodeFrequencies + )() - // Node defs now available after comfyApp.setup. - // Explicitly initialize nodeSearchService to avoid indexing delay when - // node search is triggered - useNodeDefStore().nodeSearchService.searchNode('') - }, - { timeout: 1000 } - ) + // Node defs now available after comfyApp.setup. + // Explicitly initialize nodeSearchService to avoid indexing delay when + // node search is triggered + useNodeDefStore().nodeSearchService.searchNode('') + }, 1000) }