mm_websocket_server 1.0.2 → 2.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.
package/README.md CHANGED
@@ -1,159 +1,117 @@
1
1
  # mm_websocket_server
2
2
 
3
- WebSocket 服务器入站适配器模块。继承 [mm_corejs](https://gitee.com/qiuwenwu91/mm_corejs) 的 `Mod` 基类,基于 `ws` 独立监听模式,提供连接管理、消息处理管道与 JSON-RPC 2.0 消息插件,可脱离完整框架独立运行。
3
+ 最薄的 WebSocket 服务器入站适配器。继承 `mm_mod` 的 `Mod`,把 `ws` 的每条消息直接交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。
4
4
 
5
5
  - 入口:`index.js`(CommonJS)
6
- - 导出:`WebSocketServer`(服务器模块)、`MessagePlugin`(JSON-RPC 消息插件)
7
- - 运行时依赖:`mm_corejs`、`ws`
8
- - 测试覆盖率:100%(`jest.config.js` 四项阈值)
6
+ - 导出:`WebSocketServer`、`admin`、`websocketServerAdmin`
7
+ - 运行时依赖:`mm_mod`、`mm_event`、`mm_corejs`、`ws`
9
8
 
10
- ## 特性
9
+ ## 消息路径
11
10
 
12
- - **独立监听**:`WebSocket.Server` 按 `host / port / path` 直接监听,仅处理升级握手,不提供普通 HTTP 服务。
13
- - **生命周期**:`init`(校验服务器设置)→ `start`(监听)→ `stop`(终止全部连接并关闭服务)→ `dispose`(清空注册表),状态机由 `mm_corejs` 驱动。
14
- - **消息管道**:入站消息经 `Handle` 的 `before / check / main / render` 阶段处理,管道产出 `result` 时自动回发来源客户端;`getHandle()` 可注册业务钩子(echo、鉴权、改写等)。
15
- - **连接管理**:`Store` 登记在线客户端,支持 `send / broadcast / closeClient / listClients(条件查询与分页)/ hasClient / setClientOptions`。
16
- - **JSON-RPC 2.0 插件**:`MessagePlugin` 挂载后接管 RPC 请求,支持请求-响应、通知回推、主动推送与服务器反向调用客户端方法。
17
- - **选项透传**:连接级 `options`(user_id / device_id / agent_id 等)随每条入站消息透传给管道钩子与 RPC 处理函数,适配多用户 / 多设备 / 多智能体场景。
18
-
19
- ## 安装
20
-
21
- ```bash
22
- npm install mm_websocket_server
11
+ ```
12
+ WebSocket.Server → connection → ctx = { client_id, path, ws }
13
+ → eventManager.run(path, params, ctx, options)
14
+ → 结果非空时回发来源连接
23
15
  ```
24
16
 
25
- ## 环境要求
26
-
27
- - Node.js >= 22.5.0
28
- - 模块规范:CommonJS
17
+ 事件 `target` 取握手请求路径(第一个 `?` 之前)。`params` 为 `{ client_id, data }`,`data` 在 `json: true` 时尝试 `JSON.parse`(失败回落原文)。
29
18
 
30
19
  ## 快速开始
31
20
 
32
21
  ```js
33
22
  'use strict';
34
23
 
35
- const { WebSocketServer, MessagePlugin } = require('mm_websocket_server');
24
+ const { WebSocketServer } = require('mm_websocket_server');
36
25
 
37
- /** 最小 Server 容器:满足 Base 契约即可(见 example/demo.js 完整实现) */
38
- const server = {
39
- services: new Map(),
40
- getService(name) { return this.services.get(name); },
41
- hasService(name) { return this.services.has(name); },
42
- setService(name, instance) { this.services.set(name, instance); return this; },
43
- getManager() { return undefined; },
44
- hasManager() { return false; }
45
- };
26
+ const websocket = new WebSocketServer({ name: 'websocket', host: '127.0.0.1', port: 8090 });
27
+ const events = websocket.getEventManager();
46
28
 
47
- const wss = new WebSocketServer({
48
- name: 'websocket',
49
- host: '127.0.0.1',
50
- port: 9200,
51
- path: '/',
52
- json: true
53
- }, server);
54
-
55
- // 业务钩子:非 RPC 消息直接回发 echo
56
- wss.getHandle().on('main', (ctx, params) => ({ type: 'echo', data: params.data }), 200, 'echo');
57
-
58
- // JSON-RPC 插件:登记方法
59
- const rpc = new MessagePlugin();
60
- wss.use(rpc);
61
- rpc.addMethod('sum', (params) => params.reduce((a, b) => a + b, 0));
62
-
63
- // 事件
64
- wss.on('connection', (client) => wss.setClientOptions(client.id, { user_id: 'u1' }));
65
- wss.on('message', (client_id, data) => console.log(client_id, data));
66
- wss.on('close', (client_id) => console.log('断开', client_id));
67
- wss.on('error', (err) => console.error(err.message));
68
-
69
- await wss.init();
70
- await wss.start();
71
-
72
- // 主动操作
73
- await wss.run({}, { action: 'list' });
74
- await wss.run({}, { action: 'broadcast', data: { type: 'notice', text: 'hello all' } });
75
-
76
- await wss.stop();
77
- await wss.dispose();
78
- ```
29
+ events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx) => ({
30
+ client_id: ctx.client_id,
31
+ data: params.data
32
+ });
79
33
 
80
- ## 服务器配置(config)
34
+ await websocket.init();
35
+ await websocket.start();
81
36
 
82
- 顶层 `config` 字段即服务器设置,`init` 阶段统一校验。
37
+ console.log(websocket.getAddress());
38
+ ```
39
+
40
+ ## 服务器配置
83
41
 
84
42
  | 字段 | 类型 | 默认值 | 说明 |
85
43
  | --- | --- | --- | --- |
86
- | `name` | string | `'websocket'` | 模块名称(必填) |
44
+ | `name` | string | `'os'` | 模块名称(必填) |
45
+ | `title` | string | `'WebSocket服务器模块'` | 模块标题 |
46
+ | `description` | string | — | 模块描述 |
47
+ | `version` | string | `'2.0.0'` | 版本号 |
87
48
  | `host` | string | `'0.0.0.0'` | 监听地址 |
88
- | `port` | integer | `8090` | 监听端口(1-65535) |
89
- | `path` | string | `'/'` | 升级路径,须以 `/` 开头 |
90
- | `json` | boolean | `true` | 为 `true` 时入站尝试 JSON 解析(失败回落原文),出站对象自动序列化 |
49
+ | `port` | number | `8090` | 监听端口 |
50
+ | `json` | boolean | `true` | 入站尝试 JSON 解析,出站对象自动序列化 |
51
+ | `params` | object | `{ action }` | `main` 的参数约定 |
91
52
 
92
- ## API
53
+ 不限制升级路径,任何路径都可连接,路径即事件 `target`。
93
54
 
94
- ### WebSocketServer
55
+ ## API
95
56
 
96
57
  | 成员 | 说明 |
97
58
  | --- | --- |
98
- | `run(ctx, params)` | 主动操作入口,`params.action` 派发:`send`(需 `client_id` / `data`)、`broadcast`(需 `data`)、`close`(需 `client_id`)、`list`(可选 `query` 条件查询) |
99
- | `send(client_id, data)` | 向指定客户端发送数据,返回是否成功(连接非 OPEN 视为失败) |
100
- | `broadcast(data)` | 向全部在线客户端广播,返回送达数 |
101
- | `closeClient(client_id)` | 关闭指定连接,返回是否发起(登记移除与 `close` 事件由关闭回调完成) |
102
- | `listClients(query)` | 在线客户端列表(`{ id, address }`),支持 Store 条件 / 分页 / keyword |
103
- | `hasClient(client_id)` | 是否在线 |
104
- | `setClientOptions(client_id, options)` | 绑定连接级选项(`null` 清空),随入站消息透传 |
105
- | `use(plugin)` | 挂载插件(需实现 `bind(server)`) |
106
- | `getHandle()` | 消息管道,注册 `before / check / main / render` 等钩子;钩子产出 `result` 自动回发 |
59
+ | `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
60
+ | `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
61
+ | `getEventManager()` | 获取 `mm_event` 的 `websocket` 作用域事件管理器(与其他实例共享),用于注册事件 |
62
+ | `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
107
63
  | `getAddress()` | 实际监听地址,未启动为 `null` |
64
+ | `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `send` / `broadcast` / `close` / `list` |
65
+ | `send(client_id, data)` | 向指定连接发送数据,返回是否成功 |
66
+ | `broadcast(data)` | 向全部在线连接广播,返回送达数 |
67
+ | `closeClient(client_id)` | 关闭指定连接,返回是否发起 |
68
+ | `listClients()` | 在线连接列表 `{ id, path, address }` |
69
+ | `hasClient(client_id)` | 是否在线 |
70
+ | `setClientOptions(client_id, options)` | 绑定连接级选项,随入站消息作为 `options` 透传(`null` 清空) |
108
71
 
109
- ### 事件
72
+ ## 请求上下文(ctx)
110
73
 
111
- | 事件 | 参数 | 触发时机 |
112
- | --- | --- | --- |
113
- | `connection` | `(client)` | 新连接接入,`client` 为 `{ id, name, address, ws }` |
114
- | `message` | `(client_id, data)` | 入站消息经管道内置 `emit_message` 钩子分发 |
115
- | `close` | `(client_id)` | 连接关闭 |
116
- | `error` | `(err)` | 底层服务器 / 连接 / 钩子错误 |
74
+ | 字段 | 说明 |
75
+ | --- | --- |
76
+ | `client_id` | 连接编号,形如 `ws_1` |
77
+ | `path` | 握手请求路径,`?` 之后已截断 |
78
+ | `ws` | 原生 ws 连接,事件可直接写出 |
117
79
 
118
- ### 消息管道
80
+ ## 事件处理函数
119
81
 
120
- 每条入站消息以 `{ client_id, data }` 为 `params`、连接级 `options` 为第三参进入管道:
82
+ 签名固定为 `(params, ctx, options)`:
121
83
 
84
+ ```js
85
+ events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx, options) => {
86
+ return { data: params.data };
87
+ };
122
88
  ```
123
- before → check → main(业务钩子按 sort 依次执行)→ render
124
- ```
125
-
126
- 任一阶段产出非 `undefined` 的 `result` 即回发来源客户端;`check` / `before` 产出时短路后续阶段。内置 `emit_message` 钩子(sort 1000)只分发 `message` 事件、不产出结果。
127
89
 
128
- ## MessagePlugin(JSON-RPC 2.0)
90
+ 返回非 `undefined` / `null` 的结果会按 `json` 配置编码后回发来源连接;返回 `undefined` 则不回发。事件也可直接用 `ctx.ws` 写出。
129
91
 
130
- 挂载后接管 `jsonrpc` 形态消息(非 RPC 消息透传给业务钩子):
92
+ ## 主动操作
131
93
 
132
94
  ```js
133
- const rpc = new MessagePlugin();
134
- wss.use(rpc);
135
- rpc.addMethod('echo', (params, extra, options) => params); // extra.client_id,options 为连接级选项
95
+ await websocket.run({ action: 'send', client_id: 'ws_1', data: 'hi' });
96
+ await websocket.run({ action: 'broadcast', data: { type: 'notice' } });
97
+ await websocket.run({ action: 'close', client_id: 'ws_1' });
98
+ await websocket.run({ action: 'list' });
136
99
  ```
137
100
 
138
- | 成员 | 说明 |
139
- | --- | --- |
140
- | `addMethod(name, handler)` / `delMethod(name)` / `listMethods(query)` | 方法表管理,处理函数签名 `(params, extra, options)` |
141
- | `push(client_id, data)` | 任意时刻向客户端回推数据(如异步任务完成后) |
142
- | `call(client_id, method, params, options)` | 反向调用客户端方法并等待其反馈,`options.timeout_ms` 缺省 30000 |
143
- | `bind(server)` / `isBound()` | 挂载与状态查询 |
101
+ ## 生命周期
144
102
 
145
- 协议行为:请求(含 `id`)返回响应对象;通知(无 `id`)处理函数有返回值时回推服务器发起的通知 `{ jsonrpc, method, params }`;客户端把响应形态消息(含 `result` / `error`、无 `method`)发回即结算对应的反向调用。
103
+ `onStart` 创建并监听 `WebSocket.Server`,`onStop` 断开全部连接并关闭服务器。事件管理器取自 `mm_event` 的 `websocket` 作用域,由全局 `Admin` 共享,停止时不清理。端口被占用时 `start()` 抛错,模块进入 `error` 状态,可 `dispose()` 释放。
146
104
 
147
105
  ## 示例
148
106
 
149
107
  ```bash
150
- npm run demo # 启动 WebSocket 演示服务器(127.0.0.1:8090),见 example/demo.js
108
+ npm run demo # 启动演示服务器(127.0.0.1:8090),见 example/demo.js
151
109
  ```
152
110
 
153
111
  ## 测试
154
112
 
155
113
  ```bash
156
- npm test # jest --coverage,四项覆盖率阈值均为 100%
114
+ npm test # node --test
157
115
  ```
158
116
 
159
117
  ## 许可证
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { WebSocketServer } = require('..');
4
+
5
+ /** 演示监听端口 */
6
+ const DEMO_PORT = 8090;
7
+
8
+ /**
9
+ * 注册演示事件:握手路径即事件 target
10
+ * @param {Object} events 事件管理器
11
+ * @returns {void}
12
+ */
13
+ function bindEvents(events) {
14
+ events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx) => ({
15
+ type: 'echo',
16
+ client_id: ctx.client_id,
17
+ data: params.data
18
+ });
19
+
20
+ events.add({ name: 'time', target: '/time', end: true }).main = async () => ({
21
+ time: new Date().toISOString()
22
+ });
23
+
24
+ events.add({ name: 'error', target: '/error', end: true }).main = async () => {
25
+ throw new Error('boom');
26
+ };
27
+ }
28
+
29
+ /**
30
+ * 演示入口:启动服务器并打印监听地址
31
+ * @returns {Promise<void>} 无返回值
32
+ */
33
+ async function main() {
34
+ const websocket = new WebSocketServer({ name: 'demo', host: '127.0.0.1', port: DEMO_PORT });
35
+
36
+ bindEvents(websocket.getEventManager());
37
+ websocket.on('error', (err) => console.error(`[demo] 错误: ${err.message}`));
38
+
39
+ await websocket.init();
40
+ await websocket.start();
41
+
42
+ const address = websocket.getAddress();
43
+ console.log(`ws://127.0.0.1:${address.port}/echo`);
44
+ console.log(`ws://127.0.0.1:${address.port}/time`);
45
+ console.log('连接后发送任意内容,结果由事件返回并自动回发');
46
+ }
47
+
48
+ main().catch((err) => {
49
+ console.error(err);
50
+ process.exit(1);
51
+ });
package/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const WebSocketServer = require('./lib/websocket');
4
- const MessagePlugin = require('./lib/message');
5
4
  const Admin = require('mm_corejs').Admin;
