Revert "Refactor logging and formatting"

This reverts commit 83ce890ef4.
This commit is contained in:
Dariusz L
2025-06-27 05:50:47 +02:00
parent 83ce890ef4
commit 711722eb9f
16 changed files with 363 additions and 241 deletions

View File

@@ -49,24 +49,11 @@ except ImportError as e:
# Fallback jeśli logger nie jest dostępny # Fallback jeśli logger nie jest dostępny
print(f"Warning: Logger module not available: {e}") print(f"Warning: Logger module not available: {e}")
# Proste funkcje zastępcze # Proste funkcje zastępcze
def log_debug(*args): def log_debug(*args): print("[DEBUG]", *args)
print("[DEBUG]", *args) def log_info(*args): print("[INFO]", *args)
def log_warn(*args): print("[WARN]", *args)
def log_error(*args): print("[ERROR]", *args)
def log_info(*args):
print("[INFO]", *args)
def log_warn(*args):
print("[WARN]", *args)
def log_error(*args):
print("[ERROR]", *args)
def log_exception(*args): def log_exception(*args):
print("[ERROR]", *args) print("[ERROR]", *args)
traceback.print_exc() traceback.print_exc()
@@ -260,12 +247,10 @@ class CanvasNode:
# Zmienna blokująca równoczesne wykonania # Zmienna blokująca równoczesne wykonania
_processing_lock = threading.Lock() _processing_lock = threading.Lock()
def process_canvas_image(self, trigger, output_switch, cache_enabled, node_id, prompt=None, unique_id=None, def process_canvas_image(self, trigger, output_switch, cache_enabled, node_id, prompt=None, unique_id=None, input_image=None,
input_image=None,
input_mask=None): input_mask=None):
log_info( log_info(f"[CanvasNode] 🔍 process_canvas_image wejście node_id={node_id!r}, unique_id={unique_id!r}, trigger={trigger}, output_switch={output_switch}")
f"[CanvasNode] 🔍 process_canvas_image wejście node_id={node_id!r}, unique_id={unique_id!r}, trigger={trigger}, output_switch={output_switch}")
try: try:
# Sprawdź czy już trwa przetwarzanie # Sprawdź czy już trwa przetwarzanie
@@ -274,8 +259,7 @@ class CanvasNode:
# Return cached data if available to avoid breaking the flow # Return cached data if available to avoid breaking the flow
return self.get_cached_data() return self.get_cached_data()
log_info( log_info(f"Lock acquired. Starting process_canvas_image for node_id: {node_id} (fallback unique_id: {unique_id})")
f"Lock acquired. Starting process_canvas_image for node_id: {node_id} (fallback unique_id: {unique_id})")
# Use node_id as the primary key, as unique_id is proving unreliable # Use node_id as the primary key, as unique_id is proving unreliable
storage_key = node_id storage_key = node_id
@@ -312,6 +296,7 @@ class CanvasNode:
log_info("Using provided input_mask as fallback") log_info("Using provided input_mask as fallback")
processed_mask = input_mask processed_mask = input_mask
# Fallback to default tensors if nothing is loaded # Fallback to default tensors if nothing is loaded
if processed_image is None: if processed_image is None:
log_warn(f"Processed image is still None, creating default blank image.") log_warn(f"Processed image is still None, creating default blank image.")
@@ -320,12 +305,12 @@ class CanvasNode:
log_warn(f"Processed mask is still None, creating default blank mask.") log_warn(f"Processed mask is still None, creating default blank mask.")
processed_mask = torch.zeros((1, 512, 512), dtype=torch.float32) processed_mask = torch.zeros((1, 512, 512), dtype=torch.float32)
if not output_switch: if not output_switch:
log_debug(f"Output switch is OFF, returning empty tuple") log_debug(f"Output switch is OFF, returning empty tuple")
return (None, None) return (None, None)
log_debug( log_debug(f"About to return output - Image shape: {processed_image.shape}, Mask shape: {processed_mask.shape}")
f"About to return output - Image shape: {processed_image.shape}, Mask shape: {processed_mask.shape}")
self.update_persistent_cache() self.update_persistent_cache()
@@ -694,7 +679,6 @@ class BiRefNetMatting:
# Zmienna blokująca równoczesne wywołania matting # Zmienna blokująca równoczesne wywołania matting
_matting_lock = None _matting_lock = None
@PromptServer.instance.routes.post("/matting") @PromptServer.instance.routes.post("/matting")
async def matting(request): async def matting(request):
global _matting_lock global _matting_lock

View File

@@ -7,7 +7,6 @@ import {CanvasRenderer} from "./CanvasRenderer.js";
import {CanvasIO} from "./CanvasIO.js"; import {CanvasIO} from "./CanvasIO.js";
import {ImageReferenceManager} from "./ImageReferenceManager.js"; import {ImageReferenceManager} from "./ImageReferenceManager.js";
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('Canvas'); const log = createModuleLogger('Canvas');
export class Canvas { export class Canvas {
@@ -138,7 +137,6 @@ export class Canvas {
this.onSelectionChange(); this.onSelectionChange();
} }
} }
async copySelectedLayers() { async copySelectedLayers() {
return this.canvasLayers.copySelectedLayers(); return this.canvasLayers.copySelectedLayers();
} }
@@ -267,6 +265,8 @@ export class Canvas {
} }
async getFlattenedCanvasAsBlob() { async getFlattenedCanvasAsBlob() {
return this.canvasLayers.getFlattenedCanvasAsBlob(); return this.canvasLayers.getFlattenedCanvasAsBlob();
} }

View File

