@onekeyfe/hd-transport-web-device 1.2.0-alpha.133 → 1.2.0-alpha.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ import transport from '@onekeyfe/hd-transport';
2
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+
4
+ import WebUsbTransport from '../src/webusb';
5
+
6
+ const schema = {
7
+ nested: {
8
+ Ping: { fields: { message: { type: 'string', id: 1 } } },
9
+ Success: { fields: { message: { type: 'string', id: 1 } } },
10
+ MessageType: {
11
+ values: {
12
+ MessageType_Ping: 60206,
13
+ MessageType_Success: 60207,
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ function buildAcquirableTransport() {
20
+ const webusb = new WebUsbTransport() as any;
21
+ webusb.Log = { debug: jest.fn() };
22
+ webusb.rotateProtocolV2UsbGeneration = jest.fn().mockResolvedValue(undefined);
23
+ webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
24
+ webusb.connect = jest.fn().mockResolvedValue(undefined);
25
+ return webusb;
26
+ }
27
+
28
+ describe('WebUsbTransport protocol probe cache', () => {
29
+ test('acquire skips the wire probe when the protocol is already cached', async () => {
30
+ const webusb = buildAcquirableTransport();
31
+ const path = 'pro-webusb';
32
+ webusb.deviceProtocol.set(path, 'V1');
33
+ webusb.detectProtocol = jest.fn();
34
+
35
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
36
+
37
+ expect(webusb.detectProtocol).not.toHaveBeenCalled();
38
+ expect(webusb.deviceProtocol.get(path)).toBe('V1');
39
+ expect(webusb.acquiredPaths.has(path)).toBe(true);
40
+ });
41
+
42
+ test('acquire re-probes when the caller expects a different protocol than cached', async () => {
43
+ const webusb = buildAcquirableTransport();
44
+ const path = 'pro-webusb';
45
+ webusb.deviceProtocol.set(path, 'V1');
46
+ webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
47
+ webusb.deviceProtocol.set(p, 'V2');
48
+ return Promise.resolve('V2');
49
+ });
50
+
51
+ await expect(webusb.acquire({ path, expectedProtocol: 'V2' })).resolves.toBe(path);
52
+
53
+ expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V2', undefined);
54
+ expect(webusb.deviceProtocol.get(path)).toBe('V2');
55
+ });
56
+
57
+ test('acquire probes when nothing is cached for the path', async () => {
58
+ const webusb = buildAcquirableTransport();
59
+ const path = 'pro-webusb';
60
+ webusb.detectProtocol = jest.fn().mockResolvedValue('V1');
61
+
62
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
63
+
64
+ expect(webusb.detectProtocol).toHaveBeenCalledWith(path, 'V1', undefined);
65
+ });
66
+
67
+ test('acquire re-probes a stale-marked path even when a protocol is cached', async () => {
68
+ const webusb = buildAcquirableTransport();
69
+ const path = 'pro-webusb';
70
+ webusb.deviceProtocol.set(path, 'V1');
71
+ webusb.markProtocolStale(path);
72
+ webusb.detectProtocol = jest.fn().mockImplementation((p: string) => {
73
+ webusb.deviceProtocol.set(p, 'V1');
74
+ return Promise.resolve('V1');
75
+ });
76
+
77
+ await expect(webusb.acquire({ path, expectedProtocol: 'V1' })).resolves.toBe(path);
78
+
79
+ expect(webusb.detectProtocol).toHaveBeenCalledTimes(1);
80
+ expect(webusb.staleProtocolPaths.has(path)).toBe(false);
81
+ });
82
+
83
+ test('release keeps the cached V1 protocol so the next acquire can reuse it', async () => {
84
+ const webusb = new WebUsbTransport() as any;
85
+ const path = 'pro-webusb';
86
+ webusb.deviceProtocol.set(path, 'V1');
87
+ webusb.deviceProtocolHints.set(path, 'V2');
88
+ webusb.deviceEndpoints.set(path, { interfaceNumber: 0, endpointIn: 1, endpointOut: 1 });
89
+ webusb.acquiredPaths.add(path);
90
+ webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
91
+
92
+ await webusb.release(path);
93
+
94
+ expect(webusb.deviceProtocol.get(path)).toBe('V1');
95
+ expect(webusb.deviceProtocolHints.get(path)).toBe('V2');
96
+ expect(webusb.deviceEndpoints.has(path)).toBe(false);
97
+ expect(webusb.acquiredPaths.has(path)).toBe(false);
98
+ });
99
+
100
+ test('release still drops a cached V2 protocol through link invalidation', async () => {
101
+ const webusb = new WebUsbTransport() as any;
102
+ const path = 'pro2-webusb';
103
+ webusb.Log = { debug: jest.fn() };
104
+ webusb.messages = transport.parseConfigure(schema);
105
+ webusb.messagesV2 = transport.parseConfigure(schema);
106
+ webusb.writeProtocolV2UsbPacket = jest.fn().mockResolvedValue(undefined);
107
+ let markReadStarted: () => void = () => undefined;
108
+ const readStarted = new Promise<void>(resolve => {
109
+ markReadStarted = resolve;
110
+ });
111
+ webusb.readProtocolV2UsbPacket = jest.fn().mockImplementation(() => {
112
+ markReadStarted();
113
+ return new Promise<void>(() => {});
114
+ });
115
+ webusb.closeOpenDevice = jest.fn().mockResolvedValue(undefined);
116
+ webusb.deviceProtocol.set(path, 'V2');
117
+ await webusb.rotateProtocolV2UsbGeneration(path, 'test connection');
118
+
119
+ const call = webusb.callProtocolV2(path, 'Ping', { message: 'release' });
120
+ await readStarted;
121
+ await webusb.release(path);
122
+
123
+ await expect(call).rejects.toThrow('WebUSB transport released');
124
+ expect(webusb.deviceProtocol.has(path)).toBe(false);
125
+ });
126
+
127
+ test('call and post fail fast for a path that is not acquired', async () => {
128
+ const webusb = new WebUsbTransport() as any;
129
+ const path = 'pro-webusb';
130
+ webusb.Log = { debug: jest.fn() };
131
+ webusb.messages = transport.parseConfigure(schema);
132
+ webusb.messagesV2 = transport.parseConfigure(schema);
133
+ // A surviving protocol cache entry must NOT act as a session token.
134
+ webusb.deviceProtocol.set(path, 'V1');
135
+
136
+ await expect(webusb.call(path, 'Ping', {})).rejects.toMatchObject({
137
+ errorCode: HardwareErrorCode.RuntimeError,
138
+ message: expect.stringContaining('not acquired'),
139
+ });
140
+ await expect(webusb.post(path, 'Ping', {})).rejects.toMatchObject({
141
+ errorCode: HardwareErrorCode.RuntimeError,
142
+ message: expect.stringContaining('not acquired'),
143
+ });
144
+ });
145
+
146
+ test('a transfer-level reconnect marks the protocol stale for the next acquire', async () => {
147
+ const webusb = new WebUsbTransport() as any;
148
+ const path = 'pro-webusb';
149
+ webusb.Log = { debug: jest.fn() };
150
+ webusb.deviceProtocol.set(path, 'V1');
151
+ webusb.findDevice = jest.fn().mockResolvedValue({ opened: false });
152
+ webusb.getConnectedDevices = jest.fn().mockResolvedValue([]);
153
+ webusb.connect = jest.fn().mockResolvedValue(undefined);
154
+
155
+ await webusb.reconnectForPacketIoRetry(path, 'in', 0, new Error('transferIn failed'));
156
+
157
+ // The in-flight call keeps the cached protocol; only the next acquire re-probes.
158
+ expect(webusb.deviceProtocol.get(path)).toBe('V1');
159
+ expect(webusb.staleProtocolPaths.has(path)).toBe(true);
160
+ });
161
+
162
+ test('USB disconnect marks the serial stale and the listener attaches only once', () => {
163
+ const addEventListener = jest.fn();
164
+ const usb = { addEventListener } as unknown as USB;
165
+ const originalNavigator = (globalThis as any).navigator;
166
+ Object.defineProperty(globalThis, 'navigator', {
167
+ value: { usb },
168
+ configurable: true,
169
+ });
170
+ try {
171
+ const first = new WebUsbTransport() as any;
172
+ first.init({ debug: jest.fn() });
173
+ const second = new WebUsbTransport() as any;
174
+ second.init({ debug: jest.fn() });
175
+
176
+ // Module-level listener: one registration across instances.
177
+ expect(addEventListener).toHaveBeenCalledTimes(1);
178
+ const handler = addEventListener.mock.calls[0][1] as (event: {
179
+ device?: { serialNumber?: string | null };
180
+ }) => void;
181
+
182
+ const path = 'pro-webusb';
183
+ first.deviceProtocol.set(path, 'V1');
184
+ second.deviceProtocol.set(path, 'V1');
185
+ handler({ device: { serialNumber: path } });
186
+
187
+ // Routed to the most recently initialized instance; the cached value is
188
+ // retained (in-flight sessions keep working) but marked stale.
189
+ expect(second.staleProtocolPaths.has(path)).toBe(true);
190
+ expect(second.deviceProtocol.get(path)).toBe('V1');
191
+ expect(first.staleProtocolPaths.has(path)).toBe(false);
192
+
193
+ // Events without a usable serial are ignored.
194
+ handler({ device: { serialNumber: null } });
195
+ handler({});
196
+ expect(second.staleProtocolPaths.size).toBe(1);
197
+ } finally {
198
+ Object.defineProperty(globalThis, 'navigator', {
199
+ value: originalNavigator,
200
+ configurable: true,
201
+ });
202
+ }
203
+ });
204
+ });
package/dist/index.d.ts CHANGED
@@ -20,6 +20,13 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
20
20
  /** Per-path protocol type detected by active wire-level probe. */
21
21
  private deviceProtocol;
22
22
  private deviceProtocolHints;
23
+ /**
24
+ * Paths whose cached protocol must be re-probed on the next acquire (a USB
25
+ * disconnect was seen, or a transfer-level reconnect happened mid-call).
26
+ */
27
+ private staleProtocolPaths;
28
+ /** Paths currently acquired (between a successful acquire() and its release()). */
29
+ private acquiredPaths;
23
30
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
24
31
  private deviceEndpoints;
25
32
  name: string;
@@ -40,6 +47,17 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
40
47
  * Initialize WebUSB transport
41
48
  */
42
49
  init(logger: any): void;
50
+ /**
51
+ * Protocol type is a property of the physical device keyed by USB serial number.
52
+ * It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
53
+ * a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
54
+ * reconnects, which cover a missed disconnect event) only MARK the cached probe
55
+ * result stale instead of deleting it: an in-flight session keeps using the old
56
+ * value so the transfer-level reconnect retries can absorb a transient
57
+ * re-enumeration exactly as they did before the cache existed, while the next
58
+ * acquire re-probes from scratch.
59
+ */
60
+ markProtocolStale(path: string): void;
43
61
  /**
44
62
  * Configure Protocol V1 protobuf schema (legacy chunked 0x3F framing).
45
63
  */
@@ -97,6 +115,14 @@ declare class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
97
115
  connectToDevice(path: string, first: boolean): Promise<void>;
98
116
  private closeOpenDevice;
99
117
  private clearEndpointHalt;
118
+ /**
119
+ * With the protocol cache surviving release(), deviceProtocol presence no
120
+ * longer implies an active session. Guard call()/post() explicitly so a
121
+ * post-release straggler fails fast instead of silently reopening the device
122
+ * and driving it outside any session (pre-cache behavior: the deleted
123
+ * protocol entry produced the same fail-fast).
124
+ */
125
+ private assertAcquired;
100
126
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
101
127
  private getErrorMessage;
102
128
  private isRetryablePacketIoError;
package/dist/index.js CHANGED
@@ -57,6 +57,20 @@ const EXPECTED_PROTOCOL_V2_PROBE_ATTEMPTS = 2;
57
57
  function inferProtocolHintFromDeviceName$1(name) {
58
58
  return /\bpro\s*2\b/i.test(name !== null && name !== void 0 ? name : '') ? 'V2' : undefined;
59
59
  }
60
+ let activeWebUsbTransport;
61
+ let usbDisconnectListenerAttached = false;
62
+ function attachUsbDisconnectListener(usb) {
63
+ if (usbDisconnectListenerAttached)
64
+ return;
65
+ usbDisconnectListenerAttached = true;
66
+ usb.addEventListener('disconnect', event => {
67
+ var _a;
68
+ const serial = (_a = event.device) === null || _a === void 0 ? void 0 : _a.serialNumber;
69
+ if (typeof serial !== 'string' || serial.length === 0)
70
+ return;
71
+ activeWebUsbTransport === null || activeWebUsbTransport === void 0 ? void 0 : activeWebUsbTransport.markProtocolStale(serial);
72
+ });
73
+ }
60
74
  class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
61
75
  constructor() {
62
76
  super({
@@ -66,6 +80,8 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
66
80
  });
67
81
  this.deviceProtocol = new Map();
68
82
  this.deviceProtocolHints = new Map();
83
+ this.staleProtocolPaths = new Set();
84
+ this.acquiredPaths = new Set();
69
85
  this.deviceEndpoints = new Map();
70
86
  this.name = 'WebUsbTransport';
71
87
  this.stopped = false;
@@ -82,6 +98,11 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
82
98
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'WebUSB is not supported by current browsers');
83
99
  }
84
100
  this.usb = usb;
101
+ activeWebUsbTransport = this;
102
+ attachUsbDisconnectListener(usb);
103
+ }
104
+ markProtocolStale(path) {
105
+ this.staleProtocolPaths.add(path);
85
106
  }
