ws-stomp 1.1.5 → 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 +102 -29
- package/dist/index.cjs.js +88 -38
- package/dist/index.d.ts +53 -3
- package/dist/index.esm.js +85 -39
- package/package.json +18 -6
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# ws-stomp
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/ws-stomp)
|
|
4
4
|
[](https://npmcharts.com/compare/ws-stomp?minimal=true)
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### implementation of the stomp version of ws
|
|
8
8
|
|
|
9
9
|
### install
|
|
10
10
|
|
|
@@ -12,71 +12,144 @@
|
|
|
12
12
|
npm i ws-stomp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
> http
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
import { createServer } from 'http';
|
|
19
|
-
import
|
|
19
|
+
import { Stomp } from 'ws-stomp';
|
|
20
20
|
|
|
21
21
|
const server = createServer();
|
|
22
|
-
|
|
22
|
+
Stomp.server(server, '/ws');
|
|
23
23
|
server.listen(3000);
|
|
24
24
|
// ws://lcalhost:3000/ws
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
> send
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
import
|
|
30
|
+
import { Stomp } from 'ws-stomp';
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
stomp.send('/topic/something', JSON.stringify({ name: 'name' }), { token: 'token' });
|
|
34
|
-
}
|
|
32
|
+
Stomp.send('/topic/something', JSON.stringify({ name: 'name' }), { token: 'token' });
|
|
35
33
|
```
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
> subscribe
|
|
38
36
|
|
|
39
37
|
```ts
|
|
40
|
-
import
|
|
38
|
+
import { Stomp } from 'ws-stomp';
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
}
|
|
40
|
+
Stomp.subscribe('/topic/greetings', (e) => {
|
|
41
|
+
const body = e.body;
|
|
42
|
+
});
|
|
47
43
|
```
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
> unsubscribe
|
|
50
46
|
|
|
51
47
|
```ts
|
|
52
|
-
import
|
|
48
|
+
import { Stomp } from 'ws-stomp';
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
stomp.unsubscribe('/topic/greetings');
|
|
56
|
-
}
|
|
50
|
+
Stomp.unsubscribe('/topic/greetings');
|
|
57
51
|
```
|
|
58
52
|
|
|
59
|
-
|
|
53
|
+
> express
|
|
60
54
|
|
|
61
|
-
|
|
55
|
+
#### server.js
|
|
62
56
|
|
|
63
57
|
```ts
|
|
64
58
|
import express from 'express';
|
|
65
|
-
import
|
|
59
|
+
import { Stomp } from 'ws-stomp';
|
|
66
60
|
|
|
67
61
|
const app = express();
|
|
68
62
|
app.get('/send', (_, res) => {
|
|
69
|
-
|
|
63
|
+
Stomp.send('/topic/something', 'payload');
|
|
70
64
|
res.status(200).json({});
|
|
71
65
|
});
|
|
72
66
|
const server = app.listen(3000);
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
Stomp.server(server, '/ws');
|
|
68
|
+
Stomp.subscribe('/topic/greetings', (message) => {
|
|
75
69
|
console.log(message.body);
|
|
76
70
|
});
|
|
77
71
|
```
|
|
78
72
|
|
|
79
|
-
|
|
73
|
+
#### browser.js
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { Client } from '@stomp/stompjs';
|
|
77
|
+
|
|
78
|
+
const client = new Client({
|
|
79
|
+
brokerURL: 'ws://lcalhost:3000/ws',
|
|
80
|
+
onConnect: () => {
|
|
81
|
+
client.publish({ destination: '/topic/greetings', body: 'Hello World!' });
|
|
82
|
+
client.subscribe('/topic/something', (message) => {
|
|
83
|
+
console.log(message.body);
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
client.activate();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> nest
|
|
91
|
+
|
|
92
|
+
#### install
|
|
93
|
+
|
|
94
|
+
```shell
|
|
95
|
+
npm i @nestjs/websockets
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### websocket.gateway.ts
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import {
|
|
102
|
+
MessageBody,
|
|
103
|
+
SubscribeMessage,
|
|
104
|
+
WebSocketGateway,
|
|
105
|
+
WebSocketServer,
|
|
106
|
+
} from '@nestjs/websockets';
|
|
107
|
+
|
|
108
|
+
import { StompFrame, StompServer } from 'ws-stomp';
|
|
109
|
+
|
|
110
|
+
@WebSocketGateway()
|
|
111
|
+
export class WebsocketGateway {
|
|
112
|
+
@WebSocketServer()
|
|
113
|
+
private readonly server: StompServer;
|
|
114
|
+
|
|
115
|
+
@SubscribeMessage('/topic/greetings')
|
|
116
|
+
public send(@MessageBody() message: StompFrame) {
|
|
117
|
+
console.log(message.body);
|
|
118
|
+
this.server.send('/topic/something', message.body);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### app.module.ts
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { Module } from '@nestjs/common';
|
|
127
|
+
|
|
128
|
+
import { WebsocketGateway } from './websocket.gateway';
|
|
129
|
+
|
|
130
|
+
@Module({
|
|
131
|
+
providers: [WebsocketGateway],
|
|
132
|
+
})
|
|
133
|
+
export class AppModule {}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### main.ts
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { StompAdapter } from 'ws-stomp';
|
|
140
|
+
import { NestFactory } from '@nestjs/core';
|
|
141
|
+
|
|
142
|
+
import { AppModule } from './app.module';
|
|
143
|
+
|
|
144
|
+
async function bootstrap() {
|
|
145
|
+
const app = await NestFactory.create(AppModule);
|
|
146
|
+
app.useWebSocketAdapter(new StompAdapter({ app, path: '/ws' }));
|
|
147
|
+
await app.listen(3000);
|
|
148
|
+
}
|
|
149
|
+
bootstrap();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### browser.js
|
|
80
153
|
|
|
81
154
|
```ts
|
|
82
155
|
import { Client } from '@stomp/stompjs';
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var websockets = require('@nestjs/websockets');
|
|
4
|
+
var rxjs = require('rxjs');
|
|
3
5
|
var crypto = require('crypto');
|
|
4
|
-
var
|
|
6
|
+
var ws = require('ws');
|
|
5
7
|
|
|
6
|
-
class
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
this.
|
|
8
|
+
class SimpleAuthProvider {
|
|
9
|
+
login;
|
|
10
|
+
passcode;
|
|
11
|
+
constructor(login, passcode) {
|
|
12
|
+
this.login = login ?? 'anonymous';
|
|
13
|
+
this.passcode = passcode ?? 'anonymous';
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
authenticate(frame) {
|
|
16
|
+
const { login } = frame.headers;
|
|
17
|
+
const { passcode } = frame.headers;
|
|
18
|
+
const success = Boolean(login) && Boolean(passcode) && login === this.login && passcode === this.passcode;
|
|
19
|
+
return success;
|
|
14
20
|
}
|
|
15
21
|
}
|
|
16
22
|
|
|
@@ -95,16 +101,15 @@ class StompFrame {
|
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
class StompServer {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
authProvider;
|
|
104
|
+
class StompServer extends ws.WebSocketServer {
|
|
105
|
+
_clients = []; // 已订阅的客户端
|
|
106
|
+
_handlers = new Map(); // 服务端订阅处理
|
|
107
|
+
auth;
|
|
103
108
|
constructor(options) {
|
|
104
|
-
const { server, path,
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
109
|
+
const { server, path, auth } = options;
|
|
110
|
+
super({ server, path });
|
|
111
|
+
this.auth = auth;
|
|
112
|
+
this.on('connection', (ws) => {
|
|
108
113
|
ws.on('message', (data) => {
|
|
109
114
|
this.handleMessage(data, ws);
|
|
110
115
|
});
|
|
@@ -113,9 +118,6 @@ class StompServer {
|
|
|
113
118
|
});
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
|
-
static server(options) {
|
|
117
|
-
return new StompServer(options);
|
|
118
|
-
}
|
|
119
121
|
async handleMessage(data, ws) {
|
|
120
122
|
try {
|
|
121
123
|
const frame = StompFrame.parse(data);
|
|
@@ -160,7 +162,7 @@ class StompServer {
|
|
|
160
162
|
}
|
|
161
163
|
async handleConnect(frame, ws) {
|
|
162
164
|
// 认证检查(如果启用)
|
|
163
|
-
if (this.
|
|
165
|
+
if (this.auth && !(await this.auth.authenticate(frame))) {
|
|
164
166
|
this.sendError(ws, 'Authentication failed');
|
|
165
167
|
ws.close();
|
|
166
168
|
return;
|
|
@@ -174,7 +176,7 @@ class StompServer {
|
|
|
174
176
|
}
|
|
175
177
|
handleSend(frame, _ws) {
|
|
176
178
|
const { destination } = frame.headers;
|
|
177
|
-
this.
|
|
179
|
+
this._handlers.forEach((callback, key) => {
|
|
178
180
|
if (destination === key) {
|
|
179
181
|
callback(frame);
|
|
180
182
|
}
|
|
@@ -187,7 +189,7 @@ class StompServer {
|
|
|
187
189
|
this.sendError(ws, 'Missing subscription headers');
|
|
188
190
|
return;
|
|
189
191
|
}
|
|
190
|
-
this.
|
|
192
|
+
this._clients.push({
|
|
191
193
|
id: subId,
|
|
192
194
|
destination,
|
|
193
195
|
ws,
|
|
@@ -195,7 +197,7 @@ class StompServer {
|
|
|
195
197
|
}
|
|
196
198
|
handleUnsubscribe(frame, ws) {
|
|
197
199
|
const subId = frame.headers.id;
|
|
198
|
-
this.
|
|
200
|
+
this._clients = this._clients.filter((sub) => !(sub.id === subId && sub.ws === ws));
|
|
199
201
|
}
|
|
200
202
|
handleDisconnect(ws) {
|
|
201
203
|
this.cleanupSubscriptions(ws);
|
|
@@ -203,15 +205,15 @@ class StompServer {
|
|
|
203
205
|
}
|
|
204
206
|
// 清理断开连接的订阅
|
|
205
207
|
cleanupSubscriptions(ws) {
|
|
206
|
-
this.
|
|
208
|
+
this._clients = this._clients.filter((sub) => sub.ws !== ws);
|
|
207
209
|
}
|
|
208
210
|
// 清理断开连接的WebSocket
|
|
209
211
|
cleanupWebsockets() {
|
|
210
|
-
this.
|
|
212
|
+
this._clients = this._clients.filter((sub) => sub.ws.readyState === ws.WebSocket.OPEN);
|
|
211
213
|
}
|
|
212
214
|
// 服务端主动发送消息
|
|
213
215
|
send(destination, body, headers = {}) {
|
|
214
|
-
const subs = this.
|
|
216
|
+
const subs = this._clients.filter((sub) => sub.destination === destination);
|
|
215
217
|
if (subs.length) {
|
|
216
218
|
subs.forEach((sub) => {
|
|
217
219
|
const frame = new StompFrame(StompCommand.MESSAGE, {
|
|
@@ -221,7 +223,7 @@ class StompServer {
|
|
|
221
223
|
subscription: sub.id,
|
|
222
224
|
...headers,
|
|
223
225
|
}, body);
|
|
224
|
-
if (sub.ws.readyState === WebSocket.OPEN) {
|
|
226
|
+
if (sub.ws.readyState === ws.WebSocket.OPEN) {
|
|
225
227
|
sub.ws.send(frame.serialize());
|
|
226
228
|
}
|
|
227
229
|
});
|
|
@@ -230,10 +232,10 @@ class StompServer {
|
|
|
230
232
|
console.log(`${destination} unsubscribed client!`);
|
|
231
233
|
}
|
|
232
234
|
subscribe(destination, callback) {
|
|
233
|
-
this.
|
|
235
|
+
this._handlers.set(destination, callback);
|
|
234
236
|
}
|
|
235
237
|
unsubscribe(destination) {
|
|
236
|
-
this.
|
|
238
|
+
this._handlers.delete(destination);
|
|
237
239
|
}
|
|
238
240
|
sendError(ws, message) {
|
|
239
241
|
const errorFrame = new StompFrame(StompCommand.ERROR, { 'content-type': 'text/plain' }, message);
|
|
@@ -241,25 +243,73 @@ class StompServer {
|
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
|
|
244
|
-
|
|
246
|
+
class StompAdapter extends websockets.AbstractWsAdapter {
|
|
247
|
+
server;
|
|
248
|
+
_options;
|
|
249
|
+
constructor(options) {
|
|
250
|
+
super(options.app);
|
|
251
|
+
this._options = options;
|
|
252
|
+
}
|
|
253
|
+
create(_port, _options) {
|
|
254
|
+
if (this.server)
|
|
255
|
+
return this.server;
|
|
256
|
+
const { path, auth } = this._options;
|
|
257
|
+
const server = new StompServer({ server: this.httpServer, path, auth });
|
|
258
|
+
this.server = server;
|
|
259
|
+
return server;
|
|
260
|
+
}
|
|
261
|
+
bindMessageHandlers(client, handlers, transform) {
|
|
262
|
+
rxjs.fromEvent(client, 'message')
|
|
263
|
+
.pipe(rxjs.mergeMap((data) => this.bindMessageHandler(data, handlers, transform)), rxjs.filter((result) => result !== undefined))
|
|
264
|
+
.subscribe((response) => {
|
|
265
|
+
client.send(response.data);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
bindMessageHandler(message, handlers, transform) {
|
|
269
|
+
const frame = StompFrame.parse(message.data);
|
|
270
|
+
if (frame.command === StompCommand.SEND) {
|
|
271
|
+
const { destination } = frame.headers;
|
|
272
|
+
const messageHandler = handlers.find((handler) => handler.message === destination);
|
|
273
|
+
if (!messageHandler)
|
|
274
|
+
return rxjs.EMPTY;
|
|
275
|
+
return transform(messageHandler.callback(frame));
|
|
276
|
+
}
|
|
277
|
+
return rxjs.EMPTY;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
class Autowired {
|
|
282
|
+
constructor() { }
|
|
283
|
+
static container = {};
|
|
284
|
+
static register(name, instance) {
|
|
285
|
+
this.container[name] = instance;
|
|
286
|
+
}
|
|
287
|
+
static get(name) {
|
|
288
|
+
return this.container[name];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
245
291
|
class Stomp {
|
|
246
292
|
constructor() { }
|
|
247
|
-
static server(server, path,
|
|
248
|
-
const stomp = StompServer
|
|
249
|
-
Autowired.register(
|
|
293
|
+
static server(server, path, auth) {
|
|
294
|
+
const stomp = new StompServer({ server, path, auth });
|
|
295
|
+
Autowired.register(StompServer.name, stomp);
|
|
250
296
|
}
|
|
251
297
|
static send(destination, body, headers = {}) {
|
|
252
|
-
const server = Autowired.get(
|
|
298
|
+
const server = Autowired.get(StompServer.name);
|
|
253
299
|
server?.send(destination, body, headers);
|
|
254
300
|
}
|
|
255
301
|
static subscribe(destination, callback) {
|
|
256
|
-
const server = Autowired.get(
|
|
302
|
+
const server = Autowired.get(StompServer.name);
|
|
257
303
|
server?.subscribe(destination, callback);
|
|
258
304
|
}
|
|
259
305
|
static unsubscribe(destination) {
|
|
260
|
-
const server = Autowired.get(
|
|
306
|
+
const server = Autowired.get(StompServer.name);
|
|
261
307
|
server?.unsubscribe(destination);
|
|
262
308
|
}
|
|
263
309
|
}
|
|
264
310
|
|
|
265
|
-
|
|
311
|
+
exports.SimpleAuthProvider = SimpleAuthProvider;
|
|
312
|
+
exports.Stomp = Stomp;
|
|
313
|
+
exports.StompAdapter = StompAdapter;
|
|
314
|
+
exports.StompFrame = StompFrame;
|
|
315
|
+
exports.StompServer = StompServer;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { INestApplicationContext, WsMessageHandler } from '@nestjs/common';
|
|
2
|
+
import { AbstractWsAdapter, GatewayMetadata } from '@nestjs/websockets';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
4
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
1
5
|
import { Server } from 'http';
|
|
2
6
|
import { Server as Server$1 } from 'https';
|
|
3
7
|
|
|
4
8
|
declare enum StompCommand {
|
|
5
|
-
PING = "PING"
|
|
9
|
+
PING = "PING",//custom usage
|
|
6
10
|
CONNECT = "CONNECT",
|
|
7
11
|
CONNECTED = "CONNECTED",
|
|
8
12
|
SEND = "SEND",
|
|
@@ -30,13 +34,59 @@ declare class StompFrame {
|
|
|
30
34
|
interface AuthProvider {
|
|
31
35
|
authenticate: (frame: StompFrame) => Promise<boolean> | boolean;
|
|
32
36
|
}
|
|
37
|
+
declare class SimpleAuthProvider implements AuthProvider {
|
|
38
|
+
private readonly login;
|
|
39
|
+
private readonly passcode;
|
|
40
|
+
constructor(login?: string, passcode?: string);
|
|
41
|
+
authenticate(frame: StompFrame): boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface StompServerOptions {
|
|
45
|
+
server: Server | Server$1;
|
|
46
|
+
path: string;
|
|
47
|
+
auth?: AuthProvider;
|
|
48
|
+
}
|
|
49
|
+
declare class StompServer extends WebSocketServer {
|
|
50
|
+
private _clients;
|
|
51
|
+
private readonly _handlers;
|
|
52
|
+
private readonly auth?;
|
|
53
|
+
constructor(options: StompServerOptions);
|
|
54
|
+
private handleMessage;
|
|
55
|
+
private handleConnect;
|
|
56
|
+
private handleSend;
|
|
57
|
+
private handleSubscribe;
|
|
58
|
+
private handleUnsubscribe;
|
|
59
|
+
private handleDisconnect;
|
|
60
|
+
private cleanupSubscriptions;
|
|
61
|
+
private cleanupWebsockets;
|
|
62
|
+
send(destination: string, body: string, headers?: Record<string, string>): void;
|
|
63
|
+
subscribe(destination: string, callback: (frame: StompFrame) => void): void;
|
|
64
|
+
unsubscribe(destination: string): void;
|
|
65
|
+
private sendError;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface StompAdapterOptions extends Omit<StompServerOptions, 'server'> {
|
|
69
|
+
app: INestApplicationContext;
|
|
70
|
+
}
|
|
71
|
+
interface Message {
|
|
72
|
+
data: Buffer;
|
|
73
|
+
}
|
|
74
|
+
declare class StompAdapter extends AbstractWsAdapter {
|
|
75
|
+
private server?;
|
|
76
|
+
private readonly _options;
|
|
77
|
+
constructor(options: StompAdapterOptions);
|
|
78
|
+
create(_port: number, _options?: GatewayMetadata): StompServer;
|
|
79
|
+
bindMessageHandlers(client: WebSocket, handlers: WsMessageHandler[], transform: (data: Observable<StompFrame>) => Observable<Message>): void;
|
|
80
|
+
bindMessageHandler(message: Message, handlers: WsMessageHandler[], transform: (data: Observable<StompFrame>) => Observable<Message>): Observable<Message>;
|
|
81
|
+
}
|
|
33
82
|
|
|
34
83
|
declare class Stomp {
|
|
35
84
|
private constructor();
|
|
36
|
-
static server(server: Server | Server$1, path: string,
|
|
85
|
+
static server(server: Server | Server$1, path: string, auth?: AuthProvider): void;
|
|
37
86
|
static send(destination: string, body: string, headers?: Record<string, string>): void;
|
|
38
87
|
static subscribe(destination: string, callback: (frame: StompFrame) => void): void;
|
|
39
88
|
static unsubscribe(destination: string): void;
|
|
40
89
|
}
|
|
41
90
|
|
|
42
|
-
export { Stomp
|
|
91
|
+
export { SimpleAuthProvider, Stomp, StompAdapter, StompFrame, StompServer };
|
|
92
|
+
export type { AuthProvider, StompServerOptions };
|
package/dist/index.esm.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { AbstractWsAdapter } from '@nestjs/websockets';
|
|
2
|
+
import { fromEvent, mergeMap, filter, EMPTY } from 'rxjs';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
5
|
|
|
4
|
-
class
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this.
|
|
6
|
+
class SimpleAuthProvider {
|
|
7
|
+
login;
|
|
8
|
+
passcode;
|
|
9
|
+
constructor(login, passcode) {
|
|
10
|
+
this.login = login ?? 'anonymous';
|
|
11
|
+
this.passcode = passcode ?? 'anonymous';
|
|
9
12
|
}
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
authenticate(frame) {
|
|
14
|
+
const { login } = frame.headers;
|
|
15
|
+
const { passcode } = frame.headers;
|
|
16
|
+
const success = Boolean(login) && Boolean(passcode) && login === this.login && passcode === this.passcode;
|
|
17
|
+
return success;
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
|
|
@@ -93,16 +99,15 @@ class StompFrame {
|
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
class StompServer {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
authProvider;
|
|
102
|
+
class StompServer extends WebSocketServer {
|
|
103
|
+
_clients = []; // 已订阅的客户端
|
|
104
|
+
_handlers = new Map(); // 服务端订阅处理
|
|
105
|
+
auth;
|
|
101
106
|
constructor(options) {
|
|
102
|
-
const { server, path,
|
|
103
|
-
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
107
|
+
const { server, path, auth } = options;
|
|
108
|
+
super({ server, path });
|
|
109
|
+
this.auth = auth;
|
|
110
|
+
this.on('connection', (ws) => {
|
|
106
111
|
ws.on('message', (data) => {
|
|
107
112
|
this.handleMessage(data, ws);
|
|
108
113
|
});
|
|
@@ -111,9 +116,6 @@ class StompServer {
|
|
|
111
116
|
});
|
|
112
117
|
});
|
|
113
118
|
}
|
|
114
|
-
static server(options) {
|
|
115
|
-
return new StompServer(options);
|
|
116
|
-
}
|
|
117
119
|
async handleMessage(data, ws) {
|
|
118
120
|
try {
|
|
119
121
|
const frame = StompFrame.parse(data);
|
|
@@ -158,7 +160,7 @@ class StompServer {
|
|
|
158
160
|
}
|
|
159
161
|
async handleConnect(frame, ws) {
|
|
160
162
|
// 认证检查(如果启用)
|
|
161
|
-
if (this.
|
|
163
|
+
if (this.auth && !(await this.auth.authenticate(frame))) {
|
|
162
164
|
this.sendError(ws, 'Authentication failed');
|
|
163
165
|
ws.close();
|
|
164
166
|
return;
|
|
@@ -172,7 +174,7 @@ class StompServer {
|
|
|
172
174
|
}
|
|
173
175
|
handleSend(frame, _ws) {
|
|
174
176
|
const { destination } = frame.headers;
|
|
175
|
-
this.
|
|
177
|
+
this._handlers.forEach((callback, key) => {
|
|
176
178
|
if (destination === key) {
|
|
177
179
|
callback(frame);
|
|
178
180
|
}
|
|
@@ -185,7 +187,7 @@ class StompServer {
|
|
|
185
187
|
this.sendError(ws, 'Missing subscription headers');
|
|
186
188
|
return;
|
|
187
189
|
}
|
|
188
|
-
this.
|
|
190
|
+
this._clients.push({
|
|
189
191
|
id: subId,
|
|
190
192
|
destination,
|
|
191
193
|
ws,
|
|
@@ -193,7 +195,7 @@ class StompServer {
|
|
|
193
195
|
}
|
|
194
196
|
handleUnsubscribe(frame, ws) {
|
|
195
197
|
const subId = frame.headers.id;
|
|
196
|
-
this.
|
|
198
|
+
this._clients = this._clients.filter((sub) => !(sub.id === subId && sub.ws === ws));
|
|
197
199
|
}
|
|
198
200
|
handleDisconnect(ws) {
|
|
199
201
|
this.cleanupSubscriptions(ws);
|
|
@@ -201,20 +203,20 @@ class StompServer {
|
|
|
201
203
|
}
|
|
202
204
|
// 清理断开连接的订阅
|
|
203
205
|
cleanupSubscriptions(ws) {
|
|
204
|
-
this.
|
|
206
|
+
this._clients = this._clients.filter((sub) => sub.ws !== ws);
|
|
205
207
|
}
|
|
206
208
|
// 清理断开连接的WebSocket
|
|
207
209
|
cleanupWebsockets() {
|
|
208
|
-
this.
|
|
210
|
+
this._clients = this._clients.filter((sub) => sub.ws.readyState === WebSocket.OPEN);
|
|
209
211
|
}
|
|
210
212
|
// 服务端主动发送消息
|
|
211
213
|
send(destination, body, headers = {}) {
|
|
212
|
-
const subs = this.
|
|
214
|
+
const subs = this._clients.filter((sub) => sub.destination === destination);
|
|
213
215
|
if (subs.length) {
|
|
214
216
|
subs.forEach((sub) => {
|
|
215
217
|
const frame = new StompFrame(StompCommand.MESSAGE, {
|
|
216
218
|
destination,
|
|
217
|
-
'message-id':
|
|
219
|
+
'message-id': randomUUID(),
|
|
218
220
|
timestamp: `${Date.now()}`,
|
|
219
221
|
subscription: sub.id,
|
|
220
222
|
...headers,
|
|
@@ -228,10 +230,10 @@ class StompServer {
|
|
|
228
230
|
console.log(`${destination} unsubscribed client!`);
|
|
229
231
|
}
|
|
230
232
|
subscribe(destination, callback) {
|
|
231
|
-
this.
|
|
233
|
+
this._handlers.set(destination, callback);
|
|
232
234
|
}
|
|
233
235
|
unsubscribe(destination) {
|
|
234
|
-
this.
|
|
236
|
+
this._handlers.delete(destination);
|
|
235
237
|
}
|
|
236
238
|
sendError(ws, message) {
|
|
237
239
|
const errorFrame = new StompFrame(StompCommand.ERROR, { 'content-type': 'text/plain' }, message);
|
|
@@ -239,25 +241,69 @@ class StompServer {
|
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
|
|
242
|
-
|
|
244
|
+
class StompAdapter extends AbstractWsAdapter {
|
|
245
|
+
server;
|
|
246
|
+
_options;
|
|
247
|
+
constructor(options) {
|
|
248
|
+
super(options.app);
|
|
249
|
+
this._options = options;
|
|
250
|
+
}
|
|
251
|
+
create(_port, _options) {
|
|
252
|
+
if (this.server)
|
|
253
|
+
return this.server;
|
|
254
|
+
const { path, auth } = this._options;
|
|
255
|
+
const server = new StompServer({ server: this.httpServer, path, auth });
|
|
256
|
+
this.server = server;
|
|
257
|
+
return server;
|
|
258
|
+
}
|
|
259
|
+
bindMessageHandlers(client, handlers, transform) {
|
|
260
|
+
fromEvent(client, 'message')
|
|
261
|
+
.pipe(mergeMap((data) => this.bindMessageHandler(data, handlers, transform)), filter((result) => result !== undefined))
|
|
262
|
+
.subscribe((response) => {
|
|
263
|
+
client.send(response.data);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
bindMessageHandler(message, handlers, transform) {
|
|
267
|
+
const frame = StompFrame.parse(message.data);
|
|
268
|
+
if (frame.command === StompCommand.SEND) {
|
|
269
|
+
const { destination } = frame.headers;
|
|
270
|
+
const messageHandler = handlers.find((handler) => handler.message === destination);
|
|
271
|
+
if (!messageHandler)
|
|
272
|
+
return EMPTY;
|
|
273
|
+
return transform(messageHandler.callback(frame));
|
|
274
|
+
}
|
|
275
|
+
return EMPTY;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
class Autowired {
|
|
280
|
+
constructor() { }
|
|
281
|
+
static container = {};
|
|
282
|
+
static register(name, instance) {
|
|
283
|
+
this.container[name] = instance;
|
|
284
|
+
}
|
|
285
|
+
static get(name) {
|
|
286
|
+
return this.container[name];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
243
289
|
class Stomp {
|
|
244
290
|
constructor() { }
|
|
245
|
-
static server(server, path,
|
|
246
|
-
const stomp = StompServer
|
|
247
|
-
Autowired.register(
|
|
291
|
+
static server(server, path, auth) {
|
|
292
|
+
const stomp = new StompServer({ server, path, auth });
|
|
293
|
+
Autowired.register(StompServer.name, stomp);
|
|
248
294
|
}
|
|
249
295
|
static send(destination, body, headers = {}) {
|
|
250
|
-
const server = Autowired.get(
|
|
296
|
+
const server = Autowired.get(StompServer.name);
|
|
251
297
|
server?.send(destination, body, headers);
|
|
252
298
|
}
|
|
253
299
|
static subscribe(destination, callback) {
|
|
254
|
-
const server = Autowired.get(
|
|
300
|
+
const server = Autowired.get(StompServer.name);
|
|
255
301
|
server?.subscribe(destination, callback);
|
|
256
302
|
}
|
|
257
303
|
static unsubscribe(destination) {
|
|
258
|
-
const server = Autowired.get(
|
|
304
|
+
const server = Autowired.get(StompServer.name);
|
|
259
305
|
server?.unsubscribe(destination);
|
|
260
306
|
}
|
|
261
307
|
}
|
|
262
308
|
|
|
263
|
-
export { Stomp
|
|
309
|
+
export { SimpleAuthProvider, Stomp, StompAdapter, StompFrame, StompServer };
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ws-stomp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "ws-stomp is a simple Node.js server base on websocket stomp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs.js",
|
|
7
7
|
"module": "dist/index.esm.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"deprecated": "",
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
11
13
|
"exports": {
|
|
12
14
|
".": {
|
|
13
15
|
"types": "./dist/index.d.ts",
|
|
@@ -19,17 +21,21 @@
|
|
|
19
21
|
"scripts": {
|
|
20
22
|
"build": "rollup -c rollup.config.js"
|
|
21
23
|
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@nestjs/websockets": "^11.1.15"
|
|
26
|
+
},
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"ws": "^8.19.0"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
26
|
-
"@rollup/plugin-commonjs": "^29.0.
|
|
31
|
+
"@rollup/plugin-commonjs": "^29.0.1",
|
|
27
32
|
"@rollup/plugin-json": "^6.1.0",
|
|
28
33
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
29
34
|
"@rollup/plugin-replace": "^6.0.3",
|
|
30
|
-
"@rollup/plugin-terser": "^0.
|
|
35
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
31
36
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
32
|
-
"@types/node": "^24.
|
|
37
|
+
"@types/node": "^24.11.0",
|
|
38
|
+
"@types/ws": "^8.18.1",
|
|
33
39
|
"rollup": "^4.59.0",
|
|
34
40
|
"rollup-plugin-dts": "^6.3.0",
|
|
35
41
|
"tslib": "^2.8.1",
|
|
@@ -39,7 +45,13 @@
|
|
|
39
45
|
},
|
|
40
46
|
"author": "mivui",
|
|
41
47
|
"license": "MIT",
|
|
42
|
-
"keywords": [
|
|
48
|
+
"keywords": [
|
|
49
|
+
"stomp",
|
|
50
|
+
"ws",
|
|
51
|
+
"node",
|
|
52
|
+
"websocket",
|
|
53
|
+
"nest"
|
|
54
|
+
],
|
|
43
55
|
"repository": {
|
|
44
56
|
"type": "git",
|
|
45
57
|
"url": "git+https://github.com/mivui/node-ws-stomp.git"
|