@onekeyfe/hd-transport 1.2.0-alpha.7 → 1.2.0-alpha.9

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 (41) hide show
  1. package/__tests__/messages.test.js +62 -0
  2. package/__tests__/protocol-v2-link-manager.test.js +326 -0
  3. package/__tests__/protocol-v2-usb-transport-base.test.js +296 -0
  4. package/__tests__/protocol-v2.test.js +112 -4
  5. package/dist/constants.d.ts +2 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/index.d.ts +164 -6
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +352 -29
  10. package/dist/protocols/index.d.ts +2 -0
  11. package/dist/protocols/index.d.ts.map +1 -1
  12. package/dist/protocols/v2/constants.d.ts +1 -0
  13. package/dist/protocols/v2/constants.d.ts.map +1 -1
  14. package/dist/protocols/v2/decode.d.ts +1 -0
  15. package/dist/protocols/v2/decode.d.ts.map +1 -1
  16. package/dist/protocols/v2/link-manager.d.ts +35 -0
  17. package/dist/protocols/v2/link-manager.d.ts.map +1 -0
  18. package/dist/protocols/v2/sequence-cursor.d.ts +5 -0
  19. package/dist/protocols/v2/sequence-cursor.d.ts.map +1 -0
  20. package/dist/protocols/v2/session.d.ts +15 -3
  21. package/dist/protocols/v2/session.d.ts.map +1 -1
  22. package/dist/protocols/v2/usb-transport-base.d.ts +31 -0
  23. package/dist/protocols/v2/usb-transport-base.d.ts.map +1 -0
  24. package/dist/types/messages.d.ts +51 -1
  25. package/dist/types/messages.d.ts.map +1 -1
  26. package/dist/types/transport.d.ts +1 -1
  27. package/dist/types/transport.d.ts.map +1 -1
  28. package/messages-protocol-v2.json +134 -3
  29. package/package.json +2 -2
  30. package/scripts/protobuf-build.sh +46 -0
  31. package/src/constants.ts +6 -0
  32. package/src/index.ts +8 -0
  33. package/src/protocols/index.ts +3 -1
  34. package/src/protocols/v2/constants.ts +1 -0
  35. package/src/protocols/v2/decode.ts +36 -17
  36. package/src/protocols/v2/link-manager.ts +160 -0
  37. package/src/protocols/v2/sequence-cursor.ts +10 -0
  38. package/src/protocols/v2/session.ts +76 -40
  39. package/src/protocols/v2/usb-transport-base.ts +194 -0
  40. package/src/types/messages.ts +65 -1
  41. package/src/types/transport.ts +1 -1
@@ -107,8 +107,6 @@
107
107
  "MessageType_FirmwareHash": 89,
108
108
  "MessageType_UnlockPath": 93,
109
109
  "MessageType_UnlockedPathRequest": 94,
110
- "MessageType_UnLockDevice": 10030,
111
- "MessageType_UnLockDeviceResponse": 10031,
112
110
  "MessageType_SetU2FCounter": 63,
113
111
  "MessageType_GetNextU2FCounter": 80,
114
112
  "MessageType_NextU2FCounter": 81,
@@ -197,6 +195,7 @@
197
195
  "MessageType_EthereumGnosisSafeTxAck": 20118,
198
196
  "MessageType_EthereumGnosisSafeTxRequest": 20119,
199
197
  "MessageType_EthereumSignTxEIP7702OneKey": 20120,
198
+ "MessageType_EthereumSignTypedDataQR": 20121,
200
199
  "MessageType_NEMGetAddress": 67,
201
200
  "MessageType_NEMAddress": 68,
202
201
  "MessageType_NEMSignTx": 69,
@@ -454,6 +453,8 @@
454
453
  "MessageType_UiviewConfirmTxRequest": 30202,
455
454
  "MessageType_UiviewConfirmSignMessageRequest": 30203,
456
455
  "MessageType_UiviewResponse": 30204,
456
+ "MessageType_UnLockDevice": 10030,
457
+ "MessageType_UnLockDeviceResponse": 10031,
457
458
  "MessageType_DeviceFactoryInfoSet": 60000,
458
459
  "MessageType_DeviceFactoryInfoGet": 60001,
459
460
  "MessageType_DeviceFactoryInfo": 60002,
@@ -478,6 +479,12 @@
478
479
  "MessageType_Wallpaper": 60432,
479
480
  "MessageType_DeviceInfoGet": 60600,
480
481
  "MessageType_DeviceInfo": 60601,
482
+ "MessageType_DeviceStatusGet": 60602,
483
+ "MessageType_DeviceStatus": 60603,
484
+ "MessageType_DevGetOnboardingStatus": 60604,
485
+ "MessageType_DevOnboardingStatus": 60605,
486
+ "MessageType_DeviceSessionGet": 60606,
487
+ "MessageType_DeviceSession": 60607,
481
488
  "MessageType_FilesystemPermissionFix": 60800,
482
489
  "MessageType_FilesystemPathInfo": 60801,
483
490
  "MessageType_FilesystemPathInfoQuery": 60802,
@@ -492,7 +499,8 @@
492
499
  "MessageType_FilesystemFormat": 60811,
493
500
  "MessageType_DeviceFirmwareUpdateRequest": 61000,
494
501
  "MessageType_DeviceFirmwareUpdateStatusGet": 61001,
495
- "MessageType_DeviceFirmwareUpdateStatus": 61002
502
+ "MessageType_DeviceFirmwareUpdateStatus": 61002,
503
+ "MessageType_PortfolioUpdate": 61200
496
504
  },
497
505
  "reserved": [
498
506
  [90, 92],
@@ -5395,6 +5403,37 @@
5395
5403
  }
5396
5404
  }
5397
5405
  },
5406
+ "EthereumSignTypedDataQR": {
5407
+ "fields": {
5408
+ "address_n": {
5409
+ "rule": "repeated",
5410
+ "type": "uint32",
5411
+ "id": 1,
5412
+ "options": {
5413
+ "packed": false
5414
+ }
5415
+ },
5416
+ "json_data": {
5417
+ "type": "bytes",
5418
+ "id": 2
5419
+ },
5420
+ "chain_id": {
5421
+ "type": "uint64",
5422
+ "id": 3
5423
+ },
5424
+ "metamask_v4_compat": {
5425
+ "type": "bool",
5426
+ "id": 4,
5427
+ "options": {
5428
+ "default": true
5429
+ }
5430
+ },
5431
+ "request_id": {
5432
+ "type": "bytes",
5433
+ "id": 5
5434
+ }
5435
+ }
5436
+ },
5398
5437
  "EthereumGetPublicKeyOneKey": {
5399
5438
  "fields": {
5400
5439
  "address_n": {
@@ -11120,6 +11159,26 @@
11120
11159
  }
11121
11160
  }
11122
11161
  },
11162
+ "ViewRawData": {
11163
+ "fields": {
11164
+ "initial_data": {
11165
+ "rule": "required",
11166
+ "type": "string",
11167
+ "id": 1
11168
+ },
11169
+ "placeholder": {
11170
+ "rule": "required",
11171
+ "type": "uint32",
11172
+ "id": 2
11173
+ }
11174
+ }
11175
+ },
11176
+ "ViewSignLayout": {
11177
+ "values": {
11178
+ "LayoutDefault": 0,
11179
+ "LayoutSafeTxCreate": 1
11180
+ }
11181
+ },
11123
11182
  "ViewSignPage": {
11124
11183
  "fields": {
11125
11184
  "title": {
@@ -11139,6 +11198,24 @@
11139
11198
  "tip": {
11140
11199
  "type": "ViewTip",
11141
11200
  "id": 4
11201
+ },
11202
+ "raw_data": {
11203
+ "type": "ViewRawData",
11204
+ "id": 5
11205
+ },
11206
+ "slide_to_confirm": {
11207
+ "type": "bool",
11208
+ "id": 6,
11209
+ "options": {
11210
+ "default": true
11211
+ }
11212
+ },
11213
+ "layout": {
11214
+ "type": "ViewSignLayout",
11215
+ "id": 7,
11216
+ "options": {
11217
+ "default": "LayoutDefault"
11218
+ }
11142
11219
  }
11143
11220
  }
11144
11221
  },
@@ -11892,6 +11969,46 @@
11892
11969
  }
11893
11970
  }
11894
11971
  },
11972
+ "DeviceStatusGet": {
11973
+ "fields": {}
11974
+ },
11975
+ "DevOnboardingStage": {
11976
+ "values": {
11977
+ "DEV_ONBOARDING_STAGE_UNKNOWN": 0,
11978
+ "DEV_ONBOARDING_STAGE_SAFETY_CHECK": 1,
11979
+ "DEV_ONBOARDING_STAGE_PERSONALIZATION": 2,
11980
+ "DEV_ONBOARDING_STAGE_SELECT_SETUP_METHOD": 3,
11981
+ "DEV_ONBOARDING_STAGE_NEW_DEVICE": 4,
11982
+ "DEV_ONBOARDING_STAGE_SELECT_RESTORE_METHOD": 5,
11983
+ "DEV_ONBOARDING_STAGE_RESTORE_MNEMONIC": 6,
11984
+ "DEV_ONBOARDING_STAGE_RESTORE_SEEDCARD": 7,
11985
+ "DEV_ONBOARDING_STAGE_WALLET_READY": 8,
11986
+ "DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP_PROMPT": 9,
11987
+ "DEV_ONBOARDING_STAGE_SELECT_SEEDCARD_BACKUP_METHOD": 10,
11988
+ "DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP": 11,
11989
+ "DEV_ONBOARDING_STAGE_DONE": 12
11990
+ }
11991
+ },
11992
+ "DevGetOnboardingStatus": {
11993
+ "fields": {}
11994
+ },
11995
+ "DevOnboardingStatus": {
11996
+ "fields": {
11997
+ "stage": {
11998
+ "rule": "required",
11999
+ "type": "DevOnboardingStage",
12000
+ "id": 1
12001
+ },
12002
+ "status_code": {
12003
+ "type": "uint32",
12004
+ "id": 2
12005
+ },
12006
+ "detail_code": {
12007
+ "type": "uint32",
12008
+ "id": 3
12009
+ }
12010
+ }
12011
+ },
11895
12012
  "FilesystemPermissionFix": {
11896
12013
  "fields": {}
11897
12014
  },
@@ -12105,6 +12222,20 @@
12105
12222
  }
12106
12223
  },
12107
12224
  "FilesystemFormat": {
12225
+ "fields": {
12226
+ "data": {
12227
+ "rule": "required",
12228
+ "type": "bool",
12229
+ "id": 1
12230
+ },
12231
+ "user": {
12232
+ "rule": "required",
12233
+ "type": "bool",
12234
+ "id": 2
12235
+ }
12236
+ }
12237
+ },
12238
+ "PortfolioUpdate": {
12108
12239
  "fields": {}
12109
12240
  },
12110
12241
  "google": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-transport",
3
- "version": "1.2.0-alpha.7",
3
+ "version": "1.2.0-alpha.9",
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": "001372047f518aaeed5fcbe87b990b8b619a9a65"
31
+ "gitHead": "f90ef9921b742f68cce9f4e7a0425e45e8ae7ec9"
32
32
  }
@@ -167,6 +167,37 @@ const removeTopLevelMessage = (source, name) => {
167
167
  throw new Error(`Unterminated message block: ${name}`);
168
168
  };
169
169
 
170
+ const restoreTemporaryMessageType = (name, id) => {
171
+ const activePattern = new RegExp(
172
+ `^\\s*MessageType_${name}\\s*=\\s*(\\d+)\\b[^;]*;`,
173
+ 'gm'
174
+ );
175
+ const activeMatches = Array.from(proto.matchAll(activePattern));
176
+ if (activeMatches.length > 1) {
177
+ throw new Error(`Protocol V2 schema defines MessageType_${name} more than once`);
178
+ }
179
+ if (activeMatches.length === 1) {
180
+ if (Number(activeMatches[0][1]) !== id) {
181
+ throw new Error(
182
+ `Protocol V2 schema defines MessageType_${name} with id ${activeMatches[0][1]}, expected ${id}`
183
+ );
184
+ }
185
+ return;
186
+ }
187
+
188
+ const commentedPattern = new RegExp(
189
+ `^(\\s*)//\\s*(MessageType_${name}\\s*=\\s*${id}\\b[^;]*;)`,
190
+ 'gm'
191
+ );
192
+ const commentedMatches = Array.from(proto.matchAll(commentedPattern));
193
+ if (commentedMatches.length !== 1) {
194
+ throw new Error(
195
+ `Protocol V2 schema must contain exactly one commented MessageType_${name} = ${id}`
196
+ );
197
+ }
198
+ proto = proto.replace(commentedPattern, '$1$2');
199
+ };
200
+
170
201
  [
171
202
  'Initialize',
172
203
  'GetFeatures',
@@ -175,6 +206,12 @@ const removeTopLevelMessage = (source, name) => {
175
206
  ].forEach(name => {
176
207
  proto = removeTopLevelMessage(proto, name);
177
208
  });
209
+
210
+ // firmware-pro2 暂时注释了旧的解锁入口,但 SDK/app 仍需使用原协议 ID。
211
+ // 只恢复 UnLockDevice,不恢复已由 DeviceSessionGet 取代的 passphrase 消息 ID。
212
+ restoreTemporaryMessageType('UnLockDevice', 10030);
213
+ restoreTemporaryMessageType('UnLockDeviceResponse', 10031);
214
+
178
215
  fs.writeFileSync(protoPath, proto);
179
216
 
180
217
  const messageNames = new Set(
@@ -199,6 +236,14 @@ const requiredMessages = [
199
236
  'DeviceReboot',
200
237
  'DeviceInfoGet',
201
238
  'DeviceInfo',
239
+ 'DeviceStatusGet',
240
+ 'DeviceStatus',
241
+ 'DevGetOnboardingStatus',
242
+ 'DevOnboardingStatus',
243
+ 'DeviceSessionGet',
244
+ 'DeviceSession',
245
+ 'UnLockDevice',
246
+ 'UnLockDeviceResponse',
202
247
  'DeviceFirmwareUpdateRequest',
203
248
  'DeviceFirmwareUpdateStatusGet',
204
249
  'DeviceFirmwareUpdateStatus',
@@ -214,6 +259,7 @@ const requiredMessages = [
214
259
  'FilesystemDirMake',
215
260
  'FilesystemDirRemove',
216
261
  'FilesystemFormat',
262
+ 'PortfolioUpdate',
217
263
  ];
218
264
  const missingMessages = requiredMessages.filter(name => !messageNames.has(name));
219
265
  const missingMessageTypes = requiredMessages.filter(name => !messageTypeNames.has(name));
package/src/constants.ts CHANGED
@@ -29,6 +29,12 @@ export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE = 4000;
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';
@@ -74,6 +74,8 @@ export const ProtocolV1 = {
74
74
  };
75
75
 
76
76
  export const ProtocolV2 = {
77
+ isAckFrame,
78
+
77
79
  encodeFrame(
78
80
  schemas: ProtocolV2Schemas,
79
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
+ }