@onekeyfe/hd-core 1.2.2-alpha.119 → 1.2.2-alpha.120

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.
@@ -1,4 +1,4 @@
1
- import { EDeviceType, canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
1
+ import { canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { BaseMethod } from './BaseMethod';
4
4
  import TransportManager from '../data-manager/TransportManager';
@@ -6,80 +6,10 @@ import { DataManager } from '../data-manager';
6
6
  import { LoggerNames, getDeviceTypeByBleName, getLogger } from '../utils';
7
7
  import { DevicePool } from '../device/DevicePool';
8
8
 
9
- import type { SearchDevice } from '../types/device';
10
9
  import type DeviceConnector from '../device/DeviceConnector';
11
- import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
12
- import type { Device } from '../device/Device';
13
10
 
14
11
  const Log = getLogger(LoggerNames.DevicePool);
15
12
 
16
- type RequestQueueLookup = {
17
- getRequestTasksIdByConnectId: (connectId: string) => number[];
18
- getTask: (requestId: number) => { method: { device?: Device } } | undefined;
19
- };
20
-
21
- const getDescriptorKeys = (descriptor: DeviceDescriptor) =>
22
- [descriptor.path, descriptor.id].filter(
23
- (key): key is string => typeof key === 'string' && key.length > 0
24
- );
25
-
26
- const isOwnedByActiveWebUsbRequest = (
27
- descriptor: DeviceDescriptor,
28
- requestQueue?: RequestQueueLookup
29
- ) => {
30
- if (!requestQueue) return false;
31
- return getDescriptorKeys(descriptor).some(
32
- key => requestQueue.getRequestTasksIdByConnectId(key).length > 0
33
- );
34
- };
35
-
36
- const getUsbHandle = (descriptor?: DeviceDescriptor) =>
37
- (descriptor as { device?: unknown } | undefined)?.device;
38
-
39
- const getOwningRequestDevice = (
40
- descriptor: DeviceDescriptor,
41
- requestQueue?: RequestQueueLookup
42
- ) => {
43
- if (!requestQueue) return undefined;
44
- const usbHandle = getUsbHandle(descriptor);
45
- // A path alone can be shared (the all-zero bootloader path, a synthesized serial-less
46
- // path), so the owner's identity is only reported for the USB handle it actually bound.
47
- if (usbHandle === undefined) return undefined;
48
- for (const key of getDescriptorKeys(descriptor)) {
49
- for (const requestId of requestQueue.getRequestTasksIdByConnectId(key)) {
50
- const device = requestQueue.getTask(requestId)?.method.device;
51
- if (
52
- device?.features &&
53
- (device.mainId === key || device.originalDescriptor?.path === key) &&
54
- getUsbHandle(device.originalDescriptor) === usbHandle
55
- ) {
56
- return device;
57
- }
58
- }
59
- }
60
- return undefined;
61
- };
62
-
63
- // A USB path is a routing key (the USB serial, a synthesized usb-vid-pid-name, or the
64
- // bootloader placeholder), not a hardware identity, so it never fills uuid/serialNo.
65
- const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
66
- const connectId = descriptor.path || descriptor.id || null;
67
- return {
68
- connectId,
69
- uuid: '',
70
- serialNo: null,
71
- deviceId: null,
72
- deviceType: EDeviceType.Unknown,
73
- name: descriptor.name || connectId || '',
74
- commType: descriptor.commType,
75
- };
76
- };
77
-
78
- const toSearchDevice = (device: Device): SearchDevice | null => {
79
- const message = device.toMessageObject();
80
- return message ? (message as SearchDevice) : null;
81
- };
82
-
83
13
  export default class SearchDevices extends BaseMethod {
84
14
  connector?: DeviceConnector;
85
15
 
@@ -89,31 +19,19 @@ export default class SearchDevices extends BaseMethod {
89
19
  this.skipForceUpdateCheck = true;
90
20
  }
91
21
 
92
- async run(): Promise<SearchDevice[]> {
93
- const env = DataManager.getSettings('env');
94
- const isWebUsb = env === 'webusb' || env === 'desktop-webusb';
95
- const requestQueue = this.context?.requestQueue;
96
- const hasActiveWebUsbRequest = isWebUsb && (requestQueue?.getRequestTasksId().length ?? 0) > 0;
97
- // Bring up WebUSB even when schema configuration is deferred while a
98
- // business request owns the discovery lock. Failures here used to be
99
- // swallowed by configure(); keep searchDevices resolving empty.
100
- if (isWebUsb) {
101
- try {
102
- await TransportManager.ensureInitialized();
103
- } catch (error) {
104
- Log.debug('WebUSB bring-up unavailable', error);
105
- }
106
- }
107
- if (!hasActiveWebUsbRequest) await TransportManager.configure();
22
+ async run() {
23
+ await TransportManager.configure();
108
24
  const deviceDiff = await this.connector?.enumerate();
109
25
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
110
26
 
27
+ const env = DataManager.getSettings('env');
28
+
111
29
  /**
112
30
  * No need to call features during Bluetooth scaning
113
31
  * to avoid device pairing
114
32
  */
115
33
  if (DataManager.isBleConnect(env)) {
116
- const devices: SearchDevice[] = [];
34
+ const devices = [];
117
35
  const seenIds = new Set<string>();
118
36
 
119
37
  for (const device of devicesDescriptor) {
@@ -138,42 +56,30 @@ export default class SearchDevices extends BaseMethod {
138
56
  return devices;
139
57
  }
140
58
 
141
- const deviceList: SearchDevice[] = [];
59
+ const deviceList = [];
142
60
  for (const descriptor of devicesDescriptor) {
143
- if (hasActiveWebUsbRequest && isOwnedByActiveWebUsbRequest(descriptor, requestQueue)) {
144
- const cached = DevicePool.getDeviceByPath(descriptor.path);
145
- const known = cached?.features ? cached : getOwningRequestDevice(descriptor, requestQueue);
146
- const message = known ? toSearchDevice(known) : null;
147
- // Do not probe a path an in-flight request already owns. A cache miss is still a
148
- // connected device: report the owning request's device, or the path with no identity.
149
- deviceList.push(message ?? toSearchDeviceFromDescriptor(descriptor));
150
- } else {
151
- try {
152
- // Discovery is best effort. Browsers may retain WebUSB grants for devices that
153
- // are offline, busy, or not ready, so one descriptor must not abort the scan.
154
- const result = await DevicePool.getDevices([descriptor], descriptor.path, {
155
- // Discovery must actively identify the protocol instead of trusting a caller hint.
156
- connectProtocol: undefined,
157
- forceProtocolDetection: true,
158
- refreshRuntimeState: true,
159
- });
160
- for (const device of result.deviceList) {
161
- const message = toSearchDevice(device);
162
- if (message) deviceList.push(message);
163
- }
164
- } catch (error) {
165
- const errorCode =
166
- error && typeof error === 'object' && 'errorCode' in error
167
- ? (error as { errorCode?: unknown }).errorCode
168
- : undefined;
169
- Log.debug('Skip unavailable device during search', {
170
- path: descriptor.path,
171
- ...(errorCode !== undefined ? { errorCode } : {}),
172
- });
173
- }
61
+ try {
62
+ // Discovery is best effort. Browsers may retain WebUSB grants for devices that
63
+ // are offline, busy, or not ready, so one descriptor must not abort the scan.
64
+ const result = await DevicePool.getDevices([descriptor], descriptor.path, {
65
+ // Discovery must actively identify the protocol instead of trusting a caller hint.
66
+ connectProtocol: undefined,
67
+ forceProtocolDetection: true,
68
+ refreshRuntimeState: true,
69
+ });
70
+ deviceList.push(...result.deviceList);
71
+ } catch (error) {
72
+ const errorCode =
73
+ error && typeof error === 'object' && 'errorCode' in error
74
+ ? (error as { errorCode?: unknown }).errorCode
75
+ : undefined;
76
+ Log.debug('Skip unavailable device during search', {
77
+ path: descriptor.path,
78
+ ...(errorCode !== undefined ? { errorCode } : {}),
79
+ });
174
80
  }
175
81
  }
176
82
 
177
- return deviceList;
83
+ return deviceList.map(device => device.toMessageObject());
178
84
  }
179
85
  }
@@ -31,22 +31,20 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
31
31
  originalIndex: index,
32
32
  })
33
33
  );
34
- const groupedMethodParams = methodParams.reduce((groups, param) => {
34
+ // Protocol V2 DeviceSessionGet is the Initialize(session_id) equivalent: the
35
+ // SE wallet stays selected until the next Ask/Get or lock. Nested chain
36
+ // methods still resume once in callMethod; same-method addresses can share
37
+ // that session the way Protocol V1 bundles do.
38
+ const methodGroups = methodParams.reduce((groups, param) => {
35
39
  const group = groups.get(param.methodName) ?? [];
36
40
  group.push(param);
37
41
  groups.set(param.methodName, group);
38
42
  return groups;
39
43
  }, new Map<keyof CoreApi, MethodParams[]>());
40
- const requiresProtocolV2WalletHandoff =
41
- this.device.isProtocolV2() &&
42
- (this.payload.useEmptyPassphrase === true || !!this.payload.passphraseState);
43
- const methodGroups: [keyof CoreApi, MethodParams[]][] = requiresProtocolV2WalletHandoff
44
- ? methodParams.map(param => [param.methodName, [param]])
45
- : Array.from(groupedMethodParams.entries());
46
44
 
47
- let i = 0;
48
- for (const [methodName, params] of methodGroups) {
49
- const methodParams = {
45
+ let processed = 0;
46
+ for (const [methodName, params] of methodGroups.entries()) {
47
+ const methodCallParams = {
50
48
  bundle: params.map(param => ({
51
49
  ...param.params,
52
50
  })),
@@ -55,27 +53,56 @@ export default class AllNetworkGetAddress extends AllNetworkGetAddressBase {
55
53
  if (this.abortController?.signal.aborted) {
56
54
  throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
57
55
  }
58
- // call method
59
- const response = await this.callMethod(methodName, methodParams, rootFingerprint);
56
+ const isProtocolV2 = this.device.isProtocolV2();
57
+ // Displayed addresses must not be replayed if a later item fails.
58
+ const runIndividually =
59
+ isProtocolV2 &&
60
+ params.length > 1 &&
61
+ params.some(param => param._originRequestParams.showOnOneKey !== false);
62
+ let response: AllNetworkAddress[] = [];
63
+ if (!runIndividually) {
64
+ response = await this.callMethod(methodName, methodCallParams, rootFingerprint);
65
+ }
66
+
67
+ // callMethod returns failures only for skippable errors; link, cancellation,
68
+ // and wallet errors throw. Retry silent reads separately to isolate a bad
69
+ // path or unsupported coin while reusing the already selected wallet.
70
+ if (
71
+ isProtocolV2 &&
72
+ params.length > 1 &&
73
+ (runIndividually || response.every(item => !item.success))
74
+ ) {
75
+ response = [];
76
+ for (const param of params) {
77
+ if (this.abortController?.signal.aborted) {
78
+ throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
79
+ }
80
+ const itemResponse = await this.callMethod(
81
+ methodName,
82
+ { bundle: [{ ...param.params }] },
83
+ rootFingerprint
84
+ );
85
+ response.push(...itemResponse);
86
+ }
87
+ }
60
88
 
61
89
  if (this.abortController?.signal.aborted) {
62
90
  throw new Error(HardwareErrorCodeMessage[HardwareErrorCode.RepeatUnlocking]);
63
91
  }
64
92
 
65
- for (let i = 0; i < params.length; i++) {
66
- const { _originRequestParams, _originalIndex } = params[i];
67
- const responseKey = `${_originalIndex}`;
68
- resultMap[responseKey] = {
93
+ for (let index = 0; index < params.length; index++) {
94
+ const { _originRequestParams, _originalIndex } = params[index];
95
+ resultMap[`${_originalIndex}`] = {
69
96
  ..._originRequestParams,
70
- ...response[i],
97
+ ...response[index],
71
98
  };
72
99
  }
73
100
 
74
- if (this.payload?.bundle?.length > 1) {
75
- const progress = Math.round(((i + 1) / this.payload.bundle.length) * 100);
101
+ processed += params.length;
102
+ if (bundle.length > 1) {
103
+ const progress = Math.round((processed / bundle.length) * 100);
76
104
  this.postMessage(createUiMessage(UI_REQUEST.DEVICE_PROGRESS, { progress }));
77
105
  }
78
- i++;
79
106
  }
80
107
 
81
108
  for (let i = 0; i < bundle.length; i++) {
@@ -273,6 +273,12 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
273
273
 
274
274
  private loadingCommands?: DeviceCommands;
275
275
 
276
+ // DeviceSessionGet selects the SE wallet like Initialize(session_id). Nested
277
+ // all-network methods skip callAPI, so the first chain call still resumes;
278
+ // later same-domain calls reuse that session. Cardano may Ask [Standard,
279
+ // Cardano] once, which also covers later non-Cardano commands.
280
+ private protocolV2ResumedSeedDomains = new Set<'standard' | 'cardano'>();
281
+
276
282
  init() {
277
283
  this.checkDeviceId = true;
278
284
  this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE];
@@ -320,6 +326,23 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
320
326
  };
321
327
  }
322
328
 
329
+ private hasProtocolV2WalletResume(deriveCardano?: boolean) {
330
+ if (deriveCardano) {
331
+ return this.protocolV2ResumedSeedDomains.has('cardano');
332
+ }
333
+ return (
334
+ this.protocolV2ResumedSeedDomains.has('standard') ||
335
+ this.protocolV2ResumedSeedDomains.has('cardano')
336
+ );
337
+ }
338
+
339
+ private markProtocolV2WalletResumed(deriveCardano?: boolean) {
340
+ this.protocolV2ResumedSeedDomains.add('standard');
341
+ if (deriveCardano) {
342
+ this.protocolV2ResumedSeedDomains.add('cardano');
343
+ }
344
+ }
345
+
323
346
  async callMethod(
324
347
  methodName: keyof CoreApi,
325
348
  params: any & {
@@ -395,16 +418,17 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
395
418
  }
396
419
  }
397
420
 
398
- // Protocol V2 hands a wallet session to exactly one blockchain request.
399
- // The parent all-network call consumes its first handoff while fetching
400
- // the root fingerprint, so each nested chain method must resume the
401
- // requested standard or hidden wallet before sending its device command.
421
+ // Nested chain methods skip callAPI's session gate. Resume the requested
422
+ // wallet once per seed domain; DeviceSessionGet is sticky like V1
423
+ // Initialize, so later addresses and chains reuse it.
402
424
  const useEmptyPassphrase = this.payload.useEmptyPassphrase === true;
403
- // Nested Cardano methods opt in to [Standard, Cardano] if Ask rebuilds.
404
- // Other chains stay Standard-only.
405
425
  const deriveCardano = method.name.startsWith('cardano') ? true : undefined;
406
426
  const shouldResumeWalletSession = useEmptyPassphrase || !!this.payload.passphraseState;
407
- if (this.device.isProtocolV2() && shouldResumeWalletSession) {
427
+ if (
428
+ this.device.isProtocolV2() &&
429
+ shouldResumeWalletSession &&
430
+ !this.hasProtocolV2WalletResume(deriveCardano)
431
+ ) {
408
432
  const passphraseStateSafety = await this.device.checkPassphraseStateSafety(
409
433
  this.payload.passphraseState,
410
434
  useEmptyPassphrase,
@@ -415,6 +439,7 @@ export default abstract class AllNetworkGetAddressBase extends BaseMethod<
415
439
  if (!passphraseStateSafety) {
416
440
  throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError);
417
441
  }
442
+ this.markProtocolV2WalletResumed(deriveCardano);
418
443
  }
419
444
  },
420
445
  });
package/src/core/index.ts CHANGED
@@ -84,7 +84,7 @@ import type { BaseMethod } from '../api/BaseMethod';
84
84
 
85
85
  const Log = getLogger(LoggerNames.Core);
86
86
  const PRE_INITIALIZE_TTL_MS = 60 * 1000;
87
- const PRE_PENDING_CALL_TIMEOUT_MS = 5 * 1000;
87
+ const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 1000;
88
88
  const PRO2_USB_SIGNING_COOLDOWN_MS = 1000;
89
89
 
90
90
  // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
@@ -183,7 +183,6 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
183
183
  }
184
184
  };
185
185
  method.setContext?.(context);
186
- method.context = context;
187
186
 
188
187
  method.requestContext = createRequestContext(method.responseID, method.name, {
189
188
  sdkInstanceId: context.sdkInstanceId,
@@ -204,12 +203,7 @@ export const callAPI = async (context: CoreContext, message: CoreMessage) => {
204
203
  if (!method.useDevice) {
205
204
  updateMethodRequestContext(method, { status: 'running' });
206
205
  try {
207
- const env = DataManager.getSettings('env');
208
- const response =
209
- method.name === 'searchDevices' &&
210
- (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env))
211
- ? await context.methodSynchronize(() => method.run(), 'webusb-discovery')
212
- : await method.run();
206
+ const response = await method.run();
213
207
  completeMethodRequestContext(method);
214
208
  return createResponseMessage(method.responseID, true, response);
215
209
  } catch (error) {
@@ -321,27 +315,25 @@ const waitForPendingPromise = async (
321
315
  if (pendingPromise) {
322
316
  Log.debug('pre pending call promise before call method, wait for it');
323
317
  let timer: ReturnType<typeof setTimeout> | undefined;
324
- let timedOut = false;
325
- let completed = false;
326
318
  try {
327
319
  await Promise.race([
328
320
  pendingPromise,
329
- new Promise<void>(resolve => {
321
+ new Promise<void>((_, reject) => {
330
322
  timer = setTimeout(() => {
331
- timedOut = true;
332
- resolve();
323
+ reject(
324
+ ERRORS.TypedError(
325
+ HardwareErrorCode.DeviceBusy,
326
+ 'Previous device cancellation is still draining'
327
+ )
328
+ );
333
329
  }, PRE_PENDING_CALL_TIMEOUT_MS);
334
330
  }),
335
331
  ]);
336
- completed = !timedOut;
332
+ // A deadline is not evidence that old I/O is safe to reuse. Keep the
333
+ // barrier on failure; a later call may proceed only after cleanup settles.
334
+ removePrePendingCallPromise?.(connectId, pendingPromise);
337
335
  } finally {
338
336
  if (timer) clearTimeout(timer);
339
- // Keep a rejected cleanup barrier for the next caller's safety check;
340
- // only a completed cleanup or a timeout may clear it.
341
- if (timedOut || completed) removePrePendingCallPromise?.(connectId, pendingPromise);
342
- }
343
- if (timedOut) {
344
- Log.warn('pre pending call promise timed out before call method', { connectId });
345
337
  }
346
338
  Log.debug('pre pending call promise before call method done');
347
339
  }
@@ -419,26 +411,7 @@ const onCallDevice = async (
419
411
  * Polling to ensure successful connection
420
412
  */
421
413
  const pollingId = pollingManager.start(connectId);
422
- const env = DataManager.getSettings('env');
423
- const connect = () =>
424
- ensureConnected(context, method, connectId, pollingId, method.abortSignal);
425
- // Discovery may acquire USB endpoints. Finish it before initializing a public
426
- // request; once registered, that request makes discovery use cached state only.
427
- if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
428
- // Synchronization can keep the connect action queued after the caller is
429
- // cancelled. Observe that promise so a late device-not-found error does
430
- // not become an unhandled rejection.
431
- const connectPromise = context.methodSynchronize(async () => {
432
- if (method.abortSignal?.aborted) {
433
- throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
434
- }
435
- return connect();
436
- }, 'webusb-discovery');
437
- connectPromise.catch(() => undefined);
438
- device = await requestQueue.waitForTask(task, () => connectPromise);
439
- } else {
440
- device = await connect();
441
- }
414
+ device = await ensureConnected(context, method, connectId, pollingId, method.abortSignal);
442
415
  if (method.abortSignal?.aborted) {
443
416
  throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
444
417
  }
@@ -1227,7 +1200,6 @@ const ensureConnected = async (
1227
1200
  const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
1228
1201
  const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
1229
1202
  let timer: ReturnType<typeof setTimeout> | null = null;
1230
- let lastInitializeError: any;
1231
1203
  Log.debug(
1232
1204
  `EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `
1233
1205
  );
@@ -1268,9 +1240,7 @@ const ensureConnected = async (
1268
1240
  Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
1269
1241
  try {
1270
1242
  await initDeviceList(method);
1271
- lastInitializeError = undefined;
1272
1243
  } catch (error) {
1273
- lastInitializeError = error;
1274
1244
  Log.debug('device list error: ', error);
1275
1245
  if (
1276
1246
  [
@@ -1285,7 +1255,6 @@ const ensureConnected = async (
1285
1255
  }
1286
1256
  if (error.errorCode === HardwareErrorCode.TransportNotConfigured) {
1287
1257
  await TransportManager.configure();
1288
- lastInitializeError = undefined;
1289
1258
  }
1290
1259
  }
1291
1260
 
@@ -1383,22 +1352,14 @@ const ensureConnected = async (
1383
1352
  clearTimeout(timer);
1384
1353
  }
1385
1354
  Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
1386
- const preserveWebUsbInitError =
1387
- DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
1388
- const needsPermissionPrompt =
1389
- DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt;
1390
- const fallbackError = needsPermissionPrompt
1391
- ? ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission)
1392
- : ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
1393
- const errorToReject =
1394
- preserveWebUsbInitError && lastInitializeError ? lastInitializeError : fallbackError;
1395
- // Only ask the host for a WebUSB grant when the failure is actually
1396
- // "not found / needs permission". A preserved initialize error must
1397
- // not fire that prompt with a different public code.
1398
- if (needsPermissionPrompt && errorToReject === fallbackError) {
1355
+ // Browser WebUSB needs permission prompt, desktop WebUSB doesn't
1356
+ // skipWebDevicePrompt can override this behavior for special cases
1357
+ if (DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt) {
1399
1358
  postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
1359
+ reject(ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
1360
+ } else {
1361
+ reject(ERRORS.TypedError(HardwareErrorCode.DeviceNotFound));
1400
1362
  }
1401
- reject(errorToReject);
1402
1363
  return;
1403
1364
  }
1404
1365
 
@@ -33,8 +33,6 @@ export default class TransportManager {
33
33
 
34
34
  static reactNativeInit = false;
35
35
 
36
- static webUsbInit = false;
37
-
38
36
  static protocolV1MessageSchema: ProtocolV1MessageSchema = 'v1CurrentSchema';
39
37
 
40
38
  static plugin: LowlevelTransportSharedPlugin | null = null;
@@ -44,17 +42,6 @@ export default class TransportManager {
44
42
  this.defaultMessages = DataManager.getProtobufMessages();
45
43
  this.currentMessages = this.defaultMessages;
46
44
  this.protocolV1MessageSchema = 'v1CurrentSchema';
47
- this.webUsbInit = false;
48
- }
49
-
50
- static async ensureInitialized() {
51
- const env = DataManager.getSettings('env');
52
- if (env !== 'webusb' && env !== 'desktop-webusb') return;
53
- if (this.webUsbInit) return;
54
- // The emitter registers USB disconnect events; this must happen even when
55
- // schema configuration is intentionally deferred during discovery.
56
- await this.transport.init(WebUsbLogger, DevicePool.emitter);
57
- this.webUsbInit = true;
58
45
  }
59
46
 
60
47
  static async configure() {
@@ -82,7 +69,9 @@ export default class TransportManager {
82
69
  } else if (env === 'desktop-web-ble') {
83
70
  await this.transport.init(WebBleLogger, DevicePool.emitter);
84
71
  } else if (env === 'webusb' || env === 'desktop-webusb') {
85
- await this.ensureInitialized();
72
+ // The emitter is what turns a navigator.usb 'disconnect' into a
73
+ // DEVICE.DISCONNECT; without it WebUSB never reports device removal.
74
+ await this.transport.init(WebUsbLogger, DevicePool.emitter);
86
75
  } else {
87
76
  await this.transport.init(HttpLogger);
88
77
  }
@@ -1624,13 +1624,6 @@ export class Device extends EventEmitter {
1624
1624
  } else if (!acquired) {
1625
1625
  // Abort setup without acquiring a session just to send Cancel.
1626
1626
  if (mainId && deviceConnector?.disconnect) {
1627
- // The connector drops the link silently, so without this line a
1628
- // user-cancel teardown is indistinguishable in field logs from an
1629
- // idle keep-alive release or a device that left on its own.
1630
- Log.debug(
1631
- 'interruptionFromUser: disconnecting device without acquire, mainId:',
1632
- mainId
1633
- );
1634
1627
  await deviceConnector.disconnect(mainId);
1635
1628
  }
1636
1629
  if (this.connectionAttempt === attempt) this.markTransportDisconnected();