@hile/message-ws 3.0.0 → 4.0.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.
@@ -0,0 +1,11 @@
1
+ import { type MessageTransferFormat } from '@hile/message-modem';
2
+ export declare const HILE_MESSAGE_FRAME_MAGIC: Buffer<ArrayBuffer>;
3
+ export declare const HILE_MESSAGE_FRAME_VERSION = 1;
4
+ export declare const HILE_MESSAGE_FRAME_HEADER_SIZE: number;
5
+ export type MessageFrameErrorCode = 'ERR_MESSAGE_FRAME_JSON' | 'ERR_MESSAGE_FRAME_TRUNCATED' | 'ERR_MESSAGE_FRAME_MAGIC' | 'ERR_MESSAGE_FRAME_VERSION' | 'ERR_MESSAGE_FRAME_HEADER_LENGTH' | 'ERR_MESSAGE_FRAME_ENVELOPE';
6
+ export declare class MessageFrameError extends Error {
7
+ readonly code: MessageFrameErrorCode;
8
+ constructor(code: MessageFrameErrorCode, message: string);
9
+ }
10
+ export declare function encodeMessageFrame(message: MessageTransferFormat): string | Buffer;
11
+ export declare function decodeMessageFrame(raw: Buffer | ArrayBuffer | Buffer[] | Uint8Array | string, isBinary: boolean): MessageTransferFormat;
package/dist/codec.js ADDED
@@ -0,0 +1,125 @@
1
+ import { MESSAGE_MODEM_TYPE, } from '@hile/message-modem';
2
+ const FRAME_MAGIC_BYTES = Buffer.from([0x48, 0x49, 0x4c, 0x45]);
3
+ const MAX_HEADER_BYTES = 1024 * 1024;
4
+ export const HILE_MESSAGE_FRAME_MAGIC = Buffer.from(FRAME_MAGIC_BYTES);
5
+ export const HILE_MESSAGE_FRAME_VERSION = 1;
6
+ export const HILE_MESSAGE_FRAME_HEADER_SIZE = FRAME_MAGIC_BYTES.length + 1 + 4;
7
+ export class MessageFrameError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.name = 'MessageFrameError';
12
+ this.code = code;
13
+ }
14
+ }
15
+ function fail(code, message) {
16
+ throw new MessageFrameError(code, message);
17
+ }
18
+ function isRecord(value) {
19
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
20
+ }
21
+ function isBinaryPayload(value) {
22
+ return Buffer.isBuffer(value) || value instanceof Uint8Array;
23
+ }
24
+ function parseJson(value) {
25
+ try {
26
+ return JSON.parse(value);
27
+ }
28
+ catch {
29
+ fail('ERR_MESSAGE_FRAME_JSON', 'message frame contains invalid JSON');
30
+ }
31
+ }
32
+ function toBuffer(raw) {
33
+ if (Buffer.isBuffer(raw))
34
+ return raw;
35
+ if (typeof raw === 'string')
36
+ return Buffer.from(raw);
37
+ if (Array.isArray(raw))
38
+ return Buffer.concat(raw);
39
+ if (raw instanceof ArrayBuffer)
40
+ return Buffer.from(raw);
41
+ return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength);
42
+ }
43
+ function isBinaryStreamResponse(message) {
44
+ return message.mode === MESSAGE_MODEM_TYPE.RESPONSE
45
+ && message.stream === true
46
+ && message.streamVersion === 1
47
+ && isRecord(message.data)
48
+ && isBinaryPayload(message.data.payload);
49
+ }
50
+ function validateBinaryEnvelope(value) {
51
+ if (!isRecord(value)) {
52
+ fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame header must be an object');
53
+ }
54
+ if (!Number.isSafeInteger(value.id)
55
+ || value.id < 0
56
+ || value.mode !== MESSAGE_MODEM_TYPE.RESPONSE
57
+ || value.twoway !== false
58
+ || value.stream !== true
59
+ || value.streamVersion !== 1
60
+ || !isRecord(value.data)) {
61
+ fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame header is not a stream response');
62
+ }
63
+ if (Object.prototype.hasOwnProperty.call(value.data, 'payload')) {
64
+ fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame header must not contain an inline payload');
65
+ }
66
+ if (!Number.isSafeInteger(value.data.seq)
67
+ || value.data.seq < 0
68
+ || typeof value.data.final !== 'boolean'
69
+ || !(typeof value.data.status === 'string'
70
+ || typeof value.data.status === 'number')) {
71
+ fail('ERR_MESSAGE_FRAME_ENVELOPE', 'binary frame chunk metadata is invalid');
72
+ }
73
+ }
74
+ export function encodeMessageFrame(message) {
75
+ if (!isBinaryStreamResponse(message))
76
+ return JSON.stringify(message);
77
+ const payload = Buffer.from(message.data.payload.buffer, message.data.payload.byteOffset, message.data.payload.byteLength);
78
+ const { payload: _payload, ...chunkHeader } = message.data;
79
+ const header = Buffer.from(JSON.stringify({
80
+ ...message,
81
+ data: chunkHeader,
82
+ }));
83
+ if (header.length === 0 || header.length > MAX_HEADER_BYTES) {
84
+ fail('ERR_MESSAGE_FRAME_HEADER_LENGTH', 'binary frame header is too large');
85
+ }
86
+ const frame = Buffer.allocUnsafe(HILE_MESSAGE_FRAME_HEADER_SIZE + header.length + payload.length);
87
+ FRAME_MAGIC_BYTES.copy(frame, 0);
88
+ frame.writeUInt8(HILE_MESSAGE_FRAME_VERSION, FRAME_MAGIC_BYTES.length);
89
+ frame.writeUInt32BE(header.length, FRAME_MAGIC_BYTES.length + 1);
90
+ header.copy(frame, HILE_MESSAGE_FRAME_HEADER_SIZE);
91
+ payload.copy(frame, HILE_MESSAGE_FRAME_HEADER_SIZE + header.length);
92
+ return frame;
93
+ }
94
+ export function decodeMessageFrame(raw, isBinary) {
95
+ const bytes = toBuffer(raw);
96
+ if (!isBinary) {
97
+ return parseJson(bytes.toString('utf8'));
98
+ }
99
+ if (bytes.length < HILE_MESSAGE_FRAME_HEADER_SIZE) {
100
+ fail('ERR_MESSAGE_FRAME_TRUNCATED', 'binary frame is shorter than its fixed header');
101
+ }
102
+ if (!bytes.subarray(0, FRAME_MAGIC_BYTES.length).equals(FRAME_MAGIC_BYTES)) {
103
+ fail('ERR_MESSAGE_FRAME_MAGIC', 'binary frame has an invalid magic value');
104
+ }
105
+ const version = bytes.readUInt8(FRAME_MAGIC_BYTES.length);
106
+ if (version !== HILE_MESSAGE_FRAME_VERSION) {
107
+ fail('ERR_MESSAGE_FRAME_VERSION', `unsupported binary frame version: ${version}`);
108
+ }
109
+ const headerLength = bytes.readUInt32BE(FRAME_MAGIC_BYTES.length + 1);
110
+ if (headerLength === 0
111
+ || headerLength > MAX_HEADER_BYTES
112
+ || HILE_MESSAGE_FRAME_HEADER_SIZE + headerLength > bytes.length) {
113
+ fail('ERR_MESSAGE_FRAME_HEADER_LENGTH', 'binary frame header length is invalid');
114
+ }
115
+ const headerEnd = HILE_MESSAGE_FRAME_HEADER_SIZE + headerLength;
116
+ const envelope = parseJson(bytes.subarray(HILE_MESSAGE_FRAME_HEADER_SIZE, headerEnd).toString('utf8'));
117
+ validateBinaryEnvelope(envelope);
118
+ return {
119
+ ...envelope,
120
+ data: {
121
+ ...envelope.data,
122
+ payload: Buffer.from(bytes.subarray(headerEnd)),
123
+ },
124
+ };
125
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { MessageModem, type MessageTransferFormat } from '@hile/message-modem';
2
2
  import type WebSocket from 'ws';
3
+ export * from './codec.js';
3
4
  /**
4
5
  * 基于 `ws` 模块的 WebSocket 通信层。
5
6
  * exec 方法由子类实现,本类不做实现。
6
7
  *
7
8
  * 构造时传入已连接的 WebSocket 实例,自动绑定 message 事件。
8
- * 消息通过 JSON 序列化/反序列化传输。
9
+ * 普通消息通过 JSON 传输;二进制 stream response 使用 Hile 二进制帧,避免 Base64 开销。
9
10
  *
10
11
  * @example
11
12
  * class MyWs extends MessageWs {
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { MessageModem } from '@hile/message-modem';
2
+ import { decodeMessageFrame, encodeMessageFrame } from './codec.js';
3
+ export * from './codec.js';
2
4
  /**
3
5
  * 基于 `ws` 模块的 WebSocket 通信层。
4
6
  * exec 方法由子类实现,本类不做实现。
5
7
  *
6
8
  * 构造时传入已连接的 WebSocket 实例,自动绑定 message 事件。
7
- * 消息通过 JSON 序列化/反序列化传输。
9
+ * 普通消息通过 JSON 传输;二进制 stream response 使用 Hile 二进制帧,避免 Base64 开销。
8
10
  *
9
11
  * @example
10
12
  * class MyWs extends MessageWs {
@@ -26,12 +28,16 @@ export class MessageWs extends MessageModem {
26
28
  constructor(ws) {
27
29
  super();
28
30
  this.ws = ws;
29
- this.listener = (raw) => {
31
+ this.listener = (raw, isBinary) => {
30
32
  try {
31
- const msg = JSON.parse(raw.toString());
33
+ const msg = decodeMessageFrame(raw, isBinary);
32
34
  this.receive(msg);
33
35
  }
34
- catch { }
36
+ catch {
37
+ // A malformed protocol frame is fatal. Keeping the connection alive would
38
+ // hide peer incompatibility and can leave pending requests unresolved.
39
+ this.ws.close(1002, 'Invalid Hile message frame');
40
+ }
35
41
  };
36
42
  this.ws.on('message', this.listener);
37
43
  }
@@ -39,7 +45,7 @@ export class MessageWs extends MessageModem {
39
45
  if (this.ws.readyState !== this.ws.OPEN) {
40
46
  throw new Error('WebSocket is not open. Current readyState: ' + this.ws.readyState);
41
47
  }
42
- this.ws.send(JSON.stringify(data));
48
+ this.ws.send(encodeMessageFrame(data));
43
49
  }
44
50
  /**
45
51
  * 移除消息监听,释放资源
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/message-ws",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -18,13 +18,14 @@
18
18
  "access": "public"
19
19
  },
20
20
  "devDependencies": {
21
+ "@types/node": "^26.2.0",
21
22
  "@types/ws": "^8.18.1",
22
23
  "fix-esm-import-path": "^1.10.3",
23
24
  "vitest": "^4.0.18"
24
25
  },
25
26
  "dependencies": {
26
- "@hile/message-modem": "^3.0.0",
27
+ "@hile/message-modem": "^4.0.0",
27
28
  "ws": "^8.21.0"
28
29
  },
29
- "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
30
+ "gitHead": "b46cb7f3705a226f58e4d65a2ff985ea54b9a159"
30
31
  }