@onekeyfe/hd-transport 1.2.0-alpha.1 → 1.2.0-alpha.10

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 (44) hide show
  1. package/README.md +1 -1
  2. package/__tests__/messages.test.js +76 -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 +249 -81
  6. package/dist/constants.d.ts +4 -2
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/index.d.ts +501 -234
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +420 -72
  11. package/dist/protocols/index.d.ts +2 -0
  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 +1 -0
  16. package/dist/protocols/v2/decode.d.ts.map +1 -1
  17. package/dist/protocols/v2/link-manager.d.ts +35 -0
  18. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  19. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  20. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  21. package/dist/protocols/v2/session.d.ts +16 -4
  22. package/dist/protocols/v2/session.d.ts.map +1 -1
  23. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  24. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  25. package/dist/serialization/protobuf/messages.d.ts.map +1 -1
  26. package/dist/types/messages.d.ts +291 -155
  27. package/dist/types/messages.d.ts.map +1 -1
  28. package/dist/types/transport.d.ts +1 -1
  29. package/dist/types/transport.d.ts.map +1 -1
  30. package/messages-protocol-v2.json +599 -1033
  31. package/package.json +2 -2
  32. package/scripts/protobuf-build.sh +60 -142
  33. package/src/constants.ts +8 -2
  34. package/src/index.ts +8 -0
  35. package/src/protocols/index.ts +4 -1
  36. package/src/protocols/v2/constants.ts +1 -0
  37. package/src/protocols/v2/decode.ts +36 -17
  38. package/src/protocols/v2/link-manager.ts +160 -0
  39. package/src/protocols/v2/sequence-cursor.ts +10 -0
  40. package/src/protocols/v2/session.ts +80 -45
  41. package/src/protocols/v2/usb-transport-base.ts +194 -0
  42. package/src/serialization/protobuf/messages.ts +6 -1
  43. package/src/types/messages.ts +359 -185
  44. package/src/types/transport.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport",
3
- "version": "1.2.0-alpha.1",
3
+ "version": "1.2.0-alpha.10",
4
4
  "description": "Transport layer abstractions and utilities for OneKey hardware SDK.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -28,5 +28,5 @@
28
28
  "long": "^4.0.0",
29
29
  "protobufjs": "^6.11.2"
30
30
  },
31
- "gitHead": "4f4aae3a47be7bd54b4db4a10c400056784e01bd"
31
+ "gitHead": "db8135cc75ab2d8b87efa14e28517da892c7aa34"
32
32
  }
@@ -126,36 +126,6 @@ if [ -d "$SRC_PRO2_LATEST" ] && ls "$SRC_PRO2_LATEST"/messages*.proto 1>/dev/nul
126
126
  | sed 's/^option /\/\/ option /' \
127
127
  | grep -v ' reserved '
128
128
 
129
- if ! grep -q '^message TonSignData ' "$SRC_PRO2_LEGACY"/messages*.proto; then
130
- echo ''
131
- echo '// --- TON signData (kept until firmware-pro2 legacy proto exports it) ---'
132
- echo 'message TonSignData {'
133
- echo ' repeated uint32 address_n = 1;'
134
- echo ' required TonSignDataType type = 2;'
135
- echo ' required bytes payload = 3;'
136
- echo ' optional string schema = 4;'
137
- echo ' required string appdomain = 5;'
138
- echo ' required uint64 timestamp = 6;'
139
- echo ' optional string from_address = 7;'
140
- echo ' optional TonWalletVersion wallet_version = 8 [default=V4R2];'
141
- echo ' optional uint32 wallet_id = 9 [default=698983191];'
142
- echo ' optional TonWorkChain workchain = 10 [default=BASECHAIN];'
143
- echo ' optional bool is_bounceable = 11 [default=false];'
144
- echo ' optional bool is_testnet_only = 12 [default=false];'
145
- echo ''
146
- echo ' enum TonSignDataType {'
147
- echo ' TEXT = 0;'
148
- echo ' BINARY = 1;'
149
- echo ' CELL = 2;'
150
- echo ' }'
151
- echo '}'
152
- echo ''
153
- echo 'message TonSignedData {'
154
- echo ' optional bytes signature = 1;'
155
- echo ' optional bytes digest = 2;'
156
- echo '}'
157
- fi
158
-
159
129
  echo ''
160
130
  echo '// --- Protocol V2 system messages ---'
