@onekeyfe/hd-core 1.2.2-alpha.113 → 1.2.2-alpha.114

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.
@@ -62,7 +62,13 @@ describe('Core 错误输出边界', () => {
62
62
  const request = core.handleMessage({
63
63
  id: 11,
64
64
  type: IFRAME.CALL,
65
- payload: { method: 'getDeviceState', connectId: 'serial-V2' },
65
+ payload: {
66
+ method: 'getDeviceState',
67
+ connectId: 'serial-V2',
68
+ retryCount: 1,
69
+ pollIntervalTime: 1,
70
+ timeout: 1000,
71
+ },
66
72
  } as never);
67
73
  await new Promise(resolve => {
68
74
  setTimeout(resolve, 0);
@@ -87,7 +93,7 @@ describe('Core 错误输出边界', () => {
87
93
  setTimeout(resolve, 0);
88
94
  });
89
95
  expect(search).toHaveBeenCalledTimes(1);
90
- expect(initialize).toHaveBeenCalledTimes(shouldCancel ? 0 : 1);
96
+ expect(initialize).toHaveBeenCalledTimes(shouldCancel ? 0 : 2);
91
97
  } finally {
92
98
  finishSearch?.();
93
99
  await core.dispose();
@@ -4,6 +4,7 @@ jest.mock('../src/data-manager/TransportManager', () => ({
4
4
  __esModule: true,
5
5
  default: {
6
6
  configure: jest.fn(),
7
+ ensureInitialized: jest.fn(),
7
8
  },
8
9
  }));
9
10
 
@@ -21,9 +22,9 @@ jest.mock('../src/device/DevicePool', () => ({
21
22
  },
22
23
  }));
23
24
 
24
- const transportManagerMock: { default: { configure: jest.Mock } } = jest.requireMock(
25
- '../src/data-manager/TransportManager'
26
- );
25
+ const transportManagerMock: {
26
+ default: { configure: jest.Mock; ensureInitialized: jest.Mock };
27
+ } = jest.requireMock('../src/data-manager/TransportManager');
27
28
  const devicePoolMock: {
28
29
  DevicePool: { getDevices: jest.Mock; getDeviceByPath: jest.Mock };
29
30
  } = jest.requireMock('../src/device/DevicePool');
@@ -33,8 +34,10 @@ const dataManagerMock: {
33
34
  isBleConnect: jest.Mock;
34
35
  };
35
36
  } = jest.requireMock('../src/data-manager');
36
- const { configure: mockConfigureTransport } = transportManagerMock.default;
37
- const { getDevices: mockGetDevices } = devicePoolMock.DevicePool;
37
+ const { configure: mockConfigureTransport, ensureInitialized: mockEnsureInitialized } =
38
+ transportManagerMock.default;
39
+ const { getDevices: mockGetDevices, getDeviceByPath: mockGetDeviceByPath } =
40
+ devicePoolMock.DevicePool;
38
41
  const { isBleConnect: mockIsBleConnect } = dataManagerMock.DataManager;
39
42
 
40
43
  describe('SearchDevices', () => {
@@ -52,32 +55,116 @@ describe('SearchDevices', () => {
52
55
  features: { protocol },
53
56
  toMessageObject: () => ({ connectId: `serial-${protocol}` }),
54
57
  }));
55
- devicePoolMock.DevicePool.getDeviceByPath.mockImplementation(
58
+ mockGetDeviceByPath.mockImplementation(
56
59
  (path: string) => devices[['usb-V1', 'usb-V2'].indexOf(path)]
57
60
  );
61
+ const extraDevice = {
62
+ toMessageObject: () => ({ connectId: 'not-initialized' }),
63
+ };
64
+ mockGetDevices.mockResolvedValue({
65
+ devices: { 'not-initialized': extraDevice },
66
+ deviceList: [extraDevice],
67
+ });
58
68
  const method = new SearchDevices({
59
69
  id: 1,
60
70
  payload: { method: 'searchDevices' },
61
71
  } as never);
62
72
  method.init();
63
73
  method.context = {
64
- requestQueue: { getRequestTasksId: () => [2] },
74
+ requestQueue: {
75
+ getRequestTasksId: () => [2],
76
+ getRequestTasksIdByConnectId: (connectId: string) =>
77
+ connectId === 'usb-V1' || connectId === 'usb-V2' ? [2] : [],
78
+ },
65
79
  } as never;
66
80
  method.connector = {
67
81
  enumerate: jest.fn().mockResolvedValue({
68
- descriptors: [{ path: 'usb-V1' }, { path: 'usb-V2' }, { path: 'not-initialized' }],
82
+ descriptors: [
83
+ { path: 'usb-V1', commType: 'webusb' },
84
+ { path: 'usb-V2', commType: 'webusb' },
85
+ { path: 'not-initialized', commType: 'webusb' },
86
+ ],
69
87
  }),
70
88
  } as never;
71
89
 
72
90
  await expect(method.run()).resolves.toEqual([
73
91
  { connectId: 'serial-V1' },
74
92
  { connectId: 'serial-V2' },
93
+ { connectId: 'not-initialized' },
75
94
  ]);
76
- expect(mockGetDevices).not.toHaveBeenCalled();
95
+ expect(mockGetDevices).toHaveBeenCalledTimes(1);
96
+ expect(mockGetDevices).toHaveBeenCalledWith(
97
+ [{ path: 'not-initialized', commType: 'webusb' }],
98
+ 'not-initialized',
99
+ {
100
+ connectProtocol: undefined,
101
+ forceProtocolDetection: true,
102
+ refreshRuntimeState: true,
103
+ }
104
+ );
77
105
  expect(mockConfigureTransport).not.toHaveBeenCalled();
78
106
  }
79
107
  );
80
108
 
109
+ test('owned WebUSB cache miss still reports a SearchDevice without probing', async () => {
110
+ mockGetDeviceByPath.mockReturnValue(undefined);
111
+ const usbDevice = { vendorId: 0x1209, productId: 0x4f4c };
112
+ const method = new SearchDevices({
113
+ id: 1,
114
+ payload: { method: 'searchDevices' },
115
+ } as never);
116
+ method.init();
117
+ method.context = {
118
+ requestQueue: {
119
+ getRequestTasksId: () => [2],
120
+ getRequestTasksIdByConnectId: (connectId: string) => (connectId === 'serial-V2' ? [2] : []),
121
+ },
122
+ } as never;
123
+ method.connector = {
124
+ enumerate: jest.fn().mockResolvedValue({
125
+ descriptors: [
126
+ {
127
+ path: 'serial-V2',
128
+ device: usbDevice,
129
+ commType: 'webusb',
130
+ },
131
+ ],
132
+ }),
133
+ } as never;
134
+
135
+ await expect(method.run()).resolves.toEqual([
136
+ {
137
+ connectId: 'serial-V2',
138
+ uuid: 'serial-V2',
139
+ serialNo: 'serial-V2',
140
+ deviceId: null,
141
+ deviceType: 'unknown',
142
+ name: 'serial-V2',
143
+ commType: 'webusb',
144
+ },
145
+ ]);
146
+ expect(mockGetDevices).not.toHaveBeenCalled();
147
+ expect(mockConfigureTransport).not.toHaveBeenCalled();
148
+ });
149
+
150
+ test('searchDevices resolves empty when WebUSB bring-up is unavailable', async () => {
151
+ mockEnsureInitialized.mockRejectedValueOnce(
152
+ new Error('WebUSB is not supported by current browsers')
153
+ );
154
+ const method = new SearchDevices({
155
+ id: 1,
156
+ payload: { method: 'searchDevices' },
157
+ } as never);
158
+ method.init();
159
+ method.connector = {
160
+ enumerate: jest.fn().mockResolvedValue({ descriptors: [] }),
161
+ } as never;
162
+
163
+ await expect(method.run()).resolves.toEqual([]);
164
+ expect(mockEnsureInitialized).toHaveBeenCalled();
165
+ expect(mockConfigureTransport).toHaveBeenCalledTimes(1);
166
+ });
167
+
81
168
  test('搜索忽略调用方协议并主动探测,单个无响应设备不阻断后续结果', async () => {
82
169
  const unresponsiveDescriptor = {
83
170
  path: 'stale-usb-device',
@@ -1,22 +1,9 @@
1
1
  import { BaseMethod } from './BaseMethod';
2
+ import type { SearchDevice } from '../types/device';
2
3
  import type DeviceConnector from '../device/DeviceConnector';
3
4
  export default class SearchDevices extends BaseMethod {
4
5
  connector?: DeviceConnector;
5
6
  init(): void;
6
- run(): Promise<(import("..").KnownDevice | null)[] | {
7
- connectId: string;
8
- serialNo: null;
9
- uuid: string;
10
- deviceId: null;
11
- name: string | null;
12
- deviceType: import("..").IDeviceType;
13
- commType: import("packages/hd-transport/dist").OneKeyDeviceCommType;
14
- path: string;
15
- session?: string | null | undefined;
16
- debugSession?: string | null | undefined;
17
- debug: boolean;
18
- id: string;
19
- protocolType?: import("packages/hd-transport/dist").ProtocolType | undefined;
20
- }[]>;
7
+ run(): Promise<SearchDevice[]>;
21
8
  }
22
9
  //# sourceMappingURL=SearchDevices.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SearchDevices.d.ts","sourceRoot":"","sources":["../../src/api/SearchDevices.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAM1C,OAAO,KAAK,eAAe,MAAM,2BAA2B,CAAC;AAI7D,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,UAAU;IACnD,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B,IAAI;IAME,GAAG;;;;;;;;;;;;;;;CA0EV"}
1
+ {"version":3,"file":"SearchDevices.d.ts","sourceRoot":"","sources":["../../src/api/SearchDevices.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAM1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,eAAe,MAAM,2BAA2B,CAAC;AAuC7D,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,UAAU;IACnD,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B,IAAI;IAME,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;CAsFrC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":";AACA,OAAO,YAAY,MAAM,QAAQ,CAAC;AAClC,OAAO,EAEL,KAAK,6BAA6B,EAElC,KAAK,YAAY,EAIlB,MAAM,wBAAwB,CAAC;AAiChC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAsB1C,OAAO,eAAe,MAAM,2BAA2B,CAAC;AAYxD,OAAO,KAAK,EAAE,eAAe,EAAyB,MAAM,UAAU,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAmD,MAAM,WAAW,CAAC;AAI9F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAYpD,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;AA0E7D,eAAO,MAAM,OAAO,YAAmB,WAAW,WAAW,WAAW,iBA0FvE,CAAC;AAyFF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3F;AA8mBD,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,WAWpF;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,WAoB/E;AAED,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,WAQlF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,WAEzD;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,WAI3D;AAiKD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,CAMtF;AAwMD,eAAO,MAAM,MAAM,YAAa,WAAW,cAAc,MAAM,SA4G9D,CAAC;AA8GF,eAAO,MAAM,qBAAqB,gFAejC,CAAC;AAiLF,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,YAAY;IAC5C,OAAO,CAAC,cAAc,CAAoB;IAE1C,SAAgB,aAAa,EAAE,MAAM,CAAC;IAEtC,OAAO,CAAC,YAAY,CAAsB;IAE1C,OAAO,CAAC,cAAc,CAAC,CAAgB;IAGvC,OAAO,CAAC,sBAAsB,CAAoC;IAElE,OAAO,CAAC,iBAAiB,CAAoB;;IAS7C,OAAO,CAAC,cAAc;IAuChB,aAAa,CAAC,OAAO,EAAE,WAAW;IAuExC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAOV,gBAAgB;CAiC/B;AAED,eAAO,MAAM,QAAQ,YAIpB,CAAC;AAEF,eAAO,MAAM,aAAa,uBAYzB,CAAC;AAMF,eAAO,MAAM,IAAI,aACL,eAAe,aACd,GAAG,WACL,6BAA6B,8BAiBvC,CAAC;AAEF,eAAO,MAAM,eAAe;SAKrB,eAAe,CAAC,KAAK,CAAC;eAChB,GAAG;;UASf,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":";AACA,OAAO,YAAY,MAAM,QAAQ,CAAC;AAClC,OAAO,EAEL,KAAK,6BAA6B,EAElC,KAAK,YAAY,EAIlB,MAAM,wBAAwB,CAAC;AAiChC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAsB1C,OAAO,eAAe,MAAM,2BAA2B,CAAC;AAYxD,OAAO,KAAK,EAAE,eAAe,EAAyB,MAAM,UAAU,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAmD,MAAM,WAAW,CAAC;AAI9F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAYpD,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;AA0E7D,eAAO,MAAM,OAAO,YAAmB,WAAW,WAAW,WAAW,iBA0FvE,CAAC;AA2FF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAY3F;AAunBD,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,WAWpF;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,WAoB/E;AAED,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,WAQlF;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,WAEzD;AAED,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,OAAO,WAI3D;AAiKD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS,CAMtF;AA2MD,eAAO,MAAM,MAAM,YAAa,WAAW,cAAc,MAAM,SA4G9D,CAAC;AA8GF,eAAO,MAAM,qBAAqB,gFAejC,CAAC;AAiLF,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,YAAY;IAC5C,OAAO,CAAC,cAAc,CAAoB;IAE1C,SAAgB,aAAa,EAAE,MAAM,CAAC;IAEtC,OAAO,CAAC,YAAY,CAAsB;IAE1C,OAAO,CAAC,cAAc,CAAC,CAAgB;IAGvC,OAAO,CAAC,sBAAsB,CAAoC;IAElE,OAAO,CAAC,iBAAiB,CAAoB;;IAS7C,OAAO,CAAC,cAAc;IAuChB,aAAa,CAAC,OAAO,EAAE,WAAW;IAuExC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAOV,gBAAgB;CAiC/B;AAED,eAAO,MAAM,QAAQ,YAIpB,CAAC;AAEF,eAAO,MAAM,aAAa,uBAYzB,CAAC;AAMF,eAAO,MAAM,IAAI,aACL,eAAe,aACd,GAAG,WACL,6BAA6B,8BAiBvC,CAAC;AAEF,eAAO,MAAM,eAAe;SAKrB,eAAe,CAAC,KAAK,CAAC;eAChB,GAAG;;UASf,CAAC"}
@@ -6,9 +6,11 @@ export default class TransportManager {
6
6
  static defaultMessages: JSON | Record<string, any>;
7
7
  static currentMessages: JSON | Record<string, any>;
8
8
  static reactNativeInit: boolean;
9
+ static webUsbInit: boolean;
9
10
  static protocolV1MessageSchema: ProtocolV1MessageSchema;
10
11
  static plugin: LowlevelTransportSharedPlugin | null;
11
12
  static load(): void;
13
+ static ensureInitialized(): Promise<void>;
12
14
  static configure(): Promise<void>;
13
15
  static reconfigure(features?: Features): Promise<void>;
14
16
  static setTransport(TransportConstructor: any, plugin?: LowlevelTransportSharedPlugin): void;
@@ -1 +1 @@
1
- {"version":3,"file":"TransportManager.d.ts","sourceRoot":"","sources":["../../src/data-manager/TransportManager.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EAAE,6BAA6B,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACvF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAgBzC,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC;IAE5B,MAAM,CAAC,eAAe,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEnD,MAAM,CAAC,eAAe,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEnD,MAAM,CAAC,eAAe,UAAS;IAE/B,MAAM,CAAC,uBAAuB,EAAE,uBAAuB,CAAqB;IAE5E,MAAM,CAAC,MAAM,EAAE,6BAA6B,GAAG,IAAI,CAAQ;IAE3D,MAAM,CAAC,IAAI;WAOE,SAAS;WAqDT,WAAW,CAAC,QAAQ,CAAC,EAAE,QAAQ;IAyB5C,MAAM,CAAC,YAAY,CAAC,oBAAoB,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,6BAA6B;IA6BrF,MAAM,CAAC,YAAY;mBAIE,2BAA2B;IAShD,MAAM,CAAC,kBAAkB;IAIzB,MAAM,CAAC,kBAAkB;IAIzB,MAAM,CAAC,0BAA0B;CAGlC"}
1
+ {"version":3,"file":"TransportManager.d.ts","sourceRoot":"","sources":["../../src/data-manager/TransportManager.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EAAE,6BAA6B,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACvF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAgBzC,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC;IAE5B,MAAM,CAAC,eAAe,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEnD,MAAM,CAAC,eAAe,EAAE,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEnD,MAAM,CAAC,eAAe,UAAS;IAE/B,MAAM,CAAC,UAAU,UAAS;IAE1B,MAAM,CAAC,uBAAuB,EAAE,uBAAuB,CAAqB;IAE5E,MAAM,CAAC,MAAM,EAAE,6BAA6B,GAAG,IAAI,CAAQ;IAE3D,MAAM,CAAC,IAAI;WAQE,iBAAiB;WAUjB,SAAS;WAmDT,WAAW,CAAC,QAAQ,CAAC,EAAE,QAAQ;IAyB5C,MAAM,CAAC,YAAY,CAAC,oBAAoB,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,6BAA6B;IA6BrF,MAAM,CAAC,YAAY;mBAIE,2BAA2B;IAShD,MAAM,CAAC,kBAAkB;IAIzB,MAAM,CAAC,kBAAkB;IAIzB,MAAM,CAAC,0BAA0B;CAGlC"}
package/dist/index.js CHANGED
@@ -43705,6 +43705,18 @@ class TransportManager {
43705
43705
  this.defaultMessages = DataManager.getProtobufMessages();
43706
43706
  this.currentMessages = this.defaultMessages;
43707
43707
  this.protocolV1MessageSchema = 'v1CurrentSchema';
43708
+ this.webUsbInit = false;
43709
+ }
43710
+ static ensureInitialized() {
43711
+ return __awaiter(this, void 0, void 0, function* () {
43712
+ const env = DataManager.getSettings('env');
43713
+ if (env !== 'webusb' && env !== 'desktop-webusb')
43714
+ return;
43715
+ if (this.webUsbInit)
43716
+ return;
43717
+ yield this.transport.init(WebUsbLogger, DevicePool.emitter);
43718
+ this.webUsbInit = true;
43719
+ });
43708
43720
  }
43709
43721
  static configure() {
43710
43722
  return __awaiter(this, void 0, void 0, function* () {
@@ -43733,7 +43745,7 @@ class TransportManager {
43733
43745
  yield this.transport.init(WebBleLogger, DevicePool.emitter);
43734
43746
  }
43735
43747
  else if (env === 'webusb' || env === 'desktop-webusb') {
43736
- yield this.transport.init(WebUsbLogger, DevicePool.emitter);
43748
+ yield this.ensureInitialized();
43737
43749
  }
43738
43750
  else {
43739
43751
  yield this.transport.init(HttpLogger);
@@ -43816,6 +43828,7 @@ class TransportManager {
43816
43828
  }
43817
43829
  }
43818
43830
  TransportManager.reactNativeInit = false;
43831
+ TransportManager.webUsbInit = false;
43819
43832
  TransportManager.protocolV1MessageSchema = 'v1CurrentSchema';
43820
43833
  TransportManager.plugin = null;
43821
43834
 
@@ -47263,6 +47276,28 @@ class PreInitialize extends BaseMethod {
47263
47276
  }
47264
47277
 
47265
47278
  const Log$e = getLogger(exports.LoggerNames.DevicePool);
47279
+ const isOwnedByActiveWebUsbRequest = (descriptor, requestQueue) => {
47280
+ if (!requestQueue)
47281
+ return false;
47282
+ const keys = [descriptor.path, descriptor.id].filter((key) => typeof key === 'string' && key.length > 0);
47283
+ return keys.some(key => requestQueue.getRequestTasksIdByConnectId(key).length > 0);
47284
+ };
47285
+ const toSearchDeviceFromDescriptor = (descriptor) => {
47286
+ const connectId = descriptor.path || descriptor.id || null;
47287
+ return {
47288
+ connectId,
47289
+ uuid: connectId !== null && connectId !== void 0 ? connectId : '',
47290
+ serialNo: connectId,
47291
+ deviceId: null,
47292
+ deviceType: hdShared.EDeviceType.Unknown,
47293
+ name: descriptor.name || connectId || '',
47294
+ commType: descriptor.commType,
47295
+ };
47296
+ };
47297
+ const toSearchDevice = (device) => {
47298
+ const message = device.toMessageObject();
47299
+ return message ? message : null;
47300
+ };
47266
47301
  class SearchDevices extends BaseMethod {
47267
47302
  init() {
47268
47303
  this.useDevice = false;
@@ -47273,9 +47308,18 @@ class SearchDevices extends BaseMethod {
47273
47308
  var _a, _b, _c, _d, _e, _f, _g;
47274
47309
  return __awaiter(this, void 0, void 0, function* () {
47275
47310
  const env = DataManager.getSettings('env');
47276
- const useCachedWebUsbDevices = (env === 'webusb' || env === 'desktop-webusb') &&
47277
- ((_b = (_a = this.context) === null || _a === void 0 ? void 0 : _a.requestQueue.getRequestTasksId().length) !== null && _b !== void 0 ? _b : 0) > 0;
47278
- if (!useCachedWebUsbDevices)
47311
+ const isWebUsb = env === 'webusb' || env === 'desktop-webusb';
47312
+ const requestQueue = (_a = this.context) === null || _a === void 0 ? void 0 : _a.requestQueue;
47313
+ const hasActiveWebUsbRequest = isWebUsb && ((_b = requestQueue === null || requestQueue === void 0 ? void 0 : requestQueue.getRequestTasksId().length) !== null && _b !== void 0 ? _b : 0) > 0;
47314
+ if (isWebUsb) {
47315
+ try {
47316
+ yield TransportManager.ensureInitialized();
47317
+ }
47318
+ catch (error) {
47319
+ Log$e.debug('WebUSB bring-up unavailable', error);
47320
+ }
47321
+ }
47322
+ if (!hasActiveWebUsbRequest)
47279
47323
  yield TransportManager.configure();
47280
47324
  const deviceDiff = yield ((_c = this.connector) === null || _c === void 0 ? void 0 : _c.enumerate());
47281
47325
  const devicesDescriptor = (_d = deviceDiff === null || deviceDiff === void 0 ? void 0 : deviceDiff.descriptors) !== null && _d !== void 0 ? _d : [];
@@ -47293,30 +47337,35 @@ class SearchDevices extends BaseMethod {
47293
47337
  }
47294
47338
  return devices;
47295
47339
  }
47296
- if (useCachedWebUsbDevices) {
47297
- return devicesDescriptor.flatMap(descriptor => {
47298
- const device = DevicePool.getDeviceByPath(descriptor.path);
47299
- return (device === null || device === void 0 ? void 0 : device.features) ? [device.toMessageObject()] : [];
47300
- });
47301
- }
47302
47340
  const deviceList = [];
47303
47341
  for (const descriptor of devicesDescriptor) {
47304
- try {
47305
- const result = yield DevicePool.getDevices([descriptor], descriptor.path, {
47306
- connectProtocol: undefined,
47307
- forceProtocolDetection: true,
47308
- refreshRuntimeState: true,
47309
- });
47310
- deviceList.push(...result.deviceList);
47342
+ if (hasActiveWebUsbRequest && isOwnedByActiveWebUsbRequest(descriptor, requestQueue)) {
47343
+ const cached = DevicePool.getDeviceByPath(descriptor.path);
47344
+ const message = (cached === null || cached === void 0 ? void 0 : cached.features) ? toSearchDevice(cached) : null;
47345
+ deviceList.push(message !== null && message !== void 0 ? message : toSearchDeviceFromDescriptor(descriptor));
47311
47346
  }
47312
- catch (error) {
47313
- const errorCode = error && typeof error === 'object' && 'errorCode' in error
47314
- ? error.errorCode
47315
- : undefined;
47316
- Log$e.debug('Skip unavailable device during search', Object.assign({ path: descriptor.path }, (errorCode !== undefined ? { errorCode } : {})));
47347
+ else {
47348
+ try {
47349
+ const result = yield DevicePool.getDevices([descriptor], descriptor.path, {
47350
+ connectProtocol: undefined,
47351
+ forceProtocolDetection: true,
47352
+ refreshRuntimeState: true,
47353
+ });
47354
+ for (const device of result.deviceList) {
47355
+ const message = toSearchDevice(device);
47356
+ if (message)
47357
+ deviceList.push(message);
47358
+ }
47359
+ }
47360
+ catch (error) {
47361
+ const errorCode = error && typeof error === 'object' && 'errorCode' in error
47362
+ ? error.errorCode
47363
+ : undefined;
47364
+ Log$e.debug('Skip unavailable device during search', Object.assign({ path: descriptor.path }, (errorCode !== undefined ? { errorCode } : {})));
47365
+ }
47317
47366
  }
47318
47367
  }
47319
- return deviceList.map(device => device.toMessageObject());
47368
+ return deviceList;
47320
47369
  });
47321
47370
  }
47322
47371
  }
@@ -66033,6 +66082,7 @@ const waitForPendingPromise = (connectId, getPrePendingCallPromise, removePrePen
66033
66082
  Log.debug('pre pending call promise before call method, wait for it');
66034
66083
  let timer;
66035
66084
  let timedOut = false;
66085
+ let completed = false;
66036
66086
  try {
66037
66087
  yield Promise.race([
66038
66088
  pendingPromise,
@@ -66043,11 +66093,13 @@ const waitForPendingPromise = (connectId, getPrePendingCallPromise, removePrePen
66043
66093
  }, PRE_PENDING_CALL_TIMEOUT_MS);
66044
66094
  }),
66045
66095
  ]);
66096
+ completed = !timedOut;
66046
66097
  }
66047
66098
  finally {
66048
66099
  if (timer)
66049
66100
  clearTimeout(timer);
66050
- removePrePendingCallPromise === null || removePrePendingCallPromise === void 0 ? void 0 : removePrePendingCallPromise(connectId, pendingPromise);
66101
+ if (timedOut || completed)
66102
+ removePrePendingCallPromise === null || removePrePendingCallPromise === void 0 ? void 0 : removePrePendingCallPromise(connectId, pendingPromise);
66051
66103
  }
66052
66104
  if (timedOut) {
66053
66105
  Log.warn('pre pending call promise timed out before call method', { connectId });
@@ -66100,10 +66152,20 @@ const onCallDevice = (context, message, method) => __awaiter(void 0, void 0, voi
66100
66152
  const pollingId = pollingManager.start(connectId);
66101
66153
  const env = DataManager.getSettings('env');
66102
66154
  const connect = () => ensureConnected(context, method, connectId, pollingId, method.abortSignal);
66103
- device =
66104
- DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)
66105
- ? yield requestQueue.waitForTask(task, () => context.methodSynchronize(connect, 'webusb-discovery'))
66106
- : yield connect();
66155
+ if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
66156
+ const connectPromise = context.methodSynchronize(() => __awaiter(void 0, void 0, void 0, function* () {
66157
+ var _k;
66158
+ if ((_k = method.abortSignal) === null || _k === void 0 ? void 0 : _k.aborted) {
66159
+ throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.CallQueueActionCancelled);
66160
+ }
66161
+ return connect();
66162
+ }), 'webusb-discovery');
66163
+ connectPromise.catch(() => undefined);
66164
+ device = yield requestQueue.waitForTask(task, () => connectPromise);
66165
+ }
66166
+ else {
66167
+ device = yield connect();
66168
+ }
66107
66169
  if ((_e = method.abortSignal) === null || _e === void 0 ? void 0 : _e.aborted) {
66108
66170
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.CallQueueActionCancelled);
66109
66171
  }
@@ -66168,12 +66230,12 @@ const onCallDevice = (context, message, method) => __awaiter(void 0, void 0, voi
66168
66230
  return waitForPendingPromise((_a = method.connectId) !== null && _a !== void 0 ? _a : '', getPrePendingCallPromise, removePrePendingCallPromise);
66169
66231
  });
66170
66232
  const inner = () => __awaiter(void 0, void 0, void 0, function* () {
66171
- var _k, _l;
66233
+ var _l, _m;
66172
66234
  method.assertProtocolSupported(device.getProtocol(), device.getCurrentFirmwareType());
66173
66235
  protocolV2Operation = device.isProtocolV2();
66174
66236
  const versionRange = device.getCurrentMethodVersionRange(type => method.getVersionRange()[type]);
66175
- const currentFirmwareVersion = (_k = device.getCurrentFirmwareVersionString()) !== null && _k !== void 0 ? _k : '0.0.0';
66176
- const currentBleVersion = (_l = device.getCurrentBLEFirmwareVersionString()) !== null && _l !== void 0 ? _l : '0.0.0';
66237
+ const currentFirmwareVersion = (_l = device.getCurrentFirmwareVersionString()) !== null && _l !== void 0 ? _l : '0.0.0';
66238
+ const currentBleVersion = (_m = device.getCurrentBLEFirmwareVersionString()) !== null && _m !== void 0 ? _m : '0.0.0';
66177
66239
  const deviceFirmwareType = device.getCurrentFirmwareType();
66178
66240
  let newVersionStatus;
66179
66241
  if (device.features && !device.isProtocolV2()) {
@@ -66251,7 +66313,7 @@ const onCallDevice = (context, message, method) => __awaiter(void 0, void 0, voi
66251
66313
  }
66252
66314
  },
66253
66315
  prepare: () => __awaiter(void 0, void 0, void 0, function* () {
66254
- var _m, _o, _p, _q, _r, _s;
66316
+ var _o, _p, _q, _r, _s, _t;
66255
66317
  if (method.deviceId && method.checkDeviceId && !deviceIdCheckedDuringUnlockPreflight) {
66256
66318
  const isSameDeviceID = yield checkLiveDeviceId(device, method.deviceId);
66257
66319
  if (!isSameDeviceID) {
@@ -66271,7 +66333,7 @@ const onCallDevice = (context, message, method) => __awaiter(void 0, void 0, voi
66271
66333
  require: support.require,
66272
66334
  });
66273
66335
  }
66274
- const passphraseStateSafety = yield device.checkPassphraseStateSafety((_m = method.payload) === null || _m === void 0 ? void 0 : _m.passphraseState, (_o = method.payload) === null || _o === void 0 ? void 0 : _o.useEmptyPassphrase, (_p = method.payload) === null || _p === void 0 ? void 0 : _p.skipPassphraseCheck, resolveDeriveCardano(method), (_q = method.protocolV2UnlockContext) === null || _q === void 0 ? void 0 : _q.preflightMainPinSelected);
66336
+ const passphraseStateSafety = yield device.checkPassphraseStateSafety((_o = method.payload) === null || _o === void 0 ? void 0 : _o.passphraseState, (_p = method.payload) === null || _p === void 0 ? void 0 : _p.useEmptyPassphrase, (_q = method.payload) === null || _q === void 0 ? void 0 : _q.skipPassphraseCheck, resolveDeriveCardano(method), (_r = method.protocolV2UnlockContext) === null || _r === void 0 ? void 0 : _r.preflightMainPinSelected);
66275
66337
  checkPassphraseEnableState(method, device.features);
66276
66338
  if (!passphraseStateSafety) {
66277
66339
  DevicePool.clearDeviceCache(method.payload.connectId);
@@ -66289,7 +66351,7 @@ const onCallDevice = (context, message, method) => __awaiter(void 0, void 0, voi
66289
66351
  ? e
66290
66352
  : hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'open safety check failed.');
66291
66353
  }
66292
- (_s = (_r = method.device) === null || _r === void 0 ? void 0 : _r.commands) === null || _s === void 0 ? void 0 : _s.checkDisposed();
66354
+ (_t = (_s = method.device) === null || _s === void 0 ? void 0 : _s.commands) === null || _t === void 0 ? void 0 : _t.checkDisposed();
66293
66355
  }),
66294
66356
  });
66295
66357
  messageResponse = createResponseMessage(method.responseID, true, response);
@@ -66644,10 +66706,11 @@ const ensureConnected = (_context, method, connectId, pollingId, abortSignal) =>
66644
66706
  const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
66645
66707
  const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
66646
66708
  let timer = null;
66709
+ let lastInitializeError;
66647
66710
  Log.debug(`EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `);
66648
66711
  const poll = (time = POLL_INTERVAL_TIME) => __awaiter(void 0, void 0, void 0, function* () {
66649
66712
  return new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
66650
- var _t;
66713
+ var _u;
66651
66714
  const abort = () => {
66652
66715
  if (abortSignal && abortSignal.aborted) {
66653
66716
  if (timer) {
@@ -66676,8 +66739,10 @@ const ensureConnected = (_context, method, connectId, pollingId, abortSignal) =>
66676
66739
  Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
66677
66740
  try {
66678
66741
  yield initDeviceList(method);
66742
+ lastInitializeError = undefined;
66679
66743
  }
66680
66744
  catch (error) {
66745
+ lastInitializeError = error;
66681
66746
  Log.debug('device list error: ', error);
66682
66747
  if ([
66683
66748
  hdShared.HardwareErrorCode.BridgeNotInstalled,
@@ -66690,15 +66755,7 @@ const ensureConnected = (_context, method, connectId, pollingId, abortSignal) =>
66690
66755
  }
66691
66756
  if (error.errorCode === hdShared.HardwareErrorCode.TransportNotConfigured) {
66692
66757
  yield TransportManager.configure();
66693
- }
66694
- else {
66695
- const env = DataManager.getSettings('env');
66696
- if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
66697
- if (timer)
66698
- clearTimeout(timer);
66699
- reject(error);
66700
- return;
66701
- }
66758
+ lastInitializeError = undefined;
66702
66759
  }
66703
66760
  }
66704
66761
  if (abort()) {
@@ -66781,13 +66838,16 @@ const ensureConnected = (_context, method, connectId, pollingId, abortSignal) =>
66781
66838
  clearTimeout(timer);
66782
66839
  }
66783
66840
  Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
66784
- if (DataManager.isBrowserWebUsb(env) && !((_t = method.payload) === null || _t === void 0 ? void 0 : _t.skipWebDevicePrompt)) {
66841
+ const preserveWebUsbInitError = DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
66842
+ const needsPermissionPrompt = DataManager.isBrowserWebUsb(env) && !((_u = method.payload) === null || _u === void 0 ? void 0 : _u.skipWebDevicePrompt);
66843
+ const fallbackError = needsPermissionPrompt
66844
+ ? hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission)
66845
+ : hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound);
66846
+ const errorToReject = preserveWebUsbInitError && lastInitializeError ? lastInitializeError : fallbackError;
66847
+ if (needsPermissionPrompt && errorToReject === fallbackError) {
66785
66848
  postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
66786
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
66787
- }
66788
- else {
66789
- reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound));
66790
66849
  }
66850
+ reject(errorToReject);
66791
66851
  return;
66792
66852
  }
66793
66853
  if (abort()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.2-alpha.113",
3
+ "version": "1.2.2-alpha.114",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.2-alpha.113",
29
- "@onekeyfe/hd-transport": "1.2.2-alpha.113",
28
+ "@onekeyfe/hd-shared": "1.2.2-alpha.114",
29
+ "@onekeyfe/hd-transport": "1.2.2-alpha.114",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "buffer": "^6.0.3",
@@ -46,5 +46,5 @@
46
46
  "@types/w3c-web-usb": "^1.0.10",
47
47
  "@types/web-bluetooth": "^0.0.21"
48
48
  },
49
- "gitHead": "32601b82b280b9ec97161cb4206a15cf9cd72881"
49
+ "gitHead": "6490382eeeb6bda264b71c7e9bf96143840ee1cb"
50
50
  }
@@ -1,4 +1,4 @@
1
- import { canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
1
+ import { EDeviceType, canonicalizePro2BleAdvertisementName } from '@onekeyfe/hd-shared';
2
2
 
3
3
  import { BaseMethod } from './BaseMethod';
4
4
  import TransportManager from '../data-manager/TransportManager';
@@ -6,10 +6,46 @@ 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';
9
10
  import type DeviceConnector from '../device/DeviceConnector';
11
+ import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport';
12
+ import type { Device } from '../device/Device';
10
13
 
11
14
  const Log = getLogger(LoggerNames.DevicePool);
12
15
 
16
+ type RequestQueueLookup = {
17
+ getRequestTasksIdByConnectId: (connectId: string) => number[];
18
+ };
19
+
20
+ const isOwnedByActiveWebUsbRequest = (
21
+ descriptor: DeviceDescriptor,
22
+ requestQueue?: RequestQueueLookup
23
+ ) => {
24
+ if (!requestQueue) return false;
25
+ const keys = [descriptor.path, descriptor.id].filter(
26
+ (key): key is string => typeof key === 'string' && key.length > 0
27
+ );
28
+ return keys.some(key => requestQueue.getRequestTasksIdByConnectId(key).length > 0);
29
+ };
30
+
31
+ const toSearchDeviceFromDescriptor = (descriptor: DeviceDescriptor): SearchDevice => {
32
+ const connectId = descriptor.path || descriptor.id || null;
33
+ return {
34
+ connectId,
35
+ uuid: connectId ?? '',
36
+ serialNo: connectId,
37
+ deviceId: null,
38
+ deviceType: EDeviceType.Unknown,
39
+ name: descriptor.name || connectId || '',
40
+ commType: descriptor.commType,
41
+ };
42
+ };
43
+
44
+ const toSearchDevice = (device: Device): SearchDevice | null => {
45
+ const message = device.toMessageObject();
46
+ return message ? (message as SearchDevice) : null;
47
+ };
48
+
13
49
  export default class SearchDevices extends BaseMethod {
14
50
  connector?: DeviceConnector;
15
51
 
@@ -19,14 +55,22 @@ export default class SearchDevices extends BaseMethod {
19
55
  this.skipForceUpdateCheck = true;
20
56
  }
21
57
 
22
- async run() {
58
+ async run(): Promise<SearchDevice[]> {
23
59
  const env = DataManager.getSettings('env');
24
- const useCachedWebUsbDevices =
25
- (env === 'webusb' || env === 'desktop-webusb') &&
26
- (this.context?.requestQueue.getRequestTasksId().length ?? 0) > 0;
27
- // Core serializes WebUSB discovery with connection setup. A registered
28
- // business request owns the connection, including its initialization phase.
29
- if (!useCachedWebUsbDevices) await TransportManager.configure();
60
+ const isWebUsb = env === 'webusb' || env === 'desktop-webusb';
61
+ const requestQueue = this.context?.requestQueue;
62
+ const hasActiveWebUsbRequest = isWebUsb && (requestQueue?.getRequestTasksId().length ?? 0) > 0;
63
+ // Bring up WebUSB even when schema configuration is deferred while a
64
+ // business request owns the discovery lock. Failures here used to be
65
+ // swallowed by configure(); keep searchDevices resolving empty.
66
+ if (isWebUsb) {
67
+ try {
68
+ await TransportManager.ensureInitialized();
69
+ } catch (error) {
70
+ Log.debug('WebUSB bring-up unavailable', error);
71
+ }
72
+ }
73
+ if (!hasActiveWebUsbRequest) await TransportManager.configure();
30
74
  const deviceDiff = await this.connector?.enumerate();
31
75
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
32
76
 
@@ -35,7 +79,7 @@ export default class SearchDevices extends BaseMethod {
35
79
  * to avoid device pairing
36
80
  */
37
81
  if (DataManager.isBleConnect(env)) {
38
- const devices = [];
82
+ const devices: SearchDevice[] = [];
39
83
  const seenIds = new Set<string>();
40
84
 
41
85
  for (const device of devicesDescriptor) {
@@ -60,37 +104,41 @@ export default class SearchDevices extends BaseMethod {
60
104
  return devices;
61
105
  }
62
106
 
63
- if (useCachedWebUsbDevices) {
64
- return devicesDescriptor.flatMap(descriptor => {
65
- const device = DevicePool.getDeviceByPath(descriptor.path);
66
- return device?.features ? [device.toMessageObject()] : [];
67
- });
68
- }
69
-
70
- const deviceList = [];
107
+ const deviceList: SearchDevice[] = [];
71
108
  for (const descriptor of devicesDescriptor) {
72
- try {
73
- // Discovery is best effort. Browsers may retain WebUSB grants for devices that
74
- // are offline, busy, or not ready, so one descriptor must not abort the scan.
75
- const result = await DevicePool.getDevices([descriptor], descriptor.path, {
76
- // Discovery must actively identify the protocol instead of trusting a caller hint.
77
- connectProtocol: undefined,
78
- forceProtocolDetection: true,
79
- refreshRuntimeState: true,
80
- });
81
- deviceList.push(...result.deviceList);
82
- } catch (error) {
83
- const errorCode =
84
- error && typeof error === 'object' && 'errorCode' in error
85
- ? (error as { errorCode?: unknown }).errorCode
86
- : undefined;
87
- Log.debug('Skip unavailable device during search', {
88
- path: descriptor.path,
89
- ...(errorCode !== undefined ? { errorCode } : {}),
90
- });
109
+ if (hasActiveWebUsbRequest && isOwnedByActiveWebUsbRequest(descriptor, requestQueue)) {
110
+ const cached = DevicePool.getDeviceByPath(descriptor.path);
111
+ const message = cached?.features ? toSearchDevice(cached) : null;
112
+ // Do not probe a path an in-flight request already owns. A cache miss
113
+ // (passphrase switch / initSession) is still a connected device.
114
+ deviceList.push(message ?? toSearchDeviceFromDescriptor(descriptor));
115
+ } else {
116
+ try {
117
+ // Discovery is best effort. Browsers may retain WebUSB grants for devices that
118
+ // are offline, busy, or not ready, so one descriptor must not abort the scan.
119
+ const result = await DevicePool.getDevices([descriptor], descriptor.path, {
120
+ // Discovery must actively identify the protocol instead of trusting a caller hint.
121
+ connectProtocol: undefined,
122
+ forceProtocolDetection: true,
123
+ refreshRuntimeState: true,
124
+ });
125
+ for (const device of result.deviceList) {
126
+ const message = toSearchDevice(device);
127
+ if (message) deviceList.push(message);
128
+ }
129
+ } catch (error) {
130
+ const errorCode =
131
+ error && typeof error === 'object' && 'errorCode' in error
132
+ ? (error as { errorCode?: unknown }).errorCode
133
+ : undefined;
134
+ Log.debug('Skip unavailable device during search', {
135
+ path: descriptor.path,
136
+ ...(errorCode !== undefined ? { errorCode } : {}),
137
+ });
138
+ }
91
139
  }
92
140
  }
93
141
 
94
- return deviceList.map(device => device.toMessageObject());
142
+ return deviceList;
95
143
  }
96
144
  }
package/src/core/index.ts CHANGED
@@ -322,6 +322,7 @@ const waitForPendingPromise = async (
322
322
  Log.debug('pre pending call promise before call method, wait for it');
323
323
  let timer: ReturnType<typeof setTimeout> | undefined;
324
324
  let timedOut = false;
325
+ let completed = false;
325
326
  try {
326
327
  await Promise.race([
327
328
  pendingPromise,
@@ -332,11 +333,12 @@ const waitForPendingPromise = async (
332
333
  }, PRE_PENDING_CALL_TIMEOUT_MS);
333
334
  }),
334
335
  ]);
336
+ completed = !timedOut;
335
337
  } finally {
336
338
  if (timer) clearTimeout(timer);
337
- // Match the legacy behavior: a stuck cleanup must not permanently poison
338
- // this connectId. The next request will reacquire the device if needed.
339
- removePrePendingCallPromise?.(connectId, pendingPromise);
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);
340
342
  }
341
343
  if (timedOut) {
342
344
  Log.warn('pre pending call promise timed out before call method', { connectId });
@@ -422,12 +424,21 @@ const onCallDevice = async (
422
424
  ensureConnected(context, method, connectId, pollingId, method.abortSignal);
423
425
  // Discovery may acquire USB endpoints. Finish it before initializing a public
424
426
  // request; once registered, that request makes discovery use cached state only.
425
- device =
426
- DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)
427
- ? await requestQueue.waitForTask(task, () =>
428
- context.methodSynchronize(connect, 'webusb-discovery')
429
- )
430
- : await connect();
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
+ }
431
442
  if (method.abortSignal?.aborted) {
432
443
  throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
433
444
  }
@@ -1216,6 +1227,7 @@ const ensureConnected = async (
1216
1227
  const POLL_INTERVAL_TIME = (method.payload && method.payload.pollIntervalTime) || 1000;
1217
1228
  const TIME_OUT = (method.payload && method.payload.timeout) || 10000;
1218
1229
  let timer: ReturnType<typeof setTimeout> | null = null;
1230
+ let lastInitializeError: any;
1219
1231
  Log.debug(
1220
1232
  `EnsureConnected function start, MAX_RETRY_COUNT=${MAX_RETRY_COUNT}, POLL_INTERVAL_TIME=${POLL_INTERVAL_TIME} `
1221
1233
  );
@@ -1256,7 +1268,9 @@ const ensureConnected = async (
1256
1268
  Log.debug('EnsureConnected function try count: ', tryCount, ' poll interval time: ', time);
1257
1269
  try {
1258
1270
  await initDeviceList(method);
1271
+ lastInitializeError = undefined;
1259
1272
  } catch (error) {
1273
+ lastInitializeError = error;
1260
1274
  Log.debug('device list error: ', error);
1261
1275
  if (
1262
1276
  [
@@ -1271,15 +1285,7 @@ const ensureConnected = async (
1271
1285
  }
1272
1286
  if (error.errorCode === HardwareErrorCode.TransportNotConfigured) {
1273
1287
  await TransportManager.configure();
1274
- } else {
1275
- const env = DataManager.getSettings('env');
1276
- if (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env)) {
1277
- // Enumeration succeeded far enough to attempt initialization. Do not
1278
- // replace its failure with a lookup against the now-empty DeviceList.
1279
- if (timer) clearTimeout(timer);
1280
- reject(error);
1281
- return;
1282
- }
1288
+ lastInitializeError = undefined;
1283
1289
  }
1284
1290
  }
1285
1291
 
@@ -1377,14 +1383,22 @@ const ensureConnected = async (
1377
1383
  clearTimeout(timer);
1378
1384
  }
1379
1385
  Log.debug('EnsureConnected get to max try count, will return: ', tryCount);
1380
- // Browser WebUSB needs permission prompt, desktop WebUSB doesn't
1381
- // skipWebDevicePrompt can override this behavior for special cases
1382
- if (DataManager.isBrowserWebUsb(env) && !method.payload?.skipWebDevicePrompt) {
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) {
1383
1399
  postMessage(createUiMessage(UI_REQUEST.WEB_DEVICE_PROMPT_ACCESS_PERMISSION));
1384
- reject(ERRORS.TypedError(HardwareErrorCode.WebDeviceNotFoundOrNeedsPermission));
1385
- } else {
1386
- reject(ERRORS.TypedError(HardwareErrorCode.DeviceNotFound));
1387
1400
  }
1401
+ reject(errorToReject);
1388
1402
  return;
1389
1403
  }
1390
1404
 
@@ -33,6 +33,8 @@ export default class TransportManager {
33
33
 
34
34
  static reactNativeInit = false;
35
35
 
36
+ static webUsbInit = false;
37
+
36
38
  static protocolV1MessageSchema: ProtocolV1MessageSchema = 'v1CurrentSchema';
37
39
 
38
40
  static plugin: LowlevelTransportSharedPlugin | null = null;
@@ -42,6 +44,17 @@ export default class TransportManager {
42
44
  this.defaultMessages = DataManager.getProtobufMessages();
43
45
  this.currentMessages = this.defaultMessages;
44
46
  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;
45
58
  }
46
59
 
47
60
  static async configure() {
@@ -69,9 +82,7 @@ export default class TransportManager {
69
82
  } else if (env === 'desktop-web-ble') {
70
83
  await this.transport.init(WebBleLogger, DevicePool.emitter);
71
84
  } else if (env === 'webusb' || env === 'desktop-webusb') {
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);
85
+ await this.ensureInitialized();
75
86
  } else {
76
87
  await this.transport.init(HttpLogger);
77
88
  }