Skip to content

Commit c8ee72b

Browse files
fix(enrichment): clear stale manual attempts on edit
1 parent 921ad19 commit c8ee72b

3 files changed

Lines changed: 122 additions & 12 deletions

File tree

apps/sim/hooks/queries/tables.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ const { queryClient, cacheStore } = vi.hoisted(() => {
1919
cache.set(k, next)
2020
return next
2121
}),
22+
setQueriesData: vi.fn(
23+
(opts: { queryKey: readonly unknown[] }, updater: (value: unknown) => unknown) => {
24+
const prefix = JSON.stringify(opts.queryKey).slice(0, -1)
25+
const results: Array<[readonly unknown[], unknown]> = []
26+
for (const [key, value] of cache.entries()) {
27+
if (!key.startsWith(prefix)) continue
28+
const next = updater(value)
29+
cache.set(key, next)
30+
results.push([JSON.parse(key) as readonly unknown[], next])
31+
}
32+
return results
33+
}
34+
),
2235
getQueriesData: vi.fn((opts: { queryKey: readonly unknown[] }) => {
2336
const prefix = JSON.stringify(opts.queryKey).slice(0, -1)
2437
return [...cache.entries()]
@@ -63,6 +76,7 @@ import {
6376
useDeleteColumn,
6477
useRestoreTable,
6578
useUpdateColumn,
79+
useUpdateTableRow,
6680
} from '@/hooks/queries/tables'
6781
import { tableKeys } from '@/hooks/queries/utils/table-keys'
6882

@@ -220,6 +234,67 @@ describe('useUpdateColumn optimistic update', () => {
220234
})
221235
})
222236

237+
describe('useUpdateTableRow optimistic update', () => {
238+
it('clears a stale manual enrichment attempt when a mapped input changes', async () => {
239+
const rowsKey = tableKeys.infiniteRows(TABLE_ID, 'default')
240+
setCache(tableKeys.detail(TABLE_ID), {
241+
id: TABLE_ID,
242+
schema: {
243+
columns: [
244+
{ id: 'domain', name: 'Domain', type: 'string' },
245+
{ id: 'email', name: 'Email', type: 'string', workflowGroupId: 'group-1' },
246+
],
247+
workflowGroups: [
248+
{
249+
id: 'group-1',
250+
workflowId: '',
251+
type: 'enrichment',
252+
enrichmentId: 'work-email',
253+
autoRun: false,
254+
inputMappings: [{ inputName: 'companyDomain', columnName: 'domain' }],
255+
outputs: [{ blockId: '', path: '', outputId: 'email', columnName: 'email' }],
256+
},
257+
],
258+
},
259+
})
260+
setCache(rowsKey, {
261+
pages: [
262+
{
263+
rows: [
264+
{
265+
id: 'row-1',
266+
data: { domain: 'old.example' },
267+
executions: {
268+
'group-1': {
269+
status: 'completed',
270+
executionId: 'execution-1',
271+
jobId: null,
272+
workflowId: '',
273+
isManualRun: true,
274+
error: null,
275+
},
276+
},
277+
position: 0,
278+
createdAt: new Date(),
279+
updatedAt: new Date(),
280+
},
281+
],
282+
totalCount: 1,
283+
},
284+
],
285+
pageParams: [0],
286+
})
287+
288+
const hook = useUpdateTableRow({ workspaceId: WORKSPACE_ID, tableId: TABLE_ID })
289+
await hook.onMutate?.({ rowId: 'row-1', data: { domain: 'new.example' } })
290+
291+
const rows = getCache<{
292+
pages: Array<{ rows: Array<{ executions: Record<string, unknown> }> }>
293+
}>(rowsKey)
294+
expect(rows?.pages[0]?.rows[0]?.executions).toEqual({})
295+
})
296+
})
297+
223298
describe('useRestoreTable cache invalidation', () => {
224299
it('primes the table detail cache and clears stale rows for the restored table', () => {
225300
const hook = useRestoreTable()

apps/sim/lib/table/deps.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,26 @@ describe('optimisticallyScheduleNewlyEligibleGroups — enrichment inputs', () =
118118
domain: 'https://',
119119
})
120120

121-
expect(next).toBeNull()
121+
expect(next).toEqual({})
122+
})
123+
124+
it('clears a manual enrichment attempt when a mapped input changes', () => {
125+
const manualGroup = { ...group, autoRun: false }
126+
const before = makeRow(
127+
{ name: 'Person Name', domain: 'old.example' },
128+
{
129+
g1: {
130+
...completedExec('wf-g1'),
131+
isManualRun: true,
132+
},
133+
}
134+
)
135+
136+
const next = optimisticallyScheduleNewlyEligibleGroups([manualGroup], columns, before, {
137+
domain: 'new.example',
138+
})
139+
140+
expect(next).toEqual({})
122141
})
123142
})
124143

