Skip to content

Commit 905fc86

Browse files
committed
fix(copilot): retain resource removal intent
1 parent 861c966 commit 905fc86

4 files changed

Lines changed: 53 additions & 10 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1815,7 +1815,7 @@ export function useChat(
18151815
(r) => r.type === resourceUpdate.type && r.id === resourceUpdate.id
18161816
)
18171817
const resource = mergeChatResource(existing, resourceUpdate)
1818-
if (existing && resource === existing) {
1818+
if (existing && resource === existing && resourceUpdate.clearViewId !== true) {
18191819
return false
18201820
}
18211821

@@ -1861,8 +1861,6 @@ export function useChat(
18611861
scheduleDelete(persistChatId, () =>
18621862
requestJson(removeMothershipChatResourceContract, {
18631863
body: { chatId: persistChatId, resourceType, resourceId },
1864-
}).catch((err) => {
1865-
logger.warn('Failed to persist resource removal', err)
18661864
})
18671865
)
18681866
},

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,8 +726,8 @@ export function Table({
726726
viewPin.viewId,
727727
pendingCreatedViewIdRef.current
728728
)
729-
if (!transition.nextViewId) return
730729
pendingCreatedViewIdRef.current = transition.pendingCreatedViewId
730+
if (!transition.nextViewId) return
731731
preservedViewStateRef.current = null
732732
setTableParams({ view: transition.nextViewId })
733733
}, [embedded, viewPin, views, activeViewId, tableId, consumeViewPin, setTableParams])

apps/sim/lib/copilot/resources/client-persistence-queue.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,26 @@ describe('ResourcePersistenceQueue', () => {
155155
await vi.waitFor(() => expect(persist).toHaveBeenCalledTimes(2))
156156
expect(persist.mock.calls[1]).toEqual(['chat-1', { ...TABLE_RESOURCE, viewId: 'view-b' }])
157157
})
158+
159+
it('retries a failed deletion on the next flush', async () => {
160+
const first = deferred<unknown>()
161+
const persist = vi.fn<(chatId: string, update: MothershipResourceUpdate) => Promise<unknown>>()
162+
const remove = vi
163+
.fn<() => Promise<unknown>>()
164+
.mockReturnValueOnce(first.promise)
165+
.mockResolvedValueOnce({ success: true })
166+
const queue = new ResourcePersistenceQueue({ persist, onError })
167+
168+
const removal = queue.remove(TABLE_RESOURCE.type, TABLE_RESOURCE.id)
169+
removal.scheduleDelete('chat-1', remove)
170+
first.reject(new Error('offline'))
171+
await Promise.allSettled(Array.from(queue.inFlight.values()))
172+
173+
expect(queue.pendingKeys.has(`${TABLE_RESOURCE.type}:${TABLE_RESOURCE.id}`)).toBe(true)
174+
expect(onError).toHaveBeenCalledOnce()
175+
await queue.flush('chat-1')
176+
177+
expect(remove).toHaveBeenCalledTimes(2)
178+
expect(queue.pendingKeys.size).toBe(0)
179+
})
158180
})

apps/sim/lib/copilot/resources/client-persistence-queue.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class ResourcePersistenceQueue {
2525

2626
private readonly desiredUpdates = new Map<string, MothershipResourceUpdate>()
2727
private readonly failedKeys = new Set<string>()
28+
private readonly pendingRemovals = new Map<string, () => Promise<unknown>>()
2829
private readonly persistedKeys = new Set<string>()
2930
private readonly removalTokens = new Map<string, symbol>()
3031
private readonly writeTokens = new Map<string, symbol>()
@@ -45,6 +46,7 @@ export class ResourcePersistenceQueue {
4546
const trackedLocally =
4647
this.desiredUpdates.has(key) || this.pendingKeys.has(key) || this.inFlight.has(key)
4748
if (base && !trackedLocally) this.persistedKeys.add(key)
49+
this.pendingRemovals.delete(key)
4850
this.removalTokens.delete(key)
4951
const previous = this.desiredUpdates.get(key) ?? base
5052
this.desiredUpdates.set(key, mergePendingChatResourceUpdate(previous, update))
@@ -73,12 +75,14 @@ export class ResourcePersistenceQueue {
7375
const inFlight = this.inFlight.get(key)
7476
const wasPersisted = this.persistedKeys.delete(key)
7577
const removalToken = Symbol(key)
78+
this.pendingRemovals.delete(key)
7679
this.removalTokens.set(key, removalToken)
7780
this.desiredUpdates.delete(key)
7881
this.failedKeys.delete(key)
7982
return {
8083
inFlight,
8184
scheduleDelete: (chatId, remove) => {
85+
this.pendingRemovals.set(key, remove)
8286
const startRemoval = () => this.startRemoval(key, chatId, removalToken, remove)
8387
if (inFlight) {
8488
void inFlight.then(startRemoval, startRemoval)
@@ -103,14 +107,22 @@ export class ResourcePersistenceQueue {
103107
this.inFlight.clear()
104108
this.desiredUpdates.clear()
105109
this.failedKeys.clear()
110+
this.pendingRemovals.clear()
106111
this.persistedKeys.clear()
107112
this.removalTokens.clear()
108113
this.writeTokens.clear()
109114
}
110115

111116
private startPending(chatId: string): void {
112117
for (const key of this.pendingKeys) {
113-
if (!this.failedKeys.has(key) && !this.inFlight.has(key)) this.start(key, chatId)
118+
if (this.failedKeys.has(key) || this.inFlight.has(key)) continue
119+
const pendingRemoval = this.pendingRemovals.get(key)
120+
const removalToken = this.removalTokens.get(key)
121+
if (pendingRemoval && removalToken) {
122+
this.startRemoval(key, chatId, removalToken, pendingRemoval)
123+
continue
124+
}
125+
this.start(key, chatId)
114126
}
115127
}
116128

@@ -122,22 +134,33 @@ export class ResourcePersistenceQueue {
122134
): void {
123135
if (this.removalTokens.get(key) !== removalToken) return
124136

137+
this.pendingKeys.delete(key)
138+
let succeeded = false
125139
const writeToken = Symbol(key)
126140
const tracked = Promise.resolve()
127-
.then(() => {
141+
.then(async () => {
128142
if (this.removalTokens.get(key) !== removalToken) return
129-
return remove()
143+
await remove()
144+
succeeded = true
145+
})
146+
.catch((error) => {
147+
if (this.removalTokens.get(key) !== removalToken) return
148+
this.pendingKeys.add(key)
149+
this.failedKeys.add(key)
150+
this.onError(error)
130151
})
131-
.catch(this.onError)
132152
.finally(() => {
133153
if (this.writeTokens.get(key) !== writeToken) return
134154
this.writeTokens.delete(key)
135155
this.inFlight.delete(key)
136-
if (this.removalTokens.get(key) === removalToken) {
156+
if (succeeded && this.removalTokens.get(key) === removalToken) {
157+
this.pendingRemovals.delete(key)
137158
this.removalTokens.delete(key)
138159
this.persistedKeys.delete(key)
139160
}
140-
if (this.pendingKeys.has(key)) this.start(key, chatId)
161+
if (this.removalTokens.get(key) !== removalToken && this.pendingKeys.has(key)) {
162+
this.start(key, chatId)
163+
}
141164
})
142165
this.writeTokens.set(key, writeToken)
143166
this.inFlight.set(key, tracked)

0 commit comments

Comments
 (0)