From 77e117b4b5607c3e98f9d5acdc86398ee96df4e0 Mon Sep 17 00:00:00 2001 From: thegecko Date: Mon, 5 Oct 2026 13:08:50 +0100 Subject: [PATCH] Fix transfer default timeout handling --- src/webusb_device.rs | 24 ++++++++++++++++-------- test/webusb.js | 24 ++++++++++++++++++++++++ tsc/index.ts | 16 +++++++++------- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/webusb_device.rs b/src/webusb_device.rs index 0511527..0b1849a 100644 --- a/src/webusb_device.rs +++ b/src/webusb_device.rs @@ -213,7 +213,7 @@ enum EndpointCommand { } struct PendingTransfer { - deadline: Instant, + deadline: Option, response: Option, } @@ -379,7 +379,7 @@ fn handle_endpoint_command( endpoint.submit(buffer); pending.push_back(PendingTransfer { - deadline: Instant::now() + timeout, + deadline: transfer_deadline(timeout), response: Some(response), }); } @@ -413,7 +413,10 @@ fn cancel_pending_transfers(pending: &mut VecDeque) { fn expire_timed_out_transfers(pending: &mut VecDeque) { let now = Instant::now(); for pending_transfer in pending { - if pending_transfer.deadline <= now { + if pending_transfer + .deadline + .is_some_and(|deadline| deadline <= now) + { if let Some(response) = pending_transfer.response.take() { let _ = response.send(Err(format!( "{:?}", @@ -430,16 +433,21 @@ fn next_wait(pending: &VecDeque) -> Duration { pending .iter() .filter(|pending_transfer| pending_transfer.response.is_some()) - .map(|pending_transfer| { - pending_transfer - .deadline - .saturating_duration_since(Instant::now()) - }) + .filter_map(|pending_transfer| pending_transfer.deadline) + .map(|deadline| deadline.saturating_duration_since(Instant::now())) .min() .map(|timeout| timeout.min(ENDPOINT_POLL_TIMEOUT)) .unwrap_or(ENDPOINT_POLL_TIMEOUT) } +fn transfer_deadline(timeout: Duration) -> Option { + if timeout.is_zero() { + None + } else { + Some(Instant::now() + timeout) + } +} + #[napi(object)] pub struct UsbEndpoint { #[napi(writable = false)] diff --git a/test/webusb.js b/test/webusb.js index 2a1d130..8b3f92c 100644 --- a/test/webusb.js +++ b/test/webusb.js @@ -304,6 +304,30 @@ describe('Transfers', () => { await device.claimInterface(0); }); + it('does not cancel transfer IN by default', async () => { + const pending = { status: 'pending' }; + const transfer = device.transferIn(3, b2.byteLength).then( + value => ({ status: 'fulfilled', value }), + reason => ({ status: 'rejected', reason }) + ); + + const result = await Promise.race([ + transfer, + new Promise(resolve => setTimeout(() => resolve(pending), 1200)), + ]); + + assert.notEqual(result.status, 'rejected', result.reason && result.reason.message); + + if (result.status === 'pending') { + await device.transferOut(4, b2); + const completed = await transfer; + + assert.equal(completed.status, 'fulfilled', completed.reason && completed.reason.message); + assert.equal(completed.value.status, 'ok'); + assert.equal(completed.value.data.byteLength, b2.byteLength); + } + }); + it('should transfer OUT', async () => { const transferResult = await device.transferOut(4, b2); diff --git a/tsc/index.ts b/tsc/index.ts index 5bf0db3..3c9127c 100644 --- a/tsc/index.ts +++ b/tsc/index.ts @@ -3,7 +3,9 @@ import { nativeGetDevices, nativeFindDeviceByIds, nativeFindDeviceBySerial, UsbD /** * Hidden */ -const DEFAULT_TIMEOUT = 1000; +const DEFAULT_TRANSFER_TIMEOUT = 0; +const DEFAULT_CONTROL_TRANSFER_TIMEOUT = 1000; +const DEFAULT_ISOCHRONOUS_TRANSFER_TIMEOUT = 1000; /** * Hidden @@ -24,7 +26,7 @@ const toUint8Array = (data: BufferSource): Uint8Array => { /** * Hidden */ -UsbDevice.prototype.controlTransferIn = async function (setup: USBControlTransferParameters, length: number, timeout = DEFAULT_TIMEOUT): Promise { +UsbDevice.prototype.controlTransferIn = async function (setup: USBControlTransferParameters, length: number, timeout = DEFAULT_CONTROL_TRANSFER_TIMEOUT): Promise { const res = await this.nativeControlTransferIn(setup, timeout, length); return { data: res ? new DataView(res.buffer) : undefined, @@ -35,7 +37,7 @@ UsbDevice.prototype.controlTransferIn = async function (setup: USBControlTransfe /** * Hidden */ -UsbDevice.prototype.controlTransferOut = async function (setup: USBControlTransferParameters, data: BufferSource, timeout = DEFAULT_TIMEOUT): Promise { +UsbDevice.prototype.controlTransferOut = async function (setup: USBControlTransferParameters, data: BufferSource, timeout = DEFAULT_CONTROL_TRANSFER_TIMEOUT): Promise { const res = await this.nativeControlTransferOut(setup, timeout, toUint8Array(data)); return { bytesWritten: res, @@ -46,7 +48,7 @@ UsbDevice.prototype.controlTransferOut = async function (setup: USBControlTransf /** * Hidden */ -UsbDevice.prototype.transferIn = async function (endpointNumber: number, length: number, timeout = DEFAULT_TIMEOUT): Promise { +UsbDevice.prototype.transferIn = async function (endpointNumber: number, length: number, timeout = DEFAULT_TRANSFER_TIMEOUT): Promise { const res = await this.nativeTransferIn(endpointNumber, timeout, length); return { data: res ? new DataView(res.buffer) : undefined, @@ -57,7 +59,7 @@ UsbDevice.prototype.transferIn = async function (endpointNumber: number, length: /** * Hidden */ -UsbDevice.prototype.transferOut = async function (endpointNumber: number, data: BufferSource, timeout = DEFAULT_TIMEOUT): Promise { +UsbDevice.prototype.transferOut = async function (endpointNumber: number, data: BufferSource, timeout = DEFAULT_TRANSFER_TIMEOUT): Promise { const res = await this.nativeTransferOut(endpointNumber, timeout, toUint8Array(data)); return { bytesWritten: res, @@ -68,7 +70,7 @@ UsbDevice.prototype.transferOut = async function (endpointNumber: number, data: /** * Hidden */ -UsbDevice.prototype.isochronousTransferIn = async function (endpointNumber: number, packetLengths: number[], timeout = DEFAULT_TIMEOUT): Promise { +UsbDevice.prototype.isochronousTransferIn = async function (endpointNumber: number, packetLengths: number[], timeout = DEFAULT_ISOCHRONOUS_TRANSFER_TIMEOUT): Promise { const res = await this.nativeIsochronousTransferIn(endpointNumber, packetLengths, timeout); return res; } @@ -76,7 +78,7 @@ UsbDevice.prototype.isochronousTransferIn = async function (endpointNumber: numb /** * Hidden */ -UsbDevice.prototype.isochronousTransferOut = async function (endpointNumber: number, data: BufferSource, packetLengths: number[], timeout = DEFAULT_TIMEOUT): Promise { +UsbDevice.prototype.isochronousTransferOut = async function (endpointNumber: number, data: BufferSource, packetLengths: number[], timeout = DEFAULT_ISOCHRONOUS_TRANSFER_TIMEOUT): Promise { const res = await this.nativeIsochronousTransferOut(endpointNumber, toUint8Array(data), packetLengths, timeout); return res; }