@arcblock/ws 1.6.10
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/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
|
+
}
|