fix(mobile): fence queued taps across reconnects

This commit is contained in:
Brennan Benson
2026-08-27 15:15:17 -07:00
parent 0212943072
commit 04bc32905a
3 changed files with 43 additions and 4 deletions
@@ -3416,11 +3416,19 @@ export default function SessionScreen() {
}, [])
const startAccessoryRepeat = useCallback(
(input: ReturnType<typeof createTerminalLiveAccessoryInput>) => {
// Why: the controller queue sits outside RPC, so fence pre-outage taps from a later connection.
const targetClient = clientRef.current
const targetConnectedAt = targetClient?.getLastConnectedAt() ?? null
accessoryRepeatRef.current.start(
input,
createTerminalAccessoryRepeatSender(
activeHandleRef.current,
() => activeHandleRef.current,
(targetHandle) =>
activeHandleRef.current === targetHandle &&
targetClient !== null &&
clientRef.current === targetClient &&
connStateRef.current === 'connected' &&
targetClient.getLastConnectedAt() === targetConnectedAt,
(nextInput, targetHandle) => handleAccessoryKeyRef.current(nextInput, targetHandle)
)
)
@@ -155,7 +155,7 @@ describe('terminal accessory repeat', () => {
const sendToTerminal = vi.fn(async () => true)
const send = createTerminalAccessoryRepeatSender(
activeTerminal,
() => activeTerminal,
(targetHandle) => activeTerminal === targetHandle,
sendToTerminal
)
@@ -166,4 +166,35 @@ describe('terminal accessory repeat', () => {
expect(sendToTerminal).toHaveBeenCalledTimes(1)
expect(sendToTerminal).toHaveBeenCalledWith('down', 'terminal-a')
})
it('drops a queued send when its press-time connection is no longer current', async () => {
vi.useFakeTimers()
let connectionGeneration = 1
const first = deferred()
const sent: string[] = []
const sendToTerminal = vi.fn((input: string) => {
sent.push(input)
return input === 'down' ? first.promise.then(() => true) : Promise.resolve(true)
})
const createSender = () => {
const pressedConnectionGeneration = connectionGeneration
return createTerminalAccessoryRepeatSender(
'terminal-a',
() => connectionGeneration === pressedConnectionGeneration,
sendToTerminal
)
}
const repeat = createTerminalAccessoryRepeatController<string>()
repeat.start('down', createSender())
repeat.stop()
repeat.start('up', createSender())
repeat.stop()
connectionGeneration = 2
first.resolve()
await vi.runAllTimersAsync()
expect(sent).toEqual(['down'])
})
})
@@ -5,11 +5,11 @@ type TerminalAccessoryRepeatSender<TInput> = (input: TInput) => Promise<boolean>
export function createTerminalAccessoryRepeatSender<TInput>(
targetHandle: string | null,
getActiveHandle: () => string | null,
isDeliveryTargetCurrent: (targetHandle: string) => boolean,
sendToTerminal: (input: TInput, targetHandle: string) => Promise<boolean>
): TerminalAccessoryRepeatSender<TInput> {
return (input) => {
if (!targetHandle || getActiveHandle() !== targetHandle) {
if (!targetHandle || !isDeliveryTargetCurrent(targetHandle)) {
return Promise.resolve(false)
}
return sendToTerminal(input, targetHandle)