6
5
 
7
6
  let admin = new Admin();
@@ -9,11 +8,10 @@ admin.setClass(WebSocketServer);
9
8
 
10
9
  module.exports = {
11
10
  WebSocketServer,
12
- MessagePlugin,
13
11
  admin,
14
12
  /**
15
13
  * 获取指定作用域的 WebSocket 服务器
16
- * @param {string} [scope='sys'] 作用域名称
14
+ * @param {string} [scope='os'] 作用域名称
17
15
  * @param {object} [config] 配置参数
18
16
  * @returns {WebSocketServer} WebSocket 服务器实例
19
17
  */
@@ -21,6 +19,6 @@ module.exports = {
21
19
  if (!scope) {
22
20
  scope = 'os';
23
21
  }
24
- return admin.get(scope, { name: scope, ...config });
22
+ return admin.get(scope, config);
25
23
  }
26
24
  };
package/lib/websocket.js CHANGED
@@ -1,47 +1,33 @@
1
1
  'use strict';
2
2
 
3
3
  const WebSocket = require('ws');
4
- const { Mod, Store } = require('mm_corejs');
5
- const { Handle } = require('mm_handle');
4
+ const { Mod } = require('mm_mod');
5
+ const { eventAdmin } = require('mm_event');
6
6
 
7
7
  /**
8
- * 服务器设置校验 schema(校验对象为顶层 config)
8
+ * 获取请求路径(在 ? 处截断,不解析查询串)
9
+ * @param {string} url 原始请求地址
10
+ * @returns {string} 请求路径
9
11
  */