@@ -35,6 +35,7 @@ export class CanvasIO {
log.debug(`Save completed for node ${nodeId}, lock released`); log.debug(`Save completed for node ${nodeId}, lock released`);
} }
} else { } else {
// For RAM mode, we don't need the lock/state management as it's synchronous
log.info(`Starting saveToServer (RAM) for node: ${this.canvas.node.id}`); log.info(`Starting saveToServer (RAM) for node: ${this.canvas.node.id}`);
return this._performSave(fileName, outputMode); return this._performSave(fileName, outputMode);
} }
@@ -99,36 +100,50 @@ export class CanvasIO {
maskCtx.putImageData(maskData, 0, 0); maskCtx.putImageData(maskData, 0, 0);
const toolMaskCanvas = this.canvas.maskTool.getMask(); const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) { if (toolMaskCanvas) {
// Create a temp canvas for processing the mask
const tempMaskCanvas = document.createElement('canvas'); const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height; tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d'); const tempMaskCtx = tempMaskCanvas.getContext('2d');
// Clear the canvas
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
// Calculate the correct position to extract the mask
// The mask's position in world space
const maskX = this.canvas.maskTool.x; const maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y; const maskY = this.canvas.maskTool.y;
log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`); log.debug(`Extracting mask from world position (${maskX}, ${maskY}) for output area (0,0) to (${this.canvas.width}, ${this.canvas.height})`);
const sourceX = Math.max(0, -maskX);
// Calculate the source rectangle in the mask canvas that corresponds to the output area
const sourceX = Math.max(0, -maskX); // Where in the mask canvas to start reading
const sourceY = Math.max(0, -maskY); const sourceY = Math.max(0, -maskY);
const destX = Math.max(0, maskX); const destX = Math.max(0, maskX); // Where in the output canvas to start writing
const destY = Math.max(0, maskY); const destY = Math.max(0, maskY);
// Calculate the dimensions of the area to copy
const copyWidth = Math.min( const copyWidth = Math.min(
toolMaskCanvas.width - sourceX, toolMaskCanvas.width - sourceX, // Available width in source
this.canvas.width - destX this.canvas.width - destX // Available width in destination
); );
const copyHeight = Math.min( const copyHeight = Math.min(
toolMaskCanvas.height - sourceY, toolMaskCanvas.height - sourceY, // Available height in source
this.canvas.height - destY this.canvas.height - destY // Available height in destination
); );
// Only draw if there's an actual intersection
if (copyWidth > 0 && copyHeight > 0) { if (copyWidth > 0 && copyHeight > 0) {
log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`); log.debug(`Copying mask region: source(${sourceX}, ${sourceY}) to dest(${destX}, ${destY}) size(${copyWidth}, ${copyHeight})`);
tempMaskCtx.drawImage( tempMaskCtx.drawImage(
toolMaskCanvas, toolMaskCanvas,
sourceX, sourceY, copyWidth, copyHeight, sourceX, sourceY, copyWidth, copyHeight, // Source rectangle
destX, destY, copyWidth, copyHeight destX, destY, copyWidth, copyHeight // Destination rectangle
); );
} }
// Convert to proper mask format
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) { for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3]; const alpha = tempMaskData.data[i + 3];
@@ -136,6 +151,8 @@ export class CanvasIO {
tempMaskData.data[i + 3] = alpha; tempMaskData.data[i + 3] = alpha;
} }
tempMaskCtx.putImageData(tempMaskData, 0, 0); tempMaskCtx.putImageData(tempMaskData, 0, 0);
// Draw the processed mask to the final mask canvas
maskCtx.globalCompositeOperation = 'source-over'; maskCtx.globalCompositeOperation = 'source-over';
maskCtx.drawImage(tempMaskCanvas, 0, 0); maskCtx.drawImage(tempMaskCanvas, 0, 0);
} }
@@ -146,6 +163,8 @@ export class CanvasIO {
resolve({ image: imageData, mask: maskData }); resolve({ image: imageData, mask: maskData });
return; return;
} }
// --- Disk Mode (original logic) ---
const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png'); const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png');
log.info(`Saving image without mask as: ${fileNameWithoutMask}`); log.info(`Saving image without mask as: ${fileNameWithoutMask}`);
@@ -228,17 +247,20 @@ export class CanvasIO {
return new Promise((resolve) => { return new Promise((resolve) => {
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height); const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height); const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
// This logic is mostly mirrored from _performSave to ensure consistency
tempCtx.fillStyle = '#ffffff'; tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); tempCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const visibilityCanvas = document.createElement('canvas'); const visibilityCanvas = document.createElement('canvas');
visibilityCanvas.width = this.canvas.width; visibilityCanvas.width = this.canvas.width;
visibilityCanvas.height = this.canvas.height; visibilityCanvas.height = this.canvas.height;
const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true }); const visibilityCtx = visibilityCanvas.getContext('2d', { alpha: true });
maskCtx.fillStyle = '#ffffff'; maskCtx.fillStyle = '#ffffff'; // Start with a white mask (nothing masked)
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height); maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex); const sortedLayers = this.canvas.layers.sort((a, b) => a.zIndex - b.zIndex);
sortedLayers.forEach((layer) => { sortedLayers.forEach((layer) => {
// Render layer to main canvas
tempCtx.save(); tempCtx.save();
tempCtx.globalCompositeOperation = layer.blendMode || 'normal'; tempCtx.globalCompositeOperation = layer.blendMode || 'normal';
tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; tempCtx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
@@ -246,28 +268,39 @@ export class CanvasIO {
tempCtx.rotate(layer.rotation * Math.PI / 180); tempCtx.rotate(layer.rotation * Math.PI / 180);
tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); tempCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
tempCtx.restore(); tempCtx.restore();
// Render layer to visibility canvas for the mask
visibilityCtx.save(); visibilityCtx.save();
visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2); visibilityCtx.translate(layer.x + layer.width / 2, layer.y + layer.height / 2);
visibilityCtx.rotate(layer.rotation * Math.PI / 180); visibilityCtx.rotate(layer.rotation * Math.PI / 180);
visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height); visibilityCtx.drawImage(layer.image, -layer.width / 2, -layer.height / 2, layer.width, layer.height);
visibilityCtx.restore(); visibilityCtx.restore();
}); });
// Create layer visibility mask
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < visibilityData.data.length; i += 4) { for (let i = 0; i < visibilityData.data.length; i += 4) {
const alpha = visibilityData.data[i + 3]; const alpha = visibilityData.data[i + 3];
const maskValue = 255 - alpha; const maskValue = 255 - alpha; // Invert alpha to create the mask
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue; maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
maskData.data[i + 3] = 255; maskData.data[i + 3] = 255; // Solid mask
} }
maskCtx.putImageData(maskData, 0, 0); maskCtx.putImageData(maskData, 0, 0);
// Composite the tool mask on top
const toolMaskCanvas = this.canvas.maskTool.getMask(); const toolMaskCanvas = this.canvas.maskTool.getMask();
if (toolMaskCanvas) { if (toolMaskCanvas) {
// Create a temp canvas for processing the mask
const tempMaskCanvas = document.createElement('canvas'); const tempMaskCanvas = document.createElement('canvas');
tempMaskCanvas.width = this.canvas.width; tempMaskCanvas.width = this.canvas.width;
tempMaskCanvas.height = this.canvas.height; tempMaskCanvas.height = this.canvas.height;
const tempMaskCtx = tempMaskCanvas.getContext('2d'); const tempMaskCtx = tempMaskCanvas.getContext('2d');
// Clear the canvas
tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height); tempMaskCtx.clearRect(0, 0, tempMaskCanvas.width, tempMaskCanvas.height);
// Calculate the correct position to extract the mask
const maskX = this.canvas.maskTool.x; const maskX = this.canvas.maskTool.x;
const maskY = this.canvas.maskTool.y; const maskY = this.canvas.maskTool.y;
@@ -288,13 +321,19 @@ export class CanvasIO {
destX, destY, copyWidth, copyHeight destX, destY, copyWidth, copyHeight
); );
} }
// Convert the brush mask (white with alpha) to a solid white mask on black background.
const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height); const tempMaskData = tempMaskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
for (let i = 0; i < tempMaskData.data.length; i += 4) { for (let i = 0; i < tempMaskData.data.length; i += 4) {
const alpha = tempMaskData.data[i + 3]; const alpha = tempMaskData.data[i + 3];
// The painted area (alpha > 0) should become white (255).
tempMaskData.data[i] = tempMaskData.data[i+1] = tempMaskData.data[i+2] = alpha; tempMaskData.data[i] = tempMaskData.data[i+1] = tempMaskData.data[i+2] = alpha;
tempMaskData.data[i + 3] = 255; tempMaskData.data[i + 3] = 255; // Solid alpha
} }
tempMaskCtx.putImageData(tempMaskData, 0, 0); tempMaskCtx.putImageData(tempMaskData, 0, 0);
// Use 'screen' blending mode. This correctly adds the white brush mask
// to the existing layer visibility mask. (white + anything = white)
maskCtx.globalCompositeOperation = 'screen'; maskCtx.globalCompositeOperation = 'screen';
maskCtx.drawImage(tempMaskCanvas, 0, 0); maskCtx.drawImage(tempMaskCanvas, 0, 0);
} }
@@ -318,12 +357,14 @@ export class CanvasIO {
nodeId: String(nodeId), nodeId: String(nodeId),
image: image, image: image,
mask: mask, mask: mask,
}, true); }, true); // `true` requires an acknowledgment
log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`); log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`);
return true; return true;
} catch (error) { } catch (error) {
log.error(`Failed to send data for node ${nodeId}:`, error); log.error(`Failed to send data for node ${nodeId}:`, error);
// We can alert the user here or handle it silently.
// For now, let's throw to make it clear the process failed.
throw new Error(`Failed to get confirmation from server for node ${nodeId}. The workflow might not have the latest canvas data.`); throw new Error(`Failed to get confirmation from server for node ${nodeId}. The workflow might not have the latest canvas data.`);
} }
} }

