Skip to content

Commit 3a1bd18

Browse files
[feat] Add auto-refresh on task completion for RemoteWidget nodes (#4191)
Co-authored-by: filtered <[email protected]>
1 parent 2f9dcd1 commit 3a1bd18

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

src/composables/widgets/useRemoteWidget.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { LGraphNode } from '@comfyorg/litegraph'
22
import { IWidget } from '@comfyorg/litegraph'
33
import axios from 'axios'
44

5+
import { useChainCallback } from '@/composables/functional/useChainCallback'
56
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
7+
import { api } from '@/scripts/api'
68

79
const MAX_RETRIES = 5
810
const TIMEOUT = 4096
@@ -220,6 +222,46 @@ export function useRemoteWidget<
220222
node.addWidget('button', 'refresh', 'refresh', widget.refresh)
221223
}
222224

225+
/**
226+
* Add auto-refresh toggle widget and execution success listener
227+
*/
228+
function addAutoRefreshToggle() {
229+
let autoRefreshEnabled = false
230+
231+
// Handler for execution success
232+
const handleExecutionSuccess = () => {
233+
if (autoRefreshEnabled && widget.refresh) {
234+
widget.refresh()
235+
}
236+
}
237+
238+
// Add toggle widget
239+
const autoRefreshWidget = node.addWidget(
240+
'toggle',
241+
'Auto-refresh after generation',
242+
false,
243+
(value: boolean) => {
244+
autoRefreshEnabled = value
245+
},
246+
{
247+
serialize: false
248+
}
249+
)
250+
251+
// Register event listener
252+
api.addEventListener('execution_success', handleExecutionSuccess)
253+
254+
// Cleanup on node removal
255+
node.onRemoved = useChainCallback(node.onRemoved, function () {
256+
api.removeEventListener('execution_success', handleExecutionSuccess)
257+
})
258+
259+
return autoRefreshWidget
260+
}
261+
262+
// Always add auto-refresh toggle for remote widgets
263+
addAutoRefreshToggle()
264+
223265
return {
224266
getCachedValue,
225267
getValue,

tests-ui/tests/composables/widgets/useRemoteWidget.test.ts

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ vi.mock('@/stores/settingStore', () => ({
2626
})
2727
}))
2828

29+
vi.mock('@/scripts/api', () => ({
30+
api: {
31+
addEventListener: vi.fn(),
32+
removeEventListener: vi.fn()
33+
}
34+
}))
35+
36+
vi.mock('@/composables/functional/useChainCallback', () => ({
37+
useChainCallback: vi.fn((original, ...callbacks) => {
38+
return function (this: any, ...args: any[]) {
39+
original?.apply(this, args)
40+
callbacks.forEach((cb: any) => cb.apply(this, args))
41+
}
42+
})
43+
}))
44+
2945
const FIRST_BACKOFF = 1000 // backoff is 1s on first retry
3046
const DEFAULT_VALUE = 'Loading...'
3147

