@onekeyfe/hd-transport-react-native 1.2.0-alpha.6 → 1.2.0-alpha.63

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,211 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import ReactNativeBleTransport from '../index';
4
+
5
+ import messages from '@onekeyfe/hd-transport/messages.json';
6
+
7
+ jest.mock(
8
+ 'react-native',
9
+ () => ({
10
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
11
+ PermissionsAndroid: {
12
+ PERMISSIONS: {},
13
+ RESULTS: {},
14
+ request: jest.fn(),
15
+ requestMultiple: jest.fn(),
16
+ },
17
+ }),
18
+ { virtual: true }
19
+ );
20
+
21
+ jest.mock('react-native-ble-plx', () => ({
22
+ BleATTErrorCode: { InvalidHandle: 1 },
23
+ BleError: Error,
24
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
25
+ BleManager: jest.fn(),
26
+ ScanMode: { LowLatency: 2 },
27
+ }));
28
+
29
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
30
+ __esModule: true,
31
+ default: {
32
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
33
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
34
+ pairDevice: jest.fn(() => Promise.resolve()),
35
+ },
36
+ }));
37
+
38
+ const UUID = 'stale-timeout-device';
39
+
40
+ const flush = () =>
41
+ new Promise(resolve => {
42
+ setImmediate(resolve);
43
+ });
44
+
45
+ function createHarness() {
46
+ const t = new ReactNativeBleTransport({});
47
+ t.configure(messages);
48
+ (t as any).deviceProtocol.set(UUID, 'V1');
49
+ const writeWithoutResponse = jest.fn(() => Promise.resolve());
50
+ const fakeBleTransport = {
51
+ writeCharacteristic: { writeWithoutResponse },
52
+ writeWithRetry: jest.fn(() => Promise.resolve()),
53
+ };
54
+ (t as any).getCachedTransport = () => fakeBleTransport;
55
+ const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
56
+ return { t, disconnectSpy, writeWithoutResponse };
57
+ }
58
+
59
+ describe('Protocol V1 stale call timeout', () => {
60
+ beforeAll(() => {
61
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
62
+ });
63
+
64
+ afterAll(() => {
65
+ jest.useRealTimers();
66
+ });
67
+
68
+ afterEach(() => {
69
+ jest.clearAllTimers();
70
+ jest.restoreAllMocks();
71
+ });
72
+
73
+ test('superseded Initialize timeout does not tear down the shared transport', async () => {
74
+ const { t, disconnectSpy } = createHarness();
75
+
76
+ // Initialize #1: written while the device reboots, never answered.
77
+ const firstErrors: unknown[] = [];
78
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
79
+ first.catch(e => firstErrors.push(e));
80
+ await flush();
81
+
82
+ // Initialize #2 (forceRun) supersedes #1 three seconds later.
83
+ jest.advanceTimersByTime(3000);
84
+ const secondErrors: unknown[] = [];
85
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
86
+ second.catch(e => secondErrors.push(e));
87
+ await flush();
88
+
89
+ // The device answers Initialize #2 (settle its deferred at the transport seam).
90
+ expect(t.runPromise).not.toBeNull();
91
+ t.runPromise?.reject(new Error('settled by device response'));
92
+ await flush();
93
+
94
+ // FirmwareUpload is now the active call on the same transport, awaiting its response.
95
+ const uploadErrors: unknown[] = [];
96
+ const upload = t.call(UUID, 'FirmwareUpload', { payload: Buffer.alloc(300) });
97
+ upload.catch(e => uploadErrors.push(e));
98
+ await flush();
99
+ jest.advanceTimersByTime(100); // firmware upload flush delay
100
+ await flush();
101
+
102
+ // Initialize #1's 25s response timer elapses while the upload is in flight.
103
+ jest.advanceTimersByTime(25000);
104
+ await flush();
105
+
106
+ expect(disconnectSpy).not.toHaveBeenCalled();
107
+ expect(firstErrors).toHaveLength(1);
108
+ expect(uploadErrors).toHaveLength(0);
109
+ });
110
+
111
+ test('forceRun supersede settles the previous pending call immediately', async () => {
112
+ const { t } = createHarness();
113
+
114
+ const firstErrors: Array<{ errorCode?: unknown }> = [];
115
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
116
+ first.catch(e => firstErrors.push(e));
117
+ await flush();
118
+
119
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
120
+ second.catch(() => undefined);
121
+ await flush();
122
+
123
+ expect(firstErrors).toHaveLength(1);
124
+ expect(firstErrors[0]?.errorCode).toBe(HardwareErrorCode.BleForceCleanRunPromise);
125
+
126
+ t.runPromise?.reject(new Error('settle second call'));
127
+ await flush();
128
+ });
129
+
130
+ test('late write failure of a superseded call keeps the successor as owner', async () => {
131
+ const { t, disconnectSpy } = createHarness();
132
+
133
+ // First call's write hangs; its promise is controlled by the test.
134
+ let rejectFirstWrite: ((e: Error) => void) | undefined;
135
+ const fakeBleTransport = {
136
+ writeCharacteristic: {
137
+ writeWithoutResponse: jest
138
+ .fn()
139
+ .mockImplementationOnce(
140
+ () =>
141
+ new Promise((_resolve, reject) => {
142
+ rejectFirstWrite = reject;
143
+ })
144
+ )
145
+ .mockImplementation(() => Promise.resolve()),
146
+ },
147
+ writeWithRetry: jest.fn(() => Promise.resolve()),
148
+ };
149
+ (t as any).getCachedTransport = () => fakeBleTransport;
150
+
151
+ const firstErrors: unknown[] = [];
152
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
153
+ first.catch(e => firstErrors.push(e));
154
+ await flush();
155
+
156
+ // forceRun successor takes ownership while the first call is stuck writing.
157
+ const secondErrors: unknown[] = [];
158
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 5000 });
159
+ second.catch(e => secondErrors.push(e));
160
+ await flush();
161
+
162
+ // The first call's write now fails late; it must not clear the successor's slot.
163
+ rejectFirstWrite?.(new Error('late write failure'));
164
+ await flush();
165
+
166
+ expect(t.runPromise).not.toBeNull();
167
+
168
+ // The successor's genuine timeout must still tear the connection down.
169
+ jest.advanceTimersByTime(5000);
170
+ await flush();
171
+
172
+ expect(disconnectSpy).toHaveBeenCalledTimes(1);
173
+ expect(secondErrors).toHaveLength(1);
174
+ });
175
+
176
+ test('orphan timer left behind by cancel() must not disconnect the transport', async () => {
177
+ const { t, disconnectSpy } = createHarness();
178
+
179
+ // cancel() nulls the ownership slot without settling the deferred, so the
180
+ // call's response timer stays armed (reachable via DeviceCommands.dispose).
181
+ const errors: Array<{ errorCode?: unknown }> = [];
182
+ const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
183
+ p.catch(e => errors.push(e));
184
+ await flush();
185
+
186
+ t.cancel();
187
+
188
+ jest.advanceTimersByTime(5000);
189
+ await flush();
190
+
191
+ expect(disconnectSpy).not.toHaveBeenCalled();
192
+ expect(errors).toHaveLength(1);
193
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
194
+ });
195
+
196
+ test('timeout on the active call still disconnects the transport', async () => {
197
+ const { t, disconnectSpy } = createHarness();
198
+
199
+ const errors: Array<{ errorCode?: unknown }> = [];
200
+ const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
201
+ p.catch(e => errors.push(e));
202
+ await flush();
203
+
204
+ jest.advanceTimersByTime(5000);
205
+ await flush();
206
+
207
+ expect(disconnectSpy).toHaveBeenCalledTimes(1);
208
+ expect(errors).toHaveLength(1);
209
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
210
+ });
211
+ });
@@ -0,0 +1,306 @@
1
+ import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import ReactNativeBleTransport, {
4
+ BLE_WRITE_PACKET_TIMEOUT_MS,
5
+ BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD,
6
+ } from '../index';
7
+
8
+ import messages from '@onekeyfe/hd-transport/messages.json';
9
+
10
+ jest.mock(
11
+ 'react-native',
12
+ () => ({
13
+ Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
14
+ PermissionsAndroid: {
15
+ PERMISSIONS: {},
16
+ RESULTS: {},
17
+ request: jest.fn(),
18
+ requestMultiple: jest.fn(),
19
+ },
20
+ }),
21
+ { virtual: true }
22
+ );
23
+
24
+ jest.mock('react-native-ble-plx', () => ({
25
+ BleATTErrorCode: { InvalidHandle: 1 },
26
+ BleError: Error,
27
+ BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
28
+ BleManager: jest.fn(),
29
+ ScanMode: { LowLatency: 2 },
30
+ }));
31
+
32
+ jest.mock('@onekeyfe/react-native-ble-utils', () => ({
33
+ __esModule: true,
34
+ default: {
35
+ getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
36
+ getBondedPeripherals: jest.fn(() => Promise.resolve([])),
37
+ pairDevice: jest.fn(() => Promise.resolve()),
38
+ },
39
+ }));
40
+
41
+ const UUID = 'wedged-write-device';
42
+
43
+ const flush = () =>
44
+ new Promise(resolve => {
45
+ setImmediate(resolve);
46
+ });
47
+
48
+ /** Drive fake timers forward in slices until `settled()` reports done. */
49
+ async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 500) {
50
+ for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
51
+ jest.advanceTimersByTime(stepMs);
52
+ // eslint-disable-next-line no-await-in-loop
53
+ await flush();
54
+ if (settled()) return;
55
+ }
56
+ throw new Error(`fake timers exhausted after ${totalMs}ms before the call settled`);
57
+ }
58
+
59
+ /** Drive fake timers forward without requiring the work to settle. */
60
+ async function drain(totalMs: number, stepMs = 500) {
61
+ for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
62
+ jest.advanceTimersByTime(stepMs);
63
+ // eslint-disable-next-line no-await-in-loop
64
+ await flush();
65
+ }
66
+ }
67
+
68
+ function createHarness(writeImpl: () => Promise<void>) {
69
+ const t = new ReactNativeBleTransport({});
70
+ t.configure(messages);
71
+ (t as any).deviceProtocol.set(UUID, 'V1');
72
+ const writeWithoutResponse = jest.fn(writeImpl);
73
+ const fakeBleTransport = {
74
+ writeCharacteristic: { writeWithoutResponse },
75
+ writeWithRetry: jest.fn(writeImpl),
76
+ };
77
+ (t as any).getCachedTransport = () => fakeBleTransport;
78
+ const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
79
+ return { t, disconnectSpy, writeWithoutResponse };
80
+ }
81
+
82
+ describe('BLE packet write timeout', () => {
83
+ beforeAll(() => {
84
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
85
+ });
86
+
87
+ afterAll(() => {
88
+ jest.useRealTimers();
89
+ });
90
+
91
+ afterEach(() => {
92
+ jest.clearAllTimers();
93
+ jest.restoreAllMocks();
94
+ });
95
+
96
+ test('a never-settling write rejects instead of hanging the call forever', async () => {
97
+ // iOS never resolves writeWithoutResponse when the peripheral stops reporting
98
+ // "ready to send"; without a bounded write the response timeout is never armed.
99
+ const { t } = createHarness(
100
+ () =>
101
+ new Promise<void>(() => {
102
+ // never settles
103
+ })
104
+ );
105
+
106
+ const errors: Array<{ errorCode?: unknown }> = [];
107
+ let settled = false;
108
+ const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
109
+ call.catch(e => {
110
+ errors.push(e);
111
+ settled = true;
112
+ });
113
+ await flush();
114
+
115
+ await advanceUntil(() => settled, 30000);
116
+
117
+ expect(errors).toHaveLength(1);
118
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleWriteCharacteristicError);
119
+ });
120
+
121
+ test('a wedged write tears the BLE link down so the next call reconnects', async () => {
122
+ const { t, disconnectSpy } = createHarness(
123
+ () =>
124
+ new Promise<void>(() => {
125
+ // never settles
126
+ })
127
+ );
128
+
129
+ let settled = false;
130
+ const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
131
+ call.catch(() => {
132
+ settled = true;
133
+ });
134
+ await flush();
135
+
136
+ await advanceUntil(() => settled, 30000);
137
+
138
+ expect(disconnectSpy).toHaveBeenCalledWith(UUID);
139
+ });
140
+
141
+ test('the write budget is per packet, not per call', async () => {
142
+ // Five packets at 4s each: the call's write phase far outlives one packet budget,
143
+ // but no single packet does, so nothing may be torn down.
144
+ const { t, disconnectSpy, writeWithoutResponse } = createHarness(
145
+ () =>
146
+ new Promise<void>(resolve => {
147
+ setTimeout(resolve, 4000);
148
+ })
149
+ );
150
+
151
+ const errors: Array<{ errorCode?: unknown }> = [];
152
+ let settled = false;
153
+ const call = t.call(UUID, 'Ping', { message: 'x'.repeat(300) }, { timeoutMs: 120000 });
154
+ call.catch(e => {
155
+ errors.push(e);
156
+ settled = true;
157
+ });
158
+ await flush();
159
+
160
+ await advanceUntil(() => settled, 200000);
161
+
162
+ // Every packet was written: none was aborted even though the write phase as a
163
+ // whole ran far past a single packet budget.
164
+ expect(writeWithoutResponse.mock.calls.length).toBeGreaterThan(1);
165
+ expect(4000 * writeWithoutResponse.mock.calls.length).toBeGreaterThan(
166
+ BLE_WRITE_PACKET_TIMEOUT_MS
167
+ );
168
+ // Fails on the RESPONSE timeout the device never answered, not on a write timeout.
169
+ expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
170
+ });
171
+
172
+ test('a superseded call whose write wedges must not tear down the successor link', async () => {
173
+ // Only the first call's write wedges; the successor writes normally.
174
+ let writes = 0;
175
+ const { t, disconnectSpy } = createHarness(() => {
176
+ writes += 1;
177
+ return writes === 1
178
+ ? new Promise<void>(() => {
179
+ // never settles
180
+ })
181
+ : Promise.resolve();
182
+ });
183
+
184
+ const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
185
+ first.catch(() => undefined);
186
+ await flush();
187
+
188
+ // A forceRun call supersedes it and becomes the transport owner.
189
+ const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
190
+ second.catch(() => undefined);
191
+ await flush();
192
+
193
+ await drain(BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
194
+
195
+ expect(disconnectSpy).not.toHaveBeenCalled();
196
+ expect(t.runPromise).not.toBeNull();
197
+ });
198
+
199
+ test('repeated wedged writes recreate the BLE manager', async () => {
200
+ const { t, disconnectSpy } = createHarness(
201
+ () =>
202
+ new Promise<void>(() => {
203
+ // never settles
204
+ })
205
+ );
206
+ const destroy = jest.fn();
207
+ (t as any).blePlxManager = { destroy };
208
+
209
+ for (let attempt = 0; attempt < BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
210
+ (t as any).deviceProtocol.set(UUID, 'V1');
211
+ let settled = false;
212
+ const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
213
+ call.catch(() => {
214
+ settled = true;
215
+ });
216
+ // eslint-disable-next-line no-await-in-loop
217
+ await flush();
218
+ // eslint-disable-next-line no-await-in-loop
219
+ await advanceUntil(() => settled, BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
220
+ }
221
+
222
+ expect(disconnectSpy).toHaveBeenCalledTimes(BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD);
223
+ expect(destroy).toHaveBeenCalledTimes(1);
224
+ expect((t as any).blePlxManager).toBeUndefined();
225
+ });
226
+
227
+ test('slow but progressing writes are not aborted', async () => {
228
+ // Each packet takes a while but keeps completing: this must not be mistaken
229
+ // for a wedged link, even when the whole call outlives a single packet budget.
230
+ const { t, writeWithoutResponse } = createHarness(
231
+ () =>
232
+ new Promise<void>(resolve => {
233
+ setTimeout(resolve, 4000);
234
+ })
235
+ );
236
+
237
+ const errors: unknown[] = [];
238
+ let settled = false;
239
+ const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 60000 });
240
+ call.catch(e => {
241
+ errors.push(e);
242
+ settled = true;
243
+ });
244
+ await flush();
245
+
246
+ // Let every packet drain; the call then waits for a device response that never
247
+ // comes, and must fail with the RESPONSE timeout, not a write timeout.
248
+ await advanceUntil(() => settled, 90000);
249
+
250
+ expect(writeWithoutResponse).toHaveBeenCalled();
251
+ expect(errors).toHaveLength(1);
252
+ expect((errors[0] as { errorCode?: unknown })?.errorCode).toBe(
253
+ HardwareErrorCode.BleTimeoutError
254
+ );
255
+ });
256
+ });
257
+
258
+ describe('protocol probe state visibility', () => {
259
+ beforeAll(() => {
260
+ jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
261
+ });
262
+
263
+ afterAll(() => {
264
+ jest.useRealTimers();
265
+ });
266
+
267
+ afterEach(() => {
268
+ jest.clearAllTimers();
269
+ jest.restoreAllMocks();
270
+ });
271
+
272
+ test('an in-flight probe does not publish its protocol as confirmed', async () => {
273
+ const { t } = createHarness(
274
+ () =>
275
+ new Promise<void>(() => {
276
+ // never settles
277
+ })
278
+ );
279
+ (t as any).deviceProtocol.delete(UUID);
280
+
281
+ const probe = (t as any).probeProtocolV1(UUID) as Promise<boolean>;
282
+ probe.catch(() => undefined);
283
+ await flush();
284
+
285
+ // The call itself must still route as V1 while probing...
286
+ expect(t.getProtocolType(UUID)).toBe('V1');
287
+ // ...but acquire's cached-transport reuse gate must not treat it as detected.
288
+ expect((t as any).deviceProtocol.get(UUID)).toBeUndefined();
289
+
290
+ await drain(BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
291
+ });
292
+
293
+ test('a probe that confirms its protocol leaves no probing entry behind', async () => {
294
+ const { t } = createHarness(() => Promise.resolve());
295
+ (t as any).deviceProtocol.delete(UUID);
296
+
297
+ const probe = (t as any).probeProtocolV1(UUID) as Promise<boolean>;
298
+ probe.catch(() => undefined);
299
+ await flush();
300
+ // The device answers the probe.
301
+ t.runPromise?.resolve('deadbeef');
302
+ await flush();
303
+
304
+ expect((t as any).probingProtocols.size).toBe(0);
305
+ });
306
+ });
@@ -7,35 +7,10 @@ export type BleWriteCapability = {
7
7
  isWritableWithoutResponse?: boolean | null;
8
8
  };
9
9
 
10
- export type BleWriteMode = 'withResponse' | 'withoutResponse';
11
-
12
10
  export function hasWritableCapability(characteristic: BleWriteCapability) {
13
11
  return !!(characteristic.isWritableWithResponse || characteristic.isWritableWithoutResponse);
14
12
  }
15
13
 
16
- export function resolveBleWriteMode(
17
- characteristic: BleWriteCapability,
18
- preferredMode: BleWriteMode = 'withoutResponse'
19
- ): BleWriteMode {
20
- if (preferredMode === 'withoutResponse' && characteristic.isWritableWithoutResponse) {
21
- return 'withoutResponse';
22
- }
23
-
24
- if (preferredMode === 'withResponse' && characteristic.isWritableWithResponse) {
25
- return 'withResponse';
26
- }
27
-
28
- if (characteristic.isWritableWithoutResponse) {
29
- return 'withoutResponse';
30
- }
31
-
32
- if (characteristic.isWritableWithResponse) {
33
- return 'withResponse';
34
- }
35
-
36
- return preferredMode;
37
- }
38
-
39
14
  export function resolveProtocolV2PacketCapacity({
40
15
  platform,
41
16
  iosPacketLength = IOS_PACKET_LENGTH,
package/src/constants.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { createKnownBleUuidAliases, matchesKnownBleUuid } from '@onekeyfe/hd-shared';
2
+
1
3
  export const IOS_PACKET_LENGTH = 128;
2
4
  export const ANDROID_PACKET_LENGTH = 192;
3
5
  export const ANDROID_DEFAULT_MTU = 23;
@@ -40,7 +42,9 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic
40
42
  const service =
41
43
  services[serviceUuid] ??
42
44
  Object.values(services).find(
43
- item => normalizeBleUuid(item.serviceUuid) === normalizedServiceUuid
45
+ item =>
46
+ normalizeBleUuid(item.serviceUuid) === normalizedServiceUuid ||
47
+ matchesKnownBleUuid(serviceUuid, createKnownBleUuidAliases(item.serviceUuid))
44
48
  );
45
49
  if (!service) {
46
50
  return null;
@@ -51,17 +55,7 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic
51
55
  export const normalizeBleUuid = (uuid?: string | null) =>
52
56
  (uuid ?? '').replace(/-/g, '').toLowerCase();
53
57
 
54
- export const getBleUuidKey = (uuid?: string | null) => {
55
- const normalized = normalizeBleUuid(uuid);
56
- return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
57
- };
58
-
59
58
  export const isSameBleUuid = (left?: string | null, right?: string | null) => {
60
- const normalizedLeft = normalizeBleUuid(left);
61
- const normalizedRight = normalizeBleUuid(right);
62
-
63
- return (
64
- normalizedLeft === normalizedRight ||
65
- (getBleUuidKey(left) !== '' && getBleUuidKey(left) === getBleUuidKey(right))
66
- );
59
+ if (!left || !right) return false;
60
+ return matchesKnownBleUuid(left, createKnownBleUuidAliases(right));
67
61
  };