Skip to content

Commit e8c51e9

Browse files
fix(queueing): make debouncing native to queue (#682)
1 parent 8a9bc4e commit e8c51e9

File tree

2 files changed

+82
-57
lines changed
  • apps/sim

2 files changed

+82
-57
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function useSubBlockValue<T = any>(
181181
triggerWorkflowUpdate = false,
182182
options?: UseSubBlockValueOptions
183183
): readonly [T | null, (value: T) => void] {
184-
const { debounceMs = 150, isStreaming = false, onStreamingEnd } = options || {}
184+
const { isStreaming = false, onStreamingEnd } = options || {}
185185

186186
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
187187

@@ -202,8 +202,7 @@ export function useSubBlockValue<T = any>(
202202
// Previous model reference for detecting model changes
203203
const prevModelRef = useRef<string | null>(null)
204204

205-
// Debouncing refs
206-
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
205+
// Streaming refs
207206
const lastEmittedValueRef = useRef<T | null>(null)
208207
const streamingValueRef = useRef<T | null>(null)
209208
const wasStreamingRef = useRef<boolean>(false)
@@ -232,15 +231,6 @@ export function useSubBlockValue<T = any>(
232231
// Compute the modelValue based on block type
233232
const modelValue = isProviderBasedBlock ? (modelSubBlockValue as string) : null
234233

235-
// Cleanup timer on unmount
236-
useEffect(() => {
237-
return () => {
238-
if (debounceTimerRef.current) {
239-
clearTimeout(debounceTimerRef.current)
240-
}
241-
}
242-
}, [])
243-
244234
// Emit the value to socket/DB
245235
const emitValue = useCallback(
246236
(value: T) => {
@@ -299,26 +289,12 @@ export function useSubBlockValue<T = any>(
299289
storeApiKeyValue(blockId, blockType, modelValue, newValue, storeValue)
300290
}
301291

302-
// Clear any existing debounce timer
303-
if (debounceTimerRef.current) {
304-
clearTimeout(debounceTimerRef.current)
305-
debounceTimerRef.current = null
306-
}
307-
308292
// If streaming, just store the value without emitting
309293
if (isStreaming) {
310294
streamingValueRef.current = valueCopy
311295
} else {
312-
// Detect large changes for extended debounce
313-
const isLargeChange = detectLargeChange(lastEmittedValueRef.current, valueCopy)
314-
const effectiveDebounceMs = isLargeChange ? debounceMs * 2 : debounceMs
315-
316-
// Debounce the socket emission
317-
debounceTimerRef.current = setTimeout(() => {
318-
if (valueRef.current !== null && valueRef.current !== lastEmittedValueRef.current) {
319-
emitValue(valueCopy)
320-
}
321-
}, effectiveDebounceMs)
296+
// Emit immediately - let the operation queue handle debouncing and deduplication
297+
emitValue(valueCopy)
322298
}
323299

324300
if (triggerWorkflowUpdate) {
@@ -335,7 +311,6 @@ export function useSubBlockValue<T = any>(
335311
triggerWorkflowUpdate,
336312
modelValue,
337313
isStreaming,
338-
debounceMs,
339314
emitValue,
340315
]
341316
)
@@ -412,26 +387,3 @@ export function useSubBlockValue<T = any>(
412387
// Return appropriate tuple based on whether options were provided
413388
return [storeValue !== undefined ? storeValue : initialValue, setValue] as const
414389
}
415-
416-
// Helper function to detect large changes
417-
function detectLargeChange(oldValue: any, newValue: any): boolean {
418-
// Handle null/undefined
419-
if (oldValue == null && newValue == null) return false
420-
if (oldValue == null || newValue == null) return true
421-
422-
// For strings, check if it's a large paste or deletion
423-
if (typeof oldValue === 'string' && typeof newValue === 'string') {
424-
const sizeDiff = Math.abs(newValue.length - oldValue.length)
425-
// Consider it a large change if more than 50 characters changed at once
426-
return sizeDiff > 50
427-
}
428-
429-
// For arrays, check length difference
430-
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
431-
const sizeDiff = Math.abs(newValue.length - oldValue.length)
432-
return sizeDiff > 5
433-
}
434-
435-
// For other types, always treat as small change
436-
return false
437-
}

apps/sim/stores/operation-queue/store.ts

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ interface OperationQueueState {
3434

3535
const retryTimeouts = new Map<string, NodeJS.Timeout>()
3636
const operationTimeouts = new Map<string, NodeJS.Timeout>()
37+
const subblockDebounceTimeouts = new Map<string, NodeJS.Timeout>()
3738

3839
let emitWorkflowOperation:
3940
| ((operation: string, target: string, payload: any, operationId?: string) => void)
@@ -59,6 +60,54 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
5960
hasOperationError: false,
6061

6162
addToQueue: (operation) => {
63+
// Handle debouncing for subblock operations
64+
if (
65+
operation.operation.operation === 'subblock-update' &&
66+
operation.operation.target === 'subblock'
67+
) {
68+
const { blockId, subblockId } = operation.operation.payload
69+
const debounceKey = `${blockId}-${subblockId}`
70+
71+
const existingTimeout = subblockDebounceTimeouts.get(debounceKey)
72+
if (existingTimeout) {
73+
clearTimeout(existingTimeout)
74+
}
75+
76+
set((state) => ({
77+
operations: state.operations.filter(
78+
(op) =>
79+
!(
80+
op.status === 'pending' &&
81+
op.operation.operation === 'subblock-update' &&
82+
op.operation.target === 'subblock' &&
83+
op.operation.payload?.blockId === blockId &&
84+
op.operation.payload?.subblockId === subblockId
85+
)
86+
),
87+
}))
88+
89+
const timeoutId = setTimeout(() => {
90+
subblockDebounceTimeouts.delete(debounceKey)
91+
92+
const queuedOp: QueuedOperation = {
93+
...operation,
94+
timestamp: Date.now(),
95+
retryCount: 0,
96+
status: 'pending',
97+
}
98+
99+
set((state) => ({
100+
operations: [...state.operations, queuedOp],
101+
}))
102+
103+
get().processNextOperation()
104+
}, 150) // 150ms debounce for subblock operations
105+
106+
subblockDebounceTimeouts.set(debounceKey, timeoutId)
107+
return
108+
}
109+
110+
// Handle non-subblock operations (existing logic)
62111
const state = get()
63112

64113
// Check for duplicate operation ID
@@ -80,13 +129,8 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
80129
// For block operations, check the block ID specifically
81130
((operation.operation.target === 'block' &&
82131
op.operation.payload?.id === operation.operation.payload?.id) ||
83-
// For subblock operations, check blockId and subblockId
84-
(operation.operation.target === 'subblock' &&
85-
op.operation.payload?.blockId === operation.operation.payload?.blockId &&
86-
op.operation.payload?.subblockId === operation.operation.payload?.subblockId) ||
87132
// For other operations, fall back to full payload comparison
88133
(operation.operation.target !== 'block' &&
89-
operation.operation.target !== 'subblock' &&
90134
JSON.stringify(op.operation.payload) === JSON.stringify(operation.operation.payload)))
91135
)
92136

@@ -127,6 +171,7 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
127171

128172
confirmOperation: (operationId) => {
129173
const state = get()
174+
const operation = state.operations.find((op) => op.id === operationId)
130175
const newOperations = state.operations.filter((op) => op.id !== operationId)
131176

132177
const retryTimeout = retryTimeouts.get(operationId)
@@ -141,6 +186,20 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
141186
operationTimeouts.delete(operationId)
142187
}
143188

189+
// Clean up any debounce timeouts for subblock operations
190+
if (
191+
operation?.operation.operation === 'subblock-update' &&
192+
operation.operation.target === 'subblock'
193+
) {
194+
const { blockId, subblockId } = operation.operation.payload
195+
const debounceKey = `${blockId}-${subblockId}`
196+
const debounceTimeout = subblockDebounceTimeouts.get(debounceKey)
197+
if (debounceTimeout) {
198+
clearTimeout(debounceTimeout)
199+
subblockDebounceTimeouts.delete(debounceKey)
200+
}
201+
}
202+
144203
logger.debug('Removing operation from queue', {
145204
operationId,
146205
remainingOps: newOperations.length,
@@ -166,6 +225,20 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
166225
operationTimeouts.delete(operationId)
167226
}
168227

228+
// Clean up any debounce timeouts for subblock operations
229+
if (
230+
operation.operation.operation === 'subblock-update' &&
231+
operation.operation.target === 'subblock'
232+
) {
233+
const { blockId, subblockId } = operation.operation.payload
234+
const debounceKey = `${blockId}-${subblockId}`
235+
const debounceTimeout = subblockDebounceTimeouts.get(debounceKey)
236+
if (debounceTimeout) {
237+
clearTimeout(debounceTimeout)
238+
subblockDebounceTimeouts.delete(debounceKey)
239+
}
240+
}
241+
169242
if (operation.retryCount < 3) {
170243
const newRetryCount = operation.retryCount + 1
171244
const delay = 2 ** newRetryCount * 1000 // 2s, 4s, 8s

0 commit comments

Comments
 (0)