@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.
@@ -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
+ };
@@ -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
+ }