@onekeyfe/hd-transport-react-native 1.2.2-alpha.104 → 1.2.2-alpha.106
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.
- package/README.md +1 -1
- package/dist/bleNativeDisconnect.d.ts +3 -0
- package/dist/bleNativeDisconnect.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -50
- package/package.json +5 -5
- package/src/__tests__/bleNativeDisconnect.test.ts +33 -0
- package/src/__tests__/connectTimeout.test.ts +110 -2
- package/src/__tests__/protocolV2Link.test.ts +112 -2
- package/src/__tests__/staleCallTimeout.test.ts +34 -5
- package/src/bleNativeDisconnect.ts +40 -0
- package/src/index.ts +187 -54
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
-
import { BleErrorCode } from 'react-native-ble-plx';
|
|
3
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { BleErrorCode, BleManager } from 'react-native-ble-plx';
|
|
3
|
+
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
4
4
|
|
|
5
5
|
import ReactNativeBleTransport, {
|
|
6
6
|
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
@@ -131,6 +131,59 @@ describe('BLE connect timeout', () => {
|
|
|
131
131
|
jest.restoreAllMocks();
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
test('waits for singleton destruction before creating the next manager', async () => {
|
|
135
|
+
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
136
|
+
const destruction = createDeferred<void>();
|
|
137
|
+
Object.assign(bleManager, { destroy: jest.fn(() => destruction.promise) });
|
|
138
|
+
const createManager = jest.mocked(BleManager);
|
|
139
|
+
createManager.mockClear();
|
|
140
|
+
(transport as unknown as { resetPlxManager(): void }).resetPlxManager();
|
|
141
|
+
const first = transport.getPlxManager();
|
|
142
|
+
const second = transport.getPlxManager();
|
|
143
|
+
await flush();
|
|
144
|
+
expect(createManager).not.toHaveBeenCalled();
|
|
145
|
+
destruction.resolve();
|
|
146
|
+
expect(await first).toBe(await second);
|
|
147
|
+
expect(createManager).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('does not reuse a manager after asynchronous destruction fails', async () => {
|
|
151
|
+
const { bleManager } = createHarness(() => Promise.resolve());
|
|
152
|
+
let transport!: ReactNativeBleTransport;
|
|
153
|
+
jest.isolateModules(() => {
|
|
154
|
+
const { default: Transport } = jest.requireActual<typeof import('../index')>('../index');
|
|
155
|
+
transport = new Transport({});
|
|
156
|
+
});
|
|
157
|
+
transport.blePlxManager = bleManager as never;
|
|
158
|
+
const destruction = createDeferred<void>();
|
|
159
|
+
Object.assign(bleManager, { destroy: jest.fn(() => destruction.promise) });
|
|
160
|
+
(transport as unknown as { resetPlxManager(): void }).resetPlxManager();
|
|
161
|
+
const result = transport.getPlxManager();
|
|
162
|
+
const failure = expect(result).rejects.toMatchObject({
|
|
163
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
164
|
+
});
|
|
165
|
+
destruction.reject(new Error('Native destruction failed'));
|
|
166
|
+
await failure;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('bounds reset waiting without letting a new transport bypass unfinished destruction', async () => {
|
|
170
|
+
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
171
|
+
const destruction = createDeferred<void>();
|
|
172
|
+
Object.assign(bleManager, { destroy: jest.fn(() => destruction.promise) });
|
|
173
|
+
const createManager = jest.mocked(BleManager);
|
|
174
|
+
createManager.mockClear();
|
|
175
|
+
(transport as unknown as { resetPlxManager(): void }).resetPlxManager();
|
|
176
|
+
const nextTransport = new ReactNativeBleTransport({});
|
|
177
|
+
const result = nextTransport.getPlxManager().catch(error => error);
|
|
178
|
+
await flush();
|
|
179
|
+
jest.advanceTimersByTime(BLE_CONNECT_TIMEOUT_MS);
|
|
180
|
+
await expect(result).resolves.toMatchObject({ errorCode: HardwareErrorCode.PollingTimeout });
|
|
181
|
+
expect(createManager).not.toHaveBeenCalled();
|
|
182
|
+
destruction.resolve();
|
|
183
|
+
await nextTransport.getPlxManager();
|
|
184
|
+
expect(createManager).toHaveBeenCalledTimes(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
134
187
|
test('a native connect that never settles is bounded instead of blocking forever', async () => {
|
|
135
188
|
// iOS applies its own connect timeout on a serial queue; when that queue is busy
|
|
136
189
|
// the timeout never fires and acquire() blocks until the app-level 60s timeout.
|
|
@@ -155,6 +208,61 @@ describe('BLE connect timeout', () => {
|
|
|
155
208
|
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
156
209
|
});
|
|
157
210
|
|
|
211
|
+
test('stop drains its scan and removes the timer before another transport scans', async () => {
|
|
212
|
+
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
213
|
+
const nativeStop = createDeferred<void>();
|
|
214
|
+
bleManager.stopDeviceScan.mockImplementation(() => nativeStop.promise);
|
|
215
|
+
const scanned = transport.enumerate().catch(error => error);
|
|
216
|
+
await flush();
|
|
217
|
+
await flush();
|
|
218
|
+
expect(bleManager.startDeviceScan).toHaveBeenCalledTimes(1);
|
|
219
|
+
const stopping = transport.stop();
|
|
220
|
+
expect(transport.stop()).toBe(stopping);
|
|
221
|
+
let stopped = false;
|
|
222
|
+
stopping.then(() => {
|
|
223
|
+
stopped = true;
|
|
224
|
+
});
|
|
225
|
+
await flush();
|
|
226
|
+
expect(stopped).toBe(false);
|
|
227
|
+
nativeStop.resolve();
|
|
228
|
+
await stopping;
|
|
229
|
+
await expect(scanned).resolves.toMatchObject({
|
|
230
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
231
|
+
});
|
|
232
|
+
jest.advanceTimersByTime(transport.scanTimeout);
|
|
233
|
+
await flush();
|
|
234
|
+
expect(bleManager.stopDeviceScan).toHaveBeenCalledTimes(1);
|
|
235
|
+
await expect(transport.getPlxManager()).rejects.toMatchObject({
|
|
236
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('stop rejects a pending read and waits for native disconnection without destroying the shared manager', async () => {
|
|
241
|
+
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
242
|
+
const nativeDisconnect = createDeferred<void>();
|
|
243
|
+
bleManager.cancelDeviceConnection.mockImplementation(() => nativeDisconnect.promise);
|
|
244
|
+
const destroy = jest.fn();
|
|
245
|
+
Object.assign(bleManager, { destroy });
|
|
246
|
+
const read = createDeferred<void>();
|
|
247
|
+
transport.runPromise = read;
|
|
248
|
+
Object.assign(transport, { runPromiseDeviceId: UUID });
|
|
249
|
+
const readResult = read.promise.catch(error => error);
|
|
250
|
+
let stopped = false;
|
|
251
|
+
const stopping = transport.stop().then(() => {
|
|
252
|
+
stopped = true;
|
|
253
|
+
});
|
|
254
|
+
await flush();
|
|
255
|
+
await expect(readResult).resolves.toMatchObject({
|
|
256
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
257
|
+
});
|
|
258
|
+
expect(stopped).toBe(false);
|
|
259
|
+
nativeDisconnect.resolve();
|
|
260
|
+
await advanceUntil(() => stopped, 1000);
|
|
261
|
+
await stopping;
|
|
262
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
263
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
264
|
+
});
|
|
265
|
+
|
|
158
266
|
test('the connect budget leaves generous headroom over a healthy connect', () => {
|
|
159
267
|
// Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
|
|
160
268
|
// fires when the native timeout itself fails to.
|
|
@@ -4,7 +4,7 @@ import transportPackage, {
|
|
|
4
4
|
ProtocolV2,
|
|
5
5
|
TRANSPORT_EVENT,
|
|
6
6
|
} from '@onekeyfe/hd-transport';
|
|
7
|
-
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
7
|
+
import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
8
8
|
|
|
9
9
|
import ReactNativeBleTransport, {
|
|
10
10
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS,
|
|
@@ -28,7 +28,7 @@ jest.mock('react-native-ble-plx', () => ({
|
|
|
28
28
|
BleError: class BleError extends Error {},
|
|
29
29
|
BleErrorCode: {
|
|
30
30
|
DeviceAlreadyConnected: 203,
|
|
31
|
-
DeviceDisconnected:
|
|
31
|
+
DeviceDisconnected: 201,
|
|
32
32
|
DeviceMTUChangeFailed: 206,
|
|
33
33
|
OperationCancelled: 2,
|
|
34
34
|
CharacteristicNotFound: 404,
|
|
@@ -830,6 +830,116 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
830
830
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS + 5_000
|
|
831
831
|
);
|
|
832
832
|
|
|
833
|
+
test('preserves the unpaired error when iOS disconnects during the first V1 probe', async () => {
|
|
834
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
835
|
+
writeCharacteristic.writeWithResponse.mockRejectedValueOnce({
|
|
836
|
+
errorCode: 201,
|
|
837
|
+
iosErrorCode: 7,
|
|
838
|
+
reason: 'The specified device has disconnected from us.',
|
|
839
|
+
});
|
|
840
|
+
const probeProtocolV2 = jest.spyOn(transport as any, 'probeProtocolV2');
|
|
841
|
+
|
|
842
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
843
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
expect(probeProtocolV2).not.toHaveBeenCalled();
|
|
847
|
+
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
848
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test('preserves a native iOS disconnect during an expected Protocol V2 probe', async () => {
|
|
852
|
+
const { transport, uuid, writeCharacteristic, bleManager } = createHarness({
|
|
853
|
+
deviceName: 'Neo Test',
|
|
854
|
+
});
|
|
855
|
+
const nativeDisconnect = {
|
|
856
|
+
errorCode: 201,
|
|
857
|
+
iosErrorCode: 7,
|
|
858
|
+
reason: 'The specified device has disconnected from us.',
|
|
859
|
+
};
|
|
860
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce(nativeDisconnect);
|
|
861
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
862
|
+
|
|
863
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
864
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
868
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
869
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
870
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
test('preserves a native iOS disconnect during a V2-first probe without falling back to V1', async () => {
|
|
874
|
+
const { transport, uuid, writeCharacteristic } = createHarness({ deviceName: 'Neo Test' });
|
|
875
|
+
writeCharacteristic.writeWithoutResponse.mockRejectedValueOnce({
|
|
876
|
+
errorCode: 201,
|
|
877
|
+
iosErrorCode: 7,
|
|
878
|
+
reason: 'The specified device has disconnected from us.',
|
|
879
|
+
});
|
|
880
|
+
const probeProtocolV1 = jest.spyOn(transport as any, 'probeProtocolV1');
|
|
881
|
+
|
|
882
|
+
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toMatchObject({
|
|
883
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
expect(probeProtocolV1).not.toHaveBeenCalled();
|
|
887
|
+
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
888
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test.each(['V1', 'V2'] as const)(
|
|
892
|
+
'preserves terminal BLE failures from the %s probe without trying another protocol',
|
|
893
|
+
async protocol => {
|
|
894
|
+
for (const errorCode of [
|
|
895
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
896
|
+
HardwareErrorCode.BleDeviceBondedCanceled,
|
|
897
|
+
HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
898
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
899
|
+
HardwareErrorCode.BleCharacteristicNotifyError,
|
|
900
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
|
|
901
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
902
|
+
]) {
|
|
903
|
+
const { transport, uuid } = createHarness();
|
|
904
|
+
const error = ERRORS.TypedError(errorCode);
|
|
905
|
+
const call = jest
|
|
906
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
907
|
+
.mockRejectedValue(error);
|
|
908
|
+
const otherProbe = jest.spyOn(
|
|
909
|
+
transport as any,
|
|
910
|
+
protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1'
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).rejects.toBe(error);
|
|
914
|
+
|
|
915
|
+
expect(call).toHaveBeenCalledTimes(1);
|
|
916
|
+
expect(otherProbe).not.toHaveBeenCalled();
|
|
917
|
+
expect(transport.getProtocolType(uuid)).toBeUndefined();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
test.each(['V1', 'V2'] as const)(
|
|
923
|
+
'still falls back after a silent %s probe timeout',
|
|
924
|
+
async protocol => {
|
|
925
|
+
const { transport, uuid } = createHarness();
|
|
926
|
+
jest
|
|
927
|
+
.spyOn(transport as any, protocol === 'V1' ? 'callProtocolV1' : 'callProtocolV2')
|
|
928
|
+
.mockRejectedValue(ERRORS.TypedError(HardwareErrorCode.BleTimeoutError));
|
|
929
|
+
const otherProbe = jest
|
|
930
|
+
.spyOn(transport as any, protocol === 'V1' ? 'probeProtocolV2' : 'probeProtocolV1')
|
|
931
|
+
.mockResolvedValue(true);
|
|
932
|
+
|
|
933
|
+
await expect(transport.acquire({ uuid, protocolHint: protocol })).resolves.toEqual({
|
|
934
|
+
uuid,
|
|
935
|
+
protocolType: protocol === 'V1' ? 'V2' : 'V1',
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
expect(otherProbe).toHaveBeenCalledTimes(1);
|
|
939
|
+
await transport.release(uuid, true);
|
|
940
|
+
}
|
|
941
|
+
);
|
|
942
|
+
|
|
833
943
|
test('falls back to the other active probe on iOS when protocol metadata is absent', async () => {
|
|
834
944
|
const { transport, uuid } = createHarness({ deviceName: 'OneKey' });
|
|
835
945
|
const probeProtocolV1 = jest
|
|
@@ -56,6 +56,34 @@ function createHarness() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
describe('Protocol V1 stale call timeout', () => {
|
|
59
|
+
test('settles a cancelled read and waits for native teardown before completing cancel', async () => {
|
|
60
|
+
const { t, disconnectSpy } = createHarness();
|
|
61
|
+
let finishDisconnect!: () => void;
|
|
62
|
+
disconnectSpy.mockImplementationOnce(
|
|
63
|
+
() =>
|
|
64
|
+
new Promise<void>(resolve => {
|
|
65
|
+
finishDisconnect = resolve;
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
69
|
+
const result = call.catch(error => error);
|
|
70
|
+
await flush();
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
const cleanup = t.cancel().then(() => {
|
|
73
|
+
cancelled = true;
|
|
74
|
+
});
|
|
75
|
+
await flush();
|
|
76
|
+
expect(await result).toMatchObject({ errorCode: HardwareErrorCode.CallQueueActionCancelled });
|
|
77
|
+
expect(t.runPromise).toBeNull();
|
|
78
|
+
expect(disconnectSpy).toHaveBeenCalledWith(UUID);
|
|
79
|
+
expect(cancelled).toBe(false);
|
|
80
|
+
finishDisconnect();
|
|
81
|
+
await cleanup;
|
|
82
|
+
jest.advanceTimersByTime(25000);
|
|
83
|
+
await flush();
|
|
84
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
59
87
|
beforeAll(() => {
|
|
60
88
|
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
61
89
|
});
|
|
@@ -172,24 +200,25 @@ describe('Protocol V1 stale call timeout', () => {
|
|
|
172
200
|
expect(secondErrors).toHaveLength(1);
|
|
173
201
|
});
|
|
174
202
|
|
|
175
|
-
test('
|
|
203
|
+
test('a cancelled read cannot disconnect a later transport when its old deadline passes', async () => {
|
|
176
204
|
const { t, disconnectSpy } = createHarness();
|
|
177
205
|
|
|
178
|
-
// cancel() nulls the ownership slot without settling the deferred, so the
|
|
179
|
-
// call's response timer stays armed (reachable via DeviceCommands.dispose).
|
|
180
206
|
const errors: Array<{ errorCode?: unknown }> = [];
|
|
181
207
|
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
182
208
|
p.catch(e => errors.push(e));
|
|
183
209
|
await flush();
|
|
184
210
|
|
|
185
|
-
t.cancel();
|
|
211
|
+
await t.cancel();
|
|
212
|
+
await flush();
|
|
213
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
214
|
+
disconnectSpy.mockClear();
|
|
186
215
|
|
|
187
216
|
jest.advanceTimersByTime(5000);
|
|
188
217
|
await flush();
|
|
189
218
|
|
|
190
219
|
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
191
220
|
expect(errors).toHaveLength(1);
|
|
192
|
-
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.
|
|
221
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.CallQueueActionCancelled);
|
|
193
222
|
});
|
|
194
223
|
|
|
195
224
|
test('timeout on the active call still disconnects the transport', async () => {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
const BLE_PLX_DEVICE_DISCONNECTED = 201;
|
|
4
|
+
const IOS_PERIPHERAL_DISCONNECTED = 7;
|
|
5
|
+
|
|
6
|
+
type NativeBleErrorFields = {
|
|
7
|
+
errorCode?: unknown;
|
|
8
|
+
iosErrorCode?: unknown;
|
|
9
|
+
reason?: unknown;
|
|
10
|
+
message?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const nativeErrorText = (error: NativeBleErrorFields) =>
|
|
14
|
+
[error.reason, error.message]
|
|
15
|
+
.filter((value): value is string => typeof value === 'string')
|
|
16
|
+
.join(' ');
|
|
17
|
+
|
|
18
|
+
export const isNativeBleDisconnectError = (error: unknown): boolean => {
|
|
19
|
+
if (!error || typeof error !== 'object') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const nativeError = error as NativeBleErrorFields;
|
|
24
|
+
return (
|
|
25
|
+
nativeError.errorCode === BLE_PLX_DEVICE_DISCONNECTED ||
|
|
26
|
+
nativeError.iosErrorCode === IOS_PERIPHERAL_DISCONNECTED
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const toBleDisconnectHardwareError = (error: unknown) => {
|
|
31
|
+
if ((error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceDisconnected) {
|
|
32
|
+
return error as Error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nativeError = (error ?? {}) as NativeBleErrorFields;
|
|
36
|
+
return ERRORS.TypedError(
|
|
37
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
38
|
+
nativeErrorText(nativeError) || undefined
|
|
39
|
+
);
|
|
40
|
+
};
|