@onekeyfe/hd-transport 1.2.0-alpha.2 → 1.2.0-alpha.21

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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/__tests__/messages.test.js +122 -0
  3. package/__tests__/protocol-v2-link-manager.test.js +326 -0
  4. package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
  5. package/__tests__/protocol-v2.test.js +371 -107
  6. package/dist/constants.d.ts +5 -3
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/index.d.ts +912 -371
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +551 -254
  11. package/dist/protocols/index.d.ts +10 -5
  12. package/dist/protocols/index.d.ts.map +1 -1
  13. package/dist/protocols/v2/constants.d.ts +1 -0
  14. package/dist/protocols/v2/constants.d.ts.map +1 -1
  15. package/dist/protocols/v2/decode.d.ts +2 -2
  16. package/dist/protocols/v2/decode.d.ts.map +1 -1
  17. package/dist/protocols/v2/encode.d.ts +2 -3
  18. package/dist/protocols/v2/encode.d.ts.map +1 -1
  19. package/dist/protocols/v2/index.d.ts +0 -1
  20. package/dist/protocols/v2/index.d.ts.map +1 -1
  21. package/dist/protocols/v2/link-manager.d.ts +35 -0
  22. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  23. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  24. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  25. package/dist/protocols/v2/session.d.ts +15 -4
  26. package/dist/protocols/v2/session.d.ts.map +1 -1
  27. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  28. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  29. package/dist/serialization/protobuf/decode.d.ts.map +1 -1
  30. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  31. package/dist/types/messages.d.ts +535 -226
  32. package/dist/types/messages.d.ts.map +1 -1
  33. package/dist/types/transport.d.ts +5 -3
  34. package/dist/types/transport.d.ts.map +1 -1
  35. package/messages-protocol-v2.json +825 -709
  36. package/package.json +3 -3
  37. package/scripts/protobuf-build.sh +96 -310
  38. package/scripts/protobuf-patches/index.js +1 -0
  39. package/scripts/protobuf-types.js +12 -0
  40. package/src/constants.ts +21 -15
  41. package/src/index.ts +8 -0
  42. package/src/protocols/index.ts +31 -40
  43. package/src/protocols/v2/constants.ts +1 -0
  44. package/src/protocols/v2/crc8.ts +1 -1
  45. package/src/protocols/v2/decode.ts +31 -34
  46. package/src/protocols/v2/encode.ts +2 -28
  47. package/src/protocols/v2/index.ts +0 -1
  48. package/src/protocols/v2/link-manager.ts +160 -0
  49. package/src/protocols/v2/sequence-cursor.ts +10 -0
  50. package/src/protocols/v2/session.ts +79 -180
  51. package/src/protocols/v2/usb-transport-base.ts +194 -0
  52. package/src/serialization/protobuf/decode.ts +4 -1
  53. package/src/serialization/protobuf/messages.ts +6 -1
  54. package/src/types/messages.ts +637 -266
  55. package/src/types/transport.ts +3 -3
  56. package/dist/protocols/v2/debug.d.ts +0 -13
  57. package/dist/protocols/v2/debug.d.ts.map +0 -1
  58. package/src/protocols/v2/debug.ts +0 -26
@@ -1,6 +1,6 @@
1
1
  import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants';
2
2
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
3
- import { nextProtoSeq } from './encode';
3
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
4
4
  import { ProtocolV2 } from '..';
5
5
  import * as check from '../../utils/highlevel-checks';
6
6
  import { LogBlockCommand } from '../../utils/logBlockCommand';
@@ -13,6 +13,13 @@ export type ProtocolV2Schemas = {
13
13
  protocolV2: Root;
14
14
  };
15
15
 
16
+ export type ProtocolV2CallContext = {
17
+ messageName: string;
18
+ timeoutMs?: number;
19
+ highVolume: boolean;
20
+ generation: number;
21
+ };
22
+
16
23
  type ProtocolLogger = {
17
24
  debug?: (...args: any[]) => void;
18
25
  error?: (...args: any[]) => void;
@@ -22,11 +29,15 @@ export type ProtocolV2SessionOptions = {
22
29
  schemas: ProtocolV2Schemas;
23
30
  router: number;
24
31
  packetSrc?: number;
25
- writeFrame: (frame: Uint8Array) => Promise<void>;
26
- readFrame: () => Promise<Uint8Array>;
32
+ maxFrameBytes?: number;
33
+ prepareCall?: (context: ProtocolV2CallContext) => Promise<void> | void;
34
+ writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise<void>;
35
+ readFrame: (context: ProtocolV2CallContext) => Promise<Uint8Array>;
27
36
  logger?: ProtocolLogger;
28
37
  logPrefix?: string;
29
38
  createTimeoutError?: (name: string, timeoutMs: number) => Error;
39
+ sequenceCursor?: ProtocolV2SequenceCursor;
40
+ generation?: number;
30
41
  };
31
42
 
32
43
  export type ProtocolV2CallOptions = {
@@ -37,6 +48,7 @@ export type ProtocolV2CallOptions = {
37
48
  };
38
49
 
39
50
  export { concatUint8Arrays, ProtocolV2FrameAssembler };
51
+ export { ProtocolV2SequenceCursor };
40
52
 
41
53
  export function hexToBytes(hex: string): Uint8Array {
42
54
  const clean = hex.replace(/\s+/g, '');
@@ -59,15 +71,6 @@ export function bytesToHex(bytes: Uint8Array): string {
59
71
  .join('');
60
72
  }
61
73
 
62
- // Frame header bytes worth logging: SOF, len lo/hi, header CRC, router, attr, seq.
63
- // The rest of the frame is protobuf payload and may contain sensitive fields
64
- // (mnemonic words via WordAck, PINs, seeds via LoadDevice, ...), so raw frame
65
- // hex logs must never include it.
66
- const PROTOCOL_V2_DEBUG_HEADER_BYTES = 7;
67
- const PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT = 20;
68
- const PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT = 40;
69
- const PROTOCOL_V2_DEBUG_STRING_LIMIT = 512;
70
- const PROTOCOL_V2_DEBUG_DEPTH_LIMIT = 4;
71
74
  const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([
72
75
  ...LogBlockCommand,
73
76
  'FilesystemFileRead',
@@ -79,102 +82,6 @@ function shouldReduceProtocolV2Debug(name: string) {
79
82
  return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name);
80
83
  }
81
84
 
82
- function frameHeaderDebugHex(frame: Uint8Array): string {
83
- // Only the frame header is dumped as hex; the payload is logged separately
84
- // in sanitized/structured form (sanitizeProtocolV2DebugPayload).
85
- return bytesToHex(frame.slice(0, PROTOCOL_V2_DEBUG_HEADER_BYTES));
86
- }
87
-
88
- function getBinaryByteLength(value: unknown): number | undefined {
89
- if (value instanceof ArrayBuffer) {
90
- return value.byteLength;
91
- }
92
-
93
- if (ArrayBuffer.isView(value)) {
94
- return value.byteLength;
95
- }
96
-
97
- if (typeof Blob !== 'undefined' && value instanceof Blob) {
98
- return value.size;
99
- }
100
-
101
- return undefined;
102
- }
103
-
104
- function summarizeRedactedData(value: unknown): string {
105
- const byteLength = getBinaryByteLength(value);
106
- if (byteLength !== undefined) {
107
- return `[redacted data: ${byteLength} bytes]`;
108
- }
109
-
110
- if (typeof value === 'string') {
111
- return `[redacted data: string length=${value.length}]`;
112
- }
113
-
114
- if (Array.isArray(value)) {
115
- return `[redacted data: array length=${value.length}]`;
116
- }
117
-
118
- if (value && typeof value === 'object') {
119
- return `[redacted data: object keys=${Object.keys(value).length}]`;
120
- }
121
-
122
- return `[redacted data: ${typeof value}]`;
123
- }
124
-
125
- function sanitizeProtocolV2DebugPayload(value: unknown, key = '', depth = 0): unknown {
126
- if (/^(data|payload)$/i.test(key) && value !== null && value !== undefined) {
127
- return summarizeRedactedData(value);
128
- }
129
-
130
- if (/(passphrase|pin|mnemonic|seed|private)/i.test(key)) {
131
- return '[redacted sensitive value]';
132
- }
133
-
134
- const byteLength = getBinaryByteLength(value);
135
- if (byteLength !== undefined) {
136
- return `[binary: ${byteLength} bytes]`;
137
- }
138
-
139
- if (typeof value === 'string') {
140
- return value.length > PROTOCOL_V2_DEBUG_STRING_LIMIT
141
- ? `${value.slice(0, PROTOCOL_V2_DEBUG_STRING_LIMIT)}... (len=${value.length})`
142
- : value;
143
- }
144
-
145
- if (!value || typeof value !== 'object') {
146
- return value;
147
- }
148
-
149
- if (depth >= PROTOCOL_V2_DEBUG_DEPTH_LIMIT) {
150
- return Array.isArray(value)
151
- ? `[array length=${value.length}]`
152
- : `[object keys=${Object.keys(value).length}]`;
153
- }
154
-
155
- if (Array.isArray(value)) {
156
- const items = value
157
- .slice(0, PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT)
158
- .map(item => sanitizeProtocolV2DebugPayload(item, key, depth + 1));
159
- if (value.length > PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT) {
160
- items.push(`... (${value.length - PROTOCOL_V2_DEBUG_ARRAY_ITEMS_LIMIT} more)`);
161
- }
162
- return items;
163
- }
164
-
165
- const entries = Object.entries(value).slice(0, PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT);
166
- const sanitized: Record<string, unknown> = {};
167
- entries.forEach(([entryKey, entryValue]) => {
168
- sanitized[entryKey] = sanitizeProtocolV2DebugPayload(entryValue, entryKey, depth + 1);
169
- });
170
- if (Object.keys(value).length > PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT) {
171
- sanitized.__truncated__ = `${
172
- Object.keys(value).length - PROTOCOL_V2_DEBUG_OBJECT_KEYS_LIMIT
173
- } more keys`;
174
- }
175
- return sanitized;
176
- }
177
-
178
85
  const COMMON_TERMINAL_RESPONSE_TYPES = new Set([
179
86
  'Failure',
180
87
  'ButtonRequest',
@@ -229,24 +136,18 @@ export async function withProtocolTimeout<T>(
229
136
  }
230
137
  }
231
138
 
232
- // Watchdog for the write phase only. Writing a single frame (max 4608 bytes)
233
- // is a bounded transport operation, unlike the read phase where long-running
234
- // device operations (firmware install, user confirmation) can legitimately
235
- // take minutes — so no default timeout is applied to reads.
236
- export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 30000;
237
-
238
139
  export class ProtocolV2Session {
239
140
  private readonly options: ProtocolV2SessionOptions;
240
141
 
142
+ private readonly sequenceCursor: ProtocolV2SequenceCursor;
143
+
241
144
  // Serializes call() invocations: responses are matched only by type, so two
242
145
  // in-flight calls on the same session would steal each other's responses.
243
146
  private pendingCall: Promise<unknown> = Promise.resolve();
244
147
 
245
- // Per-session sequence counter (1-255, wraps skipping 0).
246
- private protoSeq = 0;
247
-
248
148
  constructor(options: ProtocolV2SessionOptions) {
249
149
  this.options = options;
150
+ this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor();
250
151
  }
251
152
 
252
153
  call(
@@ -271,47 +172,52 @@ export class ProtocolV2Session {
271
172
  schemas,
272
173
  router,
273
174
  packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
175
+ maxFrameBytes,
176
+ prepareCall,
274
177
  writeFrame,
275
178
  readFrame,
276
179
  logger,
277
180
  logPrefix = 'ProtocolV2',
278
181
  createTimeoutError,
182
+ generation = 0,
279
183
  } = this.options;
280
184
 
281
185
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
282
- this.protoSeq = nextProtoSeq(this.protoSeq);
186
+ const callContext: ProtocolV2CallContext = {
187
+ messageName: name,
188
+ timeoutMs: callOptions.timeoutMs,
189
+ highVolume: shouldReduceDebug,
190
+ generation,
191
+ };
192
+ await prepareCall?.(callContext);
193
+ const protoSeq = this.sequenceCursor.next();
283
194
  const frame = ProtocolV2.encodeFrame(schemas, name, data, {
284
195
  packetSrc,
285
196
  router,
286
- seq: this.protoSeq,
287
- logger: shouldReduceDebug ? undefined : logger,
288
- logPrefix,
289
- context: `tx:${name}`,
197
+ seq: protoSeq,
290
198
  });
291
- const expectedSeq = frame[6];
199
+ // Suppress Protocol V2 TX frame logs to avoid excessive transport diagnostics.
200
+ /*
201
+ const txMetadata = ProtocolV2.inspectFrame(schemas, frame);
292
202
 
293
203
  if (!shouldReduceDebug) {
294
- logger?.debug?.(
295
- `[${logPrefix}] TX payload name=${name}`,
296
- sanitizeProtocolV2DebugPayload(data)
297
- );
298
- logger?.debug?.(
299
- `[${logPrefix}] TX frame name=${name} len=${frame.length} router=${frame[4]} attr=${
300
- frame[5]
301
- } seq=${expectedSeq} headerHex=${frameHeaderDebugHex(frame)}`
204
+ logger?.debug?.(`[${logPrefix}] TX`, {
205
+ method: name,
206
+ type: name,
207
+ typeId: txMetadata.messageTypeId,
208
+ seq: txMetadata.seq,
209
+ bytes: frame.length,
210
+ });
211
+ }
212
+ */
213
+
214
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
215
+ throw new Error(
216
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
302
217
  );
303
218
  }
304
219
 
305
- // Lenient watchdog on the write phase only — see
306
- // PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale.
307
- await withProtocolTimeout(
308
- writeFrame(frame),
309
- PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS,
310
- () =>
311
- new Error(
312
- `Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}`
313
- )
314
- );
220
+ await writeFrame(frame, callContext);
315
221
 
316
222
  // Cancellation flag for the read loop: when the response timeout fires,
317
223
  // Promise.race alone would leave this loop running as a zombie that keeps
@@ -324,49 +230,42 @@ export class ProtocolV2Session {
324
230
  // terminal response. Consume those frames here so callers still see a
325
231
  // request/terminal-response shaped API.
326
232
  while (!cancellation.cancelled) {
327
- const rxFrame = await readFrame();
233
+ const rxFrame = await readFrame(callContext);
328
234
  if (cancellation.cancelled) {
329
235
  // Timed out while waiting: drop the late frame and stop reading.
330
236
  break;
331
237
  }
332
- if (!shouldReduceDebug) {
333
- logger?.debug?.(
334
- `[${logPrefix}] RX frame len=${rxFrame.length} router=${rxFrame[4]} attr=${
335
- rxFrame[5]
336
- } seq=${rxFrame[6]} headerHex=${frameHeaderDebugHex(rxFrame)}`
337
- );
338
- }
339
- const decoded = ProtocolV2.decodeFrame(schemas, rxFrame, {
340
- logger: shouldReduceDebug ? undefined : logger,
341
- logPrefix,
342
- context: `rx:${name}`,
343
- });
344
- if (!shouldReduceDebug && decoded.seq !== expectedSeq) {
345
- logger?.debug?.(
346
- `[${logPrefix}] seq differs for ${name}: tx=${expectedSeq}, rx=${decoded.seq}`
347
- );
348
- }
349
- if (!shouldReduceDebug) {
350
- logger?.debug?.(
351
- `[${logPrefix}] TX name=${name} seq=${expectedSeq} | RX seq=${decoded.seq} messageTypeId=${decoded.messageTypeId} pbPayload=${decoded.pbPayload.length}B`
352
- );
353
- logger?.debug?.(
354
- `[${logPrefix}] RX payload type=${decoded.type} messageTypeId=${decoded.messageTypeId}`,
355
- sanitizeProtocolV2DebugPayload(decoded.message)
356
- );
357
- }
358
-
359
- const response = check.call(decoded);
360
- if (callOptions.intermediateTypes?.includes(response.type)) {
361
- callOptions.onIntermediateResponse?.(response);
362
- } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
363
- return response;
364
- } else if (!shouldReduceDebug) {
365
- logger?.debug?.(
366
- `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
367
- '|'
368
- )} got=${response.type}`
369
- );
238
+ const isAck = ProtocolV2.isAckFrame(rxFrame);
239
+ if (!isAck) {
240
+ // Suppress Protocol V2 RX frame logs to avoid excessive transport diagnostics.
241
+ /*
242
+ const rxMetadata = ProtocolV2.inspectFrame(schemas, rxFrame);
243
+
244
+ if (!shouldReduceDebug) {
245
+ logger?.debug?.(`[${logPrefix}] RX`, {
246
+ method: name,
247
+ type: rxMetadata.type,
248
+ typeId: rxMetadata.messageTypeId,
249
+ seq: rxMetadata.seq,
250
+ bytes: rxFrame.length,
251
+ });
252
+ }
253
+ */
254
+
255
+ const decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
256
+
257
+ const response = check.call(decoded);
258
+ if (callOptions.intermediateTypes?.includes(response.type)) {
259
+ callOptions.onIntermediateResponse?.(response);
260
+ } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) {
261
+ return response;
262
+ } else if (!shouldReduceDebug) {
263
+ logger?.debug?.(
264
+ `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join(
265
+ '|'
266
+ )} got=${response.type}`
267
+ );
268
+ }
370
269
  }
371
270
  }
372
271
  // Only reachable after cancellation; the outer promise has already been
@@ -0,0 +1,194 @@
1
+ import { ProtocolV2FrameAssembler } from './frame-assembler';
2
+ import { ProtocolV2LinkManager } from './link-manager';
3
+
4
+ import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
+ import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
6
+
7
+ export type ProtocolV2UsbTransportBaseOptions = {
8
+ router: number;
9
+ maxFrameBytes: number;
10
+ logPrefix: string;
11
+ };
12
+
13
+ type ProtocolV2UsbCancellation = {
14
+ generation: number;
15
+ reason?: string;
16
+ promise: Promise<string>;
17
+ cancel: (reason: string) => void;
18
+ };
19
+
20
+ export abstract class ProtocolV2UsbTransportBase<Key> {
21
+ private readonly protocolV2UsbLinks: ProtocolV2LinkManager<Key>;
22
+
23
+ private readonly protocolV2UsbAssemblers = new Map<Key, ProtocolV2FrameAssembler>();
24
+
25
+ private readonly protocolV2UsbGenerations = new Map<Key, number>();
26
+
27
+ private readonly protocolV2UsbCancellations = new Map<Key, ProtocolV2UsbCancellation>();
28
+
29
+ private readonly protocolV2UsbOptions: ProtocolV2UsbTransportBaseOptions;
30
+
31
+ protected constructor(options: ProtocolV2UsbTransportBaseOptions) {
32
+ this.protocolV2UsbOptions = options;
33
+ this.protocolV2UsbLinks = new ProtocolV2LinkManager<Key>({
34
+ getSchemas: () => this.getProtocolV2UsbSchemas(),
35
+ classifyError: () => 'link-fatal',
36
+ onLinkInvalidated: async (key, reason) => {
37
+ this.protocolV2UsbAssemblers.get(key)?.reset();
38
+ await this.resetProtocolV2UsbNativeLink(key, reason);
39
+ await this.onProtocolV2UsbLinkInvalidated(key, reason);
40
+ },
41
+ });
42
+ }
43
+
44
+ protected abstract getProtocolV2UsbSchemas(): ProtocolV2Schemas;
45
+
46
+ protected abstract getProtocolV2UsbLogger(): ProtocolV2SessionOptions['logger'];
47
+
48
+ protected abstract writeProtocolV2UsbPacket(
49
+ key: Key,
50
+ frame: Uint8Array,
51
+ context: ProtocolV2CallContext
52
+ ): Promise<void>;
53
+
54
+ protected abstract readProtocolV2UsbPacket(
55
+ key: Key,
56
+ context: ProtocolV2CallContext
57
+ ): Promise<Uint8Array>;
58
+
59
+ protected abstract resetProtocolV2UsbNativeLink(key: Key, reason: string): Promise<void>;
60
+
61
+ protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error;
62
+
63
+ protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise<void> | void {
64
+ // Subclasses may clear protocol caches or emit platform-specific diagnostics.
65
+ }
66
+
67
+ protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise<number> {
68
+ await this.protocolV2UsbLinks.invalidateLink(key, reason);
69
+ const generation = (this.protocolV2UsbGenerations.get(key) ?? 0) + 1;
70
+ this.protocolV2UsbGenerations.set(key, generation);
71
+ this.protocolV2UsbCancellations.set(key, this.createProtocolV2UsbCancellation(generation));
72
+ this.protocolV2UsbAssemblers.set(
73
+ key,
74
+ new ProtocolV2FrameAssembler(this.protocolV2UsbOptions.maxFrameBytes)
75
+ );
76
+ return generation;
77
+ }
78
+
79
+ protected callProtocolV2Usb(
80
+ key: Key,
81
+ name: string,
82
+ data: Record<string, unknown>,
83
+ options?: TransportCallOptions
84
+ ): Promise<MessageFromOneKey> {
85
+ return this.protocolV2UsbLinks.call(
86
+ key,
87
+ () => this.createProtocolV2UsbAdapter(key),
88
+ name,
89
+ data,
90
+ options
91
+ );
92
+ }
93
+
94
+ protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise<void> {
95
+ return this.protocolV2UsbLinks.invalidateLink(key, reason);
96
+ }
97
+
98
+ protected invalidateAllProtocolV2UsbLinks(reason: string): Promise<void> {
99
+ return this.protocolV2UsbLinks.invalidateAllLinks(reason);
100
+ }
101
+
102
+ protected async disposeProtocolV2UsbLinks(reason: string): Promise<void> {
103
+ await this.protocolV2UsbLinks.dispose(reason);
104
+ this.protocolV2UsbAssemblers.clear();
105
+ this.protocolV2UsbGenerations.clear();
106
+ this.protocolV2UsbCancellations.clear();
107
+ }
108
+
109
+ private createProtocolV2UsbAdapter(key: Key) {
110
+ const generation = this.protocolV2UsbGenerations.get(key);
111
+ if (generation === undefined) {
112
+ throw new Error('Protocol V2 USB generation has not been initialized');
113
+ }
114
+ const cancellation = this.protocolV2UsbCancellations.get(key);
115
+ if (!cancellation || cancellation.generation !== generation) {
116
+ throw new Error('Protocol V2 USB cancellation state has not been initialized');
117
+ }
118
+
119
+ const assertCurrentGeneration = () => {
120
+ if (cancellation.reason) {
121
+ throw new Error(cancellation.reason);
122
+ }
123
+ if (this.protocolV2UsbGenerations.get(key) !== generation) {
124
+ throw new Error('Protocol V2 USB connection generation changed');
125
+ }
126
+ };
127
+
128
+ return {
129
+ router: this.protocolV2UsbOptions.router,
130
+ maxFrameBytes: this.protocolV2UsbOptions.maxFrameBytes,
131
+ generation,
132
+ prepareCall: () => {
133
+ assertCurrentGeneration();
134
+ this.getProtocolV2UsbAssembler(key).reset();
135
+ },
136
+ writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
137
+ assertCurrentGeneration();
138
+ await this.writeProtocolV2UsbPacket(key, frame, context);
139
+ assertCurrentGeneration();
140
+ },
141
+ readFrame: async (context: ProtocolV2CallContext) => {
142
+ assertCurrentGeneration();
143
+ const assembler = this.getProtocolV2UsbAssembler(key);
144
+ let frame = assembler.push(new Uint8Array(0));
145
+ while (!frame) {
146
+ const packetRead = this.readProtocolV2UsbPacket(key, context).then(packet => ({
147
+ packet,
148
+ }));
149
+ const cancelled = cancellation.promise.then(reason => ({ reason }));
150
+ const result = await Promise.race([packetRead, cancelled]);
151
+ if ('reason' in result) {
152
+ throw new Error(result.reason);
153
+ }
154
+ assertCurrentGeneration();
155
+ frame = assembler.push(result.packet);
156
+ }
157
+ assertCurrentGeneration();
158
+ return frame;
159
+ },
160
+ reset: (reason: string) => {
161
+ cancellation.cancel(reason);
162
+ this.protocolV2UsbAssemblers.get(key)?.reset();
163
+ },
164
+ logger: this.getProtocolV2UsbLogger(),
165
+ logPrefix: this.protocolV2UsbOptions.logPrefix,
166
+ createTimeoutError: (messageName: string, timeoutMs: number) =>
167
+ this.createProtocolV2UsbTimeoutError(messageName, timeoutMs),
168
+ };
169
+ }
170
+
171
+ private getProtocolV2UsbAssembler(key: Key): ProtocolV2FrameAssembler {
172
+ const assembler = this.protocolV2UsbAssemblers.get(key);
173
+ if (!assembler) {
174
+ throw new Error('Protocol V2 USB assembler has not been initialized');
175
+ }
176
+ return assembler;
177
+ }
178
+
179
+ private createProtocolV2UsbCancellation(generation: number): ProtocolV2UsbCancellation {
180
+ let resolveCancellation: (reason: string) => void = () => undefined;
181
+ const cancellation: ProtocolV2UsbCancellation = {
182
+ generation,
183
+ promise: new Promise(resolve => {
184
+ resolveCancellation = resolve;
185
+ }),
186
+ cancel: reason => {
187
+ if (cancellation.reason) return;
188
+ cancellation.reason = reason;
189
+ resolveCancellation(reason);
190
+ },
191
+ };
192
+ return cancellation;
193
+ }
194
+ }
@@ -70,7 +70,10 @@ function messageToJSON(Message: Message<Record<string, unknown>>, fields: Type['
70
70
  }
71
71
  // message type
72
72
  else if (field.resolvedType!.fields) {
73
- res[key] = messageToJSON(value, field.resolvedType!.fields);
73
+ // [compatibility]: absent optional sub-messages are not own properties
74
+ // of the decoded instance, so value is undefined here; decode them to
75
+ // null, matching the optional primitive-field convention above.
76
+ res[key] = value == null ? null : messageToJSON(value, (field.resolvedType as Type).fields);
74
77
  } else {
75
78
  throw new Error(`case not handled: ${key}`);
76
79
  }
@@ -30,7 +30,12 @@ export const createMessageFromName = (messages: protobuf.Root, name: string) =>
30
30
  export const createMessageFromType = (messages: protobuf.Root, typeId: number) => {
31
31
  const MessageType = messages.lookupEnum('MessageType');
32
32
 
33
- const messageName = MessageType.valuesById[typeId].replace('MessageType_', '');
33
+ const rawMessageName = MessageType.valuesById[typeId];
34
+ if (!rawMessageName) {
35
+ throw new Error(`MessageType id "${typeId}" is not defined in protobuf schema`);
36
+ }
37
+
38
+ const messageName = rawMessageName.replace('MessageType_', '');
34
39
 
35
40
  const Message = messages.lookupType(messageName);
36
41