View File

@@ -1,6 +1,5 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js"; import {snapToGrid, getSnapAdjustment} from "./utils/CommonUtils.js";
const log = createModuleLogger('CanvasInteractions'); const log = createModuleLogger('CanvasInteractions');
export class CanvasInteractions { export class CanvasInteractions {
@@ -503,6 +502,8 @@ export class CanvasInteractions {
layer.x -= finalX; layer.x -= finalX;
layer.y -= finalY; layer.y -= finalY;
}); });
// Update mask position when moving canvas
this.canvas.maskTool.updatePosition(-finalX, -finalY); this.canvas.maskTool.updatePosition(-finalX, -finalY);
this.canvas.viewport.x -= finalX; this.canvas.viewport.x -= finalX;
@@ -689,6 +690,8 @@ export class CanvasInteractions {
layer.x -= rectX; layer.x -= rectX;
layer.y -= rectY; layer.y -= rectY;
}); });
// Update mask position when resizing canvas
this.canvas.maskTool.updatePosition(-rectX, -rectY); this.canvas.maskTool.updatePosition(-rectX, -rectY);
this.canvas.viewport.x -= rectX; this.canvas.viewport.x -= rectX;

View File

@@ -2,7 +2,6 @@ import {saveImage, removeImage} from "./db.js";
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js"; import {generateUUID, generateUniqueFileName} from "./utils/CommonUtils.js";
import {withErrorHandling, createValidationError} from "./ErrorHandler.js"; import {withErrorHandling, createValidationError} from "./ErrorHandler.js";
const log = createModuleLogger('CanvasLayers'); const log = createModuleLogger('CanvasLayers');
export class CanvasLayers { export class CanvasLayers {
@@ -359,7 +358,6 @@ export class CanvasLayers {
this.canvasLayers.selectedLayer = layer; this.canvasLayers.selectedLayer = layer;
this.canvasLayers.render(); this.canvasLayers.render();
} }
isRotationHandle(x, y) { isRotationHandle(x, y) {
if (!this.canvasLayers.selectedLayer) return false; if (!this.canvasLayers.selectedLayer) return false;
@@ -430,18 +428,12 @@ export class CanvasLayers {
const handleRadius = 5; const handleRadius = 5;
const handles = { const handles = {
'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y}, 'nw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y},
'ne': { 'ne': {x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, y: this.canvasLayers.selectedLayer.y},
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
y: this.canvasLayers.selectedLayer.y
},
'se': { 'se': {
x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width, x: this.canvasLayers.selectedLayer.x + this.canvasLayers.selectedLayer.width,
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height
}, },
'sw': { 'sw': {x: this.canvasLayers.selectedLayer.x, y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height}
x: this.canvasLayers.selectedLayer.x,
y: this.canvasLayers.selectedLayer.y + this.canvasLayers.selectedLayer.height
}
}; };
for (const [position, point] of Object.entries(handles)) { for (const [position, point] of Object.entries(handles)) {
@@ -451,7 +443,6 @@ export class CanvasLayers {
} }
return null; return null;
} }
showBlendModeMenu(x, y) { showBlendModeMenu(x, y) {
const existingMenu = document.getElementById('blend-mode-menu'); const existingMenu = document.getElementById('blend-mode-menu');
if (existingMenu) { if (existingMenu) {
@@ -603,7 +594,6 @@ export class CanvasLayers {
modeElement.appendChild(slider); modeElement.appendChild(slider);
} }
} }
async getFlattenedCanvasAsBlob() { async getFlattenedCanvasAsBlob() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const tempCanvas = document.createElement('canvas'); const tempCanvas = document.createElement('canvas');
@@ -643,7 +633,6 @@ export class CanvasLayers {
}, 'image/png'); }, 'image/png');
}); });
} }
async getFlattenedSelectionAsBlob() { async getFlattenedSelectionAsBlob() {
if (this.canvasLayers.selectedLayers.length === 0) { if (this.canvasLayers.selectedLayers.length === 0) {
return null; return null;

View File

@@ -1,5 +1,4 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('CanvasRenderer'); const log = createModuleLogger('CanvasRenderer');
export class CanvasRenderer { export class CanvasRenderer {
@@ -84,7 +83,10 @@ export class CanvasRenderer {
this.drawCanvasOutline(ctx); this.drawCanvasOutline(ctx);
const maskImage = this.canvas.maskTool.getMask(); const maskImage = this.canvas.maskTool.getMask();
if (maskImage) { if (maskImage) {
// Create a clipping region to only show mask content that overlaps with the output area
ctx.save(); ctx.save();
// Only show what's visible inside the output area
if (this.canvas.maskTool.isActive) { if (this.canvas.maskTool.isActive) {
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.5; ctx.globalAlpha = 0.5;
@@ -92,6 +94,8 @@ export class CanvasRenderer {
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1.0; ctx.globalAlpha = 1.0;
} }
// Draw the mask at its world space position
ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y); ctx.drawImage(maskImage, this.canvas.maskTool.x, this.canvas.maskTool.y);
ctx.globalAlpha = 1.0; ctx.globalAlpha = 1.0;

View File

@@ -2,7 +2,6 @@ import {getCanvasState, setCanvasState, saveImage, getImage} from "./db.js";
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
import {generateUUID, cloneLayers, getStateSignature, debounce} from "./utils/CommonUtils.js"; import {generateUUID, cloneLayers, getStateSignature, debounce} from "./utils/CommonUtils.js";
import {withErrorHandling} from "./ErrorHandler.js"; import {withErrorHandling} from "./ErrorHandler.js";
const log = createModuleLogger('CanvasState'); const log = createModuleLogger('CanvasState');
export class CanvasState { export class CanvasState {

View File

@@ -742,6 +742,8 @@ async function createCanvasWidget(node, widget, app) {
const triggerWidget = node.widgets.find(w => w.name === "trigger"); const triggerWidget = node.widgets.find(w => w.name === "trigger");
const updateOutput = async () => { const updateOutput = async () => {
// Only increment trigger and run step - don't save to disk here
// Saving to disk will happen during execution_start event
triggerWidget.value = (triggerWidget.value + 1) % 99999999; triggerWidget.value = (triggerWidget.value + 1) % 99999999;
app.graph.runStep(); app.graph.runStep();
}; };
@@ -786,6 +788,10 @@ async function createCanvasWidget(node, widget, app) {
canvas.render(); canvas.render();
}; };
// Remove automatic saving on mouse events - only save during execution
// canvas.canvas.addEventListener('mouseup', updateOutput);
// canvas.canvas.addEventListener('mouseleave', updateOutput);
const mainContainer = $el("div.painterMainContainer", { const mainContainer = $el("div.painterMainContainer", {
style: { style: {
@@ -917,6 +923,7 @@ async function createCanvasWidget(node, widget, app) {
} }
node.canvasWidget = canvas; node.canvasWidget = canvas;
setTimeout(() => { setTimeout(() => {
@@ -936,6 +943,7 @@ app.registerExtension({
name: "Comfy.CanvasNode", name: "Comfy.CanvasNode",
init() { init() {
// Monkey-patch the queuePrompt function to send canvas data via WebSocket before sending the prompt
const originalQueuePrompt = app.queuePrompt; const originalQueuePrompt = app.queuePrompt;
app.queuePrompt = async function(number, prompt) { app.queuePrompt = async function(number, prompt) {
log.info("Preparing to queue prompt..."); log.info("Preparing to queue prompt...");
@@ -945,26 +953,33 @@ app.registerExtension({
const sendPromises = []; const sendPromises = [];
for (const [nodeId, canvasWidget] of canvasNodeInstances.entries()) { for (const [nodeId, canvasWidget] of canvasNodeInstances.entries()) {
// Ensure the node still exists on the graph before sending data
if (app.graph.getNodeById(nodeId) && canvasWidget.canvas && canvasWidget.canvas.canvasIO) { if (app.graph.getNodeById(nodeId) && canvasWidget.canvas && canvasWidget.canvas.canvasIO) {
log.debug(`Sending data for canvas node ${nodeId}`); log.debug(`Sending data for canvas node ${nodeId}`);
// This now returns a promise that resolves upon server ACK
sendPromises.push(canvasWidget.canvas.canvasIO.sendDataViaWebSocket(nodeId)); sendPromises.push(canvasWidget.canvas.canvasIO.sendDataViaWebSocket(nodeId));
} else { } else {
// If node doesn't exist, it might have been deleted, so we can clean up the map
log.warn(`Node ${nodeId} not found in graph, removing from instances map.`); log.warn(`Node ${nodeId} not found in graph, removing from instances map.`);
canvasNodeInstances.delete(nodeId); canvasNodeInstances.delete(nodeId);
} }
} }
try { try {
// Wait for all WebSocket messages to be acknowledged
await Promise.all(sendPromises); await Promise.all(sendPromises);
log.info("All canvas data has been sent and acknowledged by the server."); log.info("All canvas data has been sent and acknowledged by the server.");
} catch (error) { } catch (error) {
log.error("Failed to send canvas data for one or more nodes. Aborting prompt.", error); log.error("Failed to send canvas data for one or more nodes. Aborting prompt.", error);
// IMPORTANT: Stop the prompt from queueing if data transfer fails.
// You might want to show a user-facing error here.
alert(`CanvasNode Error: ${error.message}`); alert(`CanvasNode Error: ${error.message}`);
return; return; // Stop execution
} }
} }
log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt."); log.info("All pre-prompt tasks complete. Proceeding with original queuePrompt.");
// Proceed with the original queuePrompt logic
return originalQueuePrompt.apply(this, arguments); return originalQueuePrompt.apply(this, arguments);
}; };
}, },
@@ -974,16 +989,25 @@ app.registerExtension({
const onNodeCreated = nodeType.prototype.onNodeCreated; const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () { nodeType.prototype.onNodeCreated = function () {
log.debug("CanvasNode onNodeCreated: Base widget setup."); log.debug("CanvasNode onNodeCreated: Base widget setup.");
// Call original onNodeCreated to ensure widgets are created
const r = onNodeCreated?.apply(this, arguments); const r = onNodeCreated?.apply(this, arguments);
// The main initialization is moved to onAdded
return r; return r;
}; };
// onAdded is the most reliable callback for when a node is fully added to the graph and has an ID
nodeType.prototype.onAdded = async function() { nodeType.prototype.onAdded = async function() {
log.info(`CanvasNode onAdded, ID: ${this.id}`); log.info(`CanvasNode onAdded, ID: ${this.id}`);
log.debug(`Available widgets in onAdded:`, this.widgets.map(w => w.name)); log.debug(`Available widgets in onAdded:`, this.widgets.map(w => w.name));
// Prevent re-initialization if the widget already exists
if (this.canvasWidget) { if (this.canvasWidget) {
log.warn(`CanvasNode ${this.id} already initialized. Skipping onAdded setup.`); log.warn(`CanvasNode ${this.id} already initialized. Skipping onAdded setup.`);
return; return;
} }
// Now that we are in onAdded, this.id is guaranteed to be correct.
// Set the hidden node_id widget's value for backend communication.
const nodeIdWidget = this.widgets.find(w => w.name === "node_id"); const nodeIdWidget = this.widgets.find(w => w.name === "node_id");
if (nodeIdWidget) { if (nodeIdWidget) {
nodeIdWidget.value = String(this.id); nodeIdWidget.value = String(this.id);
@@ -991,6 +1015,9 @@ app.registerExtension({
} else { } else {
log.error("Could not find the hidden node_id widget!"); log.error("Could not find the hidden node_id widget!");
} }
// Create the main canvas widget and register it in our global map
// We pass `null` for the widget parameter as we are not using a pre-defined widget.
const canvasWidget = await createCanvasWidget(this, null, app); const canvasWidget = await createCanvasWidget(this, null, app);
canvasNodeInstances.set(this.id, canvasWidget); canvasNodeInstances.set(this.id, canvasWidget);
log.info(`Registered CanvasNode instance for ID: ${this.id}`); log.info(`Registered CanvasNode instance for ID: ${this.id}`);
@@ -999,8 +1026,12 @@ app.registerExtension({
const onRemoved = nodeType.prototype.onRemoved; const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () { nodeType.prototype.onRemoved = function () {
log.info(`Cleaning up canvas node ${this.id}`); log.info(`Cleaning up canvas node ${this.id}`);
// Clean up from our instance map
canvasNodeInstances.delete(this.id); canvasNodeInstances.delete(this.id);
log.info(`Deregistered CanvasNode instance for ID: ${this.id}`); log.info(`Deregistered CanvasNode instance for ID: ${this.id}`);
// Clean up execution state
if (window.canvasExecutionStates) { if (window.canvasExecutionStates) {
window.canvasExecutionStates.delete(this.id); window.canvasExecutionStates.delete(this.id);
} }
@@ -1013,6 +1044,8 @@ app.registerExtension({
if (backdrop && backdrop.contains(this.canvasWidget?.canvas)) { if (backdrop && backdrop.contains(this.canvasWidget?.canvas)) {
document.body.removeChild(backdrop); document.body.removeChild(backdrop);
} }
// Cleanup canvas resources including garbage collection
if (this.canvasWidget && this.canvasWidget.destroy) { if (this.canvasWidget && this.canvasWidget.destroy) {
this.canvasWidget.destroy(); this.canvasWidget.destroy();
} }

View File

@@ -224,7 +224,6 @@ export class ErrorHandler {
log.info('Error history cleared'); log.info('Error history cleared');
} }
} }
const errorHandler = new ErrorHandler(); const errorHandler = new ErrorHandler();
/** /**
@@ -347,6 +346,5 @@ export async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 10
throw errorHandler.handle(lastError, context, { attempts: maxRetries + 1 }); throw errorHandler.handle(lastError, context, { attempts: maxRetries + 1 });
} }
export { errorHandler }; export { errorHandler };
export default errorHandler; export default errorHandler;

View File

@@ -1,5 +1,4 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('ImageCache'); const log = createModuleLogger('ImageCache');
export class ImageCache { export class ImageCache {

View File

@@ -6,14 +6,19 @@ const log = createModuleLogger('ImageReferenceManager');
export class ImageReferenceManager { export class ImageReferenceManager {
constructor(canvas) { constructor(canvas) {
this.canvas = canvas; this.canvas = canvas;
this.imageReferences = new Map(); this.imageReferences = new Map(); // imageId -> count
this.imageLastUsed = new Map(); this.imageLastUsed = new Map(); // imageId -> timestamp
this.gcInterval = 5 * 60 * 1000; this.gcInterval = 5 * 60 * 1000; // 5 minut (nieużywane)
this.maxAge = 30 * 60 * 1000; this.maxAge = 30 * 60 * 1000; // 30 minut bez użycia
this.gcTimer = null; this.gcTimer = null;
this.isGcRunning = false; this.isGcRunning = false;
// Licznik operacji dla automatycznego GC
this.operationCount = 0; this.operationCount = 0;
this.operationThreshold = 500; this.operationThreshold = 500; // Uruchom GC po 500 operacjach
// Nie uruchamiamy automatycznego GC na czasie
// this.startGarbageCollection();
} }
/** /**
@@ -78,8 +83,14 @@ export class ImageReferenceManager {
*/ */
updateReferences() { updateReferences() {
log.debug("Updating image references..."); log.debug("Updating image references...");
// Wyczyść stare referencje
this.imageReferences.clear(); this.imageReferences.clear();
// Zbierz wszystkie używane imageId
const usedImageIds = this.collectAllUsedImageIds(); const usedImageIds = this.collectAllUsedImageIds();
// Dodaj referencje dla wszystkich używanych obrazów
usedImageIds.forEach(imageId => { usedImageIds.forEach(imageId => {
this.addReference(imageId); this.addReference(imageId);
}); });
@@ -93,11 +104,15 @@ export class ImageReferenceManager {
*/ */
collectAllUsedImageIds() { collectAllUsedImageIds() {
const usedImageIds = new Set(); const usedImageIds = new Set();
// 1. Aktualne warstwy
this.canvas.layers.forEach(layer => { this.canvas.layers.forEach(layer => {
if (layer.imageId) { if (layer.imageId) {
usedImageIds.add(layer.imageId); usedImageIds.add(layer.imageId);
} }
}); });
// 2. Historia undo
if (this.canvas.canvasState && this.canvas.canvasState.layersUndoStack) { if (this.canvas.canvasState && this.canvas.canvasState.layersUndoStack) {
this.canvas.canvasState.layersUndoStack.forEach(layersState => { this.canvas.canvasState.layersUndoStack.forEach(layersState => {
layersState.forEach(layer => { layersState.forEach(layer => {
@@ -107,6 +122,8 @@ export class ImageReferenceManager {
}); });
}); });
} }
// 3. Historia redo
if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) { if (this.canvas.canvasState && this.canvas.canvasState.layersRedoStack) {
this.canvas.canvasState.layersRedoStack.forEach(layersState => { this.canvas.canvasState.layersRedoStack.forEach(layersState => {
layersState.forEach(layer => { layersState.forEach(layer => {
@@ -128,14 +145,18 @@ export class ImageReferenceManager {
*/ */
async findUnusedImages(usedImageIds) { async findUnusedImages(usedImageIds) {
try { try {
// Pobierz wszystkie imageId z bazy danych
const allImageIds = await getAllImageIds(); const allImageIds = await getAllImageIds();
const unusedImages = []; const unusedImages = [];
const now = Date.now(); const now = Date.now();
for (const imageId of allImageIds) { for (const imageId of allImageIds) {
// Sprawdź czy obraz nie jest używany
if (!usedImageIds.has(imageId)) { if (!usedImageIds.has(imageId)) {
const lastUsed = this.imageLastUsed.get(imageId) || 0; const lastUsed = this.imageLastUsed.get(imageId) || 0;
const age = now - lastUsed; const age = now - lastUsed;
// Usuń tylko stare obrazy (grace period)
if (age > this.maxAge) { if (age > this.maxAge) {
unusedImages.push(imageId); unusedImages.push(imageId);
} else { } else {
@@ -168,10 +189,15 @@ export class ImageReferenceManager {
for (const imageId of unusedImages) { for (const imageId of unusedImages) {
try { try {
// Usuń z bazy danych
await removeImage(imageId); await removeImage(imageId);
// Usuń z cache
if (this.canvas.imageCache && this.canvas.imageCache.has(imageId)) { if (this.canvas.imageCache && this.canvas.imageCache.has(imageId)) {
this.canvas.imageCache.delete(imageId); this.canvas.imageCache.delete(imageId);
} }
// Usuń z tracking
this.imageReferences.delete(imageId); this.imageReferences.delete(imageId);
this.imageLastUsed.delete(imageId); this.imageLastUsed.delete(imageId);
@@ -200,9 +226,16 @@ export class ImageReferenceManager {
log.info("Starting garbage collection..."); log.info("Starting garbage collection...");
try { try {
// 1. Aktualizuj referencje
this.updateReferences(); this.updateReferences();
// 2. Zbierz wszystkie używane imageId
const usedImageIds = this.collectAllUsedImageIds(); const usedImageIds = this.collectAllUsedImageIds();
// 3. Znajdź nieużywane obrazy
const unusedImages = await this.findUnusedImages(usedImageIds); const unusedImages = await this.findUnusedImages(usedImageIds);
// 4. Wyczyść nieużywane obrazy
await this.cleanupUnusedImages(unusedImages); await this.cleanupUnusedImages(unusedImages);
} catch (error) { } catch (error) {
@@ -221,7 +254,8 @@ export class ImageReferenceManager {
if (this.operationCount >= this.operationThreshold) { if (this.operationCount >= this.operationThreshold) {
log.info(`Operation threshold reached (${this.operationThreshold}), triggering garbage collection`); log.info(`Operation threshold reached (${this.operationThreshold}), triggering garbage collection`);
this.operationCount = 0; this.operationCount = 0; // Reset counter
// Uruchom GC asynchronicznie, żeby nie blokować operacji
setTimeout(() => { setTimeout(() => {
this.performGarbageCollection(); this.performGarbageCollection();
}, 100); }, 100);

View File

@@ -1,5 +1,4 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('Mask_tool'); const log = createModuleLogger('Mask_tool');
export class MaskTool { export class MaskTool {
@@ -8,6 +7,8 @@ export class MaskTool {
this.mainCanvas = canvasInstance.canvas; this.mainCanvas = canvasInstance.canvas;
this.maskCanvas = document.createElement('canvas'); this.maskCanvas = document.createElement('canvas');
this.maskCtx = this.maskCanvas.getContext('2d'); this.maskCtx = this.maskCanvas.getContext('2d');
// Add position coordinates for the mask
this.x = 0; this.x = 0;
this.y = 0; this.y = 0;
@@ -26,9 +27,13 @@ export class MaskTool {
} }
initMaskCanvas() { initMaskCanvas() {
const extraSpace = 2000; // Create a larger mask canvas that can extend beyond the output area
const extraSpace = 2000; // Allow for a generous drawing area outside the output area
this.maskCanvas.width = this.canvasInstance.width + extraSpace; this.maskCanvas.width = this.canvasInstance.width + extraSpace;
this.maskCanvas.height = this.canvasInstance.height + extraSpace; this.maskCanvas.height = this.canvasInstance.height + extraSpace;
// Position the mask's origin point in the center of the expanded canvas
// This allows drawing in any direction from the output area
this.x = -extraSpace / 2; this.x = -extraSpace / 2;
this.y = -extraSpace / 2; this.y = -extraSpace / 2;
@@ -91,10 +96,16 @@ export class MaskTool {
if (!this.lastPosition) { if (!this.lastPosition) {
this.lastPosition = worldCoords; this.lastPosition = worldCoords;
} }
// Convert world coordinates to mask canvas coordinates
// Account for the mask's position in world space
const canvasLastX = this.lastPosition.x - this.x; const canvasLastX = this.lastPosition.x - this.x;
const canvasLastY = this.lastPosition.y - this.y; const canvasLastY = this.lastPosition.y - this.y;
const canvasX = worldCoords.x - this.x; const canvasX = worldCoords.x - this.x;
const canvasY = worldCoords.y - this.y; const canvasY = worldCoords.y - this.y;
// Check if drawing is within the expanded canvas bounds
// Since our canvas is much larger now, this should rarely be an issue
const canvasWidth = this.maskCanvas.width; const canvasWidth = this.maskCanvas.width;
const canvasHeight = this.maskCanvas.height; const canvasHeight = this.maskCanvas.height;
@@ -169,10 +180,19 @@ export class MaskTool {
const oldY = this.y; const oldY = this.y;
const oldWidth = oldMask.width; const oldWidth = oldMask.width;
const oldHeight = oldMask.height; const oldHeight = oldMask.height;
// Determine if we're increasing or decreasing the canvas size
const isIncreasingWidth = width > (this.canvasInstance.width); const isIncreasingWidth = width > (this.canvasInstance.width);
const isIncreasingHeight = height > (this.canvasInstance.height); const isIncreasingHeight = height > (this.canvasInstance.height);
// Create a new mask canvas
this.maskCanvas = document.createElement('canvas'); this.maskCanvas = document.createElement('canvas');
// Calculate the new size based on whether we're increasing or decreasing
const extraSpace = 2000; const extraSpace = 2000;
// If we're increasing the size, expand the mask canvas
// If we're decreasing, keep the current mask canvas size to preserve content
const newWidth = isIncreasingWidth ? width + extraSpace : Math.max(oldWidth, width + extraSpace); const newWidth = isIncreasingWidth ? width + extraSpace : Math.max(oldWidth, width + extraSpace);
const newHeight = isIncreasingHeight ? height + extraSpace : Math.max(oldHeight, height + extraSpace); const newHeight = isIncreasingHeight ? height + extraSpace : Math.max(oldHeight, height + extraSpace);
@@ -181,8 +201,11 @@ export class MaskTool {
this.maskCtx = this.maskCanvas.getContext('2d'); this.maskCtx = this.maskCanvas.getContext('2d');
if (oldMask.width > 0 && oldMask.height > 0) { if (oldMask.width > 0 && oldMask.height > 0) {
// Calculate offset to maintain the same world position of the mask content
const offsetX = this.x - oldX; const offsetX = this.x - oldX;
const offsetY = this.y - oldY; const offsetY = this.y - oldY;
// Draw the old mask at the correct position to maintain world alignment
this.maskCtx.drawImage(oldMask, offsetX, offsetY); this.maskCtx.drawImage(oldMask, offsetX, offsetY);
log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`); log.debug(`Preserved mask content with offset (${offsetX}, ${offsetY})`);
@@ -192,6 +215,7 @@ export class MaskTool {
log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`); log.info(`Canvas size change: width ${isIncreasingWidth ? 'increased' : 'decreased'}, height ${isIncreasingHeight ? 'increased' : 'decreased'}`);
} }
// Add method to update mask position
updatePosition(dx, dy) { updatePosition(dx, dy) {
this.x += dx; this.x += dx;
this.y += dy; this.y += dy;

View File

@@ -1,5 +1,4 @@
import {createModuleLogger} from "./utils/LoggerUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js";
const log = createModuleLogger('db'); const log = createModuleLogger('db');
const DB_NAME = 'CanvasNodeDB'; const DB_NAME = 'CanvasNodeDB';

View File

@@ -314,7 +314,6 @@ class Logger {
this.log(module, LogLevel.ERROR, ...args); this.log(module, LogLevel.ERROR, ...args);
} }
} }
export const logger = new Logger(); export const logger = new Logger();
export const debug = (module, ...args) => logger.debug(module, ...args); export const debug = (module, ...args) => logger.debug(module, ...args);
export const info = (module, ...args) => logger.info(module, ...args); export const info = (module, ...args) => logger.info(module, ...args);

View File

@@ -128,7 +128,7 @@ export function getStateSignature(layers) {
return JSON.stringify(layers.map((layer, index) => { return JSON.stringify(layers.map((layer, index) => {
const sig = { const sig = {
index: index, index: index,
x: Math.round(layer.x * 100) / 100, x: Math.round(layer.x * 100) / 100, // Round to avoid floating point precision issues
y: Math.round(layer.y * 100) / 100, y: Math.round(layer.y * 100) / 100,
width: Math.round(layer.width * 100) / 100, width: Math.round(layer.width * 100) / 100,
height: Math.round(layer.height * 100) / 100, height: Math.round(layer.height * 100) / 100,
@@ -137,11 +137,15 @@ export function getStateSignature(layers) {
blendMode: layer.blendMode || 'normal', blendMode: layer.blendMode || 'normal',
opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1 opacity: layer.opacity !== undefined ? Math.round(layer.opacity * 100) / 100 : 1
}; };
// Include imageId if available
if (layer.imageId) { if (layer.imageId) {
sig.imageId = layer.imageId; sig.imageId = layer.imageId;
} }
// Include image src as fallback identifier
if (layer.image && layer.image.src) { if (layer.image && layer.image.src) {
sig.imageSrc = layer.image.src.substring(0, 100); sig.imageSrc = layer.image.src.substring(0, 100); // First 100 chars to avoid huge signatures
} }
return sig; return sig;

View File

@@ -10,8 +10,8 @@ class WebSocketManager {
this.isConnecting = false; this.isConnecting = false;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10; this.maxReconnectAttempts = 10;
this.reconnectInterval = 5000; this.reconnectInterval = 5000; // 5 seconds
this.ackCallbacks = new Map(); this.ackCallbacks = new Map(); // Store callbacks for messages awaiting ACK
this.messageIdCounter = 0; this.messageIdCounter = 0;
this.connect(); this.connect();
@@ -54,6 +54,7 @@ class WebSocketManager {
this.ackCallbacks.delete(data.nodeId); this.ackCallbacks.delete(data.nodeId);
} }
} }
// Handle other incoming messages if needed
} catch (error) { } catch (error) {
log.error("Error parsing incoming WebSocket message:", error); log.error("Error parsing incoming WebSocket message:", error);
} }
@@ -72,6 +73,7 @@ class WebSocketManager {
this.socket.onerror = (error) => { this.socket.onerror = (error) => {
this.isConnecting = false; this.isConnecting = false;
log.error("WebSocket error:", error); log.error("WebSocket error:", error);
// The onclose event will be fired next, which will handle reconnection.
}; };
} catch (error) { } catch (error) {
this.isConnecting = false; this.isConnecting = false;
@@ -104,11 +106,12 @@ class WebSocketManager {
log.debug("Sent message:", data); log.debug("Sent message:", data);
if (requiresAck) { if (requiresAck) {
log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`); log.debug(`Message for nodeId ${nodeId} requires ACK. Setting up callback.`);
// Set a timeout for the ACK
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
this.ackCallbacks.delete(nodeId); this.ackCallbacks.delete(nodeId);
reject(new Error(`ACK timeout for nodeId ${nodeId}`)); reject(new Error(`ACK timeout for nodeId ${nodeId}`));
log.warn(`ACK timeout for nodeId ${nodeId}.`); log.warn(`ACK timeout for nodeId ${nodeId}.`);
}, 10000); }, 10000); // 10-second timeout
this.ackCallbacks.set(nodeId, { this.ackCallbacks.set(nodeId, {
resolve: (responseData) => { resolve: (responseData) => {
@@ -121,14 +124,18 @@ class WebSocketManager {
} }
}); });
} else { } else {
resolve(); resolve(); // Resolve immediately if no ACK is needed
} }
} else { } else {
log.warn("WebSocket not open. Queuing message."); log.warn("WebSocket not open. Queuing message.");
// Note: The current queueing doesn't support ACK promises well.
// For simplicity, we'll focus on the connected case.
// A more robust implementation would wrap the queued message in a function.
this.messageQueue.push(message); this.messageQueue.push(message);
if (!this.isConnecting) { if (!this.isConnecting) {
this.connect(); this.connect();
} }
// For now, we reject if not connected and ACK is required.
if (requiresAck) { if (requiresAck) {
reject(new Error("Cannot send message with ACK required while disconnected.")); reject(new Error("Cannot send message with ACK required while disconnected."));
} }
@@ -138,11 +145,16 @@ class WebSocketManager {
flushMessageQueue() { flushMessageQueue() {
log.debug(`Flushing ${this.messageQueue.length} queued messages.`); log.debug(`Flushing ${this.messageQueue.length} queued messages.`);
// Note: This simple flush doesn't handle ACKs for queued messages.
// This should be acceptable as data is sent right before queueing a prompt,
// at which point the socket should ideally be connected.
while (this.messageQueue.length > 0) { while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift(); const message = this.messageQueue.shift();
this.socket.send(message); this.socket.send(message);
} }
} }
} }
const wsUrl = `ws:
// Create a singleton instance of the WebSocketManager
const wsUrl = `ws://${window.location.host}/layerforge/canvas_ws`;
export const webSocketManager = new WebSocketManager(wsUrl); export const webSocketManager = new WebSocketManager(wsUrl);