@lyrify/znl 0.4.1
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.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/index.js +1 -0
- package/package.json +46 -0
- package/src/PendingManager.js +158 -0
- package/src/SendQueue.js +47 -0
- package/src/ZNL.js +773 -0
- package/src/constants.js +42 -0
- package/src/protocol.js +277 -0
package/src/constants.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议常量与默认配置
|
|
3
|
+
* 集中管理所有魔法字符串和默认值,避免散落在业务代码中
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 控制帧版本前缀,用于区分控制帧与业务帧 */
|
|
7
|
+
export const CONTROL_PREFIX = "__znl_v1__";
|
|
8
|
+
|
|
9
|
+
/** 请求类型标识符 */
|
|
10
|
+
export const CONTROL_REQ = "req";
|
|
11
|
+
|
|
12
|
+
/** 响应类型标识符 */
|
|
13
|
+
export const CONTROL_RES = "res";
|
|
14
|
+
|
|
15
|
+
/** 认证 Key 帧标识符(位于 req 帧中) */
|
|
16
|
+
export const CONTROL_AUTH = "__znl_v1_auth__";
|
|
17
|
+
|
|
18
|
+
/** slave 保活心跳帧标识符(slave → master,定时发送) */
|
|
19
|
+
export const CONTROL_HEARTBEAT = "heartbeat";
|
|
20
|
+
|
|
21
|
+
/** slave 上线注册帧标识符(slave → master,start 时自动发送) */
|
|
22
|
+
export const CONTROL_REGISTER = "register";
|
|
23
|
+
|
|
24
|
+
/** slave 下线注销帧标识符(slave → master,stop 时自动发送) */
|
|
25
|
+
export const CONTROL_UNREGISTER = "unregister";
|
|
26
|
+
|
|
27
|
+
/** 广播推送帧标识符(master → slave,publish 时发送) */
|
|
28
|
+
export const CONTROL_PUB = "pub";
|
|
29
|
+
|
|
30
|
+
/** 空 Buffer(全局复用,避免重复分配) */
|
|
31
|
+
export const EMPTY_BUFFER = Buffer.alloc(0);
|
|
32
|
+
|
|
33
|
+
/** 默认请求超时时间(毫秒) */
|
|
34
|
+
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
35
|
+
|
|
36
|
+
/** 默认心跳间隔(毫秒),0 表示禁用心跳 */
|
|
37
|
+
export const DEFAULT_HEARTBEAT_INTERVAL = 3000;
|
|
38
|
+
|
|
39
|
+
/** 默认端点配置 */
|
|
40
|
+
export const DEFAULT_ENDPOINTS = {
|
|
41
|
+
router: "tcp://127.0.0.1:6003",
|
|
42
|
+
};
|
package/src/protocol.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZMQ 帧协议层
|
|
3
|
+
*
|
|
4
|
+
* 负责所有帧的构建与解析,全部为纯函数,无副作用。
|
|
5
|
+
*
|
|
6
|
+
* 控制帧格式:
|
|
7
|
+
* 注册帧: [PREFIX, "register", (AUTH_MARKER, authKey)?]
|
|
8
|
+
* 注销帧: [PREFIX, "unregister"]
|
|
9
|
+
* 请求帧: [PREFIX, "req", requestId, (AUTH_MARKER, authKey)?, ...payload]
|
|
10
|
+
* 响应帧: [PREFIX, "res", requestId, ...payload]
|
|
11
|
+
* 广播帧: [PREFIX, "pub", topic, ...payload]
|
|
12
|
+
* Router 侧额外在最前面加一帧:[identity, ...]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
CONTROL_PREFIX,
|
|
17
|
+
CONTROL_REQ,
|
|
18
|
+
CONTROL_RES,
|
|
19
|
+
CONTROL_AUTH,
|
|
20
|
+
CONTROL_REGISTER,
|
|
21
|
+
CONTROL_UNREGISTER,
|
|
22
|
+
CONTROL_PUB,
|
|
23
|
+
CONTROL_HEARTBEAT,
|
|
24
|
+
EMPTY_BUFFER,
|
|
25
|
+
} from "./constants.js";
|
|
26
|
+
|
|
27
|
+
// ─── Identity 工具 ────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 将 identity 帧转换为可读字符串(用于日志和 Map key)
|
|
31
|
+
* @param {Buffer|string} identity
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function identityToString(identity) {
|
|
35
|
+
return Buffer.isBuffer(identity) ? identity.toString() : String(identity);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 将 identity 转换为 Buffer(ZMQ 帧要求 Buffer 格式)
|
|
40
|
+
* @param {Buffer|Uint8Array|string} identity
|
|
41
|
+
* @returns {Buffer}
|
|
42
|
+
*/
|
|
43
|
+
export function identityToBuffer(identity) {
|
|
44
|
+
if (Buffer.isBuffer(identity)) return identity;
|
|
45
|
+
if (identity instanceof Uint8Array) return Buffer.from(identity);
|
|
46
|
+
return Buffer.from(String(identity));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Payload 工具 ─────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 标准化单个帧为 ZMQ 可传输的格式
|
|
53
|
+
* - null/undefined → 空 Buffer
|
|
54
|
+
* - string → 原样保留
|
|
55
|
+
* - Buffer → 原样保留
|
|
56
|
+
* - Uint8Array → 转为 Buffer
|
|
57
|
+
* - 其他 → 抛出 TypeError
|
|
58
|
+
* @param {string|Buffer|Uint8Array|null|undefined} frame
|
|
59
|
+
* @returns {string|Buffer}
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeFrame(frame) {
|
|
62
|
+
if (frame == null) return EMPTY_BUFFER;
|
|
63
|
+
if (typeof frame === "string") return frame;
|
|
64
|
+
if (Buffer.isBuffer(frame)) return frame;
|
|
65
|
+
if (frame instanceof Uint8Array) return Buffer.from(frame);
|
|
66
|
+
throw new TypeError(
|
|
67
|
+
"payload 仅支持 string / Buffer / Uint8Array(或它们的数组)。",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 将 payload 标准化为帧数组
|
|
73
|
+
* 数组类型逐项标准化,非数组包装成单元素数组
|
|
74
|
+
* @param {string|Buffer|Uint8Array|Array} payload
|
|
75
|
+
* @returns {Array<string|Buffer>}
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeFrames(payload) {
|
|
78
|
+
if (Array.isArray(payload)) return payload.map(normalizeFrame);
|
|
79
|
+
return [normalizeFrame(payload)];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 从帧数组中提取 payload
|
|
84
|
+
* - 空帧组 → 空 Buffer
|
|
85
|
+
* - 单帧 → 直接返回(避免不必要的数组包装)
|
|
86
|
+
* - 多帧 → 返回数组(多帧 payload 原样保留)
|
|
87
|
+
* @param {Array} frames
|
|
88
|
+
* @returns {Buffer|Array}
|
|
89
|
+
*/
|
|
90
|
+
export function payloadFromFrames(frames) {
|
|
91
|
+
if (!frames?.length) return EMPTY_BUFFER;
|
|
92
|
+
return frames.length === 1 ? frames[0] : frames;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── 帧构建 ───────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 构建注册控制帧数组(slave → master,连接时自动发送)
|
|
99
|
+
* 帧结构:[PREFIX, "register", (AUTH_MARKER, authKey)?]
|
|
100
|
+
* @param {string} [authKey] - 可选认证 Key
|
|
101
|
+
* @returns {Array}
|
|
102
|
+
*/
|
|
103
|
+
export function buildRegisterFrames(authKey = "") {
|
|
104
|
+
const frames = [CONTROL_PREFIX, CONTROL_REGISTER];
|
|
105
|
+
if (authKey) frames.push(CONTROL_AUTH, authKey);
|
|
106
|
+
return frames;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 构建注销控制帧数组(slave → master,断开时自动发送)
|
|
111
|
+
* 帧结构:[PREFIX, "unregister"]
|
|
112
|
+
* @returns {Array}
|
|
113
|
+
*/
|
|
114
|
+
export function buildUnregisterFrames() {
|
|
115
|
+
return [CONTROL_PREFIX, CONTROL_UNREGISTER];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 构建请求控制帧数组
|
|
120
|
+
* 帧结构:[PREFIX, "req", requestId, (AUTH_MARKER, authKey)?, ...payloadFrames]
|
|
121
|
+
* @param {string} requestId - UUID
|
|
122
|
+
* @param {Array<string|Buffer>} payloadFrames - 已标准化的 payload 帧
|
|
123
|
+
* @param {string} [authKey] - 可选认证 Key
|
|
124
|
+
* @returns {Array}
|
|
125
|
+
*/
|
|
126
|
+
export function buildRequestFrames(requestId, payloadFrames, authKey = "") {
|
|
127
|
+
const header = [CONTROL_PREFIX, CONTROL_REQ, requestId];
|
|
128
|
+
if (authKey) header.push(CONTROL_AUTH, authKey);
|
|
129
|
+
return [...header, ...payloadFrames];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 构建响应控制帧数组
|
|
134
|
+
* 帧结构:[PREFIX, "res", requestId, ...payloadFrames]
|
|
135
|
+
* @param {string} requestId
|
|
136
|
+
* @param {Array<string|Buffer>} payloadFrames
|
|
137
|
+
* @returns {Array}
|
|
138
|
+
*/
|
|
139
|
+
export function buildResponseFrames(requestId, payloadFrames) {
|
|
140
|
+
return [CONTROL_PREFIX, CONTROL_RES, String(requestId), ...payloadFrames];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 构建广播控制帧数组(master → slave)
|
|
145
|
+
* 帧结构:[PREFIX, "pub", topic, ...payloadFrames]
|
|
146
|
+
* @param {string} topic - 消息主题
|
|
147
|
+
* @param {Array<string|Buffer>} payloadFrames - 已标准化的 payload 帧
|
|
148
|
+
* @returns {Array}
|
|
149
|
+
*/
|
|
150
|
+
export function buildPublishFrames(topic, payloadFrames) {
|
|
151
|
+
return [CONTROL_PREFIX, CONTROL_PUB, String(topic), ...payloadFrames];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── 帧解析 ───────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 解析 ZMQ 原始帧,识别控制帧并提取语义字段
|
|
158
|
+
*
|
|
159
|
+
* 返回 kind 说明:
|
|
160
|
+
* - "register" → slave 上线注册(携带可选 authKey)
|
|
161
|
+
* - "unregister" → slave 主动下线注销
|
|
162
|
+
* - "publish" → master 广播消息(携带 topic)
|
|
163
|
+
* - "request" → 对端主动发起的 RPC 请求
|
|
164
|
+
* - "response" → 对端返回的 RPC 响应(匹配 pending 请求)
|
|
165
|
+
* - "message" → 非控制帧,普通消息透传
|
|
166
|
+
*
|
|
167
|
+
* @param {Array} frames - 不含 identity 帧的帧数组
|
|
168
|
+
* @returns {{
|
|
169
|
+
* kind : "register"|"unregister"|"publish"|"request"|"response"|"message",
|
|
170
|
+
* requestId : string|null,
|
|
171
|
+
* authKey : string|null,
|
|
172
|
+
* topic : string|null,
|
|
173
|
+
* payloadFrames: Array
|
|
174
|
+
* }}
|
|
175
|
+
*/
|
|
176
|
+
export function parseControlFrames(frames) {
|
|
177
|
+
// 至少需要 2 帧且首帧匹配前缀才能识别控制帧
|
|
178
|
+
if (frames.length < 2 || frames[0]?.toString() !== CONTROL_PREFIX) {
|
|
179
|
+
return {
|
|
180
|
+
kind: "message",
|
|
181
|
+
requestId: null,
|
|
182
|
+
authKey: null,
|
|
183
|
+
topic: null,
|
|
184
|
+
payloadFrames: frames,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const action = frames[1]?.toString();
|
|
189
|
+
|
|
190
|
+
// ── 注册帧:[PREFIX, "register", (AUTH_MARKER, authKey)?] ─────────────────
|
|
191
|
+
if (action === CONTROL_REGISTER) {
|
|
192
|
+
let authKey = null;
|
|
193
|
+
if (frames.length >= 4 && frames[2]?.toString() === CONTROL_AUTH) {
|
|
194
|
+
authKey = frames[3]?.toString() ?? "";
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: "register",
|
|
198
|
+
requestId: null,
|
|
199
|
+
authKey,
|
|
200
|
+
topic: null,
|
|
201
|
+
payloadFrames: [],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── 注销帧:[PREFIX, "unregister"] ────────────────────────────────────────
|
|
206
|
+
if (action === CONTROL_UNREGISTER) {
|
|
207
|
+
return {
|
|
208
|
+
kind: "unregister",
|
|
209
|
+
requestId: null,
|
|
210
|
+
authKey: null,
|
|
211
|
+
topic: null,
|
|
212
|
+
payloadFrames: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── 心跳帧:[PREFIX, "heartbeat"] ─────────────────────────────────────────
|
|
217
|
+
if (action === CONTROL_HEARTBEAT) {
|
|
218
|
+
return {
|
|
219
|
+
kind: "heartbeat",
|
|
220
|
+
requestId: null,
|
|
221
|
+
authKey: null,
|
|
222
|
+
topic: null,
|
|
223
|
+
payloadFrames: [],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── 广播帧:[PREFIX, "pub", topic, ...payloadFrames] ─────────────────────
|
|
228
|
+
if (action === CONTROL_PUB && frames.length >= 3) {
|
|
229
|
+
const topic = frames[2]?.toString() ?? "";
|
|
230
|
+
return {
|
|
231
|
+
kind: "publish",
|
|
232
|
+
requestId: null,
|
|
233
|
+
authKey: null,
|
|
234
|
+
topic,
|
|
235
|
+
payloadFrames: frames.slice(3),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── 请求 / 响应帧:[PREFIX, "req"/"res", requestId, ...] ─────────────────
|
|
240
|
+
if (
|
|
241
|
+
(action === CONTROL_REQ || action === CONTROL_RES) &&
|
|
242
|
+
frames.length >= 3
|
|
243
|
+
) {
|
|
244
|
+
const requestId = frames[2]?.toString();
|
|
245
|
+
if (requestId) {
|
|
246
|
+
let payloadStart = 3;
|
|
247
|
+
let authKey = null;
|
|
248
|
+
|
|
249
|
+
// 请求帧中可能携带认证 Key(仅 CONTROL_REQ 有此结构)
|
|
250
|
+
if (
|
|
251
|
+
action === CONTROL_REQ &&
|
|
252
|
+
frames.length >= 5 &&
|
|
253
|
+
frames[3]?.toString() === CONTROL_AUTH
|
|
254
|
+
) {
|
|
255
|
+
authKey = frames[4]?.toString() ?? "";
|
|
256
|
+
payloadStart = 5;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
kind: action === CONTROL_REQ ? "request" : "response",
|
|
261
|
+
requestId,
|
|
262
|
+
authKey,
|
|
263
|
+
topic: null,
|
|
264
|
+
payloadFrames: frames.slice(payloadStart),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── 无法识别:透传为普通消息 ──────────────────────────────────────────────
|
|
270
|
+
return {
|
|
271
|
+
kind: "message",
|
|
272
|
+
requestId: null,
|
|
273
|
+
authKey: null,
|
|
274
|
+
topic: null,
|
|
275
|
+
payloadFrames: frames,
|
|
276
|
+
};
|
|
277
|
+
}
|