10
- const SERVER_SCHEMA = {
11
- type: 'object',
12
- properties: {
13
- host: { type: 'string', min_length: 1 },
14
- port: { type: 'integer', minimum: 1, maximum: 65535 },
15
- path: { type: 'string', pattern: '^/' },
16
- json: { type: 'boolean' }
17
- }
18
- };
12
+ function getPath(url) {
13
+ const index = url.indexOf('?');
14
+ return index === -1 ? url : url.slice(0, index);
15
+ }
19
16
 
20
17
  /**
21
- * WebSocketServer —— 纯 WebSocket 服务器(入站适配器模块)。
22
- *
23
- * 继承 mm_corejs 的 Mod:生命周期由 Lifecycle 模板方法驱动,
24
- * 调用参数校验由 config.params 声明式定义;
25
- * 服务器设置(host / port / path / json)为顶层 config 字段,
26
- * 在 onInit 阶段按 SERVER_SCHEMA 统一校验。
18
+ * WebSocketServer —— 最薄的 WebSocket 传输层(入站适配器模块)。
27
19
  *
28
- * 采用 ws 独立监听模式(standalone):直接按 host / port 监听,
29
- * 仅处理 WebSocket 升级握手,不提供普通 HTTP 服务。
30
- *
31
- * 职责:
32
- * 1. 监听:onStart 创建 WebSocket.Server 并按 host / port / path 独立监听;
33
- * 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
34
- * 3. 消息管道:收到的消息经 Handle 的 before/check/main/render 阶段处理,
35
- * 管道产出 result 时自动回发来源客户端;getHandle() 可注册自定义钩子;
36
- * 4. 事件分发:connection / message / close / error 经 EventEmitter 向外广播;
37
- * 5. 主动操作:run(ctx, params) 按 params.action 派发 send / broadcast / close / list。
20
+ * 只做三件事:
21
+ * 1. 生命周期:由 Mod 的 init / start / stop 驱动 WebSocket.Server;
22
+ * 2. 消息转交:以握手请求路径为 target,直接调用
23
+ * EventManager.run(path, params, ctx, options),不做路由、不做插件;
24
+ * 3. 结果回发:事件返回非空结果时回发来源客户端,事件也可用 ctx.ws 自行写出。
38
25
  *
39
26
  * 用法:
40
- * const wss = new WebSocketServer({ name: 'websocket', host: '127.0.0.1', port: 8090 }, server);
41
- * wss.getHandle().on('main', (ctx, params) => ({ type: 'echo', data: params.data }), 100, 'echo');
27
+ * const wss = new WebSocketServer({ name: 'websocket', port: 8090 });
28
+ * wss.getEventManager().add({ name: 'echo', target: '*' }).main = async (params) => params.data;
42
29
  * await wss.init();
43
30
  * await wss.start();
44
- * await wss.run({}, { action: 'list' });
45
31
  */
