mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
## Summary Replaces all 7 production `as Error` type assertions with proper `instanceof Error` narrowing or a new `toError()` helper, and adds an ESLint rule to prevent new ones. First slice of #11429 (the `as Error` category — 9 total occurrences, 7 production + 2 in a test file left untouched). ## Changes - **What**: - New `src/utils/errorUtil.ts` exporting `toError(value: unknown): Error` and `getErrorMessage(value: unknown): string | undefined`. `toError` returns the value unchanged if already an `Error`, otherwise wraps it (handles strings, `undefined`, JSON-serializable objects, and circular refs via `String()` fallback). - Refactored 7 production call sites: - `src/services/gateway/registrySearchGateway.ts` — `toError(error)` for `lastError` assignment in fallback loop - `src/platform/cloud/onboarding/auth.ts` (×2) — `toError(error)` for `captureApiError` Sentry calls - `src/renderer/extensions/vueNodes/widgets/composables/audio/useAudioRecorder.ts` — `toError(err)` before forwarding to `options.onError` - `src/extensions/core/load3d/LoaderManager.ts` — replaced `error as Error & { response?: ... }` cast inside `isNotFoundError` with `'response' in error` + nested narrowing - `apps/desktop-ui/src/stores/maintenanceTaskStore.ts` — inline `error instanceof Error ? error.message : String(error)` - `apps/desktop-ui/src/components/maintenance/TaskListPanel.vue` — inline `error instanceof Error ? error.message : undefined` - New ESLint rule (`no-restricted-syntax` block named `comfy/no-unsafe-error-assertion`) banning `TSAsExpression TSTypeReference[typeName.name='Error']` in `src/**` and `apps/*/src/**`, with test files (`*.test.ts`, `*.spec.ts`) excluded. - 12 unit tests for the new helpers in `src/utils/errorUtil.test.ts`. - **Breaking**: none - **Dependencies**: none ## Review Focus - The lint rule is scoped to non-test source files. Test files retain freedom to use `as Error` for fixture construction; only 2 occurrences exist (in `teamWorkspaceStore.test.ts` and `errorDialog.spec.ts`) and they're intentional. - `toError` is duplicated as inline `instanceof` narrowing in `apps/desktop-ui/` rather than imported, since the desktop-ui workspace doesn't share `@/utils/` with the main app and adding a path mapping for one helper felt heavier than two inline guards. - Remaining `as`-on-DOM categories (HTMLElement ×133, HTMLInputElement ×55, HTMLCanvasElement ×36, KeyboardEvent ×7, Element ×3, MouseEvent ×2, Event ×2) are intentionally left for follow-up PRs to keep this one reviewable. Refs #11429 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11845-refactor-replace-unsafe-as-Error-assertions-with-type-guards-3546d73d36508137a015c4f9e8708f23) by [Unito](https://www.unito.io)
190 lines
5.6 KiB
TypeScript
190 lines
5.6 KiB
TypeScript
import type { InstallValidation } from '@comfyorg/comfyui-electron-types'
|
|
import { defineStore } from 'pinia'
|
|
import { computed, ref } from 'vue'
|
|
|
|
import { DESKTOP_MAINTENANCE_TASKS } from '@/constants/desktopMaintenanceTasks'
|
|
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
|
import { electronAPI } from '@/utils/envUtil'
|
|
|
|
/** State of a maintenance task, managed by the maintenance task store. */
|
|
type MaintenanceTaskState = 'warning' | 'error' | 'OK' | 'skipped'
|
|
|
|
// Type not exported by API
|
|
type ValidationState = InstallValidation['basePath']
|
|
// Add index to API type
|
|
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
|
|
|
|
/** State of a maintenance task, managed by the maintenance task store. */
|
|
class MaintenanceTaskRunner {
|
|
constructor(readonly task: MaintenanceTask) {}
|
|
|
|
private _state?: MaintenanceTaskState
|
|
/** The current state of the task. Setter also controls {@link resolved} as a side-effect. */
|
|
get state() {
|
|
return this._state
|
|
}
|
|
|
|
/** Updates the task state and {@link resolved} status. */
|
|
setState(value: MaintenanceTaskState) {
|
|
// Mark resolved
|
|
if (this._state === 'error' && value === 'OK') this.resolved = true
|
|
// Mark unresolved (if previously resolved)
|
|
if (value === 'error') this.resolved &&= false
|
|
|
|
this._state = value
|
|
}
|
|
|
|
/** `true` if the task has been resolved (was `error`, now `OK`). This is a side-effect of the {@link state} setter. */
|
|
resolved?: boolean
|
|
|
|
/** Whether the task state is currently being refreshed. */
|
|
refreshing?: boolean
|
|
/** Whether the task is currently running. */
|
|
executing?: boolean
|
|
/** The error message that occurred when the task failed. */
|
|
error?: string
|
|
|
|
update(update: IndexedUpdate) {
|
|
const state = update[this.task.id]
|
|
|
|
this.refreshing = state === undefined
|
|
if (state) this.setState(state)
|
|
}
|
|
|
|
finaliseUpdate(update: IndexedUpdate) {
|
|
this.refreshing = false
|
|
this.setState(update[this.task.id] ?? 'skipped')
|
|
}
|
|
|
|
/** Wraps the execution of a maintenance task, updating state and rethrowing errors. */
|
|
async execute(task: MaintenanceTask) {
|
|
try {
|
|
this.executing = true
|
|
const success = await task.execute()
|
|
if (!success) return false
|
|
|
|
this.error = undefined
|
|
return true
|
|
} catch (error) {
|
|
this.error = error instanceof Error ? error.message : String(error)
|
|
throw error
|
|
} finally {
|
|
this.executing = false
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* User-initiated maintenance tasks. Currently only used by the desktop app maintenance view.
|
|
*
|
|
* Includes running state, task list, and execution / refresh logic.
|
|
* @returns The maintenance task store
|
|
*/
|
|
export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
|
/** Refresh should run for at least this long, even if it completes much faster. Ensures refresh feels like it is doing something. */
|
|
const electron = electronAPI()
|
|
|
|
// Reactive state
|
|
const lastUpdate = ref<InstallValidation | null>(null)
|
|
const isRefreshing = ref(false)
|
|
const isRunningTerminalCommand = computed(() =>
|
|
tasks.value
|
|
.filter((task) => task.usesTerminal)
|
|
.some((task) => getRunner(task)?.executing)
|
|
)
|
|
const isRunningInstallationFix = computed(() =>
|
|
tasks.value
|
|
.filter((task) => task.isInstallationFix)
|
|
.some((task) => getRunner(task)?.executing)
|
|
)
|
|
|
|
const unsafeBasePath = computed(
|
|
() => lastUpdate.value?.unsafeBasePath === true
|
|
)
|
|
const unsafeBasePathReason = computed(
|
|
() => lastUpdate.value?.unsafeBasePathReason
|
|
)
|
|
|
|
// Task list
|
|
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
|
|
|
const taskRunners = ref(
|
|
new Map<MaintenanceTask['id'], MaintenanceTaskRunner>(
|
|
DESKTOP_MAINTENANCE_TASKS.map((x) => [x.id, new MaintenanceTaskRunner(x)])
|
|
)
|
|
)
|
|
|
|
/** True if any tasks are in an error state. */
|
|
const anyErrors = computed(() =>
|
|
tasks.value.some((task) => getRunner(task).state === 'error')
|
|
)
|
|
|
|
/**
|
|
* Returns the matching state object for a task.
|
|
* @param task Task to get the matching state object for
|
|
* @returns The state object for this task
|
|
*/
|
|
const getRunner = (task: MaintenanceTask) => taskRunners.value.get(task.id)!
|
|
|
|
/**
|
|
* Updates the task list with the latest validation state.
|
|
* @param validationUpdate Update details passed in by electron
|
|
*/
|
|
const processUpdate = (validationUpdate: InstallValidation) => {
|
|
lastUpdate.value = validationUpdate
|
|
const update = validationUpdate as IndexedUpdate
|
|
isRefreshing.value = true
|
|
|
|
// Update each task state
|
|
for (const task of tasks.value) {
|
|
getRunner(task).update(update)
|
|
}
|
|
|
|
// Final update
|
|
if (!update.inProgress && isRefreshing.value) {
|
|
isRefreshing.value = false
|
|
|
|
for (const task of tasks.value) {
|
|
getRunner(task).finaliseUpdate(update)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Clears the resolved status of tasks (when changing filters) */
|
|
const clearResolved = () => {
|
|
for (const task of tasks.value) {
|
|
getRunner(task).resolved &&= false
|
|
}
|
|
}
|
|
|
|
/** @todo Refreshes Electron tasks only. */
|
|
const refreshDesktopTasks = async () => {
|
|
isRefreshing.value = true
|
|
await electron.Validation.validateInstallation(processUpdate)
|
|
}
|
|
|
|
const execute = async (task: MaintenanceTask) => {
|
|
const success = await getRunner(task).execute(task)
|
|
if (success && task.isInstallationFix) {
|
|
await refreshDesktopTasks()
|
|
}
|
|
return success
|
|
}
|
|
|
|
return {
|
|
tasks,
|
|
isRefreshing,
|
|
isRunningTerminalCommand,
|
|
isRunningInstallationFix,
|
|
unsafeBasePath,
|
|
unsafeBasePathReason,
|
|
execute,
|
|
getRunner,
|
|
processUpdate,
|
|
clearResolved,
|
|
/** True if any tasks are in an error state. */
|
|
anyErrors,
|
|
refreshDesktopTasks
|
|
}
|
|
})
|