86
107
  configure(signedData) {
87
108
  const messages = parseConfigure$1(signedData);
@@ -156,18 +177,30 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
156
177
  yield this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
157
178
  yield this.closeOpenDevice(input.path);
158
179
  yield this.connect((_a = input.path) !== null && _a !== void 0 ? _a : '', true);
159
- const deviceName = (_b = this.deviceList.find(device => device.path === input.path)) === null || _b === void 0 ? void 0 : _b.device.productName;
160
- const protocolHint = input.expectedProtocol
161
- ? undefined
162
- : (_d = (_c = input.protocolHint) !== null && _c !== void 0 ? _c : this.deviceProtocolHints.get(input.path)) !== null && _d !== void 0 ? _d : inferProtocolHintFromDeviceName$1(deviceName);
163
- if (protocolHint) {
164
- this.deviceProtocolHints.set(input.path, protocolHint);
180
+ if (this.staleProtocolPaths.has(input.path)) {
181
+ this.staleProtocolPaths.delete(input.path);
182
+ this.deviceProtocol.delete(input.path);
183
+ }
184
+ const cachedProtocol = this.deviceProtocol.get(input.path);
185
+ if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
186
+ this.deviceProtocol.delete(input.path);
165
187
  }
166
- yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
188
+ if (!this.deviceProtocol.has(input.path)) {
189
+ const deviceName = (_b = this.deviceList.find(device => device.path === input.path)) === null || _b === void 0 ? void 0 : _b.device.productName;
190
+ const protocolHint = input.expectedProtocol
191
+ ? undefined
192
+ : (_d = (_c = input.protocolHint) !== null && _c !== void 0 ? _c : this.deviceProtocolHints.get(input.path)) !== null && _d !== void 0 ? _d : inferProtocolHintFromDeviceName$1(deviceName);
193
+ if (protocolHint) {
194
+ this.deviceProtocolHints.set(input.path, protocolHint);
195
+ }
196
+ yield this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
197
+ }
198
+ this.acquiredPaths.add(input.path);
167
199
  return yield Promise.resolve(input.path);
168
200
  }
