diff --git a/.changeset/fuzzy-spies-divide.md b/.changeset/fuzzy-spies-divide.md new file mode 100644 index 000000000..029c65883 --- /dev/null +++ b/.changeset/fuzzy-spies-divide.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/device-transport-kit-web-hid": patch +--- + +Update reconnection event to trigger error only after a specific time diff --git a/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.test.ts b/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.test.ts index f2f9c7f5f..f837dd81d 100644 --- a/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.test.ts +++ b/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.test.ts @@ -120,7 +120,7 @@ describe("WebHidDeviceConnection", () => { }); describe("anticipating loss of connection after sending an APDU", () => { - test("sendApdu(whatever, true) should wait for reconnection before resolving if the response is a success", async () => { + it("sendApdu(whatever, true) should wait for reconnection before resolving if the response is a success", async () => { // given device.sendReport = jest.fn(() => Promise.resolve( @@ -130,12 +130,14 @@ describe("WebHidDeviceConnection", () => { } as HIDInputReportEvent), ), ); + const connection = new WebHidDeviceConnection( { device, apduSender, apduReceiver, onConnectionTerminated, deviceId }, logger, ); let hasResolved = false; + const responsePromise = connection .sendApdu(Uint8Array.from([]), true) .then((response) => { @@ -164,7 +166,7 @@ describe("WebHidDeviceConnection", () => { ); }); - test("sendApdu(whatever, true) should not wait for reconnection if the response is not a success", async () => { + it("sendApdu(whatever, true) should not wait for reconnection if the response is not a success", async () => { // given device.sendReport = jest.fn(() => Promise.resolve( @@ -191,7 +193,7 @@ describe("WebHidDeviceConnection", () => { ); }); - test("sendApdu(whatever, true) should return an error if the device gets disconnected while waiting for reconnection", async () => { + it("sendApdu(whatever, true) should return an error if the device gets disconnected while waiting for reconnection", async () => { // given device.sendReport = jest.fn(() => Promise.resolve( @@ -220,7 +222,7 @@ describe("WebHidDeviceConnection", () => { }); describe("connection lost before sending an APDU", () => { - test("sendApdu(whatever, false) should return an error if the device connection has been lost and times out", async () => { + it("sendApdu(whatever, false) should return an error if the device connection has been lost and times out", async () => { // given device.sendReport = jest.fn(() => Promise.resolve( @@ -246,7 +248,7 @@ describe("WebHidDeviceConnection", () => { expect(response).toEqual(Left(new ReconnectionFailedError())); }); - test("sendApdu(whatever, false) should wait for reconnection to resolve", async () => { + it("sendApdu(whatever, false) should wait for reconnection to resolve", async () => { // given device.sendReport = jest.fn(() => Promise.resolve( @@ -283,7 +285,13 @@ describe("WebHidDeviceConnection", () => { const response = await responsePromise; - expect(response).toEqual(Left(new WebHidSendReportError())); + expect(response).toEqual( + Left( + new WebHidSendReportError( + new Error("Device disconnected while waiting for device response"), + ), + ), + ); }); }); }); diff --git a/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.ts b/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.ts index fd0da7e4c..e493dfd3e 100644 --- a/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.ts +++ b/packages/transport/web-hid/src/api/transport/WebHidDeviceConnection.ts @@ -10,7 +10,7 @@ import { ReconnectionFailedError, } from "@ledgerhq/device-management-kit"; import { type Either, Left, Maybe, Nothing, Right } from "purify-ts"; -import { Subject } from "rxjs"; +import { firstValueFrom, from, retry, Subject } from "rxjs"; import { RECONNECT_DEVICE_TIMEOUT } from "@api/data/WebHidConfig"; import { WebHidSendReportError } from "@api/model/Errors"; @@ -23,6 +23,8 @@ type WebHidDeviceConnectionConstructorArgs = { onConnectionTerminated: () => void; }; +type Timer = ReturnType; + /** * Class to manage the connection with a USB HID device. * It sends APDU commands to the device and receives the responses. @@ -44,7 +46,7 @@ export class WebHidDeviceConnection implements DeviceConnection { /** Flag to indicate if the connection is waiting for a reconnection */ private waitingForReconnection = false; /** Timeout to wait for the device to reconnect */ - private lostConnectionTimeout: NodeJS.Timeout | null = null; + private lostConnectionTimeout: Timer | null = null; /** Flag to indicate if the connection is terminated */ private terminated = false; @@ -114,11 +116,9 @@ export class WebHidDeviceConnection implements DeviceConnection { if (this.waitingForReconnection || !this.device.opened) { const waitingForDeviceResponse = this.device.opened && this._pendingApdu.isJust(); - const reconnectionRes = await this.waitForReconnection( waitingForDeviceResponse, ); - if (reconnectionRes.isLeft()) { return reconnectionRes; } @@ -129,8 +129,16 @@ export class WebHidDeviceConnection implements DeviceConnection { this._logger.debug("Sending Frame", { data: { frame: frame.getRawData() }, }); + try { - await this._device.sendReport(0, frame.getRawData()); + await firstValueFrom( + from(this._device.sendReport(0, frame.getRawData())).pipe( + retry({ + count: 3, + delay: 500, + }), + ), + ); } catch (error) { this._logger.error("Error sending frame", { data: { error } }); return Promise.resolve(Left(new WebHidSendReportError(error))); @@ -174,7 +182,13 @@ export class WebHidDeviceConnection implements DeviceConnection { const sub = this.reconnectionSubject.subscribe({ next: (res) => { if (waitingForDeviceResponse) { - this._sendApduSubject.error(new WebHidSendReportError()); + this._sendApduSubject.error( + new WebHidSendReportError( + new Error( + "Device disconnected while waiting for device response", + ), + ), + ); } if (res === "success") { @@ -182,6 +196,7 @@ export class WebHidDeviceConnection implements DeviceConnection { } else { resolve(Left(res)); } + sub.unsubscribe(); }, }); @@ -207,16 +222,11 @@ export class WebHidDeviceConnection implements DeviceConnection { this._device.oninputreport = (event) => this.receiveHidInputReport(event); if (this.lostConnectionTimeout) { - this._logger.info("⏱️🔌 Device reconnected"); clearTimeout(this.lostConnectionTimeout); } await device.open(); - - if (this._pendingApdu.isJust()) { - this._sendApduSubject.error(new WebHidSendReportError()); - } - + this._logger.info("⏱️🔌 Device reconnected"); this.waitingForReconnection = false; this.reconnectionSubject.next("success"); }