@arcblock/ws 1.6.10
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE +13 -0
- package/README.md +85 -0
- package/lib/browser.js +5 -0
- package/lib/client/base.js +108 -0
- package/lib/client/browser.js +6 -0
- package/lib/client/index.js +7 -0
- package/lib/index.js +7 -0
- package/lib/logger.js +12 -0
- package/lib/server/index.js +381 -0
- package/package.json +48 -0
package/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2018-2019 ArcBlock
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
package/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Blocklet Server WebSocket
|
2
|
+
|
3
|
+
> Blocklet Server PubSub base on Websocket and Phoenix Protocol
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
1. WsServer
|
8
|
+
|
9
|
+
```javascript
|
10
|
+
const { WsServer } = require('@arcblock/ws');
|
11
|
+
|
12
|
+
/**
|
13
|
+
* @params {Object} opts
|
14
|
+
* @params {http.Server} opts.httpServer
|
15
|
+
* @params {String} opts.pathname default to '/websocket'
|
16
|
+
* @params {Function} opts.authenticate
|
17
|
+
*/
|
18
|
+
const wsServer = new WsServer({
|
19
|
+
httpServer: http.createServer(),
|
20
|
+
authenticate: (req, cb) => {
|
21
|
+
const { searchParams } = new URL(req.url, `http://${req.headers.host || 'unknown'}`);
|
22
|
+
const token = searchParams.get('token');
|
23
|
+
if (!token) {
|
24
|
+
cb(new Error('token not found'), null);
|
25
|
+
return;
|
26
|
+
}
|
27
|
+
|
28
|
+
// custom logic for validate token
|
29
|
+
const authInfo = validateToken(token);
|
30
|
+
|
31
|
+
// if validate success
|
32
|
+
cb(null, authInfo);
|
33
|
+
|
34
|
+
// if validate error
|
35
|
+
cb(new Error('validate fail'), null);
|
36
|
+
},
|
37
|
+
});
|
38
|
+
|
39
|
+
// attach to httpServer(httpServer has passed by constructor)
|
40
|
+
wsSerer.attach();
|
41
|
+
|
42
|
+
// push message
|
43
|
+
wsServer.push('blocklet.installed', data);
|
44
|
+
wsServer.push('notification.create', data);
|
45
|
+
```
|
46
|
+
|
47
|
+
2. WsClient
|
48
|
+
|
49
|
+
WsClient is inherited from [Phoenix](https://www.npmjs.com/package/phoenix)([source](https://github.com/phoenixframework/phoenix/blob/master/assets/js/phoenix.js)),
|
50
|
+
|
51
|
+
```javascript
|
52
|
+
import WsClient from '@arcblock/ws/lib/client';
|
53
|
+
|
54
|
+
// create instance
|
55
|
+
const socket = new WsClient(`//${window.location.hostname}`, {
|
56
|
+
// params will be passed to server through url
|
57
|
+
params: () => ({
|
58
|
+
// token is used for authentication
|
59
|
+
token: window.localStorage.getItem('abt_node_login_token'),
|
60
|
+
}),
|
61
|
+
|
62
|
+
// Defaults to none
|
63
|
+
logger: (type, msg, data) => console.log(type, msg, data),
|
64
|
+
});
|
65
|
+
|
66
|
+
// connect
|
67
|
+
socket.connect();
|
68
|
+
|
69
|
+
// add subscriber
|
70
|
+
socket.on('blocklet.installed', callback1);
|
71
|
+
socket.on('notification.create', callback2);
|
72
|
+
|
73
|
+
// remove subscriber
|
74
|
+
socket.off('blocklet.installed', callback1);
|
75
|
+
socket.off('notification.create', callback2);
|
76
|
+
|
77
|
+
// disconnect
|
78
|
+
socket.disconnect(() => {
|
79
|
+
// after disconnected...
|
80
|
+
});
|
81
|
+
```
|
82
|
+
|
83
|
+
3. Hooks
|
84
|
+
|
85
|
+
It's very simple to create hooks in react apps.
|
package/lib/browser.js
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
const createLogger = require('../logger');
|
2
|
+
|
3
|
+
module.exports = (Socket, EventEmitter, transport) => {
|
4
|
+
return class WsClient extends Socket {
|
5
|
+
constructor(endpoint, opts = {}) {
|
6
|
+
super(endpoint, { transport, ...opts });
|
7
|
+
|
8
|
+
this._logger = createLogger('client', opts.silent);
|
9
|
+
this.emitter = new EventEmitter();
|
10
|
+
|
11
|
+
this.onOpen(() => {
|
12
|
+
this._logger.debug('socket open', endpoint);
|
13
|
+
});
|
14
|
+
this.onClose(() => {
|
15
|
+
this._logger.debug('socket close', endpoint);
|
16
|
+
});
|
17
|
+
this.onError((err) => {
|
18
|
+
this._logger.error('socket error', err.error);
|
19
|
+
});
|
20
|
+
this.onMessage((message) => {
|
21
|
+
this._logger.debug('socket message', { message });
|
22
|
+
});
|
23
|
+
}
|
24
|
+
|
25
|
+
on(event, handler) {
|
26
|
+
this.ensureJoinChannel(event);
|
27
|
+
this.emitter.on(event, handler);
|
28
|
+
}
|
29
|
+
|
30
|
+
off(event, handler) {
|
31
|
+
if (handler) {
|
32
|
+
this.emitter.off(event, handler);
|
33
|
+
} else {
|
34
|
+
this.emitter.removeAllListeners(event);
|
35
|
+
}
|
36
|
+
|
37
|
+
this.ensureLeaveChannel(event);
|
38
|
+
}
|
39
|
+
|
40
|
+
disconnect(callback, code, reason) {
|
41
|
+
this.emitter.eventNames().forEach((event) => {
|
42
|
+
this.emitter.removeAllListeners(event);
|
43
|
+
});
|
44
|
+
super.disconnect(callback, code, reason);
|
45
|
+
}
|
46
|
+
|
47
|
+
/**
|
48
|
+
* private
|
49
|
+
*/
|
50
|
+
ensureJoinChannel(event) {
|
51
|
+
const count = this.emitter.listenerCount(event);
|
52
|
+
if (count > 0) {
|
53
|
+
return;
|
54
|
+
}
|
55
|
+
|
56
|
+
const topic = event;
|
57
|
+
const channel = this.channel(topic);
|
58
|
+
channel
|
59
|
+
.join()
|
60
|
+
.receive('ok', (message) => {
|
61
|
+
this._logger.debug('join success', { event, message });
|
62
|
+
})
|
63
|
+
.receive('error', (error) => {
|
64
|
+
this._logger.error('join error', { event, error });
|
65
|
+
})
|
66
|
+
.receive('timeout', () => {
|
67
|
+
this._logger.debug('join timeout', { event });
|
68
|
+
});
|
69
|
+
channel.on(event, ({ status, response: data }) => {
|
70
|
+
if (status === 'ok') {
|
71
|
+
this.emitter.emit(event, data);
|
72
|
+
} else {
|
73
|
+
this._logger.debug('response error', { event, status, data });
|
74
|
+
}
|
75
|
+
});
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* private
|
80
|
+
*/
|
81
|
+
ensureLeaveChannel(event) {
|
82
|
+
const count = this.emitter.listenerCount(event);
|
83
|
+
if (count > 0) {
|
84
|
+
return;
|
85
|
+
}
|
86
|
+
|
87
|
+
const topic = event;
|
88
|
+
const channel = this.channels.find((c) => c.topic === topic);
|
89
|
+
if (!channel) {
|
90
|
+
return;
|
91
|
+
}
|
92
|
+
|
93
|
+
this.remove(channel);
|
94
|
+
channel
|
95
|
+
.leave()
|
96
|
+
.receive('ok', (message) => {
|
97
|
+
this._logger.debug('leave success', { event, message });
|
98
|
+
})
|
99
|
+
.receive('error', (err) => {
|
100
|
+
this._logger.error('leave error', { event, err });
|
101
|
+
})
|
102
|
+
.receive('timeout', () => {
|
103
|
+
this._logger.debug('leave timeout', { event });
|
104
|
+
});
|
105
|
+
channel.off(event);
|
106
|
+
}
|
107
|
+
};
|
108
|
+
};
|
package/lib/index.js
ADDED
package/lib/logger.js
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
const debug = require('debug');
|
2
|
+
|
3
|
+
module.exports = (subModule, silent = false) => {
|
4
|
+
const d = debug(['@arcblock/ws', subModule].join(':'));
|
5
|
+
return {
|
6
|
+
debug: d,
|
7
|
+
warn: d,
|
8
|
+
info: d,
|
9
|
+
trace: silent ? d : console.error,
|
10
|
+
error: silent ? d : console.error,
|
11
|
+
};
|
12
|
+
};
|
@@ -0,0 +1,381 @@
|
|
1
|
+
const EventEmitter = require('events');
|
2
|
+
const cluster = require('cluster');
|
3
|
+
|
4
|
+
const uuid = require('uuid');
|
5
|
+
const WebSocket = require('ws');
|
6
|
+
const eventHub = cluster.isMaster ? require('@arcblock/event-hub/single') : require('@arcblock/event-hub');
|
7
|
+
|
8
|
+
const createLogger = require('../logger');
|
9
|
+
|
10
|
+
const sleep = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout));
|
11
|
+
|
12
|
+
const reply = (socket, topic, event, response, status = 'ok') => {
|
13
|
+
if (socket.readyState === WebSocket.OPEN) {
|
14
|
+
const res = JSON.stringify([socket.joinRef, socket.ref, topic, event, { status, response }]);
|
15
|
+
socket.send(res);
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
const noop = () => {};
|
20
|
+
const defaultHooks = {
|
21
|
+
preJoinChannel: noop,
|
22
|
+
postJoinChannel: noop,
|
23
|
+
preLeaveChannel: noop,
|
24
|
+
postLeaveChannel: noop,
|
25
|
+
postBroadcast: noop,
|
26
|
+
postSend: noop,
|
27
|
+
receiveMessage: noop,
|
28
|
+
};
|
29
|
+
|
30
|
+
const refreshHeartbeat = (socket) => {
|
31
|
+
socket.heartbeatAt = Date.now();
|
32
|
+
};
|
33
|
+
|
34
|
+
const HEARTBEAT_TIMEOUT = 60 * 1000;
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Create a websocket server
|
38
|
+
*
|
39
|
+
* @param {Object} opts
|
40
|
+
* @param {String} opts.pathname - which path to mount the socket server
|
41
|
+
* @param {Object} opts.authenticate - authentication function to be called on connection
|
42
|
+
* @param {Object} opts.hooks - hooks to be called on events
|
43
|
+
* @param {Object} opts.logger - logger used to log messages
|
44
|
+
* @param {Object} opts.broadcastEventName - used in cluster mode, default is '@arcblock/ws:broadcast'
|
45
|
+
* @param {Object} opts.heartbeatTimeout - maximum non-response time of a connection socket
|
46
|
+
* @class WsServer
|
47
|
+
* @extends {EventEmitter}
|
48
|
+
*/
|
49
|
+
class WsServer extends EventEmitter {
|
50
|
+
constructor(opts = {}) {
|
51
|
+
super();
|
52
|
+
this.pathname = opts.pathname;
|
53
|
+
this.authenticate = opts.authenticate || null;
|
54
|
+
this.hooks = Object.assign({}, defaultHooks, opts.hooks || {});
|
55
|
+
this.logger = opts.logger || createLogger('server', opts.silent);
|
56
|
+
this.heartbeatTimeout = opts.heartbeatTimeout || HEARTBEAT_TIMEOUT;
|
57
|
+
|
58
|
+
this.wss = new WebSocket.Server({ noServer: true, clientTracking: false });
|
59
|
+
this.wss.on('connection', this.onWssConnection.bind(this));
|
60
|
+
this.wss.on('close', this.onWssClose.bind(this));
|
61
|
+
this.wss.on('error', this.onWssError.bind(this));
|
62
|
+
|
63
|
+
this.topics = {}; // <topic>: Set<socket>
|
64
|
+
|
65
|
+
this.broadcastEventName = opts.broadcastEventName || '@arcblock/ws:broadcast';
|
66
|
+
eventHub.on(this.broadcastEventName, (data) => this._doBroadCast(data));
|
67
|
+
}
|
68
|
+
|
69
|
+
attach(server) {
|
70
|
+
server.on('upgrade', this.onConnect.bind(this));
|
71
|
+
}
|
72
|
+
|
73
|
+
onConnect(request, socket, head) {
|
74
|
+
const { pathname } = new URL(request.url, `http://${request.headers.host || 'unknown'}`);
|
75
|
+
this.logger.debug('connect attempt', { pathname });
|
76
|
+
if (this.pathname && pathname !== this.pathname) {
|
77
|
+
socket.destroy();
|
78
|
+
return;
|
79
|
+
}
|
80
|
+
|
81
|
+
if (!this.authenticate) {
|
82
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
83
|
+
this.wss.emit('connection', ws);
|
84
|
+
});
|
85
|
+
return;
|
86
|
+
}
|
87
|
+
|
88
|
+
this.authenticate(request, (err, authInfo) => {
|
89
|
+
if (err) {
|
90
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
91
|
+
socket.destroy();
|
92
|
+
return;
|
93
|
+
}
|
94
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
95
|
+
ws.authInfo = authInfo;
|
96
|
+
this.wss.emit('connection', ws);
|
97
|
+
});
|
98
|
+
});
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* Broadcast message to all subscribers of a topic, can be used as
|
103
|
+
* - broadcast(event, data)
|
104
|
+
* - broadcast(topic, event, data)
|
105
|
+
* - broadcast(topic, event, data, options)
|
106
|
+
*/
|
107
|
+
async broadcast(...args) {
|
108
|
+
let topic;
|
109
|
+
let event;
|
110
|
+
let data;
|
111
|
+
let options = {};
|
112
|
+
let cb = () => {};
|
113
|
+
|
114
|
+
if (typeof args[args.length - 1] === 'function') {
|
115
|
+
cb = args.pop();
|
116
|
+
}
|
117
|
+
|
118
|
+
if (args.length < 2) {
|
119
|
+
throw new Error('Broadcasting requires at least 2 arguments');
|
120
|
+
}
|
121
|
+
if (args.length === 2) {
|
122
|
+
[event, data] = args;
|
123
|
+
topic = event;
|
124
|
+
} else if (args.length === 3) {
|
125
|
+
[topic, event, data] = args;
|
126
|
+
} else {
|
127
|
+
[topic, event, data, options] = args;
|
128
|
+
}
|
129
|
+
|
130
|
+
const enableLog = options.enableLog !== undefined ? !!options.enableLog : true;
|
131
|
+
const replyId = uuid.v4();
|
132
|
+
|
133
|
+
// Count of clients what will receive the message
|
134
|
+
// The count is NOT reliable
|
135
|
+
let count = 0;
|
136
|
+
eventHub.on(replyId, ({ count: c } = {}) => {
|
137
|
+
if (c) {
|
138
|
+
count += c;
|
139
|
+
}
|
140
|
+
});
|
141
|
+
|
142
|
+
eventHub.broadcast(this.broadcastEventName, { topic, event, data, options, enableLog, replyId });
|
143
|
+
|
144
|
+
// wait 600ms for message sending by each process
|
145
|
+
await sleep(600);
|
146
|
+
eventHub.off(replyId);
|
147
|
+
|
148
|
+
const opts = { count, topic, event, data, options };
|
149
|
+
cb(opts);
|
150
|
+
try {
|
151
|
+
await this.hooks.postBroadcast(opts);
|
152
|
+
} catch (error) {
|
153
|
+
this.logger.error('postBroadcast error', { error });
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
async _doBroadCast({ topic, event, data, enableLog, replyId } = {}) {
|
158
|
+
try {
|
159
|
+
let count = 0;
|
160
|
+
|
161
|
+
if (this.topics[topic] && this.topics[topic].size) {
|
162
|
+
this.topics[topic].forEach((socket) => {
|
163
|
+
const noHeartbeatTime = Date.now() - socket.heartbeatAt;
|
164
|
+
if (noHeartbeatTime > this.heartbeatTimeout) {
|
165
|
+
this.logger.error(`Socket has no heartbeat within ${Math.floor(noHeartbeatTime / 1000)} seconds`, {
|
166
|
+
topic,
|
167
|
+
id: socket.id,
|
168
|
+
});
|
169
|
+
this.topics[topic].delete(socket);
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
|
173
|
+
count++;
|
174
|
+
if (enableLog) {
|
175
|
+
this.logger.info('broadcast message to', { topic, event, id: socket.id });
|
176
|
+
}
|
177
|
+
reply(socket, topic, event, data);
|
178
|
+
});
|
179
|
+
}
|
180
|
+
|
181
|
+
if (count === 0 && enableLog) {
|
182
|
+
this.logger.info('no connections when broadcast message', { topic, event });
|
183
|
+
}
|
184
|
+
|
185
|
+
if (count > 0 && replyId) {
|
186
|
+
eventHub.broadcast(replyId, { count });
|
187
|
+
}
|
188
|
+
} catch (error) {
|
189
|
+
this.logger.error('_doBroadcast error', { error });
|
190
|
+
}
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* Send message to 1 subscriber of a topic, can be used as
|
195
|
+
* - send(socket, event, data)
|
196
|
+
* - send(socket, topic, event, data)
|
197
|
+
* - send(socket, topic, event, data, options)
|
198
|
+
*/
|
199
|
+
async send(...args) {
|
200
|
+
let socket;
|
201
|
+
let topic;
|
202
|
+
let event;
|
203
|
+
let data;
|
204
|
+
let options = {};
|
205
|
+
|
206
|
+
if (args.length < 3) {
|
207
|
+
throw new Error('send requires at least 3 arguments');
|
208
|
+
}
|
209
|
+
if (args.length === 3) {
|
210
|
+
[socket, event, data] = args;
|
211
|
+
topic = event;
|
212
|
+
} else if (args.length === 4) {
|
213
|
+
[socket, topic, event, data] = args;
|
214
|
+
} else {
|
215
|
+
[socket, topic, event, data, options] = args;
|
216
|
+
}
|
217
|
+
|
218
|
+
const opts = { enableLog: true, ...options };
|
219
|
+
if (!socket) {
|
220
|
+
this.logger.error('socket does not exist');
|
221
|
+
return;
|
222
|
+
}
|
223
|
+
|
224
|
+
if (opts.enableLog) {
|
225
|
+
this.logger.info('send message to', { topic, event, id: socket.id });
|
226
|
+
}
|
227
|
+
|
228
|
+
reply(socket, topic, event, data);
|
229
|
+
|
230
|
+
try {
|
231
|
+
await this.hooks.postSend({ topic, event, data, options });
|
232
|
+
} catch (error) {
|
233
|
+
this.logger.error('postSend error', { error });
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
/**
|
238
|
+
* private
|
239
|
+
* @param {socket} socket
|
240
|
+
*/
|
241
|
+
async onWssConnection(socket) {
|
242
|
+
socket.id = uuid.v4();
|
243
|
+
refreshHeartbeat(socket);
|
244
|
+
this.logger.debug('socket connected', { id: socket.id });
|
245
|
+
|
246
|
+
socket.on('message', async (msg) => {
|
247
|
+
this.logger.debug('socket onmessage', { msg });
|
248
|
+
let joinRef;
|
249
|
+
let ref;
|
250
|
+
let topic;
|
251
|
+
let event;
|
252
|
+
let payload;
|
253
|
+
try {
|
254
|
+
[joinRef, ref, topic, event, payload] = JSON.parse(msg);
|
255
|
+
} catch (err) {
|
256
|
+
this.logger.error('parse socket message error', { id: socket.id, error: err });
|
257
|
+
return;
|
258
|
+
}
|
259
|
+
|
260
|
+
if (!topic || !event) {
|
261
|
+
this.logger.warn('Invalid message format, topic/event fields are required');
|
262
|
+
return;
|
263
|
+
}
|
264
|
+
|
265
|
+
socket.joinRef = joinRef;
|
266
|
+
socket.ref = ref;
|
267
|
+
|
268
|
+
if (topic === 'phoenix' && event === 'heartbeat') {
|
269
|
+
// heartbeat
|
270
|
+
reply(socket, topic, event);
|
271
|
+
refreshHeartbeat(socket);
|
272
|
+
return;
|
273
|
+
}
|
274
|
+
|
275
|
+
if (event === 'phx_join') {
|
276
|
+
// pre hook
|
277
|
+
try {
|
278
|
+
await this.hooks.preJoinChannel({ joinRef, ref, topic, event, payload });
|
279
|
+
} catch (error) {
|
280
|
+
this.logger.error('preJoinChannel error', { error });
|
281
|
+
reply(socket, topic, `chan_reply_${ref}`, { message: error.message }, 'error');
|
282
|
+
return;
|
283
|
+
}
|
284
|
+
|
285
|
+
// join
|
286
|
+
if (!this.topics[topic]) {
|
287
|
+
this.topics[topic] = new Set();
|
288
|
+
}
|
289
|
+
this.topics[topic].add(socket);
|
290
|
+
|
291
|
+
reply(socket, topic, `chan_reply_${ref}`);
|
292
|
+
this.emit('channel.join', { socket, topic, event, payload });
|
293
|
+
|
294
|
+
// post hook
|
295
|
+
try {
|
296
|
+
await this.hooks.postJoinChannel({ joinRef, ref, topic, event, payload });
|
297
|
+
} catch (error) {
|
298
|
+
this.logger.error('postJoinChannel error', { error });
|
299
|
+
}
|
300
|
+
|
301
|
+
return;
|
302
|
+
}
|
303
|
+
|
304
|
+
if (event === 'phx_leave') {
|
305
|
+
// pre hook
|
306
|
+
try {
|
307
|
+
await this.hooks.preLeaveChannel({ joinRef, ref, topic, event, payload });
|
308
|
+
} catch (error) {
|
309
|
+
this.logger.error('preLeaveChannel error', { error });
|
310
|
+
reply(socket, topic, `chan_reply_${ref}`, { message: error.message }, 'error');
|
311
|
+
return;
|
312
|
+
}
|
313
|
+
|
314
|
+
// leave
|
315
|
+
this._leaveChannel(socket, topic);
|
316
|
+
reply(socket, topic, `chan_reply_${ref}`);
|
317
|
+
|
318
|
+
// post hook
|
319
|
+
try {
|
320
|
+
await this.hooks.postLeaveChannel({ joinRef, ref, topic, event, payload });
|
321
|
+
} catch (error) {
|
322
|
+
this.logger.error('postLeaveChannel error', { error });
|
323
|
+
}
|
324
|
+
|
325
|
+
return;
|
326
|
+
}
|
327
|
+
|
328
|
+
// pre hook
|
329
|
+
try {
|
330
|
+
await this.hooks.receiveMessage({ socket, joinRef, ref, topic, event, payload });
|
331
|
+
} catch (error) {
|
332
|
+
this.logger.error('receiveMessage error', { error });
|
333
|
+
reply(socket, topic, `chan_reply_${ref}`, { message: error.message }, 'error');
|
334
|
+
return;
|
335
|
+
}
|
336
|
+
|
337
|
+
reply(socket, topic, `chan_reply_${ref}`, {});
|
338
|
+
});
|
339
|
+
|
340
|
+
socket.on('close', () => {
|
341
|
+
this.logger.debug('socket onclose', { id: socket.id });
|
342
|
+
|
343
|
+
Object.keys(this.topics).forEach((topic) => this._leaveChannel(socket, topic));
|
344
|
+
});
|
345
|
+
|
346
|
+
socket.on('error', (err) => {
|
347
|
+
this.logger.error('socket onerror', { id: socket.id, error: err });
|
348
|
+
Object.keys(this.topics).forEach((topic) => this._leaveChannel(socket, topic));
|
349
|
+
});
|
350
|
+
}
|
351
|
+
|
352
|
+
/**
|
353
|
+
* private
|
354
|
+
*/
|
355
|
+
onWssClose() {
|
356
|
+
this.logger.debug('ws server onclose');
|
357
|
+
this.emit('close');
|
358
|
+
}
|
359
|
+
|
360
|
+
/**
|
361
|
+
* private
|
362
|
+
*/
|
363
|
+
onWssError(error) {
|
364
|
+
this.logger.error('ws server error', { error });
|
365
|
+
this.emit('error', error);
|
366
|
+
}
|
367
|
+
|
368
|
+
_leaveChannel(socket, topic) {
|
369
|
+
// unsubscribe
|
370
|
+
if (this.topics[topic]) {
|
371
|
+
this.topics[topic].delete(socket);
|
372
|
+
}
|
373
|
+
|
374
|
+
this.emit('channel.leave', { socket, topic });
|
375
|
+
if (!this.topics[topic] || !this.topics[topic].size) {
|
376
|
+
this.emit('channel.destroy', { socket, topic });
|
377
|
+
}
|
378
|
+
}
|
379
|
+
}
|
380
|
+
|
381
|
+
module.exports = WsServer;
|
package/package.json
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
{
|
2
|
+
"name": "@arcblock/ws",
|
3
|
+
"version": "1.6.10",
|
4
|
+
"description": "OCAP Chain websocket server and client",
|
5
|
+
"keywords": [
|
6
|
+
"websocket"
|
7
|
+
],
|
8
|
+
"author": "arcblock <engineer@arcblock.io>",
|
9
|
+
"homepage": "https://github.com/ArcBlock/asset-chain#readme",
|
10
|
+
"license": "Apache-2.0",
|
11
|
+
"main": "./lib/index.js",
|
12
|
+
"browser": "./lib/browser.js",
|
13
|
+
"directories": {
|
14
|
+
"lib": "lib",
|
15
|
+
"test": "__tests__"
|
16
|
+
},
|
17
|
+
"files": [
|
18
|
+
"lib"
|
19
|
+
],
|
20
|
+
"publishConfig": {
|
21
|
+
"access": "public"
|
22
|
+
},
|
23
|
+
"repository": {
|
24
|
+
"type": "git",
|
25
|
+
"url": "git+https://github.com/ArcBlock/asset-chain.git"
|
26
|
+
},
|
27
|
+
"scripts": {
|
28
|
+
"lint": "eslint tests lib",
|
29
|
+
"lint:fix": "eslint --fix tests lib",
|
30
|
+
"test": "jest --forceExit --detectOpenHandles",
|
31
|
+
"coverage": "npm run test -- --coverage"
|
32
|
+
},
|
33
|
+
"bugs": {
|
34
|
+
"url": "https://github.com/ArcBlock/asset-chain/issues"
|
35
|
+
},
|
36
|
+
"dependencies": {
|
37
|
+
"@arcblock/event-hub": "1.6.10",
|
38
|
+
"debug": "^4.3.3",
|
39
|
+
"eventemitter3": "^4.0.4",
|
40
|
+
"phoenix": "1.5.12",
|
41
|
+
"uuid": "^8.3.0",
|
42
|
+
"ws": "^8.2.2"
|
43
|
+
},
|
44
|
+
"devDependencies": {
|
45
|
+
"get-port": "^5.1.1"
|
46
|
+
},
|
47
|
+
"gitHead": "ab272e8db3a15c6571cc7fae7cc3d3e0fdd4bdb1"
|
48
|
+
}
|