169
201
  catch (e) {
170
202
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
203
+ this.acquiredPaths.delete(input.path);
171
204
  yield this.closeOpenDevice(input.path);
172
205
  throw e;
173
206
  }
@@ -351,8 +384,14 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
351
384
  }
352
385
  });
353
386
  }
387
+ assertAcquired(path) {
388
+ if (!this.acquiredPaths.has(path)) {
389
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device is not acquired for ${path}`);
390
+ }
391
+ }
354
392
  post(session, name, data) {
355
393
  return __awaiter(this, void 0, void 0, function* () {
394
+ this.assertAcquired(session);
356
395
  if (this.deviceProtocol.get(session) === 'V2') {
357
396
  yield this.sendProtocolV2UsbFlowControl(session, name, data);
358
397
  return;
@@ -385,6 +424,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
385
424
  var _a;
386
425
  return __awaiter(this, void 0, void 0, function* () {
387
426
  this.Log.debug(`[WebUsbTransport] transfer${direction} failed, retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage(error)}`);
427
+ this.staleProtocolPaths.add(path);
388
428
  yield hdShared.wait(attempt * PACKET_IO_RETRY_DELAY);
389
429
  try {
390
430
  const currentDevice = yield this.findDevice(path);
@@ -599,6 +639,7 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
599
639
  if (this.messages == null) {
600
640
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.TransportNotConfigured);
601
641
  }
642
+ this.assertAcquired(path);
602
643
  const device = yield this.findDevice(path);
603
644
  if (!device) {
604
645
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
@@ -678,10 +719,9 @@ class WebUsbTransport extends transport.ProtocolV2UsbTransportBase {
678
719
  }
679
720
  release(path) {
680
721
  return __awaiter(this, void 0, void 0, function* () {
722
+ this.acquiredPaths.delete(path);
681
723
  yield this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
682
724
  yield this.closeOpenDevice(path);
683
- this.deviceProtocol.delete(path);
684
- this.deviceProtocolHints.delete(path);
685
725
  this.deviceEndpoints.delete(path);
686
726
  });
687
727
  }
package/dist/webusb.d.ts CHANGED
@@ -12,6 +12,8 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
12
12
  private protocolV2SchemaSource;
13
13
  private deviceProtocol;
14
14
  private deviceProtocolHints;
15
+ private staleProtocolPaths;
16
+ private acquiredPaths;
15
17
  private deviceEndpoints;
16
18
  name: string;
17
19
  stopped: boolean;
@@ -24,6 +26,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
24
26
  interfaceId: number;
25
27
  constructor();
26
28
  init(logger: any): void;
29
+ markProtocolStale(path: string): void;
27
30
  configure(signedData: any): void;
28
31
  configureProtocolV2(signedData: any): void;
29
32
  promptDeviceAccess(): Promise<USBDevice | null>;
@@ -40,6 +43,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
40
43
  connectToDevice(path: string, first: boolean): Promise<void>;
41
44
  private closeOpenDevice;
42
45
  private clearEndpointHalt;
46
+ private assertAcquired;
43
47
  post(session: string, name: string, data: Record<string, unknown>): Promise<void>;
44
48
  private getErrorMessage;
45
49
  private isRetryablePacketIoError;
@@ -1 +1 @@
1
- {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webusb.d.ts","sourceRoot":"","sources":["../src/webusb.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport-web-device",
3
- "version": "1.2.0-alpha.133",
3
+ "version": "1.2.0-alpha.134",
4
4
  "author": "OneKey",
5
5
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
6
6
  "license": "MIT",
@@ -21,13 +21,13 @@
21
21
  "lint:fix": "eslint . --fix"
22
22
  },
23
23
  "dependencies": {
24
- "@onekeyfe/hd-shared": "1.2.0-alpha.133",
25
- "@onekeyfe/hd-transport": "1.2.0-alpha.133"
24
+ "@onekeyfe/hd-shared": "1.2.0-alpha.134",
25
+ "@onekeyfe/hd-transport": "1.2.0-alpha.134"
26
26
  },
27
27
  "devDependencies": {
28
- "@onekeyfe/hd-transport-electron": "1.2.0-alpha.133",
28
+ "@onekeyfe/hd-transport-electron": "1.2.0-alpha.134",
29
29
  "@types/w3c-web-usb": "^1.0.6",
30
30
  "@types/web-bluetooth": "^0.0.17"
31
31
  },
32
- "gitHead": "b6d3307b47c535c876655afd99a6681399453508"
32
+ "gitHead": "47de2b0244584f6a541a84b0f94e4676bf63221a"
33
33
  }
package/src/webusb.ts CHANGED
@@ -69,6 +69,26 @@ interface TransferCancelToken {
69
69
  cancelled: boolean;
70
70
  }
71
71
 
72
+ /**
73
+ * The navigator.usb disconnect listener is module-scoped and attached at most
74
+ * once: listeners on navigator.usb are global and never garbage collected, so a
75
+ * per-instance listener would retain every transport instance created across
76
+ * SDK re-initializations. Events are routed to the most recently initialized
77
+ * transport instance instead.
78
+ */
79
+ let activeWebUsbTransport: WebUsbTransport | undefined;
80
+ let usbDisconnectListenerAttached = false;
81
+
82
+ function attachUsbDisconnectListener(usb: USB) {
83
+ if (usbDisconnectListenerAttached) return;
84
+ usbDisconnectListenerAttached = true;
85
+ usb.addEventListener('disconnect', event => {
86
+ const serial = event.device?.serialNumber;
87
+ if (typeof serial !== 'string' || serial.length === 0) return;
88
+ activeWebUsbTransport?.markProtocolStale(serial);
89
+ });
90
+ }
91
+
72
92
  export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string> {
73
93
  messages: ReturnType<typeof transport.parseConfigure> | undefined;
74
94
 
@@ -82,6 +102,15 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
82
102
 
83
103
  private deviceProtocolHints: Map<string, ProtocolType> = new Map();
84
104
 
105
+ /**
106
+ * Paths whose cached protocol must be re-probed on the next acquire (a USB
107
+ * disconnect was seen, or a transfer-level reconnect happened mid-call).
108
+ */
109
+ private staleProtocolPaths: Set<string> = new Set();
110
+
111
+ /** Paths currently acquired (between a successful acquire() and its release()). */
112
+ private acquiredPaths: Set<string> = new Set();
113
+
85
114
  /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */
86
115
  private deviceEndpoints: Map<string, DeviceEndpoints> = new Map();
87
116
 
@@ -129,6 +158,22 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
129
158
  );
130
159
  }
131
160
  this.usb = usb;
161
+ activeWebUsbTransport = this;
162
+ attachUsbDisconnectListener(usb);
163
+ }
164
+
165
+ /**
166
+ * Protocol type is a property of the physical device keyed by USB serial number.
167
+ * It can only change across a device reboot (e.g. normal ↔ bootloader mode), and
168
+ * a reboot always surfaces as a USB disconnect. Disconnects (and transfer-level
169
+ * reconnects, which cover a missed disconnect event) only MARK the cached probe
170
+ * result stale instead of deleting it: an in-flight session keeps using the old
171
+ * value so the transfer-level reconnect retries can absorb a transient
172
+ * re-enumeration exactly as they did before the cache existed, while the next
173
+ * acquire re-probes from scratch.
174
+ */
175
+ markProtocolStale(path: string) {
176
+ this.staleProtocolPaths.add(path);
132
177
  }
133
178
 
134
179
  /**
@@ -231,20 +276,36 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
231
276
  await this.rotateProtocolV2UsbGeneration(input.path, 'WebUSB transport acquired');
232
277
  await this.closeOpenDevice(input.path);
233
278
  await this.connect(input.path ?? '', true);
234
- const deviceName = this.deviceList.find(device => device.path === input.path)?.device
235
- .productName;
236
- const protocolHint = input.expectedProtocol
237
- ? undefined
238
- : input.protocolHint ??
239
- this.deviceProtocolHints.get(input.path) ??
240
- inferProtocolHintFromDeviceName(deviceName);
241
- if (protocolHint) {
242
- this.deviceProtocolHints.set(input.path, protocolHint);
279
+ if (this.staleProtocolPaths.has(input.path)) {
280
+ // The device disconnected (possibly rebooting into another mode) since
281
+ // the protocol was probed — drop the cache so it is re-probed below.
282
+ this.staleProtocolPaths.delete(input.path);
283
+ this.deviceProtocol.delete(input.path);
243
284
  }
244
- await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
285
+ const cachedProtocol = this.deviceProtocol.get(input.path);
286
+ if (cachedProtocol && input.expectedProtocol && cachedProtocol !== input.expectedProtocol) {
287
+ // The caller expects a different protocol than the cached probe result;
288
+ // the cache is stale — drop it and re-probe on the wire below.
289
+ this.deviceProtocol.delete(input.path);
290
+ }
291
+ if (!this.deviceProtocol.has(input.path)) {
292
+ const deviceName = this.deviceList.find(device => device.path === input.path)?.device
293
+ .productName;
294
+ const protocolHint = input.expectedProtocol
295
+ ? undefined
296
+ : input.protocolHint ??
297
+ this.deviceProtocolHints.get(input.path) ??
298
+ inferProtocolHintFromDeviceName(deviceName);
299
+ if (protocolHint) {
300
+ this.deviceProtocolHints.set(input.path, protocolHint);
301
+ }
302
+ await this.detectProtocol(input.path, input.expectedProtocol, protocolHint);
303
+ }
304
+ this.acquiredPaths.add(input.path);
245
305
  return await Promise.resolve(input.path);
246
306
  } catch (e) {
247
307
  this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e));
308
+ this.acquiredPaths.delete(input.path);
248
309
  await this.closeOpenDevice(input.path);
249
310
  throw e;
250
311
  }
@@ -480,7 +541,24 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
480
541
  }
481
542
  }
482
543
 
544
+ /**
545
+ * With the protocol cache surviving release(), deviceProtocol presence no
546
+ * longer implies an active session. Guard call()/post() explicitly so a
547
+ * post-release straggler fails fast instead of silently reopening the device
548
+ * and driving it outside any session (pre-cache behavior: the deleted
549
+ * protocol entry produced the same fail-fast).
550
+ */
551
+ private assertAcquired(path: string) {
552
+ if (!this.acquiredPaths.has(path)) {
553
+ throw ERRORS.TypedError(
554
+ HardwareErrorCode.RuntimeError,
555
+ `Device is not acquired for ${path}`
556
+ );
557
+ }
558
+ }
559
+
483
560
  async post(session: string, name: string, data: Record<string, unknown>) {
561
+ this.assertAcquired(session);
484
562
  if (this.deviceProtocol.get(session) === 'V2') {
485
563
  await this.sendProtocolV2UsbFlowControl(session, name, data);
486
564
  return;
@@ -522,6 +600,11 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
522
600
  error
523
601
  )}`
524
602
  );