@@ -40,7 +56,9 @@ function createMockConfig(overrides = {}): RemoteWidgetConfig {
4056
const createMockOptions = (inputOverrides = {}) => ({
4157
remoteConfig: createMockConfig(inputOverrides),
4258
defaultValue: DEFAULT_VALUE,
43-
node: {} as any,
59+
node: {
60+
addWidget: vi.fn()
61+
} as any,
4462
widget: {} as any
4563
})
4664

@@ -499,4 +517,168 @@ describe('useRemoteWidget', () => {
499517
expect(data2).toEqual(DEFAULT_VALUE)
500518
})
501519
})
520+
521+
describe('auto-refresh on task completion', () => {
522+
it('should add auto-refresh toggle widget', () => {
523+
const mockNode = {
524+
addWidget: vi.fn(),
525+
widgets: []
526+
}
527+
const mockWidget = {
528+
refresh: vi.fn()
529+
}
530+
531+
useRemoteWidget({
532+
remoteConfig: createMockConfig(),
533+
defaultValue: DEFAULT_VALUE,
534+
node: mockNode as any,
535+
widget: mockWidget as any
536+
})
537+
538+
// Should add auto-refresh toggle widget
539+
expect(mockNode.addWidget).toHaveBeenCalledWith(
540+
'toggle',
541+
'Auto-refresh after generation',
542+
false,
543+
expect.any(Function),
544+
{
545+
serialize: false
546+
}
547+
)
548+
})
549+
550+
it('should register event listener when enabled', async () => {
551+
const { api } = await import('@/scripts/api')
552+
553+
const mockNode = {
554+
addWidget: vi.fn(),
555+
widgets: []
556+
}
557+
const mockWidget = {
558+
refresh: vi.fn()
559+
}
560+
561+
useRemoteWidget({
562+
remoteConfig: createMockConfig(),
563+
defaultValue: DEFAULT_VALUE,
564+
node: mockNode as any,
565+
widget: mockWidget as any
566+
})
567+
568+
// Event listener should be registered immediately
569+
expect(api.addEventListener).toHaveBeenCalledWith(
570+
'execution_success',
571+
expect.any(Function)
572+
)
573+
})
574+
575+
it('should refresh widget when workflow completes successfully', async () => {
576+
const { api } = await import('@/scripts/api')
577+
let executionSuccessHandler: (() => void) | undefined
578+
579+
// Capture the event handler
580+
vi.mocked(api.addEventListener).mockImplementation((event, handler) => {
581+
if (event === 'execution_success') {
582+
executionSuccessHandler = handler as () => void
583+
}
584+
})
585+
586+
const mockNode = {
587+
addWidget: vi.fn(),
588+
widgets: []
589+
}
590+
const mockWidget = {} as any
591+
592+
useRemoteWidget({
593+
remoteConfig: createMockConfig(),
594+
defaultValue: DEFAULT_VALUE,
595+
node: mockNode as any,
596+
widget: mockWidget
597+
})
598+
599+
// Spy on the refresh function that was added by useRemoteWidget
600+
const refreshSpy = vi.spyOn(mockWidget, 'refresh')
601+
602+
// Get the toggle callback and enable auto-refresh
603+
const toggleCallback = mockNode.addWidget.mock.calls.find(
604+
(call) => call[0] === 'toggle'
605+
)?.[3]
606+
toggleCallback?.(true)
607+
608+
// Simulate workflow completion
609+
executionSuccessHandler?.()
610+
611+
expect(refreshSpy).toHaveBeenCalled()
612+
})
613+
614+
it('should not refresh when toggle is disabled', async () => {
615+
const { api } = await import('@/scripts/api')
616+
let executionSuccessHandler: (() => void) | undefined
617+
618+
// Capture the event handler
619+
vi.mocked(api.addEventListener).mockImplementation((event, handler) => {
620+
if (event === 'execution_success') {
621+
executionSuccessHandler = handler as () => void
622+
}
623+
})
624+
625+
const mockNode = {
626+
addWidget: vi.fn(),
627+
widgets: []
628+
}
629+
const mockWidget = {} as any
630+
631+
useRemoteWidget({
632+
remoteConfig: createMockConfig(),
633+
defaultValue: DEFAULT_VALUE,
634+
node: mockNode as any,
635+
widget: mockWidget
636+
})
637+
638+
// Spy on the refresh function that was added by useRemoteWidget
639+
const refreshSpy = vi.spyOn(mockWidget, 'refresh')
640+
641+
// Toggle is disabled by default
642+
// Simulate workflow completion
643+
executionSuccessHandler?.()
644+
645+
expect(refreshSpy).not.toHaveBeenCalled()
646+
})
647+
648+
it('should cleanup event listener on node removal', async () => {
649+
const { api } = await import('@/scripts/api')
650+
let executionSuccessHandler: (() => void) | undefined
651+
652+
// Capture the event handler
653+
vi.mocked(api.addEventListener).mockImplementation((event, handler) => {
654+
if (event === 'execution_success') {
655+
executionSuccessHandler = handler as () => void
656+
}
657+
})
658+
659+
const mockNode = {
660+
addWidget: vi.fn(),
661+
widgets: [],
662+
onRemoved: undefined as any
663+
}
664+
const mockWidget = {
665+
refresh: vi.fn()
666+
}
667+
668+
useRemoteWidget({
669+
remoteConfig: createMockConfig(),
670+
defaultValue: DEFAULT_VALUE,
671+
node: mockNode as any,
672+
widget: mockWidget as any
673+
})
674+
675+
// Simulate node removal
676+
mockNode.onRemoved?.()
677+
678+
expect(api.removeEventListener).toHaveBeenCalledWith(
679+
'execution_success',
680+
executionSuccessHandler
681+
)
682+
})
683+
})
502684
})

0 commit comments

Comments
 (0)