apps/sim/lib/table/deps.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,9 @@ export function getGroupInvalidationColumns(group: WorkflowGroup): string[] {
139139
* workflow group whose deps were unmet *before* the patch and are satisfied
140140
* *after*, OR whose dep column was touched by the patch (the server will
141141
* cancel+re-run via `deriveExecClearsForDataPatch` + the in-flight cancel
142-
* orchestration), return a new `executions` map with that group flipped to
143-
* `pending`. Enrichments are only flipped when a provider can build a request.
142+
* orchestration), return a new `executions` map. Eligible automatic groups are
143+
* flipped to `pending`; terminal manual groups and automatic enrichments that
144+
* cannot build provider params are cleared to mirror the server's re-arm.
144145
* The cell renderer treats `pending` as "Queued".
145146
*
146147
* Returns `null` when nothing changed, so callers can short-circuit.
@@ -160,10 +161,29 @@ export function optimisticallyScheduleNewlyEligibleGroups(
160161
const patchedColumns = new Set(Object.keys(patch))
161162

162163
let next: RowExecutions | null = null
163-
let flipped = 0
164+
let changed = 0
164165
let skipped = 0
165166
for (const group of groups) {
167+
const exec = beforeRow.executions?.[group.id]
168+
const depTouched = getGroupInvalidationColumns(group).some((columnId) =>
169+
patchedColumns.has(columnId)
170+
)
171+
172+
const clearInvalidatedTerminalExecution = (): boolean => {
173+
if (
174+
!depTouched ||
175+
(exec?.status !== 'completed' && exec?.status !== 'error' && exec?.status !== 'cancelled')
176+
) {
177+
return false
178+
}
179+
if (next === null) next = { ...(beforeRow.executions ?? {}) }
180+
delete next[group.id]
181+
changed++
182+
return true
183+
}
184+
166185
if (group.autoRun === false) {
186+
if (areGroupDepsSatisfied(group, afterRow)) clearInvalidatedTerminalExecution()
167187
skipped++
168188
continue
169189
}
@@ -177,12 +197,12 @@ export function optimisticallyScheduleNewlyEligibleGroups(
177197
enrichment &&
178198
!getEnrichmentRowReadiness(enrichment, group, afterRow.data, columns).ready
179199
) {
200+
clearInvalidatedTerminalExecution()
180201
skipped++
181202
continue
182203
}
183204
}
184205

185-
const exec = beforeRow.executions?.[group.id]
186206
if (exec?.status === 'pending' && exec.jobId) {
187207
skipped++
188208
continue
@@ -196,10 +216,6 @@ export function optimisticallyScheduleNewlyEligibleGroups(
196216
// flight downstream groups, so optimistically flip to `pending`
197217
// regardless of current exec status (queued/running included — they're
198218
// about to be cancelled and re-run).
199-
const depTouched = getGroupInvalidationColumns(group).some((columnId) =>
200-
patchedColumns.has(columnId)
201-
)
202-
203219
if (!depTouched && (exec?.status === 'queued' || exec?.status === 'running')) {
204220
skipped++
205221
continue
@@ -209,7 +225,7 @@ export function optimisticallyScheduleNewlyEligibleGroups(
209225
continue
210226
}
211227

212-
flipped++
228+
changed++
213229
if (next === null) next = { ...(beforeRow.executions ?? {}) }
214230
const pending: RowExecutionMetadata = {
215231
status: 'pending',
@@ -221,8 +237,8 @@ export function optimisticallyScheduleNewlyEligibleGroups(
221237
}
222238
next[group.id] = pending
223239
}
224-
if (flipped > 0) {
225-
logger.debug(`[OptimisticCascade] row=${beforeRow.id} flipped=${flipped} skipped=${skipped}`)
240+
if (changed > 0) {
241+
logger.debug(`[OptimisticCascade] row=${beforeRow.id} changed=${changed} skipped=${skipped}`)
226242
}
227243
return next
228244
}

0 commit comments

Comments
 (0)