ws-stomp-server 1.0.7 → 1.0.8

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,265 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var WebSocket = require('ws');
5
+
6
+ class Autowired {
7
+ constructor() { }
8
+ static container = {};
9
+ static register(name, instance) {
10
+ this.container[name] = instance;
11
+ }
12
+ static get(name) {
13
+ return this.container[name];
14
+ }
15
+ }
16
+
17
+ var StompCommand;
18
+ (function (StompCommand) {
19
+ StompCommand["PING"] = "PING";
20
+ StompCommand["CONNECT"] = "CONNECT";
21
+ StompCommand["CONNECTED"] = "CONNECTED";
22
+ StompCommand["SEND"] = "SEND";
23
+ StompCommand["SUBSCRIBE"] = "SUBSCRIBE";
24
+ StompCommand["UNSUBSCRIBE"] = "UNSUBSCRIBE";
25
+ StompCommand["ACK"] = "ACK";
26
+ StompCommand["NACK"] = "NACK";
27
+ StompCommand["BEGIN"] = "BEGIN";
28
+ StompCommand["COMMIT"] = "COMMIT";
29
+ StompCommand["ABORT"] = "ABORT";
30
+ StompCommand["DISCONNECT"] = "DISCONNECT";
31
+ StompCommand["MESSAGE"] = "MESSAGE";
32
+ StompCommand["RECEIPT"] = "RECEIPT";
33
+ StompCommand["ERROR"] = "ERROR";
34
+ })(StompCommand || (StompCommand = {}));
35
+ const BYTE = {
36
+ // LINEFEED byte (octet 10)
37
+ LF: '\x0A',
38
+ // NULL byte (octet 0)
39
+ NULL: '\x00',
40
+ };
41
+ class StompFrame {
42
+ command;
43
+ headers = {};
44
+ body = '';
45
+ constructor(command, headers = {}, body = '') {
46
+ this.command = command;
47
+ this.headers = headers;
48
+ this.body = body;
49
+ }
50
+ // 序列化为STOMP协议字符串
51
+ serialize() {
52
+ let output = `${this.command}${BYTE.LF}`;
53
+ for (const [key, value] of Object.entries(this.headers)) {
54
+ output += `${key}:${value}${BYTE.LF}`;
55
+ }
56
+ output += `${BYTE.LF}${this.body}${BYTE.NULL}`;
57
+ return output;
58
+ }
59
+ // 解析原始数据为STOMP帧
60
+ static parse(data) {
61
+ const frame = data.toString();
62
+ if (frame === BYTE.LF) {
63
+ return new StompFrame(StompCommand.PING);
64
+ }
65
+ const lines = frame.split(BYTE.LF);
66
+ let command = '';
67
+ const headers = {};
68
+ let body = '';
69
+ let inHeaderSection = true;
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const line = lines[i].trim();
72
+ if (i === 0 && !line.endsWith(BYTE.NULL)) {
73
+ // 首行为命令行
74
+ command = line;
75
+ }
76
+ else if (!line || line === '') {
77
+ inHeaderSection = false; // 开始进入 body
78
+ }
79
+ else if (inHeaderSection) {
80
+ const [key, value] = line.split(':', 2);
81
+ if (key !== undefined && value !== undefined) {
82
+ headers[key.trim()] = value.trim();
83
+ }
84
+ }
85
+ else {
86
+ body += `${line}\n`; // 注意这里可能需要处理最后多出的一个换行
87
+ }
88
+ // 如果遇到 \0,则说明是最后一个字符
89
+ if (line.includes(BYTE.NULL)) {
90
+ break;
91
+ }
92
+ }
93
+ body = body ? body.slice(0, -1).replace(/\x00/g, '') : ''; //处理body数据
94
+ return new StompFrame(command, headers, body);
95
+ }
96
+ }
97
+
98
+ class StompServer {
99
+ ws;
100
+ subscribeClients = []; //已订阅的客户端
101
+ subscribehandler = new Map(); //服务端订阅处理
102
+ authProvider;
103
+ constructor(options) {
104
+ const { server, path, authProvider } = options;
105
+ this.authProvider = authProvider;
106
+ this.ws = new WebSocket.WebSocketServer({ server, path });
107
+ this.ws.on('connection', (ws) => {
108
+ ws.on('message', (data) => {
109
+ this.handleMessage(data, ws);
110
+ });
111
+ ws.on('close', () => {
112
+ this.cleanupSubscriptions(ws);
113
+ });
114
+ });
115
+ }
116
+ static server(options) {
117
+ return new StompServer(options);
118
+ }
119
+ async handleMessage(data, ws) {
120
+ try {
121
+ const frame = StompFrame.parse(data);
122
+ // console.log(frame);
123
+ switch (frame.command) {
124
+ case StompCommand.CONNECT:
125
+ await this.handleConnect(frame, ws);
126
+ break;
127
+ case StompCommand.SEND:
128
+ this.handleSend(frame, ws);
129
+ break;
130
+ case StompCommand.SUBSCRIBE:
131
+ this.handleSubscribe(frame, ws);
132
+ break;
133
+ case StompCommand.UNSUBSCRIBE:
134
+ this.handleUnsubscribe(frame, ws);
135
+ break;
136
+ case StompCommand.DISCONNECT:
137
+ this.handleDisconnect(ws);
138
+ break;
139
+ case StompCommand.PING: {
140
+ this.cleanupWebsockets();
141
+ const timer = setTimeout(() => {
142
+ clearTimeout(timer);
143
+ ws.ping();
144
+ ws.send(BYTE.LF);
145
+ }, 1000);
146
+ // console.log('ping');
147
+ break;
148
+ }
149
+ case StompCommand.ACK:
150
+ case StompCommand.NACK:
151
+ break;
152
+ default:
153
+ this.sendError(ws, `Unsupported command: ${frame.command}`);
154
+ }
155
+ }
156
+ catch (error) {
157
+ console.log(error);
158
+ this.sendError(ws, 'Invalid frame format');
159
+ }
160
+ }
161
+ async handleConnect(frame, ws) {
162
+ // 认证检查(如果启用)
163
+ if (this.authProvider && !(await this.authProvider.authenticate(frame))) {
164
+ this.sendError(ws, 'Authentication failed');
165
+ ws.close();
166
+ return;
167
+ }
168
+ const heartBeat = frame.headers['heart-beat'] || '0,0';
169
+ const connectedFrame = new StompFrame(StompCommand.CONNECTED, {
170
+ version: '1.2',
171
+ 'heart-beat': heartBeat,
172
+ });
173
+ ws.send(connectedFrame.serialize());
174
+ }
175
+ handleSend(frame, _ws) {
176
+ const { destination } = frame.headers;
177
+ this.subscribehandler.forEach((callback, key) => {
178
+ if (destination === key) {
179
+ callback(frame);
180
+ }
181
+ });
182
+ }
183
+ handleSubscribe(frame, ws) {
184
+ const subId = frame.headers.id;
185
+ const { destination } = frame.headers;
186
+ if (!subId || !destination) {
187
+ this.sendError(ws, 'Missing subscription headers');
188
+ return;
189
+ }
190
+ this.subscribeClients.push({
191
+ id: subId,
192
+ destination,
193
+ ws,
194
+ });
195
+ }
196
+ handleUnsubscribe(frame, ws) {
197
+ const subId = frame.headers.id;
198
+ this.subscribeClients = this.subscribeClients.filter((sub) => !(sub.id === subId && sub.ws === ws));
199
+ }
200
+ handleDisconnect(ws) {
201
+ this.cleanupSubscriptions(ws);
202
+ ws.close();
203
+ }
204
+ // 清理断开连接的订阅
205
+ cleanupSubscriptions(ws) {
206
+ this.subscribeClients = this.subscribeClients.filter((sub) => sub.ws !== ws);
207
+ }
208
+ // 清理断开连接的WebSocket
209
+ cleanupWebsockets() {
210
+ this.subscribeClients = this.subscribeClients.filter((sub) => sub.ws.readyState === WebSocket.OPEN);
211
+ }
212
+ // 服务端主动发送消息
213
+ send(destination, body, headers = {}) {
214
+ const subs = this.subscribeClients.filter((sub) => sub.destination === destination);
215
+ if (subs.length) {
216
+ subs.forEach((sub) => {
217
+ const frame = new StompFrame(StompCommand.MESSAGE, {
218
+ destination,
219
+ 'message-id': crypto.randomUUID(),
220
+ timestamp: `${Date.now()}`,
221
+ subscription: sub.id,
222
+ ...headers,
223
+ }, body);
224
+ if (sub.ws.readyState === WebSocket.OPEN) {
225
+ sub.ws.send(frame.serialize());
226
+ }
227
+ });
228
+ return;
229
+ }
230
+ console.log(`${destination} unsubscribed client!`);
231
+ }
232
+ subscribe(destination, callback) {
233
+ this.subscribehandler.set(destination, callback);
234
+ }
235
+ unsubscribe(destination) {
236
+ this.subscribehandler.delete(destination);
237
+ }
238
+ sendError(ws, message) {
239
+ const errorFrame = new StompFrame(StompCommand.ERROR, { 'content-type': 'text/plain' }, message);
240
+ ws.send(errorFrame.serialize());
241
+ }
242
+ }
243
+
244
+ const className = 'stompServer';
245
+ class Stomp {
246
+ constructor() { }
247
+ static server(server, path, authProvider) {
248
+ const stomp = StompServer.server({ server, path, authProvider });
249
+ Autowired.register(className, stomp);
250
+ }
251
+ static get() {
252
+ return Autowired.get(className);
253
+ }
254
+ static send(destination, body, headers = {}) {
255
+ this.get()?.send(destination, body, headers);
256
+ }
257
+ static subscribe(destination, callback) {
258
+ this.get()?.subscribe(destination, callback);
259
+ }
260
+ static unsubscribe(destination) {
261
+ this.get()?.unsubscribe(destination);
262
+ }
263
+ }
264
+
265
+ exports.Stomp = Stomp;
package/package.json CHANGED
@@ -1,26 +1,25 @@
1
1
  {
2
2
  "name": "ws-stomp-server",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "ws-stomp-server",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "dist/index.cjs.js",
7
+ "module": "dist/index.esm.js",
7
8
  "types": "dist/index.d.ts",
8
9
  "files": [
9
10
  "dist"
10
11
  ],
11
- "engines": {
12
- "node": ">=14.17.0"
13
- },
14
- "scripts": {
15
- "build": "rollup -c rollup.config.js"
16
- },
17
12
  "exports": {
18
13
  ".": {
19
14
  "types": "./dist/index.d.ts",
20
- "import": "./dist/index.js"
15
+ "import": "./dist/index.esm.js",
16
+ "require": "./dist/index.cjs.js"
21
17
  },
22
18
  "./*": "./*"
23
19
  },
20
+ "scripts": {
21
+ "build": "rollup -c rollup.config.js"
22
+ },
24
23
  "dependencies": {
25
24
  "ws": "^8.19.0"
26
25
  },
@@ -28,14 +27,17 @@
28
27
  "@rollup/plugin-commonjs": "^29.0.0",
29
28
  "@rollup/plugin-json": "^6.1.0",
30
29
  "@rollup/plugin-node-resolve": "^16.0.3",
30
+ "@rollup/plugin-replace": "^6.0.3",
31
+ "@rollup/plugin-terser": "^0.4.4",
31
32
  "@rollup/plugin-typescript": "^12.3.0",
32
33
  "@types/node": "^24.10.7",
33
- "@types/ws": "^8.18.1",
34
+ "happy-dom": "^20.1.0",
34
35
  "rollup": "^4.55.1",
35
36
  "rollup-plugin-dts": "^6.3.0",
36
37
  "tslib": "^2.8.1",
37
38
  "typescript": "^5.9.3",
38
- "typescript-eslint-standard": "^9.75.0"
39
+ "typescript-eslint-standard": "^9.75.0",
40
+ "vitest": "^4.0.16"
39
41
  },
40
42
  "author": "mivui",
41
43
  "license": "MIT",
File without changes