46
32
  class WebSocketServer extends Mod {
47
33
  /**
@@ -50,63 +36,62 @@ class WebSocketServer extends Mod {
50
36
  static config = {
51
37
  name: 'os',
52
38
  title: 'WebSocket服务器模块',
53
- description: 'WebSocket 服务器,管理连接并分发消息',
54
- version: '1.0.0',
39
+ description: 'WebSocket 传输层:消息直达事件管理器,不做路由与插件',
40
+ version: '2.0.0',
55
41
  host: '0.0.0.0',
56
42
  port: 8090,
57
- path: '/',
58
43
  json: true,
59
- options: {},
60
44
  params: {
61
45
  type: 'object',
62
46
  required: ['action'],
63
47
  properties: {
64
48
  action: { type: 'string', enum: ['send', 'broadcast', 'close', 'list'] },
65
49
  client_id: { type: 'string', min_length: 1 },
66
- data: {},
67
- query: { type: 'object' }
50
+ data: {}
68
51
  }
69
52
  }
70
53
  };
71
54
 
72
55
  /**
73
- * ws 协议层实例(独立监听模式,内部自管 HTTP 升级)
56
+ * ws 服务器实例
74
57
  */
75
58
  #wss = null;
76
59
 
77
60
  /**
78
- * 连接注册表:Store 存储,主键 id、名称字段 name
61
+ * 事件管理器(取自 mm_event 的 websocket 作用域,与其他实例共享)
79
62
  */
80
- #clients = new Store({ key: 'id', name: 'name' });
63
+ #events = null;
81
64
 
82
65
  /**
83
- * 消息处理管道(before/check/main/render/error/success/after)
84
- * 每个实例独立 Handle,避免多实例共享管道导致钩子泄漏
66
+ * 在线连接表:client_id -> 连接信息
85
67
  */
86
- #handle = new Handle();
68
+ #clients = new Map();
87
69
 
88
70
  /**
89
71
  * 客户端编号自增计数器
90
72
  */
91
73
  #counter = 0;
92
74
 
93
- // ---------- 生命周期钩子 ----------
94
75
  /**
95
- * 初始化钩子:校验服务器设置并装配消息管道
76
+ * 构造函数
77
+ * @param {Object} config 模块配置
78
+ * @param {Object} server 框架实例
96
79
  */
97
- async onInit() {
98
- await this._validateConfig();
99
- this._initHandle();
80
+ constructor(config, server) {
81
+ super(config, server);
82
+ this.#events = eventAdmin('websocket');
100
83
  }
101
84
 
85
+ // ---------- 生命周期钩子 ----------
102
86
  /**
103
- * 启动钩子:创建 ws 独立服务器并开始监听
87
+ * 启动钩子:创建 ws 服务器并开始监听
88
+ * @returns {Promise<void>} 无返回值
104
89
  */
105
90
  async onStart() {
106
- const host = this.config.host;
107
- const port = this.config.port;
108
- const path = this.config.path;
109
- const wss = new WebSocket.Server({ host, port, path });
91
+ const wss = new WebSocket.Server({
92
+ host: this.config.host,
93
+ port: this.config.port
94
+ });
110
95
  this.#wss = wss;
111
96
  await this._listen(wss);
112
97
  wss.on('error', (err) => this._emitError(err));
@@ -114,20 +99,14 @@ class WebSocketServer extends Mod {
114
99
  }
115
100
 
116
101
  /**
117
- * 停止钩子:关闭全部连接并关闭底层服务器
102
+ * 停止钩子:断开全部连接并关闭服务器(事件管理器与其他实例共享,不清理)
103
+ * @returns {Promise<void>} 无返回值
118
104
  */
119
105
  async onStop() {
120
106
  this._closeAllClients();
121
107
  await this._closeServer();
122
108
  }
123
109
 
124
- /**
125
- * 释放钩子:清空连接注册表
126
- */
127
- async onDispose() {
128
- this.#clients.clear();
129
- }
130
-
131
110
  // ---------- 执行入口 ----------
132
111
  /**
133
112
  * 模块主逻辑:按 action 派发操作
@@ -141,7 +120,7 @@ class WebSocketServer extends Mod {
141
120
  if (action === 'send') return this._actionSend(params);
142
121
  if (action === 'broadcast') return this._actionBroadcast(params);
143
122
  if (action === 'close') return this._actionClose(params);
144
- return this._actionList(params);
123
+ return this._actionList();
145
124
  }
146
125
 
147
126
  /**
@@ -158,7 +137,7 @@ class WebSocketServer extends Mod {
158
137
  /**
159
138
  * 执行广播操作
160
139
  * @param {Object} params 调用参数,含 data
161
- * @returns {Object} 操作结果,count 为送达客户端数
140
+ * @returns {Object} 操作结果,count 为送达连接数
162
141
  */
163
142
  _actionBroadcast(params) {
164
143
  const count = this.broadcast(params.data);
@@ -178,177 +157,174 @@ class WebSocketServer extends Mod {
178
157
 
179
158
  /**
180
159
  * 执行列出连接操作
181
- * @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
182
- * @returns {Object} 操作结果,clients 为在线客户端列表
160
+ * @returns {Object} 操作结果,clients 为在线连接列表
161
+ */
162
+ _actionList() {
163
+ return { action: 'list', clients: this.listClients() };
164
+ }
165
+
166
+ // ---------- 事件管理器 ----------
167
+ /**
168
+ * 获取事件管理器,用于注册事件
169
+ * @returns {Object} 事件管理器实例
183
170
  */
184
- _actionList(params) {
185
- return { action: 'list', clients: this.listClients(params.query) };
171
+ getEventManager() {
172
+ return this.#events;
173
+ }
174
+
175
+ /**
176
+ * 注入外部事件管理器
177
+ * @param {Object} manager 事件管理器实例
178
+ * @returns {WebSocketServer} 当前实例
179
+ * @throws {TypeError} manager 不具备 run 方法时
180
+ */
181
+ setEventManager(manager) {
182
+ if (!manager || typeof manager.run !== 'function') {
183
+ throw new TypeError(`${this._label()} manager must provide run()`);
184
+ }
185
+ this.#events = manager;
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * 获取实际监听地址
191
+ * @returns {Object|null} 监听地址信息,未启动时为 null
192
+ */
193
+ getAddress() {
194
+ const wss = this.#wss;
195
+ return wss ? wss.address() : null;
186
196
  }
187
197
 
188
198
  // ---------- 公开操作 ----------
189
199
  /**
190
- * 向指定客户端发送数据
200
+ * 向指定连接发送数据
191
201
  * @param {string} client_id 客户端 ID
192
202
  * @param {string|Object} data 待发送数据
193
203
  * @returns {boolean} 是否发送成功
194
204
  */
195
205
  send(client_id, data) {
196
- const client = this.#clients.getById(client_id);
206
+ const client = this.#clients.get(client_id);
197
207
  if (!client) return false;
198
- const payload = this._encodeData(data);
199
- return this._sendToClient(client, payload);
208
+ return this._sendToClient(client, this._encodeData(data));
200
209
  }
201
210
 
202
211
  /**
203
- * 向全部在线客户端广播数据
212
+ * 向全部在线连接广播数据
204
213
  * @param {string|Object} data 待广播数据
205
- * @returns {number} 送达的客户端数量
214
+ * @returns {number} 送达的连接数量
206
215
  */
207
216
  broadcast(data) {
208
217
  const payload = this._encodeData(data);
209
218
  let count = 0;
210
- for (const client of this.#clients.list()) {
219
+ for (const client of this.#clients.values()) {
211
220
  if (this._sendToClient(client, payload)) count += 1;
212
221
  }
213
222
  return count;
214
223
  }
215
224
 
216
225
  /**
217
- * 关闭指定客户端连接(登记移除与 close 事件由连接关闭回调完成)
226
+ * 关闭指定连接(连接表由关闭回调清理)
218
227
  * @param {string} client_id 客户端 ID
219
228
  * @returns {boolean} 是否发起关闭
220
229
  */
221
230
  closeClient(client_id) {
222
- const client = this.#clients.getById(client_id);
231
+ const client = this.#clients.get(client_id);
223
232
  if (!client) return false;
224
233
  client.ws.close();
225
234
  return true;
226
235
  }
227
236
 
228
237
  /**
229
- * 列出在线客户端(支持 Store 条件查询与分页)
230
- * @param {Object} query 查询条件,可含 page_now / page_size / keyword
231
- * @returns {Array<Object>} 客户端信息列表,含 id 与 address
238
+ * 列出在线连接
239
+ * @returns {Array<Object>} 连接信息列表,含 id、path 与 address
232
240
  */
233
- listClients(query) {
234
- return this.#clients.list(query).map((client) => ({ id: client.id, address: client.address }));
241
+ listClients() {
242
+ const list = [];
243
+ for (const client of this.#clients.values()) {
244
+ list.push({ id: client.id, path: client.path, address: client.address });
245
+ }
246
+ return list;
235
247
  }
236
248
 
237
249
  /**
238
- * 判断客户端是否在线
250
+ * 判断连接是否在线
239
251
  * @param {string} client_id 客户端 ID
240
252
  * @returns {boolean} 是否在线
241
253
  */
242
254
  hasClient(client_id) {
243
- return !!this.#clients.getById(client_id);
255
+ return this.#clients.has(client_id);
244
256
  }
245
257
 
246
258
  /**
247
- * 绑定连接级选项(如 user_id / device_id / agent_id),入站消息将作为管道 options 透传给钩子与 RPC 处理函数
259
+ * 绑定连接级选项,入站消息将作为 options 透传给事件
248
260
  * @param {string} client_id 客户端 ID
249
261
  * @param {Object|null} options 选项对象,null 时清空
250
- * @returns {boolean} 是否设置成功(客户端不在线时失败)
262
+ * @returns {boolean} 是否设置成功(连接不在线时失败)
251
263
  */
252
264
  setClientOptions(client_id, options) {
253
- const client = this.#clients.getById(client_id);
265
+ const client = this.#clients.get(client_id);
254
266
  if (!client) return false;
255
267
  client.options = options || null;
256
268
  return true;
257
269
  }
258
270
 
259
- /**
260
- * 读取连接级选项,未绑定时返回空对象
261
- * @param {string} client_id 客户端 ID
262
- * @returns {Object} 连接级选项
263
- */
264
- _clientOptions(client_id) {
265
- const client = this.#clients.getById(client_id);
266
- if (!client || !client.options) return {};
267
- return client.options;
268
- }
269
-
270
- /**
271
- * 挂载插件:调用 plugin.bind(this),由插件自行向管道注册钩子;
272
- * 如 MessagePlugin 挂载后接管 JSON-RPC 请求
273
- * @param {Object} plugin 具备 bind(server) 方法的插件实例
274
- * @returns {WebsocketServer} 当前实例
275
- * @throws {TypeError} plugin 不具备 bind 方法时
276
- */
277
- use(plugin) {
278
- if (!plugin || typeof plugin.bind !== 'function') {
279
- throw new TypeError(`${this._label()} plugin must provide bind()`);
280
- }
281
- plugin.bind(this);
282
- return this;
283
- }
284
-
285
- /**
286
- * 获取消息处理管道,供外部注册 before/check/main/render 等钩子;
287
- * 钩子产出 result 时自动回发来源客户端
288
- * @returns {Handle} 消息管道实例
289
- */
290
- getHandle() {
291
- return this.#handle;
292
- }
293
-
294
- /**
295
- * 获取实际监听地址
296
- * @returns {Object|null} 监听地址信息,未启动时为 null
297
- */
298
- getAddress() {
299
- const wss = this.#wss;
300
- return wss ? wss.address() : null;
301
- }
302
-
303
271
  // ---------- 连接处理 ----------
304
272
  /**
305
- * 登记新的 WebSocket 连接并绑定事件
273
+ * 登记新的连接并绑定回调
306
274
  * @param {Object} ws ws 连接实例
307
- * @param {Object} req 升级请求对象
275
+ * @param {Object} req 握手请求对象
308
276
  */
309
277
  _handleConnection(ws, req) {
310
278
  this.#counter += 1;
311
279
  const client_id = `ws_${this.#counter}`;
312
280
  const client = {
313
281
  id: client_id,
314
- name: client_id,
282
+ path: getPath(req.url || '/'),
315
283
  address: this._remoteAddress(req),
316
- ws
284
+ ws,
285
+ options: null
317
286
  };
318
- this.#clients.add(client);
287
+ this.#clients.set(client_id, client);
319
288
  ws.on('message', (buf) => this._handleMessage(client_id, buf));
320
- ws.on('close', () => this._handleClose(client_id));
289
+ ws.on('close', () => this.#clients.delete(client_id));
321
290
  ws.on('error', (err) => this._emitError(err));
322
- this._safeEmit('connection', client);
323
291
  }
324
292
 
325
293
  /**
326
- * 处理收到的消息:解码后进入消息管道,产出 result 时自动回发
294
+ * 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
327
295
  * @param {string} client_id 客户端 ID
328
296
  * @param {Object} buf 数据缓冲区
329
297
  * @returns {Promise<void>} 无返回值
330
298
  */
331
299
  async _handleMessage(client_id, buf) {
332
- const decoded = this._decodeData(buf);
333
- const params = { client_id, data: decoded };
334
- const ctx = { client_id };
335
- const done = await this.#handle.run(ctx, params, this._clientOptions(client_id));
336
- if (done.error) this._emitError(done.error);
337
- if (done.result !== undefined) this.send(client_id, done.result);
300
+ const client = this.#clients.get(client_id);
301
+ if (!client) return;
302
+ const params = { client_id, data: this._decodeData(buf) };
303
+ const ctx = { client_id, path: client.path, ws: client.ws };
304
+ const options = this._clientOptions(client);
305
+ try {
306
+ const result = await this.#events.run(client.path, params, ctx, options);
307
+ const error = ctx.error || options.error;
308
+ if (error) this._emitError(error);
309
+ if (result !== undefined && result !== null) this.send(client_id, result);
310
+ } catch (err) {
311
+ this._emitError(err);
312
+ }
338
313
  }
339
314
 
340
315
  /**
341
- * 处理连接关闭
342
- * @param {string} client_id 客户端 ID
316
+ * 获取连接级选项(复制一份,避免事件管理器回写)
317
+ * @param {Object} client 连接信息
318
+ * @returns {Object} 选项对象
343
319
  */
344
- _handleClose(client_id) {
345
- const existed = this.#clients.delById(client_id);
346
- if (existed) this._safeEmit('close', client_id);
320
+ _clientOptions(client) {
321
+ if (!client.options) return {};
322
+ return { ...client.options };
347
323
  }
348
324
 
349
325
  /**
350
326
  * 获取远端地址字符串
351
- * @param {Object} req 升级请求对象
327
+ * @param {Object} req 握手请求对象
352
328
  * @returns {string} 形如 ip:port 的地址
353
329
  */
354
330
  _remoteAddress(req) {
@@ -393,8 +369,8 @@ class WebSocketServer extends Mod {
393
369
 
394
370
  // ---------- 发送与清理 ----------
395
371
  /**
396
- * 向单个客户端发送已编码数据
397
- * @param {Object} client 客户端信息
372
+ * 向单个连接发送已编码数据
373
+ * @param {Object} client 连接信息
398
374
  * @param {string} payload 已编码数据
399
375
  * @returns {boolean} 是否发送成功
400
376
  */
@@ -405,10 +381,10 @@ class WebSocketServer extends Mod {
405
381
  }
406
382
 
407
383
  /**
408
- * 关闭全部客户端连接并清空注册表
384
+ * 断开全部连接并清空连接表
409
385
  */
410
386
  _closeAllClients() {
411
- for (const client of this.#clients.list()) {
387
+ for (const client of this.#clients.values()) {
412
388
  client.ws.terminate();
413
389
  }
414
390
  this.#clients.clear();
@@ -430,7 +406,7 @@ class WebSocketServer extends Mod {
430
406
  }
431
407
 
432
408
  /**
433
- * 关闭 ws 服务器(含内部 HTTP 升级服务与残留连接)
409
+ * 关闭 ws 服务器(连接已在之前全部终止)
434
410
  * @returns {Promise<void>} 无返回值
435
411
  */
436
412
  _closeServer() {
@@ -438,35 +414,11 @@ class WebSocketServer extends Mod {
438
414
  this.#wss = null;
439
415
  if (!wss) return Promise.resolve();
440
416
  return new Promise((resolve) => {
441
- const http_server = wss.httpServer;
442
- if (http_server && typeof http_server.closeAllConnections === 'function') {
443
- http_server.closeAllConnections();
444
- }
445
- wss.close(resolve);
417
+ wss.close(() => resolve());
446
418
  });
447
419
  }
448
420
 
449
421
  // ---------- 校验辅助 ----------
450
- /**
451
- * 装配消息管道:内置 main 钩子负责分发 message 事件(sort 置后,
452
- * 不返回值,业务钩子产出的 result 由 _handleMessage 统一回发)
453
- */
454
- _initHandle() {
455
- this.#handle.on('main', (params, ctx) => {
456
- this._safeEmit('message', params.client_id, params.data);
457
- }, 1000, 'emit_message');
458
- }
459
-
460
- /**
461
- * 校验顶层 config 中的服务器设置
462
- * @returns {Promise<void>} 无返回值
463
- * @throws {Error} 校验不通过时
464
- */
465
- async _validateConfig() {
466
- const tip = await this._validate(this.config, SERVER_SCHEMA);
467
- if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
468
- }
469
-
470
422
  /**
471
423
  * 断言调用参数含 client_id
472
424
  * @param {Object} params 调用参数
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mm_websocket_server",
3
- "version": "1.0.2",
4
- "description": "WebSocket 服务器入站适配器模块,继承 mm_corejs 的 Mod 基类,支持独立运行。",
3
+ "version": "2.0.0",
4
+ "description": "最薄的 WebSocket 服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
5
  "keywords": [
6
6
  "超级美眉",
7
7
  "入站适配器",
@@ -21,7 +21,8 @@
21
21
  "main": "index.js",
22
22
  "files": [
23
23
  "index.js",
24
- "lib"
24
+ "lib",
25
+ "example"
25
26
  ],
26
27
  "engines": {
27
28
  "node": ">=22.5.0"
@@ -37,13 +38,12 @@
37
38
  "scripts": {
38
39
  "start": "node example/demo.js",
39
40
  "demo": "node example/demo.js",
40
- "test": "jest --coverage"
41
+ "test": "node --test \"test/*.test.js\""
41
42
  },
42
43
  "dependencies": {
43
44
  "mm_corejs": "^1.1.4",
45
+ "mm_event": "^2.2.2",
46
+ "mm_mod": "^1.0.1",
44
47
  "ws": "^8.22.0"
45
- },
46
- "devDependencies": {
47
- "jest": "^30.5.2"
48
48
  }
49
49
  }
package/lib/message.js DELETED
@@ -1,150 +0,0 @@
1
- 'use strict';
2
-
3
- const { JsonRpc } = require('mm_corejs');
4
-
5
- /**
6
- * MessagePlugin —— JSON-RPC 2.0 消息插件(mm_websocket_server 的可选业务组件)。
7
- *
8
- * 职责:
9
- * 1. 方法表:内部持有 JsonRpc 核心,addMethod / delMethod / listMethods 登记可调用方法;
10
- * 2. 管道挂载:bind(websocket) 后向消息管道注册 main 钩子 "json_rpc"(sort 100,
11
- * 先于内置 emit_message 执行),命中 JSON-RPC 请求时以响应对象作为管道 result,
12
- * 由 WebSocketServer 自动回发来源客户端;
13
- * 3. 通知回推:通知(无 id)处理函数返回非 undefined 结果时,产出服务器发起的
14
- * 通知对象 { jsonrpc, method, params: result } 回推来源客户端;无结果或出错时
15
- * 不产出;非 JSON-RPC 消息(解析失败、缺 jsonrpc 字段)透传,控制权交还管道;
16
- * 4. 主动推送:push(client_id, data) 委托 WebSocketServer.send,处理函数或服务器代码
17
- * 可在任意时刻(如异步任务完成后)向指定客户端回推结果;
18
- * 5. 反向调用:call(client_id, method, params, options) 向客户端发起 JSON-RPC 请求
19
- * 并等待其反馈;入站消息为响应形态(含 result/error、无 method)时被优先识别
20
- * 并结算待响应调用,实现服务器实时调用客户端方法;
21
- * 6. 编码适配:json 模式产出对象(由 send 序列化),文本模式字符串原样、其余 JSON 序列化。
22
- *
23
- * 用法:
24
- * const ws = new WebSocketServer({ name: 'websocket', port: 9200, json: true }, server);
25
- * const rpc = new MessagePlugin();
26
- * ws.use(rpc);
27
- * rpc.addMethod('echo', (params) => params);
28
- * await ws.init();
29
- * await ws.start();
30
- */
31
- class MessagePlugin {
32
- /**
33
- * JSON-RPC 协议核心
34
- */
35
- #rpc = new JsonRpc();
36
-
37
- /**
38
- * 已挂载的 WebSocketServer 实例,未挂载时为 null
39
- */
40
- #server = null;
41
-
42
- /**
43
- * 挂载到 WebSocketServer 的消息管道,注册内置 main 钩子
44
- * @param {Object} server 具备 getHandle() 的 WebSocketServer 实例
45
- * @returns {MessagePlugin} 当前实例
46
- * @throws {TypeError} server 不具备 getHandle 方法时
47
- */
48
- bind(server) {
49
- if (!server || typeof server.getHandle !== 'function') {
50
- throw new TypeError('[MessagePlugin] server must provide getHandle()');
51
- }
52
- this.#server = server;
53
- server.getHandle().on('main', (params, ctx, options) => {
54
- return this._execute(params, ctx, options);
55
- }, 100, 'json_rpc');
56
- return this;
57
- }
58
-
59
- /**
60
- * 判断是否已挂载
61
- * @returns {boolean} 是否已挂载到 WebSocketServer
62
- */
63
- isBound() {
64
- return this.#server !== null;
65
- }
66
-
67
- /**
68
- * 登记 RPC 方法
69
- * @param {string} name 方法名
70
- * @param {Function} handler 处理函数,签名 handler(params, extra, options),extra 含 client_id,options 为管道请求选项
71
- * @returns {MessagePlugin} 当前实例
72
- */
73
- addMethod(name, handler) {
74
- this.#rpc.addMethod(name, handler);
75
- return this;
76
- }
77
-
78
- /**
79
- * 删除 RPC 方法
80
- * @param {string} name 方法名
81
- * @returns {boolean} 是否删除成功
82
- */
83
- delMethod(name) {
84
- return this.#rpc.delMethod(name);
85
- }
86
-
87
- /**
88
- * 列出 RPC 方法(支持 Store 条件查询与分页)
89
- * @param {Object} [query] 查询条件,缺省时列出全部
90
- * @returns {Array<Object>} 方法列表,含 name
91
- */
92
- listMethods(query) {
93
- return this.#rpc.listMethods(query);
94
- }
95
-
96
- /**
97
- * 主动推送:向指定客户端回推数据(可用于异步任务完成后通知)
98
- * @param {string} client_id 目标客户端 ID
99
- * @param {string|Object} data 待推送数据,对象时按编码模式序列化
100
- * @returns {boolean} 是否发送成功
101
- * @throws {Error} 插件尚未挂载到服务器时
102
- */
103
- push(client_id, data) {
104
- if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
105
- const payload = this._format(data);
106
- return this.#server.send(client_id, payload);
107
- }
108
-
109
- /**
110
- * 实时调用客户端方法:发送 JSON-RPC 请求并等待客户端反馈 result
111
- * @param {string} client_id 目标客户端 ID
112
- * @param {string} method 客户端方法名
113
- * @param {*} [params] 调用参数
114
- * @param {Object} [options] 选项,含 timeout_ms(缺省 30000,0 为不超时)
115
- * @returns {Promise<*>} 客户端反馈的 result;错误响应、送达失败或超时时 reject
116
- * @throws {Error} 插件尚未挂载到服务器时
117
- */
118
- call(client_id, method, params, options = {}) {
119
- if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
120
- return this.#rpc.call((request) => this.#server.send(client_id, this._format(request)), method, params, options);
121
- }
122
-
123
- /**
124
- * 内置 main 钩子逻辑:优先识别客户端反馈,其次执行 RPC 请求并格式化响应
125
- * @param {Object} params 消息参数,含 client_id 与 data
126
- * @param {Object} ctx 执行上下文
127
- * @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
128
- * @returns {Promise<Object|string|undefined>} 响应对象或 JSON 字符串;反馈、透传与无结果通知时为 undefined
129
- */
130
- async _execute(params, ctx, options) {
131
- if (this.#rpc.acceptResponse(params.data)) return undefined;
132
- const extra = { client_id: params.client_id };
133
- const response = await this.#rpc.handle(params.data, extra, options);
134
- if (response === undefined) return undefined;
135
- return this._format(response);
136
- }
137
-
138
- /**
139
- * 按服务器 json 配置格式化数据
140
- * @param {Object|string} data 响应对象或待推送数据
141
- * @returns {Object|string} json 模式返回原值,文本模式字符串原样、其余 JSON 序列化
142
- */
143
- _format(data) {
144
- if (this.#server.config.json) return data;
145
- if (typeof data === 'string') return data;
146
- return JSON.stringify(data);
147
- }
148
- }
149
-
150
- module.exports = MessagePlugin;