603
+ // The device dropped off the bus mid-call and may have rebooted into a
604
+ // different mode. The in-flight retry keeps the known protocol, but the
605
+ // next acquire must re-probe — this also self-heals a stale cache when the
606
+ // USB disconnect event itself was missed.
607
+ this.staleProtocolPaths.add(path);
525
608
  await wait(attempt * PACKET_IO_RETRY_DELAY);
526
609
 
527
610
  try {
@@ -755,6 +838,7 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
755
838
  if (this.messages == null) {
756
839
  throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
757
840
  }
841
+ this.assertAcquired(path);
758
842
 
759
843
  const device = await this.findDevice(path);
760
844
  if (!device) {
@@ -871,10 +955,14 @@ export default class WebUsbTransport extends ProtocolV2UsbTransportBase<string>
871
955
  * Release device
872
956
  */
873
957
  async release(path: string) {
958
+ this.acquiredPaths.delete(path);
874
959
  await this.invalidateProtocolV2UsbLink(path, 'WebUSB transport released');
875
960
  await this.closeOpenDevice(path);
876
- this.deviceProtocol.delete(path);
877
- this.deviceProtocolHints.delete(path);
961
+ // Keep deviceProtocol/deviceProtocolHints across release: the probe result is a
962
+ // physical-device property, so the next acquire can skip the wire-level probe.
963
+ // V2 entries are still dropped by onProtocolV2UsbLinkInvalidated via the link
964
+ // invalidation above, and any entry is re-probed after a USB disconnect or a
965
+ // transfer-level reconnect (see markProtocolStale).
878
966
  this.deviceEndpoints.delete(path);
879
967
  }
880
968