161
131
  grep -hv \
@@ -164,83 +134,50 @@ if [ -d "$SRC_PRO2_LATEST" ] && ls "$SRC_PRO2_LATEST"/messages*.proto 1>/dev/nul
164
134
  "$SRC_PRO2_LATEST"/messages*.proto \
165
135
  | grep -v ' reserved '
166
136
 
167
- if ! grep -q '^message GetOnboardingStatus ' "$SRC_PRO2_LATEST"/messages*.proto; then
168
- echo ''
169
- echo '// --- Onboarding status (kept until firmware-pro2 latest proto exports it) ---'
170
- echo 'enum OnboardingStep {'
171
- echo ' ONBOARDING_STEP_UNKNOWN = 0;'
172
- echo ' ONBOARDING_STEP_DEVICE_VERIFICATION = 1;'
173
- echo ' ONBOARDING_STEP_PERSONALIZATION = 2;'
174
- echo ' ONBOARDING_STEP_SETUP = 3;'
175
- echo ' ONBOARDING_STEP_FIRMWARE = 4;'
176
- echo '}'
177
- echo ''
178
- echo 'message GetOnboardingStatus {'
179
- echo '}'
180
- echo ''
181
- echo 'message OnboardingStatus {'
182
- echo ' message Setup {'
183
- echo ' message NewDevice {'
184
- echo ' optional bool seedcard_backup = 1;'
185
- echo ' }'
186
- echo ' message Restore {'
187
- echo ' optional bool mnemonic = 1;'
188
- echo ' optional bool seedcard = 2;'
189
- echo ' }'
190
- echo ' optional NewDevice new_device = 1;'
191
- echo ' optional Restore restore = 2;'
192
- echo ' }'
193
- echo ' required OnboardingStep step = 1;'
194
- echo ' optional Setup setup = 2;'
195
- echo ' optional uint32 detail_code = 3;'
196
- echo ' optional string detail_str = 4;'
197
- echo '}'
198
- fi
199
137
  } > "$TMP_PROTO"
200
138
 
201
- if ! grep -q 'MessageType_TonSignData' "$TMP_PROTO"; then
202
- node - "$TMP_PROTO" <<'NODE'
203
- const fs = require('fs');
204
-
205
- const protoPath = process.argv[2];
206
- const proto = fs.readFileSync(protoPath, 'utf8');
207
- const updated = proto.replace(
208
- /( MessageType_TonTxAck\s*=\s*11907[^\n]*;\n)/,
209
- `$1 MessageType_TonSignData = 11908 [(wire_in) = true];\n MessageType_TonSignedData = 11909 [(wire_out) = true];\n`
210
- );
211
-
212
- if (updated === proto) {
213
- throw new Error('Unable to insert TON signData MessageType entries into Pro2 schema');
214
- }
215
-
216
- fs.writeFileSync(protoPath, updated);
217
- NODE
218
- fi
219
-
220
- if ! grep -q 'MessageType_GetOnboardingStatus' "$TMP_PROTO"; then
221
- node - "$TMP_PROTO" <<'NODE'
139
+ node - "$TMP_PROTO" <<'NODE'
222
140
  const fs = require('fs');
223
141
 
224
142
  const protoPath = process.argv[2];
225
- const proto = fs.readFileSync(protoPath, 'utf8');
226
- const updated = proto.replace(
227
- /( MessageType_DeviceInfo\s*=\s*60601[^\n]*;\n)/,
228
- `$1 MessageType_GetOnboardingStatus = 60602;\n MessageType_OnboardingStatus = 60603;\n`
229
- );
230
-
231
- if (updated === proto) {
232
- throw new Error('Unable to insert onboarding MessageType entries into Pro2 schema');
233
- }
143
+ let proto = fs.readFileSync(protoPath, 'utf8');
144
+
145
+ const removeTopLevelMessage = (source, name) => {
146
+ const pattern = new RegExp(`(^|\\n)message\\s+${name}\\s*\\{`, 'm');
147
+ const match = pattern.exec(source);
148
+ if (!match) return source;
149
+
150
+ const start = match.index + (match[1] ? match[1].length : 0);
151
+ let index = start;
152
+ let depth = 0;
153
+ while (index < source.length) {
154
+ const char = source[index];
155
+ if (char === '{') {
156
+ depth += 1;
157
+ } else if (char === '}') {
158
+ depth -= 1;
159
+ if (depth === 0) {
160
+ index += 1;
161
+ while (index < source.length && /\s/.test(source[index])) index += 1;
162
+ return `${source.slice(0, start)}${source.slice(index)}`;
163
+ }
164
+ }
165
+ index += 1;
166
+ }
167
+ throw new Error(`Unterminated message block: ${name}`);
168
+ };
234
169
 
235
- fs.writeFileSync(protoPath, updated);
236
- NODE
237
- fi
170
+ [
171
+ 'Initialize',
172
+ 'GetFeatures',
173
+ 'OnekeyGetFeatures',
174
+ 'Features',
175
+ ].forEach(name => {
176
+ proto = removeTopLevelMessage(proto, name);
177
+ });
238
178
 
239
- node - "$TMP_PROTO" <<'NODE'
240
- const fs = require('fs');
179
+ fs.writeFileSync(protoPath, proto);
241
180
 
242
- const protoPath = process.argv[2];
243
- const proto = fs.readFileSync(protoPath, 'utf8');
244
181
  const messageNames = new Set(
245
182
  Array.from(proto.matchAll(/^\s*message\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/gm)).map(
246
183
  match => match[1]
@@ -252,17 +189,29 @@ const messageTypeNames = new Set(
252
189
  )
253
190
  );
254
191
  const requiredMessages = [
192
+ 'DeviceFactoryInfoSet',
193
+ 'DeviceFactoryInfoGet',
194
+ 'DeviceFactoryInfo',
195
+ 'ProtocolInfoRequest',
196
+ 'ProtocolInfo',
255
197
  'Ping',
256
198
  'Success',
257
199
  'Failure',
258
- 'DevReboot',
259
- 'DevGetDeviceInfo',
200
+ 'DeviceReboot',
201
+ 'DeviceInfoGet',
260
202
  'DeviceInfo',
261
- 'DevFirmwareUpdate',
262
- 'DevFirmwareInstallProgress',
263
- 'DevGetFirmwareUpdateStatus',
264
- 'DevFirmwareUpdateStatus',
265
- 'FilesystemFixPermission',
203
+ 'DeviceStatusGet',
204
+ 'DeviceStatus',
205
+ 'DevGetOnboardingStatus',
206
+ 'DevOnboardingStatus',
207
+ 'DeviceSessionGet',
208
+ 'DeviceSession',
209
+ 'DeviceSessionAskPin',
210
+ 'DeviceSessionPinResult',
211
+ 'DeviceFirmwareUpdateRequest',
212
+ 'DeviceFirmwareUpdateStatusGet',
213
+ 'DeviceFirmwareUpdateStatus',
214
+ 'FilesystemPermissionFix',
266
215
  'FilesystemPathInfo',
267
216
  'FilesystemPathInfoQuery',
268
217
  'FilesystemFile',
@@ -274,10 +223,7 @@ const requiredMessages = [
274
223
  'FilesystemDirMake',
275
224
  'FilesystemDirRemove',
276
225
  'FilesystemFormat',
277
- 'TonSignData',
278
- 'TonSignedData',
279
- 'GetOnboardingStatus',
280
- 'OnboardingStatus',
226
+ 'PortfolioUpdate',
281
227
  ];
282
228
  const missingMessages = requiredMessages.filter(name => !messageNames.has(name));
283
229
  const missingMessageTypes = requiredMessages.filter(name => !messageTypeNames.has(name));
@@ -288,39 +234,11 @@ if (missingMessages.length > 0 || missingMessageTypes.length > 0) {
288
234
  ', '
289
235
  )}], messageTypes=[${missingMessageTypes.join(
290
236
  ', '
291
- )}]. Make sure submodules/firmware-pro2 is checked out on branch dev_romloader_split ` +
292
- '(origin/dev_romloader_split), which contains the Filesystem*/DevFirmwareUpdate/DevReboot messages.'
237
+ )}]. Make sure submodules/firmware-pro2 is checked out on branch dev ` +
238
+ '(origin/dev), which contains the latest Protocol V2 Device*/Filesystem* messages.'
293
239
  );
294
240
  }
295
241
 
296
- // Provisional wire IDs injected by this script for messages the firmware proto does
297
- // not export yet (pending firmware confirmation, see
298
- // docs/protocol-v2-deviceinfo-field-gaps.md). If the firmware submodule starts
299
- // exporting these MessageType entries itself, the injection above is skipped and the
300
- // firmware-assigned IDs flow into TMP_PROTO — assert they still match the IDs the SDK
301
- // was built against, and fail loudly on any drift.
302
- const expectedInjectedIds = {
303
- TonSignData: 11908,
304
- TonSignedData: 11909,
305
- GetOnboardingStatus: 60602,
306
- OnboardingStatus: 60603,
307
- };
308
- const actualIds = {};
309
- for (const match of proto.matchAll(/^\s*MessageType_([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(\d+)/gm)) {
310
- actualIds[match[1]] = Number(match[2]);
311
- }
312
- const idMismatches = Object.entries(expectedInjectedIds)
313
- .filter(([name, id]) => name in actualIds && actualIds[name] !== id)
314
- .map(([name, id]) => `${name}: expected ${id}, firmware proto has ${actualIds[name]}`);
315
-
316
- if (idMismatches.length > 0) {
317
- throw new Error(
318
- `Protocol V2 injected MessageType wire IDs conflict with the firmware proto: ${idMismatches.join(
319
- '; '
320
- )}. Update the injected IDs in protobuf-build.sh and the registry section in ` +
321
- 'docs/protocol-v2-deviceinfo-field-gaps.md.'
322
- );
323
- }
324
242
  NODE
325
243
 
326
244
  npx pbjs -t json \
@@ -345,8 +263,8 @@ else
345
263
  # Unlike the optional Pro1 (legacy) section above, the firmware-pro2 submodule is
346
264
  # the only source of the Protocol V2 schema, so a missing checkout is a hard error.
347
265
  echo "firmware-pro2 latest protobuf schema not found at $SRC_PRO2_LATEST"
348
- echo "The Protocol V2 schema requires firmware-pro2 on branch dev_romloader_split."
266
+ echo "The Protocol V2 schema requires firmware-pro2 on branch dev."
349
267
  echo "Run: git submodule update --init submodules/firmware-pro2"
350
- echo "Then: git -C submodules/firmware-pro2 fetch origin dev_romloader_split && git -C submodules/firmware-pro2 checkout origin/dev_romloader_split"
268
+ echo "Then: git -C submodules/firmware-pro2 fetch origin dev && git -C submodules/firmware-pro2 checkout origin/dev"
351
269
  exit 1
352
270
  fi
package/src/constants.ts CHANGED
@@ -20,15 +20,21 @@ export const PROTOCOL_V1_ENVELOPE_HEADER_SIZE = 1 + 1 + PROTOCOL_V1_MESSAGE_HEAD
20
20
 
21
21
  // ---- Protocol V2 (Pro2 USB / BLE transports) ----
22
22
 
23
- /** Protocol V2 单帧最大长度,包含 header、payload 和 CRC;需容纳 4096 数据块及 protobuf 开销。 */
23
+ /** Protocol V2 单帧最大长度,包含 header、payload 和 CRC;需容纳 4000 数据块及 protobuf 开销。 */
24
24
  export const PROTOCOL_V2_FRAME_MAX_BYTES = 4608;
25
25
 
26
26
  /** WebUSB 下 FilesystemFileWrite 的文件数据分块大小。 */
27
- export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE = 4096;
27
+ export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE = 4000;
28
28
 
29
29
  /** BLE 下 FilesystemFileWrite 的文件数据分块大小。 */
30
30
  export const PROTOCOL_V2_BLE_FILE_CHUNK_SIZE = 1800;
31
31
 
32
+ /** BLE 下 FilesystemFileRead 的文件数据分块大小,受 Pro2 UART 1024B TX 缓冲限制。 */
33
+ export const PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE = 900;
34
+
35
+ /** Pro2 BLE/UART 接收 FIFO 必须容纳完整 Proto Link 帧。 */
36
+ export const PROTOCOL_V2_BLE_FRAME_MAX_BYTES = 2048;
37
+
32
38
  /** @deprecated 使用按传输区分的 PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE / PROTOCOL_V2_BLE_FILE_CHUNK_SIZE。 */
33
39
  export const PROTOCOL_V2_FILE_CHUNK_SIZE = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
34
40
 
package/src/index.ts CHANGED
@@ -10,8 +10,11 @@ import {
10
10
  } from './serialization';
11
11
  import { PROTOCOL_V2_SYS_MESSAGE_THRESHOLD, ProtocolV1, ProtocolV2 } from './protocols';
12
12
  import * as protocolV2Codec from './protocols/v2';
13
+ import { ProtocolV2LinkManager } from './protocols/v2/link-manager';
14
+ import { ProtocolV2UsbTransportBase } from './protocols/v2/usb-transport-base';
13
15
  import {
14
16
  ProtocolV2FrameAssembler,
17
+ ProtocolV2SequenceCursor,
15
18
  ProtocolV2Session,
16
19
  bytesToHex,
17
20
  concatUint8Arrays,
@@ -49,6 +52,8 @@ export * from './protocols';
49
52
  export * as protocolV1 from './protocols/v1';
50
53
  export * as protocolV2 from './protocols/v2';
51
54
  export * from './protocols/v2/session';
55
+ export * from './protocols/v2/link-manager';
56
+ export * from './protocols/v2/usb-transport-base';
52
57
 
53
58
  export default {
54
59
  check,
@@ -58,7 +63,10 @@ export default {
58
63
  ProtocolV2,
59
64
  PROTOCOL_V2_SYS_MESSAGE_THRESHOLD,
60
65
  ProtocolV2FrameAssembler,
66
+ ProtocolV2LinkManager,
67
+ ProtocolV2SequenceCursor,
61
68
  ProtocolV2Session,
69
+ ProtocolV2UsbTransportBase,
62
70
  bytesToHex,
63
71
  concatUint8Arrays,
64
72
  createMessageFromName,
@@ -6,7 +6,7 @@ import { decodeMessage as decodeV1Message } from './v1/receive';
6
6
  import { createMessageFromName, createMessageFromType } from '../serialization/protobuf/messages';
7
7
  import { encode as encodeProtobuf } from '../serialization/protobuf/encode';
8
8
  import { decode as decodeProtobuf } from '../serialization/protobuf/decode';
9
- import { decodeFrame as decodeV2Frame, encodeProtobufFrame } from './v2';
9
+ import { decodeFrame as decodeV2Frame, encodeProtobufFrame, isAckFrame } from './v2';
10
10
 
11
11
  import type { Root } from 'protobufjs/light';
12
12
  import type { ProtocolV2DebugLogger } from './v2';
@@ -38,6 +38,7 @@ const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas)
38
38
  };
39
39
 
40
40
  const PROTOCOL_V2_LEGACY_DECODE_ALLOWLIST = new Set([
41
+ 'Failure',
41
42
  'ButtonRequest',
42
43
  'EntropyRequest',
43
44
  'PinMatrixRequest',
@@ -73,6 +74,8 @@ export const ProtocolV1 = {
73
74
  };
74
75
 
75
76
  export const ProtocolV2 = {
77
+ isAckFrame,
78
+
76
79
  encodeFrame(
77
80
  schemas: ProtocolV2Schemas,
78
81
  name: string,
@@ -4,3 +4,4 @@ export const PROTO_HEAD_CRC_SIZE = 8;
4
4
  export const CRC8_INIT = 0x30;
5
5
  export const PACKET_SIZE = 4096;
6
6
  export const PROTO_DATA_TYPE_PACKET = 0;
7
+ export const PROTO_DATA_TYPE_ACK = 1;
@@ -1,4 +1,4 @@
1
- import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
1
+ import { PROTO_DATA_TYPE_ACK, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants';
2
2
  import { crc8 } from './crc8';
3
3
  import { logProtocolV2Debug } from './debug';
4
4
 
@@ -13,20 +13,7 @@ export interface ProtoV2Frame {
13
13
  seq: number;
14
14
  }
15
15
 
16
- /**
17
- * Parse and validate a Protocol V2 response frame.
18
- *
19
- * Validates:
20
- * - SOF byte (0x5A)
21
- * - Header CRC (bytes 0-2)
22
- * - Frame CRC (full frame except last byte)
23
- *
24
- * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
25
- */
26
- export function decodeFrame(
27
- data: Uint8Array,
28
- debugOptions?: ProtocolV2FrameDebugOptions
29
- ): ProtoV2Frame {
16
+ function validateFrame(data: Uint8Array): number {
30
17
  if (data.length < PROTO_HEAD_CRC_SIZE) {
31
18
  throw new Error(`Protocol V2 frame too short: ${data.length} bytes`);
32
19
  }
@@ -43,7 +30,6 @@ export function decodeFrame(
43
30
  throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`);
44
31
  }
45
32
 
46
- // Verify pre-header CRC (bytes 0-2)
47
33
  const expectedHeaderCrc = crc8(data, 3);
48
34
  if (data[3] !== expectedHeaderCrc) {
49
35
  throw new Error(
@@ -53,7 +39,6 @@ export function decodeFrame(
53
39
  );
54
40
  }
55
41
 
56
- // Verify frame CRC (all bytes except last)
57
42
  const expectedFrameCrc = crc8(data, frameLen - 1);
58
43
  if (data[frameLen - 1] !== expectedFrameCrc) {
59
44
  throw new Error(
@@ -63,6 +48,40 @@ export function decodeFrame(
63
48
  );
64
49
  }
65
50
 
51
+ return frameLen;
52
+ }
53
+
54
+ export function isAckFrame(data: Uint8Array): boolean {
55
+ // eslint-disable-next-line no-bitwise
56
+ if (data.length < 6 || (data[5] & 0x03) !== PROTO_DATA_TYPE_ACK) {
57
+ return false;
58
+ }
59
+
60
+ const frameLen = validateFrame(data);
61
+ if (frameLen !== PROTO_HEAD_CRC_SIZE) {
62
+ throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`);
63
+ }
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Parse and validate a Protocol V2 response frame.
69
+ *
70
+ * Validates:
71
+ * - SOF byte (0x5A)
72
+ * - Header CRC (bytes 0-2)
73
+ * - Frame CRC (full frame except last byte)
74
+ *
75
+ * Returns the decoded messageTypeId, raw protobuf payload, and sequence number.
76
+ */
77
+ export function decodeFrame(
78
+ data: Uint8Array,
79
+ debugOptions?: ProtocolV2FrameDebugOptions
80
+ ): ProtoV2Frame {
81
+ const frameLen = validateFrame(data);
82
+ const expectedHeaderCrc = data[3];
83
+ const expectedFrameCrc = data[frameLen - 1];
84
+
66
85
  const seq = data[6];
67
86
  // Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte
68
87
  const payloadData = data.slice(7, frameLen - 1);
@@ -0,0 +1,160 @@
1
+ import { ProtocolV2SequenceCursor } from './sequence-cursor';
2
+ import { ProtocolV2Session, getErrorMessage } from './session';
3
+
4
+ import type { MessageFromOneKey, TransportCallOptions } from '../../types';
5
+ import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session';
6
+
7
+ export type ProtocolV2LinkErrorClassification = 'link-fatal' | 'recoverable';
8
+
9
+ export interface ProtocolV2LinkAdapter {
10
+ router: number;
11
+ maxFrameBytes?: number;
12
+ generation: number;
13
+ prepareCall(context: ProtocolV2CallContext): Promise<void> | void;
14
+ writeFrame(frame: Uint8Array, context: ProtocolV2CallContext): Promise<void>;
15
+ readFrame(context: ProtocolV2CallContext): Promise<Uint8Array>;
16
+ reset(reason: string): Promise<void> | void;
17
+ logger?: ProtocolV2SessionOptions['logger'];
18
+ logPrefix?: string;
19
+ createTimeoutError?: ProtocolV2SessionOptions['createTimeoutError'];
20
+ }
21
+
22
+ export type ProtocolV2LinkManagerOptions<Key> = {
23
+ getSchemas: () => ProtocolV2Schemas;
24
+ classifyError: (error: unknown) => ProtocolV2LinkErrorClassification;
25
+ onLinkInvalidated?: (key: Key, reason: string) => Promise<void> | void;
26
+ };
27
+
28
+ type ProtocolV2Link = {
29
+ adapter: ProtocolV2LinkAdapter;
30
+ session: ProtocolV2Session;
31
+ state: {
32
+ invalidatedReason?: string;
33
+ };
34
+ };
35
+
36
+ export class ProtocolV2LinkManager<Key> {
37
+ private readonly links = new Map<Key, ProtocolV2Link>();
38
+
39
+ private readonly sequences = new Map<Key, ProtocolV2SequenceCursor>();
40
+
41
+ private readonly callQueues = new Map<Key, Promise<unknown>>();
42
+
43
+ private readonly options: ProtocolV2LinkManagerOptions<Key>;
44
+
45
+ constructor(options: ProtocolV2LinkManagerOptions<Key>) {
46
+ this.options = options;
47
+ }
48
+
49
+ call(
50
+ key: Key,
51
+ createAdapter: () => ProtocolV2LinkAdapter,
52
+ name: string,
53
+ data: Record<string, unknown>,
54
+ options?: TransportCallOptions
55
+ ): Promise<MessageFromOneKey> {
56
+ const run = () => this.executeCall(key, createAdapter, name, data, options);
57
+ const previous = this.callQueues.get(key) ?? Promise.resolve();
58
+ const result = previous.then(run, run);
59
+ const queue = result.catch(() => undefined);
60
+ this.callQueues.set(key, queue);
61
+ result
62
+ .then(
63
+ () => this.clearSettledCallQueue(key, queue),
64
+ () => this.clearSettledCallQueue(key, queue)
65
+ )
66
+ .catch(() => undefined);
67
+ return result;
68
+ }
69
+
70
+ async invalidateLink(key: Key, reason: string): Promise<void> {
71
+ const link = this.links.get(key);
72
+ if (!link) return;
73
+
74
+ this.links.delete(key);
75
+ link.state.invalidatedReason = reason;
76
+ await link.adapter.reset(reason);
77
+ await this.options.onLinkInvalidated?.(key, reason);
78
+ }
79
+
80
+ async invalidateAllLinks(reason: string): Promise<void> {
81
+ await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason)));
82
+ }
83
+
84
+ async dispose(reason: string): Promise<void> {
85
+ await this.invalidateAllLinks(reason);
86
+ this.sequences.clear();
87
+ this.callQueues.clear();
88
+ }
89
+
90
+ private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link {
91
+ const existing = this.links.get(key);
92
+ if (existing) return existing;
93
+
94
+ const adapter = createAdapter();
95
+ const state: ProtocolV2Link['state'] = {};
96
+ const assertLinkActive = () => {
97
+ if (state.invalidatedReason) {
98
+ throw new Error(state.invalidatedReason);
99
+ }
100
+ };
101
+ let sequenceCursor = this.sequences.get(key);
102
+ if (!sequenceCursor) {
103
+ sequenceCursor = new ProtocolV2SequenceCursor();
104
+ this.sequences.set(key, sequenceCursor);
105
+ }
106
+ const session = new ProtocolV2Session({
107
+ schemas: this.options.getSchemas(),
108
+ router: adapter.router,
109
+ maxFrameBytes: adapter.maxFrameBytes,
110
+ generation: adapter.generation,
111
+ sequenceCursor,
112
+ prepareCall: async context => {
113
+ assertLinkActive();
114
+ await adapter.prepareCall(context);
115
+ assertLinkActive();
116
+ },
117
+ writeFrame: async (frame, context) => {
118
+ assertLinkActive();
119
+ await adapter.writeFrame(frame, context);
120
+ assertLinkActive();
121
+ },
122
+ readFrame: async context => {
123
+ assertLinkActive();
124
+ const frame = await adapter.readFrame(context);
125
+ assertLinkActive();
126
+ return frame;
127
+ },
128
+ logger: adapter.logger,
129
+ logPrefix: adapter.logPrefix,
130
+ createTimeoutError: adapter.createTimeoutError,
131
+ });
132
+ const link = { adapter, session, state };
133
+ this.links.set(key, link);
134
+ return link;
135
+ }
136
+
137
+ private async executeCall(
138
+ key: Key,
139
+ createAdapter: () => ProtocolV2LinkAdapter,
140
+ name: string,
141
+ data: Record<string, unknown>,
142
+ options?: TransportCallOptions
143
+ ): Promise<MessageFromOneKey> {
144
+ try {
145
+ return await this.getOrCreateLink(key, createAdapter).session.call(name, data, options);
146
+ } catch (error) {
147
+ if (this.options.classifyError(error) === 'link-fatal') {
148
+ const errorMessage = getErrorMessage(error) || 'unknown error';
149
+ await this.invalidateLink(key, `Protocol V2 link-fatal error: ${errorMessage}`);
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ private clearSettledCallQueue(key: Key, queue: Promise<unknown>) {
156
+ if (this.callQueues.get(key) === queue) {
157
+ this.callQueues.delete(key);
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,10 @@
1
+ import { nextProtoSeq } from './encode';
2
+
3
+ export class ProtocolV2SequenceCursor {
4
+ private current = 0;
5
+
6
+ next(): number {
7
+ this.current = nextProtoSeq(this.current);
8
+ return this.current;
9
+ }
10
+ }