diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index e755197..537a3a4 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -8,7 +8,7 @@ import { app } from "../../scripts/app.js"; // @ts-ignore import { ComfyApp } from "../../scripts/app.js"; import { ClipboardManager } from "./utils/ClipboardManager.js"; -import { createDistanceFieldMask } from "./utils/ImageAnalysis.js"; +import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js"; const log = createModuleLogger('CanvasLayers'); export class CanvasLayers { constructor(canvas) { @@ -361,9 +361,9 @@ export class CanvasLayers { const blendArea = layer.blendArea ?? 0; const needsBlendAreaEffect = blendArea > 0; if (needsBlendAreaEffect) { - log.info(`Applying blend area effect for layer ${layer.id}`); + log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`); // Get or create distance field mask - let maskCanvas = this.getDistanceFieldMask(layer.image, blendArea); + const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea); if (maskCanvas) { // Create a temporary canvas for the masked layer const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(layer.width, layer.height); @@ -400,7 +400,7 @@ export class CanvasLayers { } ctx.restore(); } - getDistanceFieldMask(image, blendArea) { + getDistanceFieldMaskSync(image, blendArea) { // Check cache first let imageCache = this.distanceFieldCache.get(image); if (!imageCache) { @@ -411,7 +411,7 @@ export class CanvasLayers { if (!maskCanvas) { try { log.info(`Creating distance field mask for blendArea: ${blendArea}%`); - maskCanvas = createDistanceFieldMask(image, blendArea); + maskCanvas = createDistanceFieldMaskSync(image, blendArea); log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`); imageCache.set(blendArea, maskCanvas); } diff --git a/js/utils/ClipboardManager.js b/js/utils/ClipboardManager.js index 4685fd7..991ead4 100644 --- a/js/utils/ClipboardManager.js +++ b/js/utils/ClipboardManager.js @@ -1,5 +1,6 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { showNotification, showInfoNotification } from "./NotificationUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js"; // @ts-ignore import { api } from "../../../scripts/api.js"; // @ts-ignore @@ -7,17 +8,13 @@ import { ComfyApp } from "../../../scripts/app.js"; const log = createModuleLogger('ClipboardManager'); export class ClipboardManager { constructor(canvas) { - this.canvas = canvas; - this.clipboardPreference = 'system'; // 'system', 'clipspace' - } - /** - * Main paste handler that delegates to appropriate methods - * @param {AddMode} addMode - The mode for adding the layer - * @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace') - * @returns {Promise} - True if successful, false otherwise - */ - async handlePaste(addMode = 'mouse', preference = 'system') { - try { + /** + * Main paste handler that delegates to appropriate methods + * @param {AddMode} addMode - The mode for adding the layer + * @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace') + * @returns {Promise} - True if successful, false otherwise + */ + this.handlePaste = withErrorHandling(async (addMode = 'mouse', preference = 'system') => { log.info(`ClipboardManager handling paste with preference: ${preference}`); if (this.canvas.canvasLayers.internalClipboard.length > 0) { log.info("Found layers in internal clipboard, pasting layers"); @@ -34,19 +31,13 @@ export class ClipboardManager { } log.info("Attempting paste from system clipboard"); return await this.trySystemClipboardPaste(addMode); - } - catch (err) { - log.error("ClipboardManager paste operation failed:", err); - return false; - } - } - /** - * Attempts to paste from ComfyUI Clipspace - * @param {AddMode} addMode - The mode for adding the layer - * @returns {Promise} - True if successful, false otherwise - */ - async tryClipspacePaste(addMode) { - try { + }, 'ClipboardManager.handlePaste'); + /** + * Attempts to paste from ComfyUI Clipspace + * @param {AddMode} addMode - The mode for adding the layer + * @returns {Promise} - True if successful, false otherwise + */ + this.tryClipspacePaste = withErrorHandling(async (addMode) => { log.info("Attempting to paste from ComfyUI Clipspace"); ComfyApp.pasteFromClipspace(this.canvas.node); if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) { @@ -62,11 +53,57 @@ export class ClipboardManager { } } return false; - } - catch (clipspaceError) { - log.warn("ComfyUI Clipspace paste failed:", clipspaceError); - return false; - } + }, 'ClipboardManager.tryClipspacePaste'); + /** + * Loads a local file via the ComfyUI backend endpoint + * @param {string} filePath - The file path to load + * @param {AddMode} addMode - The mode for adding the layer + * @returns {Promise} - True if successful, false otherwise + */ + this.loadFileViaBackend = withErrorHandling(async (filePath, addMode) => { + if (!filePath) { + throw createValidationError("File path is required", { filePath }); + } + log.info("Loading file via ComfyUI backend:", filePath); + const response = await api.fetchApi("/ycnode/load_image_from_path", { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: filePath + }) + }); + if (!response.ok) { + const errorData = await response.json(); + throw createNetworkError(`Backend failed to load image: ${errorData.error}`, { + filePath, + status: response.status, + statusText: response.statusText + }); + } + const data = await response.json(); + if (!data.success) { + throw createFileError(`Backend returned error: ${data.error}`, { filePath, backendError: data.error }); + } + log.info("Successfully loaded image via ComfyUI backend:", filePath); + const img = new Image(); + const success = await new Promise((resolve) => { + img.onload = async () => { + log.info("Successfully loaded image from backend response"); + await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode); + resolve(true); + }; + img.onerror = () => { + log.warn("Failed to load image from backend response"); + resolve(false); + }; + img.src = data.image_data; + }); + return success; + }, 'ClipboardManager.loadFileViaBackend'); + this.canvas = canvas; + this.clipboardPreference = 'system'; // 'system', 'clipspace' } /** * System clipboard paste - handles both image data and text paths @@ -248,55 +285,6 @@ export class ClipboardManager { this.showFilePathMessage(filePath); return false; } - /** - * Loads a local file via the ComfyUI backend endpoint - * @param {string} filePath - The file path to load - * @param {AddMode} addMode - The mode for adding the layer - * @returns {Promise} - True if successful, false otherwise - */ - async loadFileViaBackend(filePath, addMode) { - try { - log.info("Loading file via ComfyUI backend:", filePath); - const response = await api.fetchApi("/ycnode/load_image_from_path", { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath - }) - }); - if (!response.ok) { - const errorData = await response.json(); - log.debug("Backend failed to load image:", errorData.error); - return false; - } - const data = await response.json(); - if (!data.success) { - log.debug("Backend returned error:", data.error); - return false; - } - log.info("Successfully loaded image via ComfyUI backend:", filePath); - const img = new Image(); - const success = await new Promise((resolve) => { - img.onload = async () => { - log.info("Successfully loaded image from backend response"); - await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode); - resolve(true); - }; - img.onerror = () => { - log.warn("Failed to load image from backend response"); - resolve(false); - }; - img.src = data.image_data; - }); - return success; - } - catch (error) { - log.debug("Error loading file via ComfyUI backend:", error); - return false; - } - } /** * Prompts the user to select a file when a local path is detected * @param {string} originalPath - The original file path from clipboard diff --git a/js/utils/IconLoader.js b/js/utils/IconLoader.js index 2764939..26e909f 100644 --- a/js/utils/IconLoader.js +++ b/js/utils/IconLoader.js @@ -1,5 +1,6 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('IconLoader'); // Define tool constants for LayerForge export const LAYERFORGE_TOOLS = { @@ -53,63 +54,63 @@ export class IconLoader { constructor() { this._iconCache = {}; this._loadingPromises = new Map(); - log.info('IconLoader initialized'); - } - /** - * Preload all LayerForge tool icons - */ - preloadToolIcons() { - log.info('Starting to preload LayerForge tool icons'); - const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => { - return this.loadIcon(tool); - }); - return Promise.all(loadPromises).then(() => { + /** + * Preload all LayerForge tool icons + */ + this.preloadToolIcons = withErrorHandling(async () => { + log.info('Starting to preload LayerForge tool icons'); + const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => { + return this.loadIcon(tool); + }); + await Promise.all(loadPromises); log.info(`Successfully preloaded ${loadPromises.length} tool icons`); - }).catch(error => { - log.error('Error preloading tool icons:', error); - }); - } - /** - * Load a specific icon by tool name - */ - async loadIcon(tool) { - // Check if already cached - if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) { - return this._iconCache[tool]; - } - // Check if already loading - if (this._loadingPromises.has(tool)) { - return this._loadingPromises.get(tool); - } - // Create fallback canvas first - const fallbackCanvas = this.createFallbackIcon(tool); - this._iconCache[tool] = fallbackCanvas; - // Start loading the SVG icon - const loadPromise = new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - this._iconCache[tool] = img; - this._loadingPromises.delete(tool); - log.debug(`Successfully loaded icon for tool: ${tool}`); - resolve(img); - }; - img.onerror = (error) => { - log.warn(`Failed to load SVG icon for tool: ${tool}, using fallback`); - this._loadingPromises.delete(tool); - // Keep the fallback canvas in cache - reject(error); - }; - const iconData = LAYERFORGE_TOOL_ICONS[tool]; - if (iconData) { - img.src = iconData; + }, 'IconLoader.preloadToolIcons'); + /** + * Load a specific icon by tool name + */ + this.loadIcon = withErrorHandling(async (tool) => { + if (!tool) { + throw createValidationError("Tool name is required", { tool }); } - else { - log.warn(`No icon data found for tool: ${tool}`); - reject(new Error(`No icon data for tool: ${tool}`)); + // Check if already cached + if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) { + return this._iconCache[tool]; } - }); - this._loadingPromises.set(tool, loadPromise); - return loadPromise; + // Check if already loading + if (this._loadingPromises.has(tool)) { + return this._loadingPromises.get(tool); + } + // Create fallback canvas first + const fallbackCanvas = this.createFallbackIcon(tool); + this._iconCache[tool] = fallbackCanvas; + // Start loading the SVG icon + const loadPromise = new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + this._iconCache[tool] = img; + this._loadingPromises.delete(tool); + log.debug(`Successfully loaded icon for tool: ${tool}`); + resolve(img); + }; + img.onerror = (error) => { + log.warn(`Failed to load SVG icon for tool: ${tool}, using fallback`); + this._loadingPromises.delete(tool); + // Keep the fallback canvas in cache + reject(error); + }; + const iconData = LAYERFORGE_TOOL_ICONS[tool]; + if (iconData) { + img.src = iconData; + } + else { + log.warn(`No icon data found for tool: ${tool}`); + reject(createValidationError(`No icon data for tool: ${tool}`, { tool, availableTools: Object.keys(LAYERFORGE_TOOL_ICONS) })); + } + }); + this._loadingPromises.set(tool, loadPromise); + return loadPromise; + }, 'IconLoader.loadIcon'); + log.info('IconLoader initialized'); } /** * Create a fallback canvas icon with colored background and text diff --git a/js/utils/ImageAnalysis.js b/js/utils/ImageAnalysis.js index 45c7334..35c05c7 100644 --- a/js/utils/ImageAnalysis.js +++ b/js/utils/ImageAnalysis.js @@ -1,5 +1,6 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('ImageAnalysis'); /** * Creates a distance field mask based on the alpha channel of an image. @@ -8,7 +9,18 @@ const log = createModuleLogger('ImageAnalysis'); * @param blendArea - The percentage (0-100) of the area to apply blending * @returns HTMLCanvasElement containing the distance field mask */ -export function createDistanceFieldMask(image, blendArea) { +/** + * Synchronous version of createDistanceFieldMask for use in synchronous rendering + */ +export function createDistanceFieldMaskSync(image, blendArea) { + if (!image) { + log.error("Image is required for distance field mask"); + return createCanvas(1, 1).canvas; + } + if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) { + log.error("Blend area must be a number between 0 and 100"); + return createCanvas(1, 1).canvas; + } const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true }); if (!ctx) { log.error('Failed to create canvas context for distance field mask'); @@ -84,6 +96,12 @@ export function createDistanceFieldMask(image, blendArea) { ctx.putImageData(maskData, 0, 0); return canvas; } +/** + * Async version with error handling for use in async contexts + */ +export const createDistanceFieldMask = withErrorHandling(function (image, blendArea) { + return createDistanceFieldMaskSync(image, blendArea); +}, 'createDistanceFieldMask'); /** * Calculates the Euclidean distance transform of a binary mask. * Uses a two-pass algorithm for efficiency. @@ -183,7 +201,16 @@ function calculateDistanceFromEdges(width, height) { * @param blendArea - The percentage (0-100) of the area to apply blending * @returns HTMLCanvasElement containing the radial gradient mask */ -export function createRadialGradientMask(width, height, blendArea) { +export const createRadialGradientMask = withErrorHandling(function (width, height, blendArea) { + if (typeof width !== 'number' || width <= 0) { + throw createValidationError("Width must be a positive number", { width }); + } + if (typeof height !== 'number' || height <= 0) { + throw createValidationError("Height must be a positive number", { height }); + } + if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) { + throw createValidationError("Blend area must be a number between 0 and 100", { blendArea }); + } const { canvas, ctx } = createCanvas(width, height); if (!ctx) { log.error('Failed to create canvas context for radial gradient mask'); @@ -200,4 +227,4 @@ export function createRadialGradientMask(width, height, blendArea) { ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); return canvas; -} +}, 'createRadialGradientMask'); diff --git a/js/utils/ImageUploadUtils.js b/js/utils/ImageUploadUtils.js index 312aa2b..d4cef58 100644 --- a/js/utils/ImageUploadUtils.js +++ b/js/utils/ImageUploadUtils.js @@ -1,5 +1,6 @@ import { api } from "../../../scripts/api.js"; import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js"; const log = createModuleLogger('ImageUploadUtils'); /** * Uploads an image blob to ComfyUI server and returns image element @@ -7,7 +8,13 @@ const log = createModuleLogger('ImageUploadUtils'); * @param options - Upload options * @returns Promise with upload result */ -export async function uploadImageBlob(blob, options = {}) { +export const uploadImageBlob = withErrorHandling(async function (blob, options = {}) { + if (!blob) { + throw createValidationError("Blob is required", { blob }); + } + if (blob.size === 0) { + throw createValidationError("Blob cannot be empty", { blobSize: blob.size }); + } const { filenamePrefix = 'layerforge', overwrite = true, type = 'temp', nodeId } = options; // Generate unique filename const timestamp = Date.now(); @@ -30,9 +37,12 @@ export async function uploadImageBlob(blob, options = {}) { body: formData, }); if (!response.ok) { - const error = new Error(`Failed to upload image: ${response.statusText}`); - log.error('Image upload failed:', error); - throw error; + throw createNetworkError(`Failed to upload image: ${response.statusText}`, { + status: response.status, + statusText: response.statusText, + filename, + blobSize: blob.size + }); } const data = await response.json(); log.debug('Image uploaded successfully:', data); @@ -52,7 +62,7 @@ export async function uploadImageBlob(blob, options = {}) { }; imageElement.onerror = (error) => { log.error("Failed to load uploaded image", error); - reject(new Error("Failed to load uploaded image")); + reject(createNetworkError("Failed to load uploaded image", { error, imageUrl, filename })); }; imageElement.src = imageUrl; }); @@ -62,14 +72,17 @@ export async function uploadImageBlob(blob, options = {}) { imageUrl, imageElement }; -} +}, 'uploadImageBlob'); /** * Uploads canvas content as image blob * @param canvas - Canvas element or Canvas object with canvasLayers * @param options - Upload options * @returns Promise with upload result */ -export async function uploadCanvasAsImage(canvas, options = {}) { +export const uploadCanvasAsImage = withErrorHandling(async function (canvas, options = {}) { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } let blob = null; // Handle different canvas types if (canvas.canvasLayers && typeof canvas.canvasLayers.getFlattenedCanvasAsBlob === 'function') { @@ -81,26 +94,37 @@ export async function uploadCanvasAsImage(canvas, options = {}) { blob = await new Promise(resolve => canvas.toBlob(resolve)); } else { - throw new Error("Unsupported canvas type"); + throw createValidationError("Unsupported canvas type", { + canvas, + hasCanvasLayers: !!canvas.canvasLayers, + isHTMLCanvas: canvas instanceof HTMLCanvasElement + }); } if (!blob) { - throw new Error("Failed to generate canvas blob"); + throw createValidationError("Failed to generate canvas blob", { canvas, options }); } return uploadImageBlob(blob, options); -} +}, 'uploadCanvasAsImage'); /** * Uploads canvas with mask as image blob * @param canvas - Canvas object with canvasLayers * @param options - Upload options * @returns Promise with upload result */ -export async function uploadCanvasWithMaskAsImage(canvas, options = {}) { +export const uploadCanvasWithMaskAsImage = withErrorHandling(async function (canvas, options = {}) { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } if (!canvas.canvasLayers || typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob !== 'function') { - throw new Error("Canvas does not support mask operations"); + throw createValidationError("Canvas does not support mask operations", { + canvas, + hasCanvasLayers: !!canvas.canvasLayers, + hasMaskMethod: !!(canvas.canvasLayers && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') + }); } const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); if (!blob) { - throw new Error("Failed to generate canvas with mask blob"); + throw createValidationError("Failed to generate canvas with mask blob", { canvas, options }); } return uploadImageBlob(blob, options); -} +}, 'uploadCanvasWithMaskAsImage'); diff --git a/js/utils/MaskProcessingUtils.js b/js/utils/MaskProcessingUtils.js index ae36b68..091a399 100644 --- a/js/utils/MaskProcessingUtils.js +++ b/js/utils/MaskProcessingUtils.js @@ -1,5 +1,6 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('MaskProcessingUtils'); /** * Processes an image to create a mask with inverted alpha channel @@ -7,7 +8,10 @@ const log = createModuleLogger('MaskProcessingUtils'); * @param options - Processing options * @returns Promise with processed mask as HTMLCanvasElement */ -export async function processImageToMask(sourceImage, options = {}) { +export const processImageToMask = withErrorHandling(async function (sourceImage, options = {}) { + if (!sourceImage) { + throw createValidationError("Source image is required", { sourceImage }); + } const { targetWidth = sourceImage.width, targetHeight = sourceImage.height, invertAlpha = true, maskColor = { r: 255, g: 255, b: 255 } } = options; log.debug('Processing image to mask:', { sourceSize: { width: sourceImage.width, height: sourceImage.height }, @@ -18,7 +22,7 @@ export async function processImageToMask(sourceImage, options = {}) { // Create temporary canvas for processing const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { - throw new Error("Failed to get 2D context for mask processing"); + throw createValidationError("Failed to get 2D context for mask processing"); } // Draw the source image tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight); @@ -44,7 +48,7 @@ export async function processImageToMask(sourceImage, options = {}) { tempCtx.putImageData(imageData, 0, 0); log.debug('Mask processing completed'); return tempCanvas; -} +}, 'processImageToMask'); /** * Processes image data with custom pixel transformation * @param sourceImage - Source image or canvas element @@ -52,11 +56,17 @@ export async function processImageToMask(sourceImage, options = {}) { * @param options - Processing options * @returns Promise with processed image as HTMLCanvasElement */ -export async function processImageWithTransform(sourceImage, pixelTransform, options = {}) { +export const processImageWithTransform = withErrorHandling(async function (sourceImage, pixelTransform, options = {}) { + if (!sourceImage) { + throw createValidationError("Source image is required", { sourceImage }); + } + if (!pixelTransform || typeof pixelTransform !== 'function') { + throw createValidationError("Pixel transform function is required", { pixelTransform }); + } const { targetWidth = sourceImage.width, targetHeight = sourceImage.height } = options; const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { - throw new Error("Failed to get 2D context for image processing"); + throw createValidationError("Failed to get 2D context for image processing"); } tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight); const imageData = tempCtx.getImageData(0, 0, targetWidth, targetHeight); @@ -70,28 +80,37 @@ export async function processImageWithTransform(sourceImage, pixelTransform, opt } tempCtx.putImageData(imageData, 0, 0); return tempCanvas; -} +}, 'processImageWithTransform'); /** * Crops an image to a specific region * @param sourceImage - Source image or canvas * @param cropArea - Crop area {x, y, width, height} * @returns Promise with cropped image as HTMLCanvasElement */ -export async function cropImage(sourceImage, cropArea) { +export const cropImage = withErrorHandling(async function (sourceImage, cropArea) { + if (!sourceImage) { + throw createValidationError("Source image is required", { sourceImage }); + } + if (!cropArea || typeof cropArea !== 'object') { + throw createValidationError("Crop area is required", { cropArea }); + } const { x, y, width, height } = cropArea; + if (width <= 0 || height <= 0) { + throw createValidationError("Crop area must have positive width and height", { cropArea }); + } log.debug('Cropping image:', { sourceSize: { width: sourceImage.width, height: sourceImage.height }, cropArea }); const { canvas, ctx } = createCanvas(width, height); if (!ctx) { - throw new Error("Failed to get 2D context for image cropping"); + throw createValidationError("Failed to get 2D context for image cropping"); } ctx.drawImage(sourceImage, x, y, width, height, // Source rectangle 0, 0, width, height // Destination rectangle ); return canvas; -} +}, 'cropImage'); /** * Applies a mask to an image using viewport positioning * @param maskImage - Mask image or canvas @@ -101,7 +120,16 @@ export async function cropImage(sourceImage, cropArea) { * @param maskColor - Mask color (default: white) * @returns Promise with processed mask for viewport */ -export async function processMaskForViewport(maskImage, targetWidth, targetHeight, viewportOffset, maskColor = { r: 255, g: 255, b: 255 }) { +export const processMaskForViewport = withErrorHandling(async function (maskImage, targetWidth, targetHeight, viewportOffset, maskColor = { r: 255, g: 255, b: 255 }) { + if (!maskImage) { + throw createValidationError("Mask image is required", { maskImage }); + } + if (!viewportOffset || typeof viewportOffset !== 'object') { + throw createValidationError("Viewport offset is required", { viewportOffset }); + } + if (targetWidth <= 0 || targetHeight <= 0) { + throw createValidationError("Target dimensions must be positive", { targetWidth, targetHeight }); + } log.debug("Processing mask for viewport:", { sourceSize: { width: maskImage.width, height: maskImage.height }, targetSize: { width: targetWidth, height: targetHeight }, @@ -109,7 +137,7 @@ export async function processMaskForViewport(maskImage, targetWidth, targetHeigh }); const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { - throw new Error("Failed to get 2D context for viewport mask processing"); + throw createValidationError("Failed to get 2D context for viewport mask processing"); } // Calculate source coordinates based on viewport offset const sourceX = -viewportOffset.x; @@ -139,4 +167,4 @@ export async function processMaskForViewport(maskImage, targetWidth, targetHeigh tempCtx.putImageData(imageData, 0, 0); log.debug("Viewport mask processing completed"); return tempCanvas; -} +}, 'processMaskForViewport'); diff --git a/js/utils/PreviewUtils.js b/js/utils/PreviewUtils.js index c6bce1d..94eda61 100644 --- a/js/utils/PreviewUtils.js +++ b/js/utils/PreviewUtils.js @@ -1,4 +1,5 @@ import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('PreviewUtils'); /** * Creates a preview image from canvas and updates node @@ -7,7 +8,13 @@ const log = createModuleLogger('PreviewUtils'); * @param options - Preview options * @returns Promise with created Image element */ -export async function createPreviewFromCanvas(canvas, node, options = {}) { +export const createPreviewFromCanvas = withErrorHandling(async function (canvas, node, options = {}) { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + if (!node) { + throw createValidationError("Node is required", { node }); + } const { includeMask = true, updateNodeImages = true, customBlob } = options; log.debug('Creating preview from canvas:', { includeMask, @@ -19,7 +26,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) { // Get blob from canvas if not provided if (!blob) { if (!canvas.canvasLayers) { - throw new Error("Canvas does not have canvasLayers"); + throw createValidationError("Canvas does not have canvasLayers", { canvas }); } if (includeMask && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') { blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); @@ -28,11 +35,14 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) { blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob(); } else { - throw new Error("Canvas does not support required blob generation methods"); + throw createValidationError("Canvas does not support required blob generation methods", { + canvas, + availableMethods: Object.getOwnPropertyNames(canvas.canvasLayers) + }); } } if (!blob) { - throw new Error("Failed to generate canvas blob for preview"); + throw createValidationError("Failed to generate canvas blob for preview", { canvas, options }); } // Create preview image const previewImage = new Image(); @@ -49,7 +59,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) { }; previewImage.onerror = (error) => { log.error("Failed to load preview image", error); - reject(new Error("Failed to load preview image")); + reject(createValidationError("Failed to load preview image", { error, blob: blob?.size })); }; }); // Update node images if requested @@ -58,7 +68,7 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) { log.debug("Node images updated with new preview"); } return previewImage; -} +}, 'createPreviewFromCanvas'); /** * Creates a preview image from a blob * @param blob - Image blob @@ -66,7 +76,13 @@ export async function createPreviewFromCanvas(canvas, node, options = {}) { * @param updateNodeImages - Whether to update node.imgs (default: false) * @returns Promise with created Image element */ -export async function createPreviewFromBlob(blob, node, updateNodeImages = false) { +export const createPreviewFromBlob = withErrorHandling(async function (blob, node, updateNodeImages = false) { + if (!blob) { + throw createValidationError("Blob is required", { blob }); + } + if (blob.size === 0) { + throw createValidationError("Blob cannot be empty", { blobSize: blob.size }); + } log.debug('Creating preview from blob:', { blobSize: blob.size, updateNodeImages, @@ -84,7 +100,7 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false }; previewImage.onerror = (error) => { log.error("Failed to load preview image from blob", error); - reject(new Error("Failed to load preview image from blob")); + reject(createValidationError("Failed to load preview image from blob", { error, blobSize: blob.size })); }; }); if (updateNodeImages && node) { @@ -92,7 +108,7 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false log.debug("Node images updated with blob preview"); } return previewImage; -} +}, 'createPreviewFromBlob'); /** * Updates node preview after canvas changes * @param canvas - Canvas object @@ -100,7 +116,13 @@ export async function createPreviewFromBlob(blob, node, updateNodeImages = false * @param includeMask - Whether to include mask in preview * @returns Promise with updated preview image */ -export async function updateNodePreview(canvas, node, includeMask = true) { +export const updateNodePreview = withErrorHandling(async function (canvas, node, includeMask = true) { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + if (!node) { + throw createValidationError("Node is required", { node }); + } log.info('Updating node preview:', { nodeId: node.id, includeMask @@ -119,7 +141,7 @@ export async function updateNodePreview(canvas, node, includeMask = true) { }); log.info('Node preview updated successfully'); return previewImage; -} +}, 'updateNodePreview'); /** * Clears node preview images * @param node - ComfyUI node @@ -154,8 +176,17 @@ export function getCurrentPreview(node) { * @param processor - Custom processing function that takes canvas and returns blob * @returns Promise with processed preview image */ -export async function createCustomPreview(canvas, node, processor) { +export const createCustomPreview = withErrorHandling(async function (canvas, node, processor) { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + if (!node) { + throw createValidationError("Node is required", { node }); + } + if (!processor || typeof processor !== 'function') { + throw createValidationError("Processor function is required", { processor }); + } log.debug('Creating custom preview:', { nodeId: node.id }); const blob = await processor(canvas); return createPreviewFromBlob(blob, node, true); -} +}, 'createCustomPreview'); diff --git a/js/utils/ResourceManager.js b/js/utils/ResourceManager.js index fc598d0..a652d36 100644 --- a/js/utils/ResourceManager.js +++ b/js/utils/ResourceManager.js @@ -1,6 +1,13 @@ // @ts-ignore import { $el } from "../../../scripts/ui.js"; -export function addStylesheet(url) { +import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js"; +const log = createModuleLogger('ResourceManager'); +export const addStylesheet = withErrorHandling(function (url) { + if (!url) { + throw createValidationError("URL is required", { url }); + } + log.debug('Adding stylesheet:', { url }); if (url.endsWith(".js")) { url = url.substr(0, url.length - 2) + "css"; } @@ -10,8 +17,12 @@ export function addStylesheet(url) { type: "text/css", href: url.startsWith("http") ? url : getUrl(url), }); -} + log.debug('Stylesheet added successfully:', { finalUrl: url }); +}, 'addStylesheet'); export function getUrl(path, baseUrl) { + if (!path) { + throw createValidationError("Path is required", { path }); + } if (baseUrl) { return new URL(path, baseUrl).toString(); } @@ -20,11 +31,21 @@ export function getUrl(path, baseUrl) { return new URL("../" + path, import.meta.url).toString(); } } -export async function loadTemplate(path, baseUrl) { +export const loadTemplate = withErrorHandling(async function (path, baseUrl) { + if (!path) { + throw createValidationError("Path is required", { path }); + } const url = getUrl(path, baseUrl); + log.debug('Loading template:', { path, url }); const response = await fetch(url); if (!response.ok) { - throw new Error(`Failed to load template: ${url}`); + throw createNetworkError(`Failed to load template: ${url}`, { + url, + status: response.status, + statusText: response.statusText + }); } - return await response.text(); -} + const content = await response.text(); + log.debug('Template loaded successfully:', { path, contentLength: content.length }); + return content; +}, 'loadTemplate'); diff --git a/js/utils/WebSocketManager.js b/js/utils/WebSocketManager.js index c805b69..d0b8e26 100644 --- a/js/utils/WebSocketManager.js +++ b/js/utils/WebSocketManager.js @@ -1,30 +1,23 @@ import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js"; const log = createModuleLogger('WebSocketManager'); class WebSocketManager { constructor(url) { this.url = url; - this.socket = null; - this.messageQueue = []; - this.isConnecting = false; - this.reconnectAttempts = 0; - this.maxReconnectAttempts = 10; - this.reconnectInterval = 5000; // 5 seconds - this.ackCallbacks = new Map(); - this.messageIdCounter = 0; - this.connect(); - } - connect() { - if (this.socket && this.socket.readyState === WebSocket.OPEN) { - log.debug("WebSocket is already open."); - return; - } - if (this.isConnecting) { - log.debug("Connection attempt already in progress."); - return; - } - this.isConnecting = true; - log.info(`Connecting to WebSocket at ${this.url}...`); - try { + this.connect = withErrorHandling(() => { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + log.debug("WebSocket is already open."); + return; + } + if (this.isConnecting) { + log.debug("Connection attempt already in progress."); + return; + } + if (!this.url) { + throw createValidationError("WebSocket URL is required", { url: this.url }); + } + this.isConnecting = true; + log.info(`Connecting to WebSocket at ${this.url}...`); this.socket = new WebSocket(this.url); this.socket.onopen = () => { this.isConnecting = false; @@ -61,14 +54,71 @@ class WebSocketManager { }; this.socket.onerror = (error) => { this.isConnecting = false; - log.error("WebSocket error:", error); + throw createNetworkError("WebSocket connection error", { error, url: this.url }); }; - } - catch (error) { - this.isConnecting = false; - log.error("Failed to create WebSocket connection:", error); - this.handleReconnect(); - } + }, 'WebSocketManager.connect'); + this.sendMessage = withErrorHandling(async (data, requiresAck = false) => { + if (!data || typeof data !== 'object') { + throw createValidationError("Message data is required", { data }); + } + const nodeId = data.nodeId; + if (requiresAck && !nodeId) { + throw createValidationError("A nodeId is required for messages that need acknowledgment", { data, requiresAck }); + } + return new Promise((resolve, reject) => { + const message = JSON.stringify(data); + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(message); + log.debug("Sent message:", data); + if (requiresAck && nodeId) { + log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`); + const timeout = setTimeout(() => { + this.ackCallbacks.delete(nodeId); + reject(createNetworkError(`ACK timeout for nodeId ${nodeId}`, { nodeId, timeout: 10000 })); + log.warn(`ACK timeout for nodeId ${nodeId}.`); + }, 10000); // 10-second timeout + this.ackCallbacks.set(nodeId, { + resolve: (responseData) => { + clearTimeout(timeout); + resolve(responseData); + }, + reject: (error) => { + clearTimeout(timeout); + reject(error); + } + }); + } + else { + resolve(); // Resolve immediately if no ACK is needed + } + } + else { + log.warn("WebSocket not open. Queuing message."); + this.messageQueue.push(message); + if (!this.isConnecting) { + this.connect(); + } + if (requiresAck) { + reject(createNetworkError("Cannot send message with ACK required while disconnected", { + socketState: this.socket?.readyState, + isConnecting: this.isConnecting + })); + } + else { + resolve(); + } + } + }); + }, 'WebSocketManager.sendMessage'); + this.socket = null; + this.messageQueue = []; + this.isConnecting = false; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 10; + this.reconnectInterval = 5000; // 5 seconds + this.ackCallbacks = new Map(); + this.messageIdCounter = 0; + this.connect(); } handleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { @@ -80,53 +130,6 @@ class WebSocketManager { log.error("Max reconnect attempts reached. Giving up."); } } - sendMessage(data, requiresAck = false) { - return new Promise((resolve, reject) => { - const nodeId = data.nodeId; - if (requiresAck && !nodeId) { - return reject(new Error("A nodeId is required for messages that need acknowledgment.")); - } - const message = JSON.stringify(data); - if (this.socket && this.socket.readyState === WebSocket.OPEN) { - this.socket.send(message); - log.debug("Sent message:", data); - if (requiresAck && nodeId) { - log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`); - const timeout = setTimeout(() => { - this.ackCallbacks.delete(nodeId); - reject(new Error(`ACK timeout for nodeId ${nodeId}`)); - log.warn(`ACK timeout for nodeId ${nodeId}.`); - }, 10000); // 10-second timeout - this.ackCallbacks.set(nodeId, { - resolve: (responseData) => { - clearTimeout(timeout); - resolve(responseData); - }, - reject: (error) => { - clearTimeout(timeout); - reject(error); - } - }); - } - else { - resolve(); // Resolve immediately if no ACK is needed - } - } - else { - log.warn("WebSocket not open. Queuing message."); - this.messageQueue.push(message); - if (!this.isConnecting) { - this.connect(); - } - if (requiresAck) { - reject(new Error("Cannot send message with ACK required while disconnected.")); - } - else { - resolve(); - } - } - }); - } flushMessageQueue() { log.debug(`Flushing ${this.messageQueue.length} queued messages.`); while (this.messageQueue.length > 0) { diff --git a/js/utils/mask_utils.js b/js/utils/mask_utils.js index 671405b..0f92098 100644 --- a/js/utils/mask_utils.js +++ b/js/utils/mask_utils.js @@ -1,4 +1,5 @@ import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('MaskUtils'); export function new_editor(app) { if (!app) @@ -125,24 +126,25 @@ export function press_maskeditor_cancel(app) { * @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia * @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski) */ -export function start_mask_editor_with_predefined_mask(canvasInstance, maskImage, sendCleanImage = true) { - if (!canvasInstance || !maskImage) { - log.error('Canvas instance and mask image are required'); - return; +export const start_mask_editor_with_predefined_mask = withErrorHandling(function (canvasInstance, maskImage, sendCleanImage = true) { + if (!canvasInstance) { + throw createValidationError('Canvas instance is required', { canvasInstance }); + } + if (!maskImage) { + throw createValidationError('Mask image is required', { maskImage }); } canvasInstance.startMaskEditor(maskImage, sendCleanImage); -} +}, 'start_mask_editor_with_predefined_mask'); /** * Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska) * @param {Canvas} canvasInstance - Instancja Canvas */ -export function start_mask_editor_auto(canvasInstance) { +export const start_mask_editor_auto = withErrorHandling(function (canvasInstance) { if (!canvasInstance) { - log.error('Canvas instance is required'); - return; + throw createValidationError('Canvas instance is required', { canvasInstance }); } canvasInstance.startMaskEditor(null, true); -} +}, 'start_mask_editor_auto'); // Duplikowane funkcje zostały przeniesione do ImageUtils.ts: // - create_mask_from_image_src -> createMaskFromImageSrc // - canvas_to_mask_image -> canvasToMaskImage diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts index 0baad27..0e3e1a6 100644 --- a/src/CanvasLayers.ts +++ b/src/CanvasLayers.ts @@ -8,7 +8,7 @@ import {app} from "../../scripts/app.js"; // @ts-ignore import {ComfyApp} from "../../scripts/app.js"; import { ClipboardManager } from "./utils/ClipboardManager.js"; -import { createDistanceFieldMask } from "./utils/ImageAnalysis.js"; +import { createDistanceFieldMaskSync } from "./utils/ImageAnalysis.js"; import type { Canvas } from './Canvas'; import type { Layer, Point, AddMode, ClipboardPreference } from './types'; @@ -421,9 +421,9 @@ export class CanvasLayers { const needsBlendAreaEffect = blendArea > 0; if (needsBlendAreaEffect) { - log.info(`Applying blend area effect for layer ${layer.id}`); + log.debug(`Applying blend area effect for layer ${layer.id}, blendArea: ${blendArea}%`); // Get or create distance field mask - let maskCanvas = this.getDistanceFieldMask(layer.image, blendArea); + const maskCanvas = this.getDistanceFieldMaskSync(layer.image, blendArea); if (maskCanvas) { // Create a temporary canvas for the masked layer @@ -463,7 +463,7 @@ export class CanvasLayers { ctx.restore(); } - private getDistanceFieldMask(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null { + private getDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement | null { // Check cache first let imageCache = this.distanceFieldCache.get(image); if (!imageCache) { @@ -475,7 +475,7 @@ export class CanvasLayers { if (!maskCanvas) { try { log.info(`Creating distance field mask for blendArea: ${blendArea}%`); - maskCanvas = createDistanceFieldMask(image, blendArea); + maskCanvas = createDistanceFieldMaskSync(image, blendArea); log.info(`Distance field mask created successfully, size: ${maskCanvas.width}x${maskCanvas.height}`); imageCache.set(blendArea, maskCanvas); } catch (error) { diff --git a/src/utils/ClipboardManager.ts b/src/utils/ClipboardManager.ts index e38374c..ecf4ef6 100644 --- a/src/utils/ClipboardManager.ts +++ b/src/utils/ClipboardManager.ts @@ -1,5 +1,6 @@ import {createModuleLogger} from "./LoggerUtils.js"; import { showNotification, showInfoNotification } from "./NotificationUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js"; // @ts-ignore import {api} from "../../../scripts/api.js"; @@ -26,62 +27,51 @@ export class ClipboardManager { * @param {ClipboardPreference} preference - Clipboard preference ('system' or 'clipspace') * @returns {Promise} - True if successful, false otherwise */ - async handlePaste(addMode: AddMode = 'mouse', preference: ClipboardPreference = 'system'): Promise { - try { - log.info(`ClipboardManager handling paste with preference: ${preference}`); + handlePaste = withErrorHandling(async (addMode: AddMode = 'mouse', preference: ClipboardPreference = 'system'): Promise => { + log.info(`ClipboardManager handling paste with preference: ${preference}`); - if (this.canvas.canvasLayers.internalClipboard.length > 0) { - log.info("Found layers in internal clipboard, pasting layers"); - this.canvas.canvasLayers.pasteLayers(); + if (this.canvas.canvasLayers.internalClipboard.length > 0) { + log.info("Found layers in internal clipboard, pasting layers"); + this.canvas.canvasLayers.pasteLayers(); + return true; + } + + if (preference === 'clipspace') { + log.info("Attempting paste from ComfyUI Clipspace"); + const success = await this.tryClipspacePaste(addMode); + if (success) { return true; } - - if (preference === 'clipspace') { - log.info("Attempting paste from ComfyUI Clipspace"); - const success = await this.tryClipspacePaste(addMode); - if (success) { - return true; - } - log.info("No image found in ComfyUI Clipspace"); - } - - log.info("Attempting paste from system clipboard"); - return await this.trySystemClipboardPaste(addMode); - - } catch (err) { - log.error("ClipboardManager paste operation failed:", err); - return false; + log.info("No image found in ComfyUI Clipspace"); } - } + + log.info("Attempting paste from system clipboard"); + return await this.trySystemClipboardPaste(addMode); + }, 'ClipboardManager.handlePaste'); /** * Attempts to paste from ComfyUI Clipspace * @param {AddMode} addMode - The mode for adding the layer * @returns {Promise} - True if successful, false otherwise */ - async tryClipspacePaste(addMode: AddMode): Promise { - try { - log.info("Attempting to paste from ComfyUI Clipspace"); - ComfyApp.pasteFromClipspace(this.canvas.node); + tryClipspacePaste = withErrorHandling(async (addMode: AddMode): Promise => { + log.info("Attempting to paste from ComfyUI Clipspace"); + ComfyApp.pasteFromClipspace(this.canvas.node); - if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) { - const clipspaceImage = this.canvas.node.imgs[0]; - if (clipspaceImage && clipspaceImage.src) { - log.info("Successfully got image from ComfyUI Clipspace"); - const img = new Image(); - img.onload = async () => { - await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode); - }; - img.src = clipspaceImage.src; - return true; - } + if (this.canvas.node.imgs && this.canvas.node.imgs.length > 0) { + const clipspaceImage = this.canvas.node.imgs[0]; + if (clipspaceImage && clipspaceImage.src) { + log.info("Successfully got image from ComfyUI Clipspace"); + const img = new Image(); + img.onload = async () => { + await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode); + }; + img.src = clipspaceImage.src; + return true; } - return false; - } catch (clipspaceError) { - log.warn("ComfyUI Clipspace paste failed:", clipspaceError); - return false; } - } + return false; + }, 'ClipboardManager.tryClipspacePaste'); /** * System clipboard paste - handles both image data and text paths @@ -290,57 +280,57 @@ export class ClipboardManager { * @param {AddMode} addMode - The mode for adding the layer * @returns {Promise} - True if successful, false otherwise */ - async loadFileViaBackend(filePath: string, addMode: AddMode): Promise { - try { - log.info("Loading file via ComfyUI backend:", filePath); - - const response = await api.fetchApi("/ycnode/load_image_from_path", { - method: "POST", - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - file_path: filePath - }) - }); - - if (!response.ok) { - const errorData = await response.json(); - log.debug("Backend failed to load image:", errorData.error); - return false; - } - - const data = await response.json(); - - if (!data.success) { - log.debug("Backend returned error:", data.error); - return false; - } - - log.info("Successfully loaded image via ComfyUI backend:", filePath); - - const img = new Image(); - const success: boolean = await new Promise((resolve) => { - img.onload = async () => { - log.info("Successfully loaded image from backend response"); - await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode); - resolve(true); - }; - img.onerror = () => { - log.warn("Failed to load image from backend response"); - resolve(false); - }; - - img.src = data.image_data; - }); - - return success; - - } catch (error) { - log.debug("Error loading file via ComfyUI backend:", error); - return false; + loadFileViaBackend = withErrorHandling(async (filePath: string, addMode: AddMode): Promise => { + if (!filePath) { + throw createValidationError("File path is required", { filePath }); } - } + + log.info("Loading file via ComfyUI backend:", filePath); + + const response = await api.fetchApi("/ycnode/load_image_from_path", { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + file_path: filePath + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw createNetworkError(`Backend failed to load image: ${errorData.error}`, { + filePath, + status: response.status, + statusText: response.statusText + }); + } + + const data = await response.json(); + + if (!data.success) { + throw createFileError(`Backend returned error: ${data.error}`, { filePath, backendError: data.error }); + } + + log.info("Successfully loaded image via ComfyUI backend:", filePath); + + const img = new Image(); + const success: boolean = await new Promise((resolve) => { + img.onload = async () => { + log.info("Successfully loaded image from backend response"); + await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode); + resolve(true); + }; + img.onerror = () => { + log.warn("Failed to load image from backend response"); + resolve(false); + }; + + img.src = data.image_data; + }); + + return success; + }, 'ClipboardManager.loadFileViaBackend'); /** * Prompts the user to select a file when a local path is detected diff --git a/src/utils/IconLoader.ts b/src/utils/IconLoader.ts index 9896924..1146406 100644 --- a/src/utils/IconLoader.ts +++ b/src/utils/IconLoader.ts @@ -1,5 +1,6 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('IconLoader'); @@ -81,24 +82,25 @@ export class IconLoader { /** * Preload all LayerForge tool icons */ - preloadToolIcons(): Promise { + preloadToolIcons = withErrorHandling(async (): Promise => { log.info('Starting to preload LayerForge tool icons'); const loadPromises = Object.keys(LAYERFORGE_TOOL_ICONS).map(tool => { return this.loadIcon(tool); }); - return Promise.all(loadPromises).then(() => { - log.info(`Successfully preloaded ${loadPromises.length} tool icons`); - }).catch(error => { - log.error('Error preloading tool icons:', error); - }); - } + await Promise.all(loadPromises); + log.info(`Successfully preloaded ${loadPromises.length} tool icons`); + }, 'IconLoader.preloadToolIcons'); /** * Load a specific icon by tool name */ - async loadIcon(tool: string): Promise { + loadIcon = withErrorHandling(async (tool: string): Promise => { + if (!tool) { + throw createValidationError("Tool name is required", { tool }); + } + // Check if already cached if (this._iconCache[tool] && this._iconCache[tool] instanceof HTMLImageElement) { return this._iconCache[tool] as HTMLImageElement; @@ -136,13 +138,13 @@ export class IconLoader { img.src = iconData; } else { log.warn(`No icon data found for tool: ${tool}`); - reject(new Error(`No icon data for tool: ${tool}`)); + reject(createValidationError(`No icon data for tool: ${tool}`, { tool, availableTools: Object.keys(LAYERFORGE_TOOL_ICONS) })); } }); this._loadingPromises.set(tool, loadPromise); return loadPromise; - } + }, 'IconLoader.loadIcon'); /** * Create a fallback canvas icon with colored background and text diff --git a/src/utils/ImageAnalysis.ts b/src/utils/ImageAnalysis.ts index 9bfda94..ed350e7 100644 --- a/src/utils/ImageAnalysis.ts +++ b/src/utils/ImageAnalysis.ts @@ -1,5 +1,6 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('ImageAnalysis'); @@ -10,7 +11,19 @@ const log = createModuleLogger('ImageAnalysis'); * @param blendArea - The percentage (0-100) of the area to apply blending * @returns HTMLCanvasElement containing the distance field mask */ -export function createDistanceFieldMask(image: HTMLImageElement, blendArea: number): HTMLCanvasElement { +/** + * Synchronous version of createDistanceFieldMask for use in synchronous rendering + */ +export function createDistanceFieldMaskSync(image: HTMLImageElement, blendArea: number): HTMLCanvasElement { + if (!image) { + log.error("Image is required for distance field mask"); + return createCanvas(1, 1).canvas; + } + if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) { + log.error("Blend area must be a number between 0 and 100"); + return createCanvas(1, 1).canvas; + } + const { canvas, ctx } = createCanvas(image.width, image.height, '2d', { willReadFrequently: true }); if (!ctx) { @@ -95,6 +108,13 @@ export function createDistanceFieldMask(image: HTMLImageElement, blendArea: numb return canvas; } +/** + * Async version with error handling for use in async contexts + */ +export const createDistanceFieldMask = withErrorHandling(function(image: HTMLImageElement, blendArea: number): HTMLCanvasElement { + return createDistanceFieldMaskSync(image, blendArea); +}, 'createDistanceFieldMask'); + /** * Calculates the Euclidean distance transform of a binary mask. * Uses a two-pass algorithm for efficiency. @@ -214,7 +234,17 @@ function calculateDistanceFromEdges(width: number, height: number): Float32Array * @param blendArea - The percentage (0-100) of the area to apply blending * @returns HTMLCanvasElement containing the radial gradient mask */ -export function createRadialGradientMask(width: number, height: number, blendArea: number): HTMLCanvasElement { +export const createRadialGradientMask = withErrorHandling(function(width: number, height: number, blendArea: number): HTMLCanvasElement { + if (typeof width !== 'number' || width <= 0) { + throw createValidationError("Width must be a positive number", { width }); + } + if (typeof height !== 'number' || height <= 0) { + throw createValidationError("Height must be a positive number", { height }); + } + if (typeof blendArea !== 'number' || blendArea < 0 || blendArea > 100) { + throw createValidationError("Blend area must be a number between 0 and 100", { blendArea }); + } + const { canvas, ctx } = createCanvas(width, height); if (!ctx) { @@ -236,4 +266,4 @@ export function createRadialGradientMask(width: number, height: number, blendAre ctx.fillRect(0, 0, width, height); return canvas; -} +}, 'createRadialGradientMask'); diff --git a/src/utils/ImageUploadUtils.ts b/src/utils/ImageUploadUtils.ts index 28504cc..00e79b5 100644 --- a/src/utils/ImageUploadUtils.ts +++ b/src/utils/ImageUploadUtils.ts @@ -1,5 +1,6 @@ import { api } from "../../../scripts/api.js"; import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js"; const log = createModuleLogger('ImageUploadUtils'); @@ -35,7 +36,14 @@ export interface UploadImageResult { * @param options - Upload options * @returns Promise with upload result */ -export async function uploadImageBlob(blob: Blob, options: UploadImageOptions = {}): Promise { +export const uploadImageBlob = withErrorHandling(async function(blob: Blob, options: UploadImageOptions = {}): Promise { + if (!blob) { + throw createValidationError("Blob is required", { blob }); + } + if (blob.size === 0) { + throw createValidationError("Blob cannot be empty", { blobSize: blob.size }); + } + const { filenamePrefix = 'layerforge', overwrite = true, @@ -68,9 +76,12 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions = }); if (!response.ok) { - const error = new Error(`Failed to upload image: ${response.statusText}`); - log.error('Image upload failed:', error); - throw error; + throw createNetworkError(`Failed to upload image: ${response.statusText}`, { + status: response.status, + statusText: response.statusText, + filename, + blobSize: blob.size + }); } const data = await response.json(); @@ -93,7 +104,7 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions = }; imageElement.onerror = (error) => { log.error("Failed to load uploaded image", error); - reject(new Error("Failed to load uploaded image")); + reject(createNetworkError("Failed to load uploaded image", { error, imageUrl, filename })); }; imageElement.src = imageUrl; }); @@ -104,7 +115,7 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions = imageUrl, imageElement }; -} +}, 'uploadImageBlob'); /** * Uploads canvas content as image blob @@ -112,7 +123,11 @@ export async function uploadImageBlob(blob: Blob, options: UploadImageOptions = * @param options - Upload options * @returns Promise with upload result */ -export async function uploadCanvasAsImage(canvas: any, options: UploadImageOptions = {}): Promise { +export const uploadCanvasAsImage = withErrorHandling(async function(canvas: any, options: UploadImageOptions = {}): Promise { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + let blob: Blob | null = null; // Handle different canvas types @@ -123,15 +138,19 @@ export async function uploadCanvasAsImage(canvas: any, options: UploadImageOptio // Standard HTML Canvas blob = await new Promise(resolve => canvas.toBlob(resolve)); } else { - throw new Error("Unsupported canvas type"); + throw createValidationError("Unsupported canvas type", { + canvas, + hasCanvasLayers: !!canvas.canvasLayers, + isHTMLCanvas: canvas instanceof HTMLCanvasElement + }); } if (!blob) { - throw new Error("Failed to generate canvas blob"); + throw createValidationError("Failed to generate canvas blob", { canvas, options }); } return uploadImageBlob(blob, options); -} +}, 'uploadCanvasAsImage'); /** * Uploads canvas with mask as image blob @@ -139,15 +158,22 @@ export async function uploadCanvasAsImage(canvas: any, options: UploadImageOptio * @param options - Upload options * @returns Promise with upload result */ -export async function uploadCanvasWithMaskAsImage(canvas: any, options: UploadImageOptions = {}): Promise { +export const uploadCanvasWithMaskAsImage = withErrorHandling(async function(canvas: any, options: UploadImageOptions = {}): Promise { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } if (!canvas.canvasLayers || typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob !== 'function') { - throw new Error("Canvas does not support mask operations"); + throw createValidationError("Canvas does not support mask operations", { + canvas, + hasCanvasLayers: !!canvas.canvasLayers, + hasMaskMethod: !!(canvas.canvasLayers && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') + }); } const blob = await canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob(); if (!blob) { - throw new Error("Failed to generate canvas with mask blob"); + throw createValidationError("Failed to generate canvas with mask blob", { canvas, options }); } return uploadImageBlob(blob, options); -} +}, 'uploadCanvasWithMaskAsImage'); diff --git a/src/utils/MaskProcessingUtils.ts b/src/utils/MaskProcessingUtils.ts index b11a44d..89df6dc 100644 --- a/src/utils/MaskProcessingUtils.ts +++ b/src/utils/MaskProcessingUtils.ts @@ -1,6 +1,7 @@ import { createModuleLogger } from "./LoggerUtils.js"; import { createCanvas } from "./CommonUtils.js"; import { convertToImage } from "./ImageUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; const log = createModuleLogger('MaskProcessingUtils'); @@ -25,10 +26,14 @@ export interface MaskProcessingOptions { * @param options - Processing options * @returns Promise with processed mask as HTMLCanvasElement */ -export async function processImageToMask( +export const processImageToMask = withErrorHandling(async function( sourceImage: HTMLImageElement | HTMLCanvasElement, options: MaskProcessingOptions = {} ): Promise { + if (!sourceImage) { + throw createValidationError("Source image is required", { sourceImage }); + } + const { targetWidth = sourceImage.width, targetHeight = sourceImage.height, @@ -47,7 +52,7 @@ export async function processImageToMask( const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { - throw new Error("Failed to get 2D context for mask processing"); + throw createValidationError("Failed to get 2D context for mask processing"); } // Draw the source image @@ -79,7 +84,7 @@ export async function processImageToMask( log.debug('Mask processing completed'); return tempCanvas; -} +}, 'processImageToMask'); /** * Processes image data with custom pixel transformation @@ -88,11 +93,18 @@ export async function processImageToMask( * @param options - Processing options * @returns Promise with processed image as HTMLCanvasElement */ -export async function processImageWithTransform( +export const processImageWithTransform = withErrorHandling(async function( sourceImage: HTMLImageElement | HTMLCanvasElement, pixelTransform: (r: number, g: number, b: number, a: number, index: number) => [number, number, number, number], options: MaskProcessingOptions = {} ): Promise { + if (!sourceImage) { + throw createValidationError("Source image is required", { sourceImage }); + } + if (!pixelTransform || typeof pixelTransform !== 'function') { + throw createValidationError("Pixel transform function is required", { pixelTransform }); + } + const { targetWidth = sourceImage.width, targetHeight = sourceImage.height @@ -101,7 +113,7 @@ export async function processImageWithTransform( const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { - throw new Error("Failed to get 2D context for image processing"); + throw createValidationError("Failed to get 2D context for image processing"); } tempCtx.drawImage(sourceImage, 0, 0, targetWidth, targetHeight); @@ -118,7 +130,7 @@ export async function processImageWithTransform( tempCtx.putImageData(imageData, 0, 0); return tempCanvas; -} +}, 'processImageWithTransform'); /** * Crops an image to a specific region @@ -126,12 +138,23 @@ export async function processImageWithTransform( * @param cropArea - Crop area {x, y, width, height} * @returns Promise with cropped image as HTMLCanvasElement */ -export async function cropImage( +export const cropImage = withErrorHandling(async function( sourceImage: HTMLImageElement | HTMLCanvasElement, cropArea: { x: number; y: number; width: number; height: number } ): Promise { + if (!sourceImage) { + throw createValidationError("Source image is required", { sourceImage }); + } + if (!cropArea || typeof cropArea !== 'object') { + throw createValidationError("Crop area is required", { cropArea }); + } + const { x, y, width, height } = cropArea; + if (width <= 0 || height <= 0) { + throw createValidationError("Crop area must have positive width and height", { cropArea }); + } + log.debug('Cropping image:', { sourceSize: { width: sourceImage.width, height: sourceImage.height }, cropArea @@ -140,7 +163,7 @@ export async function cropImage( const { canvas, ctx } = createCanvas(width, height); if (!ctx) { - throw new Error("Failed to get 2D context for image cropping"); + throw createValidationError("Failed to get 2D context for image cropping"); } ctx.drawImage( @@ -150,7 +173,7 @@ export async function cropImage( ); return canvas; -} +}, 'cropImage'); /** * Applies a mask to an image using viewport positioning @@ -161,13 +184,23 @@ export async function cropImage( * @param maskColor - Mask color (default: white) * @returns Promise with processed mask for viewport */ -export async function processMaskForViewport( +export const processMaskForViewport = withErrorHandling(async function( maskImage: HTMLImageElement | HTMLCanvasElement, targetWidth: number, targetHeight: number, viewportOffset: { x: number; y: number }, maskColor: { r: number; g: number; b: number } = { r: 255, g: 255, b: 255 } ): Promise { + if (!maskImage) { + throw createValidationError("Mask image is required", { maskImage }); + } + if (!viewportOffset || typeof viewportOffset !== 'object') { + throw createValidationError("Viewport offset is required", { viewportOffset }); + } + if (targetWidth <= 0 || targetHeight <= 0) { + throw createValidationError("Target dimensions must be positive", { targetWidth, targetHeight }); + } + log.debug("Processing mask for viewport:", { sourceSize: { width: maskImage.width, height: maskImage.height }, targetSize: { width: targetWidth, height: targetHeight }, @@ -177,7 +210,7 @@ export async function processMaskForViewport( const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(targetWidth, targetHeight, '2d', { willReadFrequently: true }); if (!tempCtx) { - throw new Error("Failed to get 2D context for viewport mask processing"); + throw createValidationError("Failed to get 2D context for viewport mask processing"); } // Calculate source coordinates based on viewport offset @@ -214,4 +247,4 @@ export async function processMaskForViewport( log.debug("Viewport mask processing completed"); return tempCanvas; -} +}, 'processMaskForViewport'); diff --git a/src/utils/PreviewUtils.ts b/src/utils/PreviewUtils.ts index b55aa7b..4e9ab7e 100644 --- a/src/utils/PreviewUtils.ts +++ b/src/utils/PreviewUtils.ts @@ -1,4 +1,5 @@ import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; import type { ComfyNode } from '../types'; const log = createModuleLogger('PreviewUtils'); @@ -23,11 +24,18 @@ export interface PreviewOptions { * @param options - Preview options * @returns Promise with created Image element */ -export async function createPreviewFromCanvas( +export const createPreviewFromCanvas = withErrorHandling(async function( canvas: any, node: ComfyNode, options: PreviewOptions = {} ): Promise { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + if (!node) { + throw createValidationError("Node is required", { node }); + } + const { includeMask = true, updateNodeImages = true, @@ -46,7 +54,7 @@ export async function createPreviewFromCanvas( // Get blob from canvas if not provided if (!blob) { if (!canvas.canvasLayers) { - throw new Error("Canvas does not have canvasLayers"); + throw createValidationError("Canvas does not have canvasLayers", { canvas }); } if (includeMask && typeof canvas.canvasLayers.getFlattenedCanvasWithMaskAsBlob === 'function') { @@ -54,12 +62,15 @@ export async function createPreviewFromCanvas( } else if (typeof canvas.canvasLayers.getFlattenedCanvasAsBlob === 'function') { blob = await canvas.canvasLayers.getFlattenedCanvasAsBlob(); } else { - throw new Error("Canvas does not support required blob generation methods"); + throw createValidationError("Canvas does not support required blob generation methods", { + canvas, + availableMethods: Object.getOwnPropertyNames(canvas.canvasLayers) + }); } } if (!blob) { - throw new Error("Failed to generate canvas blob for preview"); + throw createValidationError("Failed to generate canvas blob for preview", { canvas, options }); } // Create preview image @@ -78,7 +89,7 @@ export async function createPreviewFromCanvas( }; previewImage.onerror = (error) => { log.error("Failed to load preview image", error); - reject(new Error("Failed to load preview image")); + reject(createValidationError("Failed to load preview image", { error, blob: blob?.size })); }; }); @@ -89,7 +100,7 @@ export async function createPreviewFromCanvas( } return previewImage; -} +}, 'createPreviewFromCanvas'); /** * Creates a preview image from a blob @@ -98,11 +109,18 @@ export async function createPreviewFromCanvas( * @param updateNodeImages - Whether to update node.imgs (default: false) * @returns Promise with created Image element */ -export async function createPreviewFromBlob( +export const createPreviewFromBlob = withErrorHandling(async function( blob: Blob, node?: ComfyNode, updateNodeImages: boolean = false ): Promise { + if (!blob) { + throw createValidationError("Blob is required", { blob }); + } + if (blob.size === 0) { + throw createValidationError("Blob cannot be empty", { blobSize: blob.size }); + } + log.debug('Creating preview from blob:', { blobSize: blob.size, updateNodeImages, @@ -122,7 +140,7 @@ export async function createPreviewFromBlob( }; previewImage.onerror = (error) => { log.error("Failed to load preview image from blob", error); - reject(new Error("Failed to load preview image from blob")); + reject(createValidationError("Failed to load preview image from blob", { error, blobSize: blob.size })); }; }); @@ -132,7 +150,7 @@ export async function createPreviewFromBlob( } return previewImage; -} +}, 'createPreviewFromBlob'); /** * Updates node preview after canvas changes @@ -141,11 +159,18 @@ export async function createPreviewFromBlob( * @param includeMask - Whether to include mask in preview * @returns Promise with updated preview image */ -export async function updateNodePreview( +export const updateNodePreview = withErrorHandling(async function( canvas: any, node: ComfyNode, includeMask: boolean = true ): Promise { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + if (!node) { + throw createValidationError("Node is required", { node }); + } + log.info('Updating node preview:', { nodeId: node.id, includeMask @@ -168,7 +193,7 @@ export async function updateNodePreview( log.info('Node preview updated successfully'); return previewImage; -} +}, 'updateNodePreview'); /** * Clears node preview images @@ -207,13 +232,23 @@ export function getCurrentPreview(node: ComfyNode): HTMLImageElement | null { * @param processor - Custom processing function that takes canvas and returns blob * @returns Promise with processed preview image */ -export async function createCustomPreview( +export const createCustomPreview = withErrorHandling(async function( canvas: any, node: ComfyNode, processor: (canvas: any) => Promise ): Promise { + if (!canvas) { + throw createValidationError("Canvas is required", { canvas }); + } + if (!node) { + throw createValidationError("Node is required", { node }); + } + if (!processor || typeof processor !== 'function') { + throw createValidationError("Processor function is required", { processor }); + } + log.debug('Creating custom preview:', { nodeId: node.id }); const blob = await processor(canvas); return createPreviewFromBlob(blob, node, true); -} +}, 'createCustomPreview'); diff --git a/src/utils/ResourceManager.ts b/src/utils/ResourceManager.ts index 42fd859..e33cbb5 100644 --- a/src/utils/ResourceManager.ts +++ b/src/utils/ResourceManager.ts @@ -1,7 +1,17 @@ // @ts-ignore import { $el } from "../../../scripts/ui.js"; +import { createModuleLogger } from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js"; + +const log = createModuleLogger('ResourceManager'); + +export const addStylesheet = withErrorHandling(function(url: string): void { + if (!url) { + throw createValidationError("URL is required", { url }); + } + + log.debug('Adding stylesheet:', { url }); -export function addStylesheet(url: string): void { if (url.endsWith(".js")) { url = url.substr(0, url.length - 2) + "css"; } @@ -11,9 +21,15 @@ export function addStylesheet(url: string): void { type: "text/css", href: url.startsWith("http") ? url : getUrl(url), }); -} + + log.debug('Stylesheet added successfully:', { finalUrl: url }); +}, 'addStylesheet'); export function getUrl(path: string, baseUrl?: string | URL): string { + if (!path) { + throw createValidationError("Path is required", { path }); + } + if (baseUrl) { return new URL(path, baseUrl).toString(); } else { @@ -22,11 +38,24 @@ export function getUrl(path: string, baseUrl?: string | URL): string { } } -export async function loadTemplate(path: string, baseUrl?: string | URL): Promise { +export const loadTemplate = withErrorHandling(async function(path: string, baseUrl?: string | URL): Promise { + if (!path) { + throw createValidationError("Path is required", { path }); + } + const url = getUrl(path, baseUrl); + log.debug('Loading template:', { path, url }); + const response = await fetch(url); if (!response.ok) { - throw new Error(`Failed to load template: ${url}`); + throw createNetworkError(`Failed to load template: ${url}`, { + url, + status: response.status, + statusText: response.statusText + }); } - return await response.text(); -} + + const content = await response.text(); + log.debug('Template loaded successfully:', { path, contentLength: content.length }); + return content; +}, 'loadTemplate'); diff --git a/src/utils/WebSocketManager.ts b/src/utils/WebSocketManager.ts index 993780b..7292485 100644 --- a/src/utils/WebSocketManager.ts +++ b/src/utils/WebSocketManager.ts @@ -1,4 +1,5 @@ import {createModuleLogger} from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError, createNetworkError } from "../ErrorHandler.js"; import type { WebSocketMessage, AckCallbacks } from "../types.js"; const log = createModuleLogger('WebSocketManager'); @@ -26,7 +27,7 @@ class WebSocketManager { this.connect(); } - connect() { + connect = withErrorHandling(() => { if (this.socket && this.socket.readyState === WebSocket.OPEN) { log.debug("WebSocket is already open."); return; @@ -37,58 +38,56 @@ class WebSocketManager { return; } + if (!this.url) { + throw createValidationError("WebSocket URL is required", { url: this.url }); + } + this.isConnecting = true; log.info(`Connecting to WebSocket at ${this.url}...`); - try { - this.socket = new WebSocket(this.url); + this.socket = new WebSocket(this.url); - this.socket.onopen = () => { - this.isConnecting = false; - this.reconnectAttempts = 0; - log.info("WebSocket connection established."); - this.flushMessageQueue(); - }; - - this.socket.onmessage = (event: MessageEvent) => { - try { - const data: WebSocketMessage = JSON.parse(event.data); - log.debug("Received message:", data); - - if (data.type === 'ack' && data.nodeId) { - const callback = this.ackCallbacks.get(data.nodeId); - if (callback) { - log.debug(`ACK received for nodeId: ${data.nodeId}, resolving promise.`); - callback.resolve(data); - this.ackCallbacks.delete(data.nodeId); - } - } - - } catch (error) { - log.error("Error parsing incoming WebSocket message:", error); - } - }; - - this.socket.onclose = (event: CloseEvent) => { - this.isConnecting = false; - if (event.wasClean) { - log.info(`WebSocket closed cleanly, code=${event.code}, reason=${event.reason}`); - } else { - log.warn("WebSocket connection died. Attempting to reconnect..."); - this.handleReconnect(); - } - }; - - this.socket.onerror = (error: Event) => { - this.isConnecting = false; - log.error("WebSocket error:", error); - }; - } catch (error) { + this.socket.onopen = () => { this.isConnecting = false; - log.error("Failed to create WebSocket connection:", error); - this.handleReconnect(); - } - } + this.reconnectAttempts = 0; + log.info("WebSocket connection established."); + this.flushMessageQueue(); + }; + + this.socket.onmessage = (event: MessageEvent) => { + try { + const data: WebSocketMessage = JSON.parse(event.data); + log.debug("Received message:", data); + + if (data.type === 'ack' && data.nodeId) { + const callback = this.ackCallbacks.get(data.nodeId); + if (callback) { + log.debug(`ACK received for nodeId: ${data.nodeId}, resolving promise.`); + callback.resolve(data); + this.ackCallbacks.delete(data.nodeId); + } + } + + } catch (error) { + log.error("Error parsing incoming WebSocket message:", error); + } + }; + + this.socket.onclose = (event: CloseEvent) => { + this.isConnecting = false; + if (event.wasClean) { + log.info(`WebSocket closed cleanly, code=${event.code}, reason=${event.reason}`); + } else { + log.warn("WebSocket connection died. Attempting to reconnect..."); + this.handleReconnect(); + } + }; + + this.socket.onerror = (error: Event) => { + this.isConnecting = false; + throw createNetworkError("WebSocket connection error", { error, url: this.url }); + }; + }, 'WebSocketManager.connect'); handleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { @@ -100,13 +99,17 @@ class WebSocketManager { } } - sendMessage(data: WebSocketMessage, requiresAck = false): Promise { - return new Promise((resolve, reject) => { - const nodeId = data.nodeId; - if (requiresAck && !nodeId) { - return reject(new Error("A nodeId is required for messages that need acknowledgment.")); - } + sendMessage = withErrorHandling(async (data: WebSocketMessage, requiresAck = false): Promise => { + if (!data || typeof data !== 'object') { + throw createValidationError("Message data is required", { data }); + } + const nodeId = data.nodeId; + if (requiresAck && !nodeId) { + throw createValidationError("A nodeId is required for messages that need acknowledgment", { data, requiresAck }); + } + + return new Promise((resolve, reject) => { const message = JSON.stringify(data); if (this.socket && this.socket.readyState === WebSocket.OPEN) { @@ -117,7 +120,7 @@ class WebSocketManager { const timeout = setTimeout(() => { this.ackCallbacks.delete(nodeId); - reject(new Error(`ACK timeout for nodeId ${nodeId}`)); + reject(createNetworkError(`ACK timeout for nodeId ${nodeId}`, { nodeId, timeout: 10000 })); log.warn(`ACK timeout for nodeId ${nodeId}.`); }, 10000); // 10-second timeout @@ -142,13 +145,16 @@ class WebSocketManager { } if (requiresAck) { - reject(new Error("Cannot send message with ACK required while disconnected.")); + reject(createNetworkError("Cannot send message with ACK required while disconnected", { + socketState: this.socket?.readyState, + isConnecting: this.isConnecting + })); } else { resolve(); } } }); - } + }, 'WebSocketManager.sendMessage'); flushMessageQueue() { log.debug(`Flushing ${this.messageQueue.length} queued messages.`); diff --git a/src/utils/mask_utils.ts b/src/utils/mask_utils.ts index 17d0918..4d82f4e 100644 --- a/src/utils/mask_utils.ts +++ b/src/utils/mask_utils.ts @@ -1,4 +1,5 @@ import {createModuleLogger} from "./LoggerUtils.js"; +import { withErrorHandling, createValidationError } from "../ErrorHandler.js"; import type { Canvas } from '../Canvas.js'; // @ts-ignore import {ComfyApp} from "../../../scripts/app.js"; @@ -146,26 +147,27 @@ export function press_maskeditor_cancel(app: ComfyApp): void { * @param {HTMLImageElement | HTMLCanvasElement} maskImage - Obraz maski do nałożenia * @param {boolean} sendCleanImage - Czy wysłać czysty obraz (bez istniejącej maski) */ -export function start_mask_editor_with_predefined_mask(canvasInstance: Canvas, maskImage: HTMLImageElement | HTMLCanvasElement, sendCleanImage = true): void { - if (!canvasInstance || !maskImage) { - log.error('Canvas instance and mask image are required'); - return; +export const start_mask_editor_with_predefined_mask = withErrorHandling(function(canvasInstance: Canvas, maskImage: HTMLImageElement | HTMLCanvasElement, sendCleanImage = true): void { + if (!canvasInstance) { + throw createValidationError('Canvas instance is required', { canvasInstance }); + } + if (!maskImage) { + throw createValidationError('Mask image is required', { maskImage }); } canvasInstance.startMaskEditor(maskImage, sendCleanImage); -} +}, 'start_mask_editor_with_predefined_mask'); /** * Uruchamia mask editor z automatycznym zachowaniem (czysty obraz + istniejąca maska) * @param {Canvas} canvasInstance - Instancja Canvas */ -export function start_mask_editor_auto(canvasInstance: Canvas): void { +export const start_mask_editor_auto = withErrorHandling(function(canvasInstance: Canvas): void { if (!canvasInstance) { - log.error('Canvas instance is required'); - return; + throw createValidationError('Canvas instance is required', { canvasInstance }); } canvasInstance.startMaskEditor(null, true); -} +}, 'start_mask_editor_auto'); // Duplikowane funkcje zostały przeniesione do ImageUtils.ts: // - create_mask_from_image_src -> createMaskFromImageSrc