Skip to content

Commit 96014bd

Browse files
authored
fix(mothership): keep a failed direct send ahead of follow-ups queued during its POST (#8738)
A direct send that failed (offline, a network blip, or a busy refusal) was re-queued at the tail, behind any follow-up typed while its POST was out, and the drain could send that follow-up first. A direct send only goes out with nothing queued ahead of it, so it now goes back to the head.
1 parent 102019b commit 96014bd

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3028,6 +3028,68 @@ describe('useChat remount send recovery', () => {
30283028
})
30293029
}
30303030

3031+
/**
3032+
* A follow-up typed while a direct send waits on its POST queues behind it.
3033+
* If that send then fails it goes back to the queue ahead of the follow-up:
3034+
* it was written first, and must still go out first.
3035+
*/
3036+
it.each(['offline', 'blip', 'busy'] as const)(
3037+
'keeps a failed direct send ahead of a follow-up queued during its POST (%s)',
3038+
async (outcome) => {
3039+
const history = idleHistory(`chat-requeue-order-${outcome}`)
3040+
mockRequestJson.mockImplementation(() => Promise.resolve({ chat: history }))
3041+
vi.spyOn(window.navigator, 'onLine', 'get').mockImplementation(() => outcome !== 'offline')
3042+
let failFirstPost: (() => void) | undefined
3043+
vi.stubGlobal('fetch', async (input: RequestInfo | URL, init?: RequestInit) => {
3044+
const url = String(input)
3045+
if (url === '/api/mothership/chat' && init?.method === 'POST') {
3046+
state.postBodies.push(JSON.parse(String(init.body)))
3047+
return new Promise<Response>((resolve, reject) => {
3048+
failFirstPost = () =>
3049+
outcome === 'busy'
3050+
? resolve(
3051+
Response.json(
3052+
{ error: 'A response is already in progress for this chat.' },
3053+
{ status: 409 }
3054+
)
3055+
)
3056+
: reject(new TypeError('Failed to fetch'))
3057+
})
3058+
}
3059+
if (url.includes('/api/mothership/chat/stream')) {
3060+
return Response.json({ error: 'Stream not found' }, { status: 404 })
3061+
}
3062+
return fetchStub(input, init)
3063+
})
3064+
const { getResult } = renderUseChatInChat(history.id, history)
3065+
await act(async () => {
3066+
void getResult().sendMessage('First, written before the follow-up')
3067+
})
3068+
await waitFor(() => failFirstPost !== undefined)
3069+
await act(async () => {
3070+
await getResult().sendMessage('Follow-up, written while the first was out')
3071+
})
3072+
await waitFor(
3073+
() => (useMothershipQueueStore.getState().queues[history.id]?.length ?? 0) === 1
3074+
)
3075+
3076+
await act(async () => {
3077+
failFirstPost?.()
3078+
await sleep(100)
3079+
})
3080+
3081+
expect(
3082+
(useMothershipQueueStore.getState().queues[history.id] ?? []).map(
3083+
(message) => message.content
3084+
)
3085+
).toEqual([
3086+
'First, written before the follow-up',
3087+
'Follow-up, written while the first was out',
3088+
])
3089+
expect(state.postBodies).toHaveLength(1)
3090+
}
3091+
)
3092+
30313093
it('holds a message sent while offline and sends it under the same id once back online', async () => {
30323094
const history = idleHistory('chat-offline-send')
30333095
mockRequestJson.mockImplementation(() => Promise.resolve({ chat: history }))

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4443,7 +4443,9 @@ export function useChat(
44434443
handOffWithdrawnSend(withdrawn)
44444444
return
44454445
}
4446-
useMothershipQueueStore.getState().enqueue(activeChatKey, {
4446+
/* Back at the head: a direct send only goes out with nothing queued ahead of
4447+
it, so anything queued while its POST was out was written after it. */
4448+
useMothershipQueueStore.getState().insertAt(activeChatKey, 0, {
44474449
...createQueuedMessage(
44484450
message,
44494451
fileAttachments,

0 commit comments

Comments
 (0)