From fa28ec8ec7d980da3f6e8d6d593cc591733854d3 Mon Sep 17 00:00:00 2001 From: Fillip Date: Fri, 10 Apr 2026 17:27:02 -0700 Subject: [PATCH] fix(path-animator,image-picker): await submit POST before closing modal (v2.4.0) Race condition: Continue/Cancel button handlers were firing submitBlocking() without awaiting it, then immediately calling close() which tore down the modal. If the fetch got delayed or dropped by the DOM teardown, the Python thread would stay blocked on event.wait() until the full timeout. Symptoms: "sometimes Continue gets stuck and doesn't advance the workflow." Fixes: - PathAnimator: All exit paths (Continue, Cancel, ESC, overlay click, header close) now await submitBlocking() before close(). On Continue failure, the modal stays open with buttons re-enabled so user can retry. - PathAnimator: submitBlocking now checks response.ok and throws on non-2xx, surfacing 404 "Session not found" errors instead of swallowing them. - PathAnimator: close() safeguard still uses fire-and-forget but now with .catch() to log errors (it's a last-resort path after primary handlers). - ImagePicker: sendSelection now checks response.ok and throws on non-2xx. - ImagePicker: All callers wrapped in try/catch so failed submits don't leave the modal in a half-closed state. Confirm failures keep the modal open with a user-visible alert. Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 2 +- web/nodes/image/FL_ImagePicker.js | 70 +++++++++++++++-------- web/nodes/wip/FL_PathAnimator.js | 93 ++++++++++++++++++++++--------- 3 files changed, 116 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b2b6b6..4ee5822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_fill-nodes" description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection." -version = "2.3.9" +version = "2.4.0" license = "LICENSE" dependencies = ["librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"] diff --git a/web/nodes/image/FL_ImagePicker.js b/web/nodes/image/FL_ImagePicker.js index 27db7c2..5dc5f34 100644 --- a/web/nodes/image/FL_ImagePicker.js +++ b/web/nodes/image/FL_ImagePicker.js @@ -190,7 +190,11 @@ function showImagePickerModal(sessionId, images, batchSize, timeoutSeconds) { clearInterval(countdownInterval); countdownInterval = null; // Timeout - cancel the job instead of sending images through - await sendSelection(sessionId, [], true); + try { + await sendSelection(sessionId, [], true); + } catch (err) { + console.error('[FL_ImagePicker] Timeout cancel failed:', err); + } closeModal(); } }, 1000); @@ -434,13 +438,23 @@ function showImagePickerModal(sessionId, images, batchSize, timeoutSeconds) { `; const cancelBtn = createButton('Cancel', '#d9534f', async () => { - await sendSelection(sessionId, [], true); + try { + await sendSelection(sessionId, [], true); + } catch (e) { + console.error('[FL_ImagePicker] Cancel submit failed:', e); + } closeModal(); }); const confirmBtn = createButton('Continue with Selected', '#4ECDC4', async () => { const selection = Array.from(selectedIndices).sort((a, b) => a - b); - await sendSelection(sessionId, selection, false); + try { + await sendSelection(sessionId, selection, false); + } catch (e) { + console.error('[FL_ImagePicker] Confirm submit failed:', e); + alert('Failed to submit selection to backend. Check console for details.'); + return; // Don't close — let user retry + } closeModal(); }, true); @@ -470,12 +484,22 @@ function showImagePickerModal(sessionId, images, batchSize, timeoutSeconds) { if (e.key === 'Escape') { e.preventDefault(); - await sendSelection(sessionId, [], true); + try { + await sendSelection(sessionId, [], true); + } catch (err) { + console.error('[FL_ImagePicker] ESC cancel failed:', err); + } closeModal(); } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); const selection = Array.from(selectedIndices).sort((a, b) => a - b); - await sendSelection(sessionId, selection, false); + try { + await sendSelection(sessionId, selection, false); + } catch (err) { + console.error('[FL_ImagePicker] Ctrl+Enter confirm failed:', err); + alert('Failed to submit selection to backend. Check console for details.'); + return; + } closeModal(); } }; @@ -532,25 +556,25 @@ function createButton(text, color, onClick, isPrimary = false) { async function sendSelection(sessionId, selection, cancelled) { console.log(`[FL_ImagePicker] Sending selection: session=${sessionId}, selection=${JSON.stringify(selection)}, cancelled=${cancelled}`); - try { - const response = await fetch('/fl_image_picker/select', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - session_id: sessionId, - selection: selection, - cancelled: cancelled - }) - }); - const result = await response.json(); - console.log('[FL_ImagePicker] Selection sent successfully:', result); - return result; - } catch (error) { - console.error('[FL_ImagePicker] Error sending selection:', error); - throw error; + const response = await fetch('/fl_image_picker/select', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_id: sessionId, + selection: selection, + cancelled: cancelled + }) + }); + if (!response.ok) { + const msg = `[FL_ImagePicker] Selection POST failed with status ${response.status}`; + console.error(msg); + throw new Error(msg); } + const result = await response.json(); + console.log('[FL_ImagePicker] Selection sent successfully:', result); + return result; } // Full-resolution preview modal diff --git a/web/nodes/wip/FL_PathAnimator.js b/web/nodes/wip/FL_PathAnimator.js index 9e0a3cd..733241a 100644 --- a/web/nodes/wip/FL_PathAnimator.js +++ b/web/nodes/wip/FL_PathAnimator.js @@ -279,20 +279,24 @@ class PathEditorModal { } async submitBlocking(pathsJson, cancelled) { - try { - await fetch('/fl_path_animator/submit', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - session_id: this.sessionId, - paths_data: pathsJson, - cancelled: cancelled, - }), - }); - console.log(`[FL_PathAnimator] Submitted session ${this.sessionId} (cancelled=${cancelled})`); - } catch (e) { - console.error('[FL_PathAnimator] Submit failed:', e); + console.log(`[FL_PathAnimator] Submitting session ${this.sessionId} (cancelled=${cancelled})`); + const response = await fetch('/fl_path_animator/submit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + session_id: this.sessionId, + paths_data: pathsJson, + cancelled: cancelled, + }), + }); + if (!response.ok) { + const msg = `Submit failed with status ${response.status}`; + console.error(`[FL_PathAnimator] ${msg}`); + throw new Error(msg); } + const result = await response.json(); + console.log(`[FL_PathAnimator] Submit OK:`, result); + return result; } refreshFooterForMode() { @@ -343,11 +347,18 @@ class PathEditorModal { if (e.key === 'Escape') { if (this.mode === 'blocking' && !this.submitted) { this.submitted = true; - this.submitBlocking(null, true); + (async () => { + try { + await this.submitBlocking(null, true); + } catch (err) { + console.error('[FL_PathAnimator] ESC cancel failed:', err); + } + this.close(); + })(); } else { this.savePaths(); + this.close(); } - this.close(); } // Ctrl+V to paste image from clipboard @@ -579,11 +590,15 @@ class PathEditorModal { this.overlay.appendChild(this.container); // Close on overlay click (cancel in blocking mode) - this.overlay.addEventListener('click', (e) => { + this.overlay.addEventListener('click', async (e) => { if (e.target === this.overlay) { if (this.mode === 'blocking' && !this.submitted) { this.submitted = true; - this.submitBlocking(null, true); + try { + await this.submitBlocking(null, true); + } catch (err) { + console.error('[FL_PathAnimator] Overlay click cancel failed:', err); + } } this.close(); } @@ -675,10 +690,14 @@ class PathEditorModal { closeBtn.style.borderColor = 'rgba(255, 255, 255, 0.1)'; closeBtn.style.transform = 'scale(1)'; }; - closeBtn.onclick = () => { + closeBtn.onclick = async () => { if (this.mode === 'blocking' && !this.submitted) { this.submitted = true; - this.submitBlocking(null, true); + try { + await this.submitBlocking(null, true); + } catch (err) { + console.error('[FL_PathAnimator] Header close cancel failed:', err); + } } this.close(); }; @@ -1940,10 +1959,16 @@ class PathEditorModal { cursor: pointer; font-size: 14px; `; - cancelBtn.onclick = () => { + cancelBtn.onclick = async () => { if (this.mode === 'blocking' && !this.submitted) { this.submitted = true; - this.submitBlocking(null, true); + cancelBtn.disabled = true; + saveBtn.disabled = true; + try { + await this.submitBlocking(null, true); + } catch (e) { + console.error('[FL_PathAnimator] Cancel submit failed:', e); + } } this.close(); }; @@ -1960,11 +1985,25 @@ class PathEditorModal { font-size: 14px; font-weight: 500; `; - saveBtn.onclick = () => { + saveBtn.onclick = async () => { this.savePaths(); if (this.mode === 'blocking' && !this.submitted) { this.submitted = true; - this.submitBlocking(this.pathsDataWidget.value, false); + cancelBtn.disabled = true; + saveBtn.disabled = true; + saveBtn.textContent = 'Sending...'; + try { + await this.submitBlocking(this.pathsDataWidget.value, false); + } catch (e) { + console.error('[FL_PathAnimator] Continue submit failed:', e); + // Re-enable so user can retry or cancel + this.submitted = false; + cancelBtn.disabled = false; + saveBtn.disabled = false; + saveBtn.textContent = 'Continue'; + alert('Failed to submit paths to backend. Check console for details.'); + return; + } } this.close(); }; @@ -2018,10 +2057,14 @@ class PathEditorModal { close() { // Safeguard: if blocking mode and we haven't submitted yet, cancel the session - // so the Python thread never permanently blocks + // so the Python thread never permanently blocks. Note: this is fire-and-forget + // because close() is synchronous — all primary exit paths (Continue, Cancel, ESC, + // overlay-click, header-close) already await submitBlocking before calling close(). if (this.mode === 'blocking' && !this.submitted) { this.submitted = true; - this.submitBlocking(null, true); + this.submitBlocking(null, true).catch(err => { + console.error('[FL_PathAnimator] Safeguard cancel failed:', err); + }); } // Clear countdown timer