@anycon/protocol 0.1.0

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 (49) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +146 -0
  3. package/README.zh-CN.md +118 -0
  4. package/dist/constants.d.ts +151 -0
  5. package/dist/constants.d.ts.map +1 -0
  6. package/dist/constants.js +159 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/crypto.d.ts +202 -0
  9. package/dist/crypto.d.ts.map +1 -0
  10. package/dist/crypto.js +218 -0
  11. package/dist/crypto.js.map +1 -0
  12. package/dist/envelope.d.ts +105 -0
  13. package/dist/envelope.d.ts.map +1 -0
  14. package/dist/envelope.js +390 -0
  15. package/dist/envelope.js.map +1 -0
  16. package/dist/errors.d.ts +5 -0
  17. package/dist/errors.d.ts.map +1 -0
  18. package/dist/errors.js +5 -0
  19. package/dist/errors.js.map +1 -0
  20. package/dist/generated.d.ts +2007 -0
  21. package/dist/generated.d.ts.map +1 -0
  22. package/dist/generated.js +4 -0
  23. package/dist/generated.js.map +1 -0
  24. package/dist/index.d.ts +17 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +16 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/method-types.d.ts +293 -0
  29. package/dist/method-types.d.ts.map +1 -0
  30. package/dist/method-types.js +2 -0
  31. package/dist/method-types.js.map +1 -0
  32. package/dist/rpc.d.ts +70 -0
  33. package/dist/rpc.d.ts.map +1 -0
  34. package/dist/rpc.js +310 -0
  35. package/dist/rpc.js.map +1 -0
  36. package/package.json +62 -0
  37. package/src/constants.ts +173 -0
  38. package/src/crypto.ts +427 -0
  39. package/src/envelope.ts +469 -0
  40. package/src/errors.ts +4 -0
  41. package/src/generated.ts +2094 -0
  42. package/src/index.ts +18 -0
  43. package/src/method-types.ts +183 -0
  44. package/src/rpc.ts +377 -0
  45. package/wasm/THIRD-PARTY-LICENSES.txt +1957 -0
  46. package/wasm/anycon_crypto.d.ts +248 -0
  47. package/wasm/anycon_crypto.js +1250 -0
  48. package/wasm/anycon_crypto_bg.wasm +0 -0
  49. package/wasm/anycon_crypto_bg.wasm.d.ts +48 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * 外层信封与控制消息的 msgpack 编解码。手写,和 `crates/protocol/src/{envelope,control}.rs`
3
+ * 的 `rmp_serde::to_vec_named` 输出字节一致:
4
+ *
5
+ * - 信封是字符串键的 map,键序 `v, from, to, kind, seq, body`;`body` 是 bin(`Uint8Array`),不是数组。
6
+ * - 控制消息是内部标签 `type` 的 map:`type` 在最前,其余字段按 Rust 声明顺序;
7
+ * `skip_serializing_if = Option::is_none` 的字段为 null / undefined 时整键省略。
8
+ *
9
+ * 字节级一致由 `tests/vectors/*.hex`(`tools/gen-vectors` 用 Rust 生成)守住。
10
+ */
11
+ import { decode, encode } from '@msgpack/msgpack'
12
+
13
+ import { ENVELOPE_VERSION } from './constants.js'
14
+ import { ProtocolError } from './errors.js'
15
+ import type { Control, EnrollFrame, Kind } from './generated.js'
16
+
17
+ export type { Control, EnrollFrame, Kind }
18
+
19
+ /** 外层信封。 */
20
+ export interface Envelope {
21
+ /** 信封版本,见 `ENVELOPE_VERSION`。 */
22
+ v: number
23
+ /** 发送设备。hub 校验它等于当前连接的设备 id。 */
24
+ from: string
25
+ /** 目标设备。发给 hub 的 `control` 填空字符串。 */
26
+ to: string
27
+ kind: Kind
28
+ /** 发送方单调递增。 */
29
+ seq: number
30
+ body: Uint8Array
31
+ }
32
+
33
+ /** 全部信封种类,对应 envelope.rs 的 `Kind`。 */
34
+ export const ENVELOPE_KINDS: readonly Kind[] = ['handshake', 'data', 'notify', 'control']
35
+
36
+ const KINDS: ReadonlySet<string> = new Set<string>(ENVELOPE_KINDS)
37
+
38
+ export function isKind(value: unknown): value is Kind {
39
+ return typeof value === 'string' && KINDS.has(value)
40
+ }
41
+
42
+ /** 构造信封,`v` 默认当前版本。 */
43
+ export function makeEnvelope(
44
+ from: string,
45
+ to: string,
46
+ kind: Kind,
47
+ seq: number,
48
+ body: Uint8Array,
49
+ ): Envelope {
50
+ return { v: ENVELOPE_VERSION, from, to, kind, seq, body }
51
+ }
52
+
53
+ export function encodeEnvelope(env: Envelope): Uint8Array {
54
+ if (!Number.isSafeInteger(env.seq) || env.seq < 0) {
55
+ throw new ProtocolError(`envelope seq must be a non-negative safe integer, got ${env.seq}`)
56
+ }
57
+ if (!Number.isInteger(env.v) || env.v < 0 || env.v > 255) {
58
+ throw new ProtocolError(`envelope v must fit in u8, got ${env.v}`)
59
+ }
60
+ if (!(env.body instanceof Uint8Array)) {
61
+ throw new ProtocolError('envelope body must be a Uint8Array')
62
+ }
63
+ // 键序固定,和 Rust 结构体声明顺序一致。
64
+ return encode({
65
+ v: env.v,
66
+ from: env.from,
67
+ to: env.to,
68
+ kind: env.kind,
69
+ seq: env.seq,
70
+ body: env.body,
71
+ })
72
+ }
73
+
74
+ export function decodeEnvelope(bytes: Uint8Array): Envelope {
75
+ let raw: unknown
76
+ try {
77
+ raw = decode(bytes)
78
+ } catch (cause) {
79
+ throw new ProtocolError('envelope is not valid msgpack', { cause })
80
+ }
81
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
82
+ throw new ProtocolError('envelope must be a msgpack map')
83
+ }
84
+ const m = raw as Record<string, unknown>
85
+ const { v, from, to, kind, seq, body } = m
86
+ if (typeof v !== 'number') throw new ProtocolError('envelope.v must be a number')
87
+ if (typeof from !== 'string') throw new ProtocolError('envelope.from must be a string')
88
+ if (typeof to !== 'string') throw new ProtocolError('envelope.to must be a string')
89
+ if (!isKind(kind)) throw new ProtocolError(`envelope.kind is unknown: ${String(kind)}`)
90
+ if (typeof seq !== 'number') throw new ProtocolError('envelope.seq must be a number')
91
+ if (!(body instanceof Uint8Array)) throw new ProtocolError('envelope.body must be bin')
92
+ // body 是输入 buffer 的视图;拷一份,避免调用方复用 WebSocket 缓冲时被改写。
93
+ return { v, from, to, kind, seq, body: body.slice() }
94
+ }
95
+
96
+ // ---------- Control ----------
97
+
98
+ type ControlType = Control['type']
99
+
100
+ /** 每个变体的字段声明顺序(不含 `type`),对应 control.rs。 */
101
+ const CONTROL_FIELD_ORDER: Readonly<Record<ControlType, readonly string[]>> = {
102
+ auth: ['device_token', 'protocol_version', 'client'],
103
+ auth_ok: ['device_id', 'server_time_ms', 'peers_online', 'quota'],
104
+ auth_fail: ['reason'],
105
+ pair_begin: ['match_id'],
106
+ pair_registered: ['expires_at_ms'],
107
+ pair_join: ['match_id'],
108
+ pair_matched: ['peer', 'role'],
109
+ pair_confirm: ['peer'],
110
+ pair_done: ['peer', 'device_id', 'device_token'],
111
+ pair_failed: ['reason'],
112
+ enroll_begin: ['node'],
113
+ enroll_started: ['enroll_id', 'node', 'expires_at_ms'],
114
+ enroll_request: ['enroll_id', 'peer', 'expires_at_ms'],
115
+ enroll_cancel: ['enroll_id'],
116
+ enroll_failed: ['enroll_id', 'reason'],
117
+ revoke: ['device'],
118
+ peer_online: ['device'],
119
+ peer_offline: ['device'],
120
+ undeliverable: ['to', 'seq'],
121
+ queue_overflow: ['to', 'dropped_seqs'],
122
+ queue_drop: ['seq'],
123
+ backpressure: ['to', 'seq'],
124
+ dropped: ['seq', 'reason'],
125
+ rate_limited: ['retry_after_ms'],
126
+ quota_update: ['quota'],
127
+ rotate_token: [],
128
+ token_rotated: ['device_token'],
129
+ }
130
+
131
+ /** Rust 侧 `#[serde(skip_serializing_if = "Option::is_none")]` 的字段:None 时整键省略,而不是编码为 nil。 */
132
+ const CONTROL_SKIP_NONE: Readonly<Record<string, ReadonlySet<string>>> = {
133
+ auth: new Set(['device_token']),
134
+ auth_ok: new Set(['quota']),
135
+ pair_done: new Set(['device_id', 'device_token']),
136
+ enroll_failed: new Set(['enroll_id']),
137
+ }
138
+
139
+ /** 全部控制消息的 `type`,对应 control.rs 的 `Control` 变体(snake_case)。 */
140
+ export const CONTROL_TYPES = Object.keys(CONTROL_FIELD_ORDER) as readonly ControlType[]
141
+
142
+ const CLIENT_INFO_ORDER = ['name', 'kind', 'platform', 'version', 'origin'] as const
143
+ const CLIENT_INFO_SKIP_NONE: ReadonlySet<string> = new Set(['origin'])
144
+ const QUOTA_ORDER = [
145
+ 'devices',
146
+ 'devices_max',
147
+ 'daemons',
148
+ 'daemons_max',
149
+ 'connections',
150
+ 'connections_max',
151
+ ] as const
152
+
153
+ type Dict = Record<string, unknown>
154
+
155
+ function isDict(v: unknown): v is Dict {
156
+ return typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Uint8Array)
157
+ }
158
+
159
+ /** 按 `order` 重排键;`skipNone` 里的键为 null / undefined 时省略;未知键保持原序追加在后(前向兼容)。 */
160
+ function orderKeys(
161
+ src: Dict,
162
+ order: readonly string[],
163
+ skipNone: ReadonlySet<string> | undefined,
164
+ head?: Dict,
165
+ ): Dict {
166
+ const out: Dict = head ? { ...head } : {}
167
+ for (const key of order) {
168
+ const value = src[key]
169
+ if (value === undefined) continue
170
+ if (value === null && skipNone?.has(key)) continue
171
+ out[key] = value
172
+ }
173
+ const known = new Set(order)
174
+ for (const key of Object.keys(src)) {
175
+ if (key === 'type' || known.has(key) || src[key] === undefined) continue
176
+ out[key] = src[key]
177
+ }
178
+ return out
179
+ }
180
+
181
+ function canonicalControl(ctrl: Control): Dict {
182
+ const order = CONTROL_FIELD_ORDER[ctrl.type]
183
+ if (!order) throw new ProtocolError(`unknown control type: ${String(ctrl.type)}`)
184
+ const src = { ...(ctrl as unknown as Dict) }
185
+ if (isDict(src.client)) {
186
+ src.client = orderKeys(src.client, CLIENT_INFO_ORDER, CLIENT_INFO_SKIP_NONE)
187
+ }
188
+ if (isDict(src.quota)) {
189
+ src.quota = orderKeys(src.quota, QUOTA_ORDER, undefined)
190
+ }
191
+ return orderKeys(src, order, CONTROL_SKIP_NONE[ctrl.type], { type: ctrl.type })
192
+ }
193
+
194
+ export function encodeControl(ctrl: Control): Uint8Array {
195
+ return encode(canonicalControl(ctrl), { ignoreUndefined: true })
196
+ }
197
+
198
+ /** 未知的 `type`(版本错位时 hub 可能发来新消息)。调用方应忽略这一帧,而不是断开。 */
199
+ export class UnknownControlError extends ProtocolError {
200
+ override readonly name = 'UnknownControlError'
201
+ constructor(readonly controlType: string) {
202
+ super(`unknown control type: ${controlType}`)
203
+ }
204
+ }
205
+
206
+ export function decodeControl(bytes: Uint8Array): Control {
207
+ let raw: unknown
208
+ try {
209
+ raw = decode(bytes)
210
+ } catch (cause) {
211
+ throw new ProtocolError('control is not valid msgpack', { cause })
212
+ }
213
+ if (!isDict(raw) || typeof raw.type !== 'string') {
214
+ throw new ProtocolError('control must be a map with a string `type`')
215
+ }
216
+ if (!Object.hasOwn(CONTROL_FIELD_ORDER, raw.type)) throw new UnknownControlError(raw.type)
217
+ return raw as unknown as Control
218
+ }
219
+
220
+ // ---------- 账号内接入的 handshake body(ADR-36) ----------
221
+
222
+ type EnrollFrameType = EnrollFrame['type']
223
+
224
+ /** `EnrollFrame` 每个变体的字段声明顺序,对应 `crates/protocol/src/enroll.rs`。 */
225
+ const ENROLL_FRAME_FIELD_ORDER: Readonly<Record<EnrollFrameType, readonly string[]>> = {
226
+ hello: ['v', 'method', 'approval', 'nonce', 'hint'],
227
+ reply: ['status', 'nonce', 'reason', 'retry_after_ms', 'attempts_left', 'salt', 'cpace'],
228
+ cpace: ['share'],
229
+ noise: ['msg'],
230
+ }
231
+ const ENROLL_FRAME_SKIP_NONE: Readonly<Record<string, ReadonlySet<string>>> = {
232
+ reply: new Set(['nonce', 'reason', 'retry_after_ms', 'attempts_left', 'salt', 'cpace']),
233
+ }
234
+ const ENROLL_HINT_ORDER = ['name', 'kind', 'platform'] as const
235
+
236
+ /** 接入期间 `handshake` 信封的 body:msgpack,`type` 在最前,其余字段按 Rust 声明顺序。 */
237
+ export function encodeEnrollFrame(frame: EnrollFrame): Uint8Array {
238
+ const order = ENROLL_FRAME_FIELD_ORDER[frame.type]
239
+ if (!order) throw new ProtocolError(`unknown enroll frame type: ${String(frame.type)}`)
240
+ const src = { ...(frame as unknown as Dict) }
241
+ if (isDict(src.hint)) src.hint = orderKeys(src.hint, ENROLL_HINT_ORDER, undefined)
242
+ const out = orderKeys(src, order, ENROLL_FRAME_SKIP_NONE[frame.type], { type: frame.type })
243
+ return encode(out, { ignoreUndefined: true })
244
+ }
245
+
246
+ /** 解 `encodeEnrollFrame` 的输出。不是 map、没有 `type` 或 `type` 未知都抛 `ProtocolError`。 */
247
+ export function decodeEnrollFrame(bytes: Uint8Array): EnrollFrame {
248
+ let raw: unknown
249
+ try {
250
+ raw = decode(bytes)
251
+ } catch (cause) {
252
+ throw new ProtocolError('enroll frame is not valid msgpack', { cause })
253
+ }
254
+ if (!isDict(raw) || typeof raw.type !== 'string') {
255
+ throw new ProtocolError('enroll frame must be a map with a string `type`')
256
+ }
257
+ if (!Object.hasOwn(ENROLL_FRAME_FIELD_ORDER, raw.type)) {
258
+ throw new ProtocolError(`unknown enroll frame type: ${raw.type}`)
259
+ }
260
+ return raw as unknown as EnrollFrame
261
+ }
262
+
263
+ /** 构造 `kind = control` 的信封(发往 hub,`to` 为空串)。 */
264
+ export function controlEnvelope(from: string, seq: number, ctrl: Control): Envelope {
265
+ return makeEnvelope(from, '', 'control', seq, encodeControl(ctrl))
266
+ }
267
+
268
+ /** 解出 `kind = control` 信封里的控制消息。 */
269
+ export function controlBody(env: Envelope): Control {
270
+ if (env.kind !== 'control') throw new ProtocolError(`envelope kind is ${env.kind}, not control`)
271
+ return decodeControl(env.body)
272
+ }
273
+
274
+ // ---------- data 信封的 body 框架(ADR-30) ----------
275
+
276
+ /**
277
+ * `kind = data` 的 body 框架,和 `crates/protocol/src/envelope.rs` 的 `mod frame` 字节一致
278
+ * (`tests/vectors/frames.json` 由 `tools/gen-vectors` 用 Rust 生成,TS 侧逐字节比对)。
279
+ *
280
+ * body 第一个字节是 `flags`:
281
+ * - `0x00` WHOLE:后面是一整条 `Transport.encrypt` 的输出;
282
+ * - `0x01` FRAGMENT:`[u32 be msg_id][u16 be index][u16 be count]` + 密文切片。同一 `msg_id` 的分片按
283
+ * `index` 顺序、不与其他消息交错地发送;接收方收齐 `count` 片后拼接再解密;
284
+ * - `0x02` RESET:无载荷。发送方发现自己的传输层失步(解密失败 / 丢片)时发给对端,对端丢弃传输并重新握手。
285
+ *
286
+ * Noise 的 nonce 是顺序的,所以**任何一片丢失都意味着整条传输失步**:收到 `backpressure`(hub 指明了被拒的 seq)
287
+ * 时发送方必须原样重发同一片(不能重新加密);`dropped`(hub 每连接每秒至多回一条)是「发错东西」的信号
288
+ * (`too_large` = 没分片 / 分片后仍超限),不据此逐条重发;接收方解密失败时按 RESET 处理。
289
+ */
290
+ export const FRAME_WHOLE = 0x00
291
+ export const FRAME_FRAGMENT = 0x01
292
+ export const FRAME_RESET = 0x02
293
+ /** 每个 `data` 信封里最多放多少密文字节(留出信封头和 hub 上限的余量)。 */
294
+ export const MAX_FRAGMENT = 200 * 1024
295
+ /** 重组时单条消息密文总长上限,防内存耗尽。 */
296
+ export const MAX_MESSAGE = 16 * 1024 * 1024
297
+ /** FRAGMENT 头长度:flags(1) + msg_id(4) + index(2) + count(2)。 */
298
+ const FRAGMENT_HEADER = 1 + 4 + 2 + 2
299
+
300
+ /** 解出来的一帧。`data` 是 `body` 的视图(零拷贝);需要长期持有时由调用方拷贝(`Reassembler` 会拷)。 */
301
+ export type DataFrame =
302
+ | { readonly kind: 'whole'; readonly data: Uint8Array }
303
+ | {
304
+ readonly kind: 'fragment'
305
+ readonly msgId: number
306
+ readonly index: number
307
+ readonly count: number
308
+ readonly data: Uint8Array
309
+ }
310
+ | { readonly kind: 'reset' }
311
+
312
+ /** 与 Rust `FrameError` 的变体一一对应(snake_case)。 */
313
+ export type FrameErrorCode =
314
+ | 'empty'
315
+ | 'unknown_flags'
316
+ | 'truncated'
317
+ | 'out_of_order'
318
+ | 'count_mismatch'
319
+ | 'too_large'
320
+
321
+ export class FrameError extends ProtocolError {
322
+ override readonly name = 'FrameError'
323
+ constructor(
324
+ readonly code: FrameErrorCode,
325
+ message: string,
326
+ ) {
327
+ super(message)
328
+ }
329
+ }
330
+
331
+ /** 把一条密文切成若干 body。短消息一个 `WHOLE`,长消息若干 `FRAGMENT`(每片 ≤ `MAX_FRAGMENT`)。 */
332
+ export function encodeFrames(ciphertext: Uint8Array, msgId: number): Uint8Array[] {
333
+ if (!Number.isInteger(msgId) || msgId < 0 || msgId > 0xffff_ffff) {
334
+ throw new ProtocolError(`frame msg_id must fit in u32, got ${msgId}`)
335
+ }
336
+ if (ciphertext.length <= MAX_FRAGMENT) {
337
+ const body = new Uint8Array(1 + ciphertext.length)
338
+ body[0] = FRAME_WHOLE
339
+ body.set(ciphertext, 1)
340
+ return [body]
341
+ }
342
+ const count = Math.ceil(ciphertext.length / MAX_FRAGMENT)
343
+ if (count > 0xffff) throw new ProtocolError(`message needs ${count} fragments, more than u16`)
344
+ const bodies: Uint8Array[] = []
345
+ for (let index = 0; index < count; index++) {
346
+ const slice = ciphertext.subarray(index * MAX_FRAGMENT, (index + 1) * MAX_FRAGMENT)
347
+ const body = new Uint8Array(FRAGMENT_HEADER + slice.length)
348
+ const view = new DataView(body.buffer)
349
+ body[0] = FRAME_FRAGMENT
350
+ view.setUint32(1, msgId, false)
351
+ view.setUint16(5, index, false)
352
+ view.setUint16(7, count, false)
353
+ body.set(slice, FRAGMENT_HEADER)
354
+ bodies.push(body)
355
+ }
356
+ return bodies
357
+ }
358
+
359
+ /** `RESET` 帧的 body:请求对端丢弃传输并重新握手。 */
360
+ export function resetBody(): Uint8Array {
361
+ return Uint8Array.of(FRAME_RESET)
362
+ }
363
+
364
+ /** 解一个 `data` 信封的 body。格式不对抛 `FrameError`。 */
365
+ export function decodeFrame(body: Uint8Array): DataFrame {
366
+ if (body.length === 0) throw new FrameError('empty', 'empty body')
367
+ const flags = body[0]!
368
+ switch (flags) {
369
+ case FRAME_WHOLE:
370
+ return { kind: 'whole', data: body.subarray(1) }
371
+ case FRAME_RESET:
372
+ return { kind: 'reset' }
373
+ case FRAME_FRAGMENT: {
374
+ if (body.length < FRAGMENT_HEADER) {
375
+ throw new FrameError('truncated', 'truncated fragment header')
376
+ }
377
+ const view = new DataView(body.buffer, body.byteOffset, body.byteLength)
378
+ return {
379
+ kind: 'fragment',
380
+ msgId: view.getUint32(1, false),
381
+ index: view.getUint16(5, false),
382
+ count: view.getUint16(7, false),
383
+ data: body.subarray(FRAGMENT_HEADER),
384
+ }
385
+ }
386
+ default:
387
+ throw new FrameError('unknown_flags', `unknown flags ${flags}`)
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 每个发送方(对端)一个。`push` 返回整条密文表示可以解密了,返回 `undefined` 表示还在等后续分片。
393
+ * 行为与 Rust `Reassembler` 一致:严格按序(`index` 必须等于期望的下一片),`index < count`,同一 `msg_id`
394
+ * 的 `count` 中途不能变;`count == 1` 的单片 FRAGMENT 也能收;一条收完后同一 `msg_id` 再来要从 0 开始。
395
+ * `WHOLE` / `RESET` 丢弃进行中的重组;分片报错时进行中的重组也被丢弃(`count == 0` 例外:头部非法,不动状态)。
396
+ * 报错后调用方应当按「传输失步」处理(丢传输、重新握手)。`index` / `count` 都是 u16,这里的比较不会溢出。
397
+ */
398
+ export class Reassembler {
399
+ #current:
400
+ | { msgId: number; count: number; next: number; chunks: Uint8Array[]; length: number }
401
+ | undefined
402
+
403
+ push(frame: DataFrame): Uint8Array | undefined {
404
+ switch (frame.kind) {
405
+ case 'whole':
406
+ this.#current = undefined
407
+ return frame.data.slice()
408
+ case 'reset':
409
+ this.#current = undefined
410
+ return undefined
411
+ case 'fragment': {
412
+ const { msgId, index, count, data } = frame
413
+ if (count === 0) throw new FrameError('count_mismatch', 'fragment count mismatch')
414
+ if (index >= count) {
415
+ this.#current = undefined
416
+ throw new FrameError(
417
+ 'out_of_order',
418
+ `fragment index ${index} is not below its count ${count}`,
419
+ )
420
+ }
421
+ let cur = this.#current
422
+ this.#current = undefined
423
+ if (cur && cur.msgId === msgId) {
424
+ if (cur.count !== count) {
425
+ throw new FrameError(
426
+ 'count_mismatch',
427
+ `fragment count changed mid-message: ${cur.count} -> ${count}`,
428
+ )
429
+ }
430
+ } else {
431
+ cur = { msgId, count, next: 0, chunks: [], length: 0 }
432
+ }
433
+ if (index !== cur.next) {
434
+ throw new FrameError(
435
+ 'out_of_order',
436
+ `fragment out of order: expected ${cur.next}, got ${index}`,
437
+ )
438
+ }
439
+ if (cur.length + data.length > MAX_MESSAGE) {
440
+ throw new FrameError('too_large', 'message too large')
441
+ }
442
+ cur.chunks.push(data.slice())
443
+ cur.length += data.length
444
+ if (index + 1 === count) {
445
+ const out = new Uint8Array(cur.length)
446
+ let at = 0
447
+ for (const chunk of cur.chunks) {
448
+ out.set(chunk, at)
449
+ at += chunk.length
450
+ }
451
+ return out
452
+ }
453
+ cur.next = index + 1
454
+ this.#current = cur
455
+ return undefined
456
+ }
457
+ }
458
+ }
459
+
460
+ /** 丢弃进行中的重组(连接断开、传输重建时调用)。 */
461
+ clear(): void {
462
+ this.#current = undefined
463
+ }
464
+ }
465
+
466
+ /** 便捷:直接喂 body。 */
467
+ export function pushBody(reassembler: Reassembler, body: Uint8Array): Uint8Array | undefined {
468
+ return reassembler.push(decodeFrame(body))
469
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** 编解码层错误。收到的字节不合法时抛出,调用方通常记日志后丢弃该帧。 */
2
+ export class ProtocolError extends Error {
3
+ override readonly name: string = 'ProtocolError'
4
+ }