@onekeyfe/hd-transport 1.2.8 → 1.2.10-alpha.2

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 (38) hide show
  1. package/__tests__/babylon-context-hash.test.js +44 -0
  2. package/__tests__/protocol-v2-file-write-pipeline.test.js +478 -0
  3. package/__tests__/protocol-v2.test.js +3 -3
  4. package/__tests__/static-protobuf.test.js +436 -0
  5. package/__tests__/transport-log.test.js +176 -0
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/index.d.ts +158 -3
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +109280 -98
  11. package/dist/protocols/index.d.ts.map +1 -1
  12. package/dist/protocols/v2/session.d.ts +7 -1
  13. package/dist/protocols/v2/session.d.ts.map +1 -1
  14. package/dist/serialization/protobuf/messages.d.ts +1 -1
  15. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  16. package/dist/types/messages.d.ts +72 -0
  17. package/dist/types/messages.d.ts.map +1 -1
  18. package/dist/types/transport.d.ts +6 -0
  19. package/dist/types/transport.d.ts.map +1 -1
  20. package/dist/utils/transportLog.d.ts.map +1 -1
  21. package/jest.config.js +3 -0
  22. package/messages-protocol-v2.json +152 -0
  23. package/package.json +6 -4
  24. package/scripts/protobuf-build.sh +37 -0
  25. package/scripts/protobuf-static-csp.js +103 -0
  26. package/scripts/protobuf-static.js +85 -0
  27. package/scripts/protobuf-types.js +1 -0
  28. package/src/constants.ts +10 -1
  29. package/src/index.ts +1 -0
  30. package/src/protocols/index.ts +10 -23
  31. package/src/protocols/v2/session.ts +326 -96
  32. package/src/serialization/protobuf/generated/codecs.d.ts +3 -0
  33. package/src/serialization/protobuf/generated/codecs.js +108990 -0
  34. package/src/serialization/protobuf/legacy-protocol-info.json +16 -0
  35. package/src/serialization/protobuf/messages.ts +12 -6
  36. package/src/types/messages.ts +97 -0
  37. package/src/types/transport.ts +25 -0
  38. package/src/utils/transportLog.ts +17 -2
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS,
3
+ PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW,
3
4
  PROTOCOL_V2_PACKET_SRC_COMMAND,
4
5
  } from '../../constants';
5
6
  import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler';
@@ -9,13 +10,18 @@ import {
9
10
  createProtocolV2LinkDisabledError,
10
11
  isProtocolV2LinkDisabledError,
11
12
  isProtocolV2LinkDisabledFailure,
13
+ isProtocolV2LinkError,
12
14
  } from './errors';
13
15
  import { ProtocolV2 } from '..';
14
16
  import * as check from '../../utils/highlevel-checks';
15
17
  import { LogBlockCommand } from '../../utils/logBlockCommand';
16
18
 
17
19
  import type { Root } from 'protobufjs/light';
18
- import type { MessageFromOneKey, TransportWriteMetrics } from '../../types';
20
+ import type {
21
+ MessageFromOneKey,
22
+ ProtocolV2FileWritePipeline,
23
+ TransportWriteMetrics,
24
+ } from '../../types';
19
25
 
20
26
  export * from './errors';
21
27
 
@@ -62,6 +68,7 @@ export type ProtocolV2CallOptions = {
62
68
  returnAfterWrite?: boolean;
63
69
  onResponseAfterWrite?: (response: MessageFromOneKey) => void;
64
70
  writeWithResponse?: boolean;
71
+ fileWritePipeline?: ProtocolV2FileWritePipeline;
65
72
  };
66
73
 
67
74
  export { concatUint8Arrays, ProtocolV2FrameAssembler };
@@ -214,6 +221,30 @@ export async function withProtocolTimeout<T>(
214
221
  }
215
222
  }
216
223
 
224
+ const resolveProtocolV2CallTimeoutMs = (timeoutMs?: number) =>
225
+ timeoutMs && timeoutMs > 0 ? timeoutMs : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
226
+
227
+ function parseFileWriteChunk(request: Record<string, unknown>) {
228
+ const file = request.file as
229
+ | { path?: unknown; offset?: unknown; total_size?: unknown; data?: { byteLength?: unknown } }
230
+ | undefined;
231
+ const offset = Number(file?.offset);
232
+ const totalSize = Number(file?.total_size);
233
+ const length = Number(file?.data?.byteLength);
234
+ if (
235
+ typeof file?.path !== 'string' ||
236
+ !Number.isSafeInteger(offset) ||
237
+ offset < 0 ||
238
+ !Number.isSafeInteger(totalSize) ||
239
+ !Number.isSafeInteger(length) ||
240
+ length <= 0 ||
241
+ offset + length > totalSize
242
+ ) {
243
+ throw new Error('Protocol V2 file write pipeline chunk is invalid');
244
+ }
245
+ return { path: file.path, offset, end: offset + length, totalSize };
246
+ }
247
+
217
248
  export class ProtocolV2Session {
218
249
  private readonly options: ProtocolV2SessionOptions;
219
250
 
@@ -293,28 +324,46 @@ export class ProtocolV2Session {
293
324
  return result;
294
325
  }
295
326
 
327
+ private reportWriteCompleted(
328
+ callOptions: ProtocolV2CallOptions,
329
+ writeStartedAt: number,
330
+ frameBytes: number
331
+ ) {
332
+ try {
333
+ callOptions.onWriteCompleted?.({
334
+ elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
335
+ frameBytes,
336
+ });
337
+ } catch (error) {
338
+ const { logger, logPrefix = 'ProtocolV2' } = this.options;
339
+ logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
340
+ }
341
+ }
342
+
343
+ private createCallTimeoutError(name: string, timeoutMs: number): Error {
344
+ const { createTimeoutError } = this.options;
345
+ return createTimeoutError
346
+ ? createTimeoutError(name, timeoutMs)
347
+ : new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`);
348
+ }
349
+
296
350
  private executeCall(
297
351
  name: string,
298
352
  data: Record<string, unknown>,
299
353
  callOptions: ProtocolV2CallOptions
300
354
  ): Promise<MessageFromOneKey> {
355
+ if (callOptions.fileWritePipeline) {
356
+ return this.executeFileWritePipeline(name, data, callOptions, callOptions.fileWritePipeline);
357
+ }
301
358
  const {
302
- schemas,
303
- router,
304
- packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
305
- maxFrameBytes,
306
359
  prepareCall,
307
360
  writeFrame,
308
361
  readFrame,
309
362
  logger,
310
363
  logPrefix = 'ProtocolV2',
311
- createTimeoutError,
312
364
  generation = 0,
313
365
  } = this.options;
314
- const timeoutMs =
315
- callOptions.timeoutMs && callOptions.timeoutMs > 0
316
- ? callOptions.timeoutMs
317
- : PROTOCOL_V2_DEFAULT_RESPONSE_TIMEOUT_MS;
366
+ const timeoutMs = resolveProtocolV2CallTimeoutMs(callOptions.timeoutMs);
318
367
  const abortController = new AbortController();
319
368
 
320
369
  const shouldReduceDebug = shouldReduceProtocolV2Debug(name);
@@ -335,29 +384,11 @@ export class ProtocolV2Session {
335
384
  if (!this.pendingResponseAfterWrite) {
336
385
  await prepareCall?.(baseCallContext);
337
386
  }
338
- const protoSeq = this.sequenceCursor.next();
339
- const frame = ProtocolV2.encodeFrame(schemas, name, data, {
340
- packetSrc,
341
- router,
342
- seq: protoSeq,
343
- });
344
-
345
- if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
346
- throw new Error(
347
- `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
348
- );
349
- }
387
+ const { seq: protoSeq, frame } = this.encodeRequestFrame(name, data);
350
388
 
351
389
  const writeStartedAt = Date.now();
352
390
  await this.serializeWrite(() => writeFrame(frame, baseCallContext));
353
- try {
354
- callOptions.onWriteCompleted?.({
355
- elapsedMs: Math.max(Date.now() - writeStartedAt, 0),
356
- frameBytes: frame.byteLength,
357
- });
358
- } catch (error) {
359
- logger?.error?.(`${logPrefix} write metrics callback failed: ${String(error)}`);
360
- }
391
+ this.reportWriteCompleted(callOptions, writeStartedAt, frame.byteLength);
361
392
 
362
393
  if (callOptions.returnAfterWrite) {
363
394
  if (callOptions.expectedTypes?.length) {
@@ -376,68 +407,8 @@ export class ProtocolV2Session {
376
407
  const rxFrame = await readFrame(baseCallContext);
377
408
  if (abortController.signal.aborted) break;
378
409
 
379
- let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
380
- try {
381
- header = ProtocolV2.inspectFrameHeader(rxFrame);
382
- } catch (cause) {
383
- throw new ProtocolV2LinkError(
384
- 'frame',
385
- `Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
386
- cause
387
- );
388
- }
389
- if (header.router !== router) {
390
- throw new ProtocolV2LinkError(
391
- 'router',
392
- `Protocol V2 router mismatch: expected ${router}, got ${header.router}`
393
- );
394
- }
395
- if (header.packetSrc !== packetSrc) {
396
- throw new ProtocolV2LinkError(
397
- 'packet-source',
398
- `Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
399
- );
400
- }
401
-
402
- let isAck: boolean;
403
- try {
404
- isAck = ProtocolV2.isAckFrame(rxFrame);
405
- } catch (cause) {
406
- throw new ProtocolV2LinkError(
407
- 'frame',
408
- `Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
409
- cause
410
- );
411
- }
412
- if (isAck) {
413
- if (header.seq !== protoSeq) {
414
- throw new ProtocolV2LinkError(
415
- 'ack-sequence',
416
- `Protocol V2 ACK sequence mismatch: expected ${protoSeq}, got ${header.seq}`
417
- );
418
- }
419
- } else {
420
- let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
421
- try {
422
- decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
423
- } catch (cause) {
424
- throw new ProtocolV2LinkError(
425
- 'frame',
426
- `Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
427
- cause
428
- );
429
- }
430
- // Firmware owns one TX sequence across all channels and packet sources,
431
- // so frames routed elsewhere create valid gaps in this session.
432
- if (this.lastResponseSequence === decoded.seq) {
433
- throw new ProtocolV2LinkError(
434
- 'response-sequence',
435
- `Protocol V2 duplicate response sequence: ${decoded.seq}`
436
- );
437
- }
438
- this.lastResponseSequence = decoded.seq;
439
-
440
- const response = check.call(decoded);
410
+ const response = this.parseResponseFrame(rxFrame, [protoSeq]);
411
+ if (response) {
441
412
  const { pendingResponseAfterWrite } = this;
442
413
  let belongsToWriteOnlyCall = false;
443
414
  if (pendingResponseAfterWrite) {
@@ -477,15 +448,274 @@ export class ProtocolV2Session {
477
448
  return withProtocolTimeout(
478
449
  runCall(),
479
450
  timeoutMs,
480
- () =>
481
- createTimeoutError
482
- ? createTimeoutError(name, timeoutMs)
483
- : new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`),
451
+ () => this.createCallTimeoutError(name, timeoutMs),
484
452
  () => {
485
453
  abortController.abort();
486
454
  }
487
455
  );
488
456
  }
457
+
458
+ private encodeRequestFrame(name: string, data: Record<string, unknown>) {
459
+ const {
460
+ schemas,
461
+ router,
462
+ packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND,
463
+ maxFrameBytes,
464
+ } = this.options;
465
+ const seq = this.sequenceCursor.next();
466
+ const frame = ProtocolV2.encodeFrame(schemas, name, data, { packetSrc, router, seq });
467
+ if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) {
468
+ throw new Error(
469
+ `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}`
470
+ );
471
+ }
472
+ return { seq, frame };
473
+ }
474
+
475
+ /**
476
+ * Validates one received frame for this session. Returns undefined for an ACK of one of
477
+ * `ackSequences`, otherwise the decoded response.
478
+ */
479
+ private parseResponseFrame(
480
+ rxFrame: Uint8Array,
481
+ ackSequences: readonly number[]
482
+ ): MessageFromOneKey | undefined {
483
+ const { schemas, router, packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND } = this.options;
484
+ let header: ReturnType<typeof ProtocolV2.inspectFrameHeader>;
485
+ try {
486
+ header = ProtocolV2.inspectFrameHeader(rxFrame);
487
+ } catch (cause) {
488
+ throw new ProtocolV2LinkError(
489
+ 'frame',
490
+ `Protocol V2 frame validation failed: ${getErrorMessage(cause)}`,
491
+ cause
492
+ );
493
+ }
494
+ if (header.router !== router) {
495
+ throw new ProtocolV2LinkError(
496
+ 'router',
497
+ `Protocol V2 router mismatch: expected ${router}, got ${header.router}`
498
+ );
499
+ }
500
+ if (header.packetSrc !== packetSrc) {
501
+ throw new ProtocolV2LinkError(
502
+ 'packet-source',
503
+ `Protocol V2 packet source mismatch: expected ${packetSrc}, got ${header.packetSrc}`
504
+ );
505
+ }
506
+
507
+ let isAck: boolean;
508
+ try {
509
+ isAck = ProtocolV2.isAckFrame(rxFrame);
510
+ } catch (cause) {
511
+ throw new ProtocolV2LinkError(
512
+ 'frame',
513
+ `Protocol V2 ACK validation failed: ${getErrorMessage(cause)}`,
514
+ cause
515
+ );
516
+ }
517
+ if (isAck) {
518
+ if (!ackSequences.includes(header.seq)) {
519
+ throw new ProtocolV2LinkError(
520
+ 'ack-sequence',
521
+ `Protocol V2 ACK sequence mismatch: expected ${ackSequences.join('|')}, got ${header.seq}`
522
+ );
523
+ }
524
+ return undefined;
525
+ }
526
+
527
+ let decoded: ReturnType<typeof ProtocolV2.decodeFrame>;
528
+ try {
529
+ decoded = ProtocolV2.decodeFrame(schemas, rxFrame);
530
+ } catch (cause) {
531
+ throw new ProtocolV2LinkError(
532
+ 'frame',
533
+ `Protocol V2 frame decode failed: ${getErrorMessage(cause)}`,
534
+ cause
535
+ );
536
+ }
537
+ // Firmware owns one TX sequence across all channels and packet sources,
538
+ // so frames routed elsewhere create valid gaps in this session.
539
+ if (this.lastResponseSequence === decoded.seq) {
540
+ throw new ProtocolV2LinkError(
541
+ 'response-sequence',
542
+ `Protocol V2 duplicate response sequence: ${decoded.seq}`
543
+ );
544
+ }
545
+ this.lastResponseSequence = decoded.seq;
546
+ return check.call(decoded);
547
+ }
548
+
549
+ /**
550
+ * Writes one file as ordered FilesystemFileWrite chunks while keeping up to
551
+ * PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW chunks in flight. Firmware still processes and
552
+ * answers every chunk in order, so responses are matched to chunks by position. Upstream
553
+ * bootloaders append at their own cursor without checking the request offset, so the echoed
554
+ * offset is what exposes a lost chunk. A device Failure, or `next` returning undefined, stops
555
+ * sending; the remaining in-flight responses are drained so the link stays usable.
556
+ */
557
+ private async executeFileWritePipeline(
558
+ name: string,
559
+ data: Record<string, unknown>,
560
+ callOptions: ProtocolV2CallOptions,
561
+ pipeline: ProtocolV2FileWritePipeline
562
+ ): Promise<MessageFromOneKey> {
563
+ if (name !== 'FilesystemFileWrite') {
564
+ throw new Error(`Protocol V2 file write pipeline does not support ${name}`);
565
+ }
566
+ if (this.pendingResponseAfterWrite) {
567
+ throw new Error('Protocol V2 file write pipeline cannot start while a response is pending');
568
+ }
569
+ const { prepareCall, writeFrame, readFrame, generation = 0 } = this.options;
570
+ const timeoutMs = resolveProtocolV2CallTimeoutMs(callOptions.timeoutMs);
571
+ const abortController = new AbortController();
572
+ const context: ProtocolV2CallContext = {
573
+ messageName: name,
574
+ timeoutMs,
575
+ highThroughput: true,
576
+ ...(callOptions.writeWithResponse === undefined
577
+ ? {}
578
+ : { writeWithResponse: callOptions.writeWithResponse }),
579
+ generation,
580
+ signal: abortController.signal,
581
+ };
582
+ // Transport errors and timeouts propagate unchanged: each transport classifies its own.
583
+ let transportFailure: unknown;
584
+ // One file can take far longer than one chunk, so the watchdog bounds each step instead.
585
+ const withStepTimeout = <T>(promise: Promise<T>, stepTimeoutMs = timeoutMs) =>
586
+ withProtocolTimeout(
587
+ promise,
588
+ stepTimeoutMs,
589
+ () => this.createCallTimeoutError(name, timeoutMs),
590
+ () => {
591
+ abortController.abort();
592
+ }
593
+ ).catch((error: unknown) => {
594
+ transportFailure = error;
595
+ throw error;
596
+ });
597
+ // ACKs and skipped frames must not extend the wait for an in-flight chunk's response.
598
+ let progressDeadline = 0;
599
+ const markProgress = () => {
600
+ progressDeadline = Date.now() + timeoutMs;
601
+ };
602
+
603
+ const inFlight: Array<{ seq: number; offset: number; end: number }> = [];
604
+ let path: string | undefined;
605
+ let totalSize: number | undefined;
606
+ let nextOffset: number | undefined;
607
+ let sourceDone = false;
608
+ let failure: MessageFromOneKey | undefined;
609
+ let lastConfirmed: MessageFromOneKey | undefined;
610
+
611
+ const sendChunk = async (request: Record<string, unknown>) => {
612
+ const chunk = parseFileWriteChunk(request);
613
+ if (path !== undefined && (chunk.path !== path || chunk.totalSize !== totalSize)) {
614
+ throw new Error('Protocol V2 file write pipeline chunk belongs to a different file');
615
+ }
616
+ if (nextOffset !== undefined && chunk.offset !== nextOffset) {
617
+ throw new Error(
618
+ `Protocol V2 file write pipeline chunk out of order: expected offset ${nextOffset}, got ${chunk.offset}`
619
+ );
620
+ }
621
+ path = chunk.path;
622
+ totalSize = chunk.totalSize;
623
+ nextOffset = chunk.end;
624
+
625
+ const { seq, frame } = this.encodeRequestFrame(name, request);
626
+ const writeStartedAt = Date.now();
627
+ await withStepTimeout(this.serializeWrite(() => writeFrame(frame, context)));
628
+ inFlight.push({ seq, offset: chunk.offset, end: chunk.end });
629
+ markProgress();
630
+ this.reportWriteCompleted(callOptions, writeStartedAt, frame.byteLength);
631
+ };
632
+ const sendNextChunk = async () => {
633
+ const request = await pipeline.next();
634
+ if (!request) {
635
+ sourceDone = true;
636
+ return;
637
+ }
638
+ await sendChunk(request);
639
+ };
640
+
641
+ await withStepTimeout(Promise.resolve().then(() => prepareCall?.(context)));
642
+ try {
643
+ await sendChunk(data);
644
+ while (!sourceDone && inFlight.length < PROTOCOL_V2_FILE_WRITE_PIPELINE_WINDOW) {
645
+ await sendNextChunk();
646
+ }
647
+
648
+ while (inFlight.length > 0) {
649
+ // Floor at 1 ms: withProtocolTimeout treats 0 as "no timeout", so an expired deadline
650
+ // must still time out.
651
+ const rxFrame = await withStepTimeout(
652
+ readFrame(context),
653
+ Math.max(progressDeadline - Date.now(), 1)
654
+ );
655
+ const response = this.parseResponseFrame(
656
+ rxFrame,
657
+ inFlight.map(item => item.seq)
658
+ );
659
+ if (!response) {
660
+ // ACK of an in-flight chunk; its FilesystemFile response follows.
661
+ } else if (response.type === 'FilesystemFile') {
662
+ const [expected] = inFlight;
663
+ const { message } = response;
664
+ if (
665
+ Number(message.processed_byte) !== expected.end ||
666
+ message.path !== path ||
667
+ Number(message.offset) !== expected.offset ||
668
+ Number(message.total_size) !== totalSize
669
+ ) {
670
+ throw new ProtocolV2LinkError(
671
+ 'frame',
672
+ `Protocol V2 file write response does not match chunk ${expected.offset}-${
673
+ expected.end
674
+ }: got offset ${String(message.offset)} processed_byte ${String(
675
+ message.processed_byte
676
+ )}`
677
+ );
678
+ }
679
+ inFlight.shift();
680
+ markProgress();
681
+ if (!failure) {
682
+ lastConfirmed = response;
683
+ pipeline.onResponse(response);
684
+ if (!sourceDone) {
685
+ await sendNextChunk();
686
+ }
687
+ }
688
+ } else if (response.type === 'Failure') {
689
+ inFlight.shift();
690
+ markProgress();
691
+ failure = failure ?? response;
692
+ sourceDone = true;
693
+ } else if (COMMON_TERMINAL_RESPONSE_TYPES.has(response.type)) {
694
+ throw new ProtocolV2LinkError(
695
+ 'frame',
696
+ `Protocol V2 file write pipeline received unexpected ${response.type}`
697
+ );
698
+ }
699
+ }
700
+ } catch (error) {
701
+ // Unread responses of in-flight chunks would answer the next call on this link, so a
702
+ // session or caller error raised with chunks in flight must invalidate it.
703
+ if (inFlight.length === 0 || error === transportFailure || isProtocolV2LinkError(error)) {
704
+ throw error;
705
+ }
706
+ throw new ProtocolV2LinkError(
707
+ 'frame',
708
+ `Protocol V2 file write pipeline aborted with chunks in flight: ${getErrorMessage(error)}`,
709
+ error
710
+ );
711
+ }
712
+
713
+ const terminal = failure ?? lastConfirmed;
714
+ if (!terminal) {
715
+ throw new ProtocolV2LinkError('frame', 'Protocol V2 file write pipeline ended unconfirmed');
716
+ }
717
+ return terminal;
718
+ }
489
719
  }
490
720
 
491
721
  export async function probeProtocolV2({
@@ -0,0 +1,3 @@
1
+ // @generated by scripts/protobuf-static.js. DO NOT EDIT.
2
+ import type { Root } from 'protobufjs/light';
3
+ export declare function installStaticCodecs(root: Root): boolean;