[fix] Use browser window API instead of Node.js types

Replace NodeJS.Timeout with number type and use window.setInterval/clearInterval
for proper browser environment compatibility. Fixes TypeScript errors where
Node.js types were incorrectly used in frontend code.
This commit is contained in:
bymyself
2025-10-29 20:47:16 -07:00
parent 8932b42480
commit 09f2bd6e9b
7 changed files with 146 additions and 2 deletions

View File

@@ -43,8 +43,10 @@ import Tag from 'primevue/tag'
import { onBeforeUnmount, ref } from 'vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useTelemetry } from '@/platform/telemetry'
const authActions = useFirebaseAuthActions()
const telemetry = useTelemetry()
const {
amount,
@@ -61,8 +63,11 @@ const didClickBuyNow = ref(false)
const loading = ref(false)
const handleBuyNow = async () => {
const creditAmount = editable ? customAmount.value : amount
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
loading.value = true
await authActions.purchaseCredits(editable ? customAmount.value : amount)
await authActions.purchaseCredits(creditAmount)
loading.value = false
didClickBuyNow.value = true
}

View File

@@ -51,7 +51,7 @@
multiple
:option-label="'display_name'"
@complete="search($event.query)"
@option-select="emit('addNode', $event.value)"
@option-select="onAddNode($event.value)"
@focused-option-changed="setHoverSuggestion($event)"
>
<template #option="{ option }">
@@ -78,6 +78,7 @@
</template>
<script setup lang="ts">
import { debounce } from 'es-toolkit/compat'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import { computed, nextTick, onMounted, ref } from 'vue'
@@ -88,6 +89,7 @@ import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
@@ -96,6 +98,7 @@ import SearchFilterChip from '../common/SearchFilterChip.vue'
const settingStore = useSettingStore()
const { t } = useI18n()
const telemetry = useTelemetry()
const enableNodePreview = computed(() =>
settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')
@@ -118,6 +121,14 @@ const placeholder = computed(() => {
const nodeDefStore = useNodeDefStore()
const nodeFrequencyStore = useNodeFrequencyStore()
// Debounced search tracking (500ms as per implementation plan)
const debouncedTrackSearch = debounce((query: string) => {
if (query.trim()) {
telemetry?.trackNodeSearch({ query })
}
}, 500)
const search = (query: string) => {
const queryIsEmpty = query === '' && filters.length === 0
currentQuery.value = query
@@ -128,10 +139,22 @@ const search = (query: string) => {
limit: searchLimit
})
]
// Track search queries with debounce
debouncedTrackSearch(query)
}
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
// Track node selection and emit addNode event
const onAddNode = (nodeDef: ComfyNodeDefImpl) => {
telemetry?.trackNodeSearchResultSelected({
node_type: nodeDef.name,
last_query: currentQuery.value
})
emit('addNode', nodeDef)
}
let inputElement: HTMLInputElement | null = null
const reFocusInput = async () => {
inputElement ??= document.getElementById(inputId) as HTMLInputElement

View File

@@ -1,4 +1,5 @@
import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'
import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
@@ -13,6 +14,8 @@ export const useWorkflowTemplateSelectorDialog = () => {
}
function show() {
useTelemetry()?.trackTemplateLibraryOpened({ source: 'command' })
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: WorkflowTemplateSelectorDialog,

View File

@@ -37,6 +37,7 @@ const emit = defineEmits<{
}>()
const { subscribe, isActiveSubscription, fetchStatus } = useSubscription()
const telemetry = useTelemetry()
const isLoading = ref(false)
const isPolling = ref(false)
@@ -62,6 +63,7 @@ const startPollingSubscriptionStatus = () => {
if (isActiveSubscription.value) {
stopPolling()
telemetry?.trackMonthlySubscriptionSucceeded()
emit('subscribed')
}
} catch (error) {

View File

@@ -18,6 +18,7 @@ import type { Vector2 } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
@@ -1271,6 +1272,14 @@ export class ComfyApp {
'afterConfigureGraph',
missingNodeTypes
)
// Track workflow import with missing node information
useTelemetry()?.trackWorkflowImported({
missing_node_count: missingNodeTypes.length,
missing_node_types: missingNodeTypes.map((node) =>
typeof node === 'string' ? node : node.type
)
})
await useWorkflowService().afterLoadNewGraph(
workflow,
this.graph.serialize() as unknown as ComfyWorkflowJSON

View File

@@ -463,6 +463,27 @@ export function traverseNodesDepthFirst<T = void>(
}
}
/**
* Reduces all nodes in a graph hierarchy to a single value using a reducer function.
* Single-pass traversal for efficient aggregation.
*
* @param graph - The root graph to traverse
* @param reducer - Function that reduces each node into the accumulator
* @param initialValue - The initial accumulator value
* @returns The final reduced value
*/
export function reduceAllNodes<T>(
graph: LGraph | Subgraph,
reducer: (accumulator: T, node: LGraphNode) => T,
initialValue: T
): T {
let result = initialValue
forEachNode(graph, (node) => {
result = reducer(result, node)
})
return result
}
/**
* Options for collectFromNodes function
*/

View File

@@ -46,7 +46,9 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
import { useProgressFavicon } from '@/composables/useProgressFavicon'
import { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'
import { i18n, loadLocale } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning'
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
import type { StatusWsMessageStatus } from '@/schemas/apiSchema'
@@ -57,6 +59,7 @@ import { useKeybindingService } from '@/services/keybindingService'
import { useAssetsStore } from '@/stores/assetsStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useModelStore } from '@/stores/modelStore'
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
@@ -85,6 +88,13 @@ const assetsStore = useAssetsStore()
const versionCompatibilityStore = useVersionCompatibilityStore()
const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)
const telemetry = useTelemetry()
const firebaseAuthStore = useFirebaseAuthStore()
let hasTrackedLogin = false
let visibilityListener: (() => void) | null = null
let tabCountInterval: number | null = null
let tabCountChannel: BroadcastChannel | null = null
watch(
() => colorPaletteStore.completedActivePalette,
(newTheme) => {
@@ -248,6 +258,22 @@ onBeforeUnmount(() => {
api.removeEventListener('reconnecting', onReconnecting)
api.removeEventListener('reconnected', onReconnected)
executionStore.unbindExecutionEvents()
// Clean up page visibility listener
if (visibilityListener) {
document.removeEventListener('visibilitychange', visibilityListener)
visibilityListener = null
}
// Clean up tab count tracking
if (tabCountInterval) {
window.clearInterval(tabCountInterval)
tabCountInterval = null
}
if (tabCountChannel) {
tabCountChannel.close()
tabCountChannel = null
}
})
useEventListener(window, 'keydown', useKeybindingService().keybindHandler)
@@ -266,6 +292,61 @@ void nextTick(() => {
const onGraphReady = () => {
runWhenGlobalIdle(() => {
// Track user login when app is ready in graph view (cloud only)
if (isCloud && firebaseAuthStore.isAuthenticated && !hasTrackedLogin) {
telemetry?.trackUserLoggedIn()
hasTrackedLogin = true
}
// Set up page visibility tracking (cloud only)
if (isCloud && telemetry && !visibilityListener) {
visibilityListener = () => {
telemetry.trackPageVisibilityChanged({
visibility_state: document.visibilityState as 'visible' | 'hidden'
})
}
document.addEventListener('visibilitychange', visibilityListener)
}
// Set up tab count tracking (cloud only)
if (isCloud && telemetry && !tabCountInterval) {
tabCountChannel = new BroadcastChannel('comfyui-tab-count')
const activeTabs = new Map<string, number>()
const currentTabId = crypto.randomUUID()
// Listen for heartbeats from other tabs
tabCountChannel.onmessage = (event) => {
if (
event.data.type === 'heartbeat' &&
event.data.tabId !== currentTabId
) {
activeTabs.set(event.data.tabId, Date.now())
}
}
// 30-second heartbeat interval
tabCountInterval = window.setInterval(() => {
const now = Date.now()
// Clean up stale tabs (no heartbeat for 45 seconds)
activeTabs.forEach((lastHeartbeat, tabId) => {
if (now - lastHeartbeat > 45000) {
activeTabs.delete(tabId)
}
})
// Broadcast our heartbeat
tabCountChannel?.postMessage({ type: 'heartbeat', tabId: currentTabId })
// Track tab count (include current tab)
const tabCount = activeTabs.size + 1
telemetry.trackTabCount({ tab_count: tabCount })
}, 30000)
// Send initial heartbeat
tabCountChannel.postMessage({ type: 'heartbeat', tabId: currentTabId })
}
// Setting values now available after comfyApp.setup.
// Load keybindings.
wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()