fix(clipboard): preserve copy order across yielding validation (#35124)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-28 09:30:26 -07:00
committed by GitHub
co-authored by OrcaWin
parent 74937b50aa
commit 07a98c2c6d
2 changed files with 50 additions and 12 deletions
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ProcessResult, ProcessSpec } from '@orca/process-host/process-spec'
import {
assertClipboardTextWriteWithinLimitWithYield,
CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS,
CLIPBOARD_TEXT_WRITE_MAX_BYTES
} from '../../shared/clipboard-text'
import { createClipboardTextPublisher } from './clipboard-text-publisher'
@@ -127,6 +128,37 @@ describe('terminal clipboard writes', () => {
}
)
it.each([false, true])(
'keeps a newer copy after yielding validation (Wayland=%s)',
async (wayland) => {
vi.useFakeTimers()
try {
const copy = writer(wayland)
const large = 'x'.repeat(CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS * 2)
const first = copy(large, 'terminal')
const second = copy('newer', 'ordinary')
await vi.runAllTimersAsync()
await Promise.all([first, second])
if (wayland) {
expect(run.mock.calls.map(([spec]) => spec.input === large)).toEqual([true, false])
expect(run.mock.calls[1]?.[0].input).toBe('newer')
expect(write).not.toHaveBeenCalled()
} else {
expect(write.mock.calls.map(([text, kind]) => [text === large, kind])).toEqual([
[true, 'terminal'],
[false, 'ordinary']
])
expect(write.mock.calls[1]?.[0]).toBe('newer')
expect(run).not.toHaveBeenCalled()
}
} finally {
vi.useRealTimers()
}
}
)
it('preserves request order across yielding validation and resumes after failure', async () => {
let release: (text: string) => void = () => undefined
const validate = vi.fn(async (text: string) => text)
+18 -12
View File
@@ -40,21 +40,25 @@ export function createClipboardTextPublisher(
if (text.length > CLIPBOARD_TEXT_WRITE_MAX_BYTES) {
return Promise.reject(new Error(CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR))
}
if (!deps.isWayland()) {
return deps.validate(text).then((safeText) => deps.write(safeText, kind))
const wayland = deps.isWayland()
if (wayland) {
if (
count >= MAX_PENDING_CLIPBOARD_WRITES ||
text.length > CLIPBOARD_TEXT_WRITE_MAX_BYTES - queuedCodeUnits
) {
return Promise.reject(new Error('Too many pending clipboard writes'))
}
count += 1
queuedCodeUnits += text.length
}
if (
count >= MAX_PENDING_CLIPBOARD_WRITES ||
text.length > CLIPBOARD_TEXT_WRITE_MAX_BYTES - queuedCodeUnits
) {
return Promise.reject(new Error('Too many pending clipboard writes'))
}
count += 1
queuedCodeUnits += text.length
// Validate inside the queue: large copies yield and must not overwrite a later request.
const next = pending
.then(async () => {
const safeText = await deps.validate(text)
if (!wayland) {
deps.write(safeText, kind)
return
}
const result = await deps.run({
program: await deps.helperPath(),
input: safeText,
@@ -68,8 +72,10 @@ export function createClipboardTextPublisher(
deps.clearCache()
})
.finally(() => {
count -= 1
queuedCodeUnits -= text.length
if (wayland) {
count -= 1
queuedCodeUnits -= text.length
}
})
pending = next.catch(() => undefined)
return next