fix: stop the composer re-sending a queued message already in flight
A queued message is removed from the queue only after its send resolves, so between dispatch and resolution it stays visible to every reader — and a composer submit merges the whole queue into its own send. Over a relay that window is seconds, long enough to deliver the same message twice. The queue now tracks which entries are awaiting the server. Dispatchers skip them, clearQueue retains them so the pending send can still remove or restore its own entry, and the flag is not persisted because a restart has no in-flight sends.
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
} from "./messageQueueStore"
|
||||
|
||||
beforeEach(() => {
|
||||
useMessageQueueStore.setState({ queuedMessages: {}, quarantinedLegacyMessages: {} })
|
||||
useMessageQueueStore.setState({ queuedMessages: {}, quarantinedLegacyMessages: {}, sendingIds: {} })
|
||||
})
|
||||
|
||||
describe("message queue runtime ownership", () => {
|
||||
@@ -49,3 +49,48 @@ describe("message queue runtime ownership", () => {
|
||||
expect(queue[0]?.content).toBe("message-5")
|
||||
})
|
||||
})
|
||||
|
||||
describe("in-flight queued sends", () => {
|
||||
test("hides a dispatched message from the sendable queue but keeps it visible", () => {
|
||||
const target = createMessageQueueTarget("session-1", "/repo", "runtime-a")!
|
||||
const store = useMessageQueueStore.getState()
|
||||
store.addToQueue(target, { content: "first" })
|
||||
store.addToQueue(target, { content: "second" })
|
||||
const [first] = useMessageQueueStore.getState().getQueueForTarget(target)
|
||||
|
||||
useMessageQueueStore.getState().markSending(target, first.id)
|
||||
|
||||
expect(useMessageQueueStore.getState().getQueueForTarget(target)).toHaveLength(2)
|
||||
const sendable = useMessageQueueStore.getState().getSendableQueue(target)
|
||||
expect(sendable).toHaveLength(1)
|
||||
expect(sendable[0]?.content).toBe("second")
|
||||
|
||||
useMessageQueueStore.getState().clearSending(target, first.id)
|
||||
expect(useMessageQueueStore.getState().getSendableQueue(target)).toHaveLength(2)
|
||||
expect(useMessageQueueStore.getState().sendingIds).toEqual({})
|
||||
})
|
||||
|
||||
test("clearQueue retains a message whose send is still awaiting the server", () => {
|
||||
const target = createMessageQueueTarget("session-1", "/repo", "runtime-a")!
|
||||
const store = useMessageQueueStore.getState()
|
||||
store.addToQueue(target, { content: "in flight" })
|
||||
store.addToQueue(target, { content: "merged by composer" })
|
||||
const [inFlight] = useMessageQueueStore.getState().getQueueForTarget(target)
|
||||
useMessageQueueStore.getState().markSending(target, inFlight.id)
|
||||
|
||||
useMessageQueueStore.getState().clearQueue(target)
|
||||
|
||||
const remaining = useMessageQueueStore.getState().getQueueForTarget(target)
|
||||
expect(remaining).toHaveLength(1)
|
||||
expect(remaining[0]?.id).toBe(inFlight.id)
|
||||
})
|
||||
|
||||
test("clearQueue drops everything once no send is in flight", () => {
|
||||
const target = createMessageQueueTarget("session-1", "/repo", "runtime-a")!
|
||||
useMessageQueueStore.getState().addToQueue(target, { content: "queued" })
|
||||
|
||||
useMessageQueueStore.getState().clearQueue(target)
|
||||
|
||||
expect(useMessageQueueStore.getState().getQueueForTarget(target)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user