iosignal 5.2.0 → 6.1.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 +163 -0
- package/dist/browser/esm/io.d.ts +17 -12
- package/dist/browser/esm/io.js +5 -5
- package/dist/browser/esm/io.js.map +1 -1
- package/dist/browser/iife/io.js +5 -5
- package/dist/browser/iife/io.js.map +1 -1
- package/dist/node/iosignal.cjs +195 -550
- package/dist/node/iosignal.js +193 -548
- package/dist/types/client/IOCore.d.ts +11 -5
- package/dist/types/client/browser/IOWebSocket.d.ts +4 -4
- package/dist/types/common/payload.d.ts +2 -2
- package/package.json +22 -4
- package/.vscode/settings.json +0 -3
- package/index.js +0 -26
- package/rollup.config.js +0 -59
- package/src/auth/BohoAuth.js +0 -196
- package/src/auth/key_providers/FileKeyProvider.js +0 -59
- package/src/auth/key_providers/RedisKeyProvider.js +0 -46
- package/src/auth/key_providers/StringKeyProvider.js +0 -54
- package/src/client/CongPacket.js +0 -143
- package/src/client/IOCongSocket.js +0 -92
- package/src/client/IOCore.js +0 -1202
- package/src/client/IOWS.js +0 -78
- package/src/client/browser/IOWebSocket.js +0 -202
- package/src/common/constants.js +0 -210
- package/src/common/payload.js +0 -82
- package/src/common/quotaTable.js +0 -68
- package/src/common/util.js +0 -64
- package/src/server/FileLogger.js +0 -30
- package/src/server/Manager.js +0 -450
- package/src/server/Metrics.js +0 -155
- package/src/server/Remote.js +0 -216
- package/src/server/RemoteCore.js +0 -427
- package/src/server/Server.js +0 -236
- package/src/server/serverOption.js +0 -56
- package/src/services/RedisService.js +0 -53
- package/src/services/constant.js +0 -7
- package/src/services/replyService.js +0 -39
- package/src/services/sudoService.js +0 -85
- package/tsconfig.json +0 -25
package/src/client/IOCore.js
DELETED
|
@@ -1,1202 +0,0 @@
|
|
|
1
|
-
import MBP from 'meta-buffer-pack'
|
|
2
|
-
import EventEmitter from "eventemitter3";
|
|
3
|
-
import { IOMsg, PAYLOAD_TYPE, SIZE_LIMIT, ENC_MODE, STATE } from '../common/constants.js'
|
|
4
|
-
import { quotaTable } from '../common/quotaTable.js'
|
|
5
|
-
import { getSignalPack } from '../common/payload.js';
|
|
6
|
-
import Boho from "boho";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {import('meta-buffer-pack').MBP} MBP
|
|
10
|
-
* @typedef {import('../common/constants.js').IOMsg} IOMsg
|
|
11
|
-
* @typedef {import('../common/constants.js').PAYLOAD_TYPE} PAYLOAD_TYPE
|
|
12
|
-
* @typedef {import('../common/constants.js').SIZE_LIMIT} SIZE_LIMIT
|
|
13
|
-
* @typedef {import('../common/constants.js').ENC_MODE} ENC_MODE
|
|
14
|
-
* @typedef {import('../common/constants.js').STATE} STATE
|
|
15
|
-
* @typedef {import('../common/quotaTable.js').quotaTable} quotaTable
|
|
16
|
-
|
|
17
|
-
* @typedef {import('boho').Boho} Boho
|
|
18
|
-
* @typedef {import('boho').Buffer} Buffer
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const Buffer = MBP.Buffer;
|
|
22
|
-
const encoder = new TextEncoder()
|
|
23
|
-
const decoder = new TextDecoder()
|
|
24
|
-
|
|
25
|
-
function byteToUrl(buffer) {
|
|
26
|
-
//ipv4(4bytes) , port(2bytes)
|
|
27
|
-
if (buffer.byteLength != 6) return
|
|
28
|
-
let address = buffer[0].toString() + "." + buffer[1].toString()
|
|
29
|
-
+ "." + buffer[2].toString() + "." + buffer[3].toString();
|
|
30
|
-
let port = (buffer[4] << 8) + buffer[5]
|
|
31
|
-
return address + ':' + port.toString()
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Core class for handling WebSocket communication.
|
|
36
|
-
* @augments {EventEmitter}
|
|
37
|
-
*/
|
|
38
|
-
export class IOCore extends EventEmitter {
|
|
39
|
-
/**
|
|
40
|
-
* @param {string} url - The WebSocket URL to connect to.
|
|
41
|
-
*/
|
|
42
|
-
constructor(url) {
|
|
43
|
-
super();
|
|
44
|
-
/**
|
|
45
|
-
* Client ID received from the server.
|
|
46
|
-
* @type {string}
|
|
47
|
-
*/
|
|
48
|
-
this.cid = "" // get from the server CID_RES
|
|
49
|
-
/**
|
|
50
|
-
* IP address received from the server.
|
|
51
|
-
* @type {string}
|
|
52
|
-
*/
|
|
53
|
-
this.ip = "" // get from the server IAM_RES message.
|
|
54
|
-
/**
|
|
55
|
-
* The WebSocket instance.
|
|
56
|
-
* @type {WebSocket | null}
|
|
57
|
-
*/
|
|
58
|
-
this.socket = null;
|
|
59
|
-
/**
|
|
60
|
-
* The default server URL.
|
|
61
|
-
* @type {string}
|
|
62
|
-
*/
|
|
63
|
-
this.url = url; // init default server url
|
|
64
|
-
/**
|
|
65
|
-
* Current connection state (number).
|
|
66
|
-
* @type {number}
|
|
67
|
-
*/
|
|
68
|
-
this.state = STATE.CLOSED; // Number type
|
|
69
|
-
/**
|
|
70
|
-
* Current connection state (string).
|
|
71
|
-
* @type {string}
|
|
72
|
-
*/
|
|
73
|
-
this.stateName = this.getStateName() // String type
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Transmitted message counter.
|
|
77
|
-
* @type {number}
|
|
78
|
-
*/
|
|
79
|
-
this.txCounter = 0;
|
|
80
|
-
/**
|
|
81
|
-
* Received message counter.
|
|
82
|
-
* @type {number}
|
|
83
|
-
*/
|
|
84
|
-
this.rxCounter = 0;
|
|
85
|
-
/**
|
|
86
|
-
* Transmitted bytes counter.
|
|
87
|
-
* @type {number}
|
|
88
|
-
*/
|
|
89
|
-
this.txBytes = 0;
|
|
90
|
-
/**
|
|
91
|
-
* Received bytes counter.
|
|
92
|
-
* @type {number}
|
|
93
|
-
*/
|
|
94
|
-
this.rxBytes = 0;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Last transmit/receive time.
|
|
98
|
-
* @type {number}
|
|
99
|
-
*/
|
|
100
|
-
this.lastTxRxTime = Date.now();
|
|
101
|
-
/**
|
|
102
|
-
* Period for connection checker.
|
|
103
|
-
* @type {number}
|
|
104
|
-
*/
|
|
105
|
-
this.connectionCheckerPeriod = SIZE_LIMIT.CONNECTION_CHECKER_PERIOD;
|
|
106
|
-
/**
|
|
107
|
-
* Interval ID for connection checker.
|
|
108
|
-
* @type {NodeJS.Timeout | null}
|
|
109
|
-
*/
|
|
110
|
-
this.connectionCheckerIntervalID = null;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Boho instance for encryption/decryption.
|
|
114
|
-
* @type {Boho}
|
|
115
|
-
*/
|
|
116
|
-
this.boho = new Boho()
|
|
117
|
-
|
|
118
|
-
this.serverTimeNonce = Buffer.alloc( Boho.MetaSize.SERVER_TIME_NONCE );
|
|
119
|
-
/**
|
|
120
|
-
* Indicates if the connection is TLS (wss).
|
|
121
|
-
* @type {boolean}
|
|
122
|
-
*/
|
|
123
|
-
this.TLS = false // true if protocol is wss(TLS)
|
|
124
|
-
/**
|
|
125
|
-
* Encryption mode.
|
|
126
|
-
* @type {number}
|
|
127
|
-
*/
|
|
128
|
-
this.encMode = ENC_MODE.AUTO;
|
|
129
|
-
/**
|
|
130
|
-
* Indicates if authentication is used.
|
|
131
|
-
* @type {boolean}
|
|
132
|
-
*/
|
|
133
|
-
this.useAuth = false;
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Nickname.
|
|
137
|
-
* @type {string}
|
|
138
|
-
*/
|
|
139
|
-
this.nick = "";
|
|
140
|
-
/**
|
|
141
|
-
* Set of subscribed channels.
|
|
142
|
-
* @type {Set<string>}
|
|
143
|
-
*/
|
|
144
|
-
this.channels = new Set()
|
|
145
|
-
/**
|
|
146
|
-
* Map of promises for message responses.
|
|
147
|
-
* @type {Map<number, Array<Function>>}
|
|
148
|
-
*/
|
|
149
|
-
this.promiseMap = new Map()
|
|
150
|
-
/**
|
|
151
|
-
* Timeout for message promises.
|
|
152
|
-
* @type {number}
|
|
153
|
-
*/
|
|
154
|
-
this.promiseTimeOut = SIZE_LIMIT.PROMISE_TIMEOUT
|
|
155
|
-
/**
|
|
156
|
-
* Message ID for promises.
|
|
157
|
-
* @type {number}
|
|
158
|
-
*/
|
|
159
|
-
this.mid = 0 // promise message id
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Quota level.
|
|
163
|
-
* @type {number}
|
|
164
|
-
*/
|
|
165
|
-
this.level = 3; // also defaultQuotaLevel
|
|
166
|
-
/**
|
|
167
|
-
* Quota table for current level.
|
|
168
|
-
* @type {object}
|
|
169
|
-
*/
|
|
170
|
-
this.quota = quotaTable[this.level];
|
|
171
|
-
/**
|
|
172
|
-
* Server settings.
|
|
173
|
-
* @type {object}
|
|
174
|
-
*/
|
|
175
|
-
this.serverSet = {}
|
|
176
|
-
/**
|
|
177
|
-
* Map of linked channels.
|
|
178
|
-
* @type {Map<string, Set<string>>}
|
|
179
|
-
*/
|
|
180
|
-
this.linkMap = new Map()
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Indicates if auto-reconnect is enabled.
|
|
184
|
-
* @type {boolean}
|
|
185
|
-
* @default true
|
|
186
|
-
* */
|
|
187
|
-
this.autoReconnect = true; // default true
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* A flag to prevent duplicate close operations.
|
|
191
|
-
* @type {boolean}
|
|
192
|
-
* @private
|
|
193
|
-
*/
|
|
194
|
-
this._closed = false; // 중복 close 방지
|
|
195
|
-
|
|
196
|
-
this.on('open', this.onOpen.bind(this))
|
|
197
|
-
this.on('close', this.onClose.bind(this))
|
|
198
|
-
this.on('socket_data', this.onData.bind(this))
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Performs common cleanup for the connection. It clears pending promises,
|
|
205
|
-
* resets the socket reference, and sets the state to closed.
|
|
206
|
-
* This method is guarded to only run once.
|
|
207
|
-
* If autoReconnect is false, it also clears the keep-alive timer.
|
|
208
|
-
*/
|
|
209
|
-
close() {
|
|
210
|
-
if (this._closed) return;
|
|
211
|
-
this._closed = true;
|
|
212
|
-
// console.log('####### IOCOre.js close() called')
|
|
213
|
-
// If auto-reconnect is disabled, we must stop the keep-alive timer.
|
|
214
|
-
if (this.autoReconnect === false) {
|
|
215
|
-
clearInterval(this.connectionCheckerIntervalID);
|
|
216
|
-
this.connectionCheckerIntervalID = null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// socket clean
|
|
220
|
-
if (this.socket) {
|
|
221
|
-
// For WebSockets, readyState are: 0-CONNECTING, 1-OPEN, 2-CLOSING, 3-CLOSED
|
|
222
|
-
// Calling close() on a CONNECTING socket will cause a browser error.
|
|
223
|
-
if (this.socket.readyState === 0) { // 0 is WebSocket.CONNECTING
|
|
224
|
-
// To avoid the error, we wait for the connection to open, then immediately close it.
|
|
225
|
-
// We also clear other handlers to prevent any other logic from running.
|
|
226
|
-
const socket = this.socket;
|
|
227
|
-
socket.onopen = () => { if (socket) socket.close(); };
|
|
228
|
-
socket.onmessage = null;
|
|
229
|
-
socket.onerror = null;
|
|
230
|
-
socket.onclose = null;
|
|
231
|
-
} else {
|
|
232
|
-
try {
|
|
233
|
-
// For other sockets (like TCP) or other WebSocket states, close directly.
|
|
234
|
-
this.socket.close?.();
|
|
235
|
-
} catch { }
|
|
236
|
-
}
|
|
237
|
-
this.socket = null;
|
|
238
|
-
}
|
|
239
|
-
this.promiseMap.clear();
|
|
240
|
-
this.emit('closed');
|
|
241
|
-
this.stateChange('closed');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Disables auto-reconnect and closes the current connection.
|
|
246
|
-
* The instance can be re-opened manually later. For complete cleanup, use destroy().
|
|
247
|
-
*/
|
|
248
|
-
stop() {
|
|
249
|
-
this.autoReconnect = false;
|
|
250
|
-
this.close();
|
|
251
|
-
this.cid = ''
|
|
252
|
-
this.stateChange('stop','stop');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Permanently destroys the instance, cleaning up all resources.
|
|
257
|
-
* The instance will not be usable after this.
|
|
258
|
-
*/
|
|
259
|
-
destroy() {
|
|
260
|
-
this.stop();
|
|
261
|
-
this.removeAllListeners();
|
|
262
|
-
|
|
263
|
-
this.channels.clear();
|
|
264
|
-
this.linkMap.clear();
|
|
265
|
-
|
|
266
|
-
// Help GC
|
|
267
|
-
this.boho = null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* The core keep-alive logic.
|
|
272
|
-
* The specific logic for checking the socket's state and reconnecting
|
|
273
|
-
* is implemented keepConnection() in the child classes (IOWS, IOCongSocket, etc.).
|
|
274
|
-
*/
|
|
275
|
-
keepAlive() {
|
|
276
|
-
this.keepConnection();
|
|
277
|
-
if( Date.now() - this.lastTxRxTime > SIZE_LIMIT.CLIENT_PING_PERIOD ){
|
|
278
|
-
this.ping();
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Redirects the connection to a new URL.
|
|
284
|
-
* @param {string} url2 - The new URL to redirect to.
|
|
285
|
-
*/
|
|
286
|
-
redirect(url2) {
|
|
287
|
-
this.stateChange('redirecting','redirecting')
|
|
288
|
-
this.close()
|
|
289
|
-
this.createConnection(url2)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Opens the WebSocket connection.
|
|
294
|
-
* @param {string} [url] - Optional URL to connect to. If not provided, uses the instance's URL.
|
|
295
|
-
*/
|
|
296
|
-
open(url) {
|
|
297
|
-
// If a connection is already active or in progress, calling open() implies a reconnect.
|
|
298
|
-
// Close the existing socket first to ensure a clean state.
|
|
299
|
-
if (this.socket) {
|
|
300
|
-
this.close();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (url) {
|
|
304
|
-
this.url = url;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (!this.url) {
|
|
308
|
-
this.emit('error', new Error('URL is not set.'));
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// The actual connection is created here.
|
|
313
|
-
this.createConnection(this.url);
|
|
314
|
-
|
|
315
|
-
// Ensure the keep-alive timer is running.
|
|
316
|
-
if (!this.connectionCheckerIntervalID) {
|
|
317
|
-
this.connectionCheckerIntervalID = setInterval(this.keepAlive.bind(this), this.connectionCheckerPeriod);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Handles the 'open' event of the WebSocket. Resets the closed flag and sets the state to open.
|
|
323
|
-
*/
|
|
324
|
-
onOpen() {
|
|
325
|
-
this._closed = false;
|
|
326
|
-
if (this.url.includes("wss://")) {
|
|
327
|
-
this.TLS = true;
|
|
328
|
-
} else {
|
|
329
|
-
this.TLS = false;
|
|
330
|
-
}
|
|
331
|
-
this.stateChange('open')
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Handles the 'close' event of the WebSocket.
|
|
336
|
-
*/
|
|
337
|
-
onClose() {
|
|
338
|
-
this.boho.isAuthorized = false;
|
|
339
|
-
this.cid = ""
|
|
340
|
-
this.stateChange('closed')
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Manually logs in with provided ID and key.
|
|
345
|
-
* @param {string} id - The user ID. or 'id.key'
|
|
346
|
-
* @param {string} key - The user key.
|
|
347
|
-
* @returns {this}
|
|
348
|
-
*/
|
|
349
|
-
login(id, key) {
|
|
350
|
-
if( this.serverTimeNonce ){
|
|
351
|
-
console.log('iosignal.login serverTimeNonce', this.serverTimeNonce)
|
|
352
|
-
this.auth(id, key)
|
|
353
|
-
this.useAuth = true
|
|
354
|
-
let auth_pack = this.boho.auth_req(this.serverTimeNonce )
|
|
355
|
-
this.send(auth_pack)
|
|
356
|
-
}
|
|
357
|
-
return this
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Sets up authentication for auto-login.
|
|
362
|
-
* @param {string} id - The user ID. or 'id.Key'
|
|
363
|
-
* @param {string} key - The user key.
|
|
364
|
-
* @returns {this}
|
|
365
|
-
*/
|
|
366
|
-
auth(id, key) {
|
|
367
|
-
if (!id && !key) {
|
|
368
|
-
this.emit('error', new Error('auth failed. no id and key.'))
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (!key && id.includes('.')) {
|
|
372
|
-
this.boho.set_id_key(id)
|
|
373
|
-
} else if (id && key) {
|
|
374
|
-
this.boho.set_id8(id)
|
|
375
|
-
this.boho.set_key(key)
|
|
376
|
-
} else {
|
|
377
|
-
this.emit('error', new Error('auth failed. no id or key.'))
|
|
378
|
-
}
|
|
379
|
-
this.useAuth = true
|
|
380
|
-
return this
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Handles incoming data from the WebSocket.
|
|
385
|
-
* @param {Buffer} buffer - The incoming data buffer.
|
|
386
|
-
*/
|
|
387
|
-
onData(buffer) {
|
|
388
|
-
let msgType = buffer[0];
|
|
389
|
-
let decoded;
|
|
390
|
-
|
|
391
|
-
if (msgType === Boho.BohoMsg.ENC_488) {
|
|
392
|
-
decoded = this.boho.decrypt_488(buffer)
|
|
393
|
-
if (decoded) {
|
|
394
|
-
msgType = decoded[0]
|
|
395
|
-
buffer = decoded
|
|
396
|
-
} else {
|
|
397
|
-
// console.log('DEC_FAIL', buffer.byteLength)
|
|
398
|
-
}
|
|
399
|
-
} else if (msgType === Boho.BohoMsg.ENC_E2E) {
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
decoded = this.boho.decrypt_488(buffer)
|
|
403
|
-
if (decoded) {
|
|
404
|
-
// console.log( 'ENC_E2E decoded ', decoded )
|
|
405
|
-
msgType = decoded[0]
|
|
406
|
-
// decoded has msg_header only.
|
|
407
|
-
buffer.set(decoded, Boho.MetaSize.ENC_488) // set decoded signal_e2e headaer.
|
|
408
|
-
buffer = buffer.subarray(Boho.MetaSize.ENC_488) // reset offset.
|
|
409
|
-
// console.log('DECODED MsgType:', IOMsg[ msgType ] )
|
|
410
|
-
} else {
|
|
411
|
-
// console.log('488 DEC_FAIL', buffer)
|
|
412
|
-
return
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
} catch (err) {
|
|
416
|
-
// console.log('E2E DEC_FAIL decryption error', err)
|
|
417
|
-
return
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
let type = IOMsg[msgType]
|
|
423
|
-
if (!type) type = Boho.BohoMsg[msgType]
|
|
424
|
-
|
|
425
|
-
// console.log( "MsgType: ", type , " LEN ", buffer.byteLength)
|
|
426
|
-
|
|
427
|
-
switch (msgType) {
|
|
428
|
-
case IOMsg.OVER_SIZE:
|
|
429
|
-
console.log('## server sent: over_size event.')
|
|
430
|
-
this.emit('over_size', 'over_size')
|
|
431
|
-
break;
|
|
432
|
-
case IOMsg.PING:
|
|
433
|
-
this.pong();
|
|
434
|
-
break;
|
|
435
|
-
|
|
436
|
-
case IOMsg.PONG:
|
|
437
|
-
break;
|
|
438
|
-
|
|
439
|
-
case IOMsg.ECHO:
|
|
440
|
-
try {
|
|
441
|
-
let str = decoder.decode(buffer.subarray(1))
|
|
442
|
-
|
|
443
|
-
this.emit('echo', str)
|
|
444
|
-
} catch (error) {
|
|
445
|
-
this.emit('error', new Error('ECHO data error'))
|
|
446
|
-
}
|
|
447
|
-
break;
|
|
448
|
-
|
|
449
|
-
case IOMsg.IAM_RES:
|
|
450
|
-
try {
|
|
451
|
-
let str = decoder.decode(buffer.subarray(1))
|
|
452
|
-
let jsonInfo = JSON.parse(str)
|
|
453
|
-
if (jsonInfo.ip) { this.ip = jsonInfo.ip; }
|
|
454
|
-
if (jsonInfo.nick) { this.nick = jsonInfo.nick; }
|
|
455
|
-
if (jsonInfo.did) { this.did = jsonInfo.did; }
|
|
456
|
-
if (jsonInfo.uid) { this.uid = jsonInfo.uid; }
|
|
457
|
-
this.emit('iam_res', str)
|
|
458
|
-
} catch (error) {
|
|
459
|
-
this.emit('error', new Error('IAM_RES data error'))
|
|
460
|
-
}
|
|
461
|
-
break;
|
|
462
|
-
|
|
463
|
-
case IOMsg.CID_RES:
|
|
464
|
-
let cidStr = decoder.decode(buffer.subarray(1))
|
|
465
|
-
this.cid = cidStr;
|
|
466
|
-
|
|
467
|
-
// **IMPORTANT** change state before subscribe.
|
|
468
|
-
this.stateChange('ready', 'cid_ready')
|
|
469
|
-
this.subscribe_channels()
|
|
470
|
-
break;
|
|
471
|
-
|
|
472
|
-
case IOMsg.QUOTA_LEVEL:
|
|
473
|
-
let quotaLevel = buffer[1]
|
|
474
|
-
this.level = quotaLevel;
|
|
475
|
-
this.quota = quotaTable[quotaLevel];
|
|
476
|
-
// console.log('[QUOTA_LEVEL]', JSON.stringify(this.quota))
|
|
477
|
-
break;
|
|
478
|
-
|
|
479
|
-
case IOMsg.AUTH_CLEAR:
|
|
480
|
-
this.useAuth = false;
|
|
481
|
-
this.boho.clearAuth();
|
|
482
|
-
this.stateChange('auth_clear', 'server request auth_clear.')
|
|
483
|
-
this.stop();
|
|
484
|
-
break;
|
|
485
|
-
|
|
486
|
-
case IOMsg.SERVER_REDIRECT:
|
|
487
|
-
let host_port;
|
|
488
|
-
let url;
|
|
489
|
-
let protocol;
|
|
490
|
-
let addressType;
|
|
491
|
-
if (buffer.byteLength == 7) { // ipv4 ,port
|
|
492
|
-
addressType = 'IPV4:PORT'
|
|
493
|
-
host_port = byteToUrl(buffer.subarray(1))
|
|
494
|
-
protocol = 'cong://'
|
|
495
|
-
} else { // domain url
|
|
496
|
-
addressType = 'URL'
|
|
497
|
-
host_port = decoder.decode(buffer.subarray(1))
|
|
498
|
-
protocol = ''
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
url = protocol + host_port
|
|
502
|
-
this.redirect(url)
|
|
503
|
-
break;
|
|
504
|
-
|
|
505
|
-
case Boho.BohoMsg.SERVER_TIME_NONCE: // SERVER_READY
|
|
506
|
-
this.stateChange('server_ready', 'server_ready')
|
|
507
|
-
if (this.useAuth) {
|
|
508
|
-
this.send(this.boho.auth_req(buffer))
|
|
509
|
-
this.stateChange('auth_req','auth_req')
|
|
510
|
-
// CID_REQ will be called, after auth_res.
|
|
511
|
-
} else {
|
|
512
|
-
// keep server_time_nonce for manual login()
|
|
513
|
-
buffer.copy( this.serverTimeNonce);
|
|
514
|
-
// CID_REQ here, if not using auth.
|
|
515
|
-
this.send(Buffer.from([IOMsg.CID_REQ]))
|
|
516
|
-
}
|
|
517
|
-
break;
|
|
518
|
-
|
|
519
|
-
case IOMsg.SERVER_SIGNAL:
|
|
520
|
-
try {
|
|
521
|
-
let str = decoder.decode(buffer.subarray(1))
|
|
522
|
-
let ss = JSON.parse(str)
|
|
523
|
-
|
|
524
|
-
if (ss.event && ss.data) {
|
|
525
|
-
this.serverSet = ss.data;
|
|
526
|
-
this.emit(ss.event, ss.data)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
} catch (error) {
|
|
530
|
-
this.emit('error', new Error('SERVER_SIGNAL parsing error'))
|
|
531
|
-
}
|
|
532
|
-
break;
|
|
533
|
-
|
|
534
|
-
case IOMsg.SET:
|
|
535
|
-
try {
|
|
536
|
-
let setPack = MBP.unpack(buffer)
|
|
537
|
-
if (setPack) {
|
|
538
|
-
this.emit(setPack.topic, ...setPack.args)
|
|
539
|
-
}
|
|
540
|
-
} catch (error) {
|
|
541
|
-
this.emit('error', new Error('SET parsing error'))
|
|
542
|
-
}
|
|
543
|
-
break;
|
|
544
|
-
|
|
545
|
-
case IOMsg.SIGNAL_E2E:
|
|
546
|
-
case IOMsg.SIGNAL:
|
|
547
|
-
try {
|
|
548
|
-
let tagLen = buffer.readUint8(1)
|
|
549
|
-
let tagBuf = buffer.subarray(2, 2 + tagLen)
|
|
550
|
-
let tag = decoder.decode(tagBuf)
|
|
551
|
-
|
|
552
|
-
let payloadType = buffer.readUint8(2 + tagLen)
|
|
553
|
-
let payloadBuffer = buffer.subarray(3 + tagLen)
|
|
554
|
-
|
|
555
|
-
/* three types of signal message.
|
|
556
|
-
> unicast message to me: tag includes @, no cid: '@*'
|
|
557
|
-
> cid_sub message: tag includes cid and @ both : 'cid@*'
|
|
558
|
-
> ch_sub message: else.
|
|
559
|
-
*/
|
|
560
|
-
// console.log('payloadType', payloadType )
|
|
561
|
-
switch (payloadType) {
|
|
562
|
-
|
|
563
|
-
case PAYLOAD_TYPE.EMPTY:
|
|
564
|
-
if (tag.indexOf('@') === 0) this.emit('@', tag, null)
|
|
565
|
-
else {
|
|
566
|
-
this.emit(tag, tag, null)
|
|
567
|
-
this.emit('message', tag, null)
|
|
568
|
-
}
|
|
569
|
-
break;
|
|
570
|
-
|
|
571
|
-
case PAYLOAD_TYPE.TEXT:
|
|
572
|
-
// !! Must remove null char before decode in JS.
|
|
573
|
-
// string payload contains null char for the c/cpp devices.
|
|
574
|
-
let payloadStringWithoutNull = payloadBuffer
|
|
575
|
-
if (payloadBuffer[payloadBuffer.byteLength - 1] === 0) {
|
|
576
|
-
payloadStringWithoutNull = payloadBuffer.subarray(0, payloadBuffer.byteLength - 1)
|
|
577
|
-
}
|
|
578
|
-
let oneString = decoder.decode(payloadStringWithoutNull)
|
|
579
|
-
if (tag.indexOf('@') === 0) this.emit('@', tag, oneString)
|
|
580
|
-
else {
|
|
581
|
-
this.emit(tag, tag, oneString)
|
|
582
|
-
this.emit('message', tag, oneString)
|
|
583
|
-
}
|
|
584
|
-
break;
|
|
585
|
-
|
|
586
|
-
case PAYLOAD_TYPE.BINARY:
|
|
587
|
-
if (tag.indexOf('@') === 0) this.emit('@', tag, payloadBuffer)
|
|
588
|
-
else {
|
|
589
|
-
this.emit(tag, tag, payloadBuffer)
|
|
590
|
-
this.emit('message', tag, payloadBuffer)
|
|
591
|
-
}
|
|
592
|
-
break;
|
|
593
|
-
|
|
594
|
-
case PAYLOAD_TYPE.OBJECT:
|
|
595
|
-
let oneObjectBuffer = decoder.decode(payloadBuffer)
|
|
596
|
-
let oneJSONObject = JSON.parse(oneObjectBuffer)
|
|
597
|
-
if (tag.indexOf('@') === 0) this.emit('@', tag, oneJSONObject)
|
|
598
|
-
else {
|
|
599
|
-
this.emit(tag, tag, oneJSONObject)
|
|
600
|
-
this.emit('message', tag, oneJSONObject)
|
|
601
|
-
}
|
|
602
|
-
break;
|
|
603
|
-
|
|
604
|
-
case PAYLOAD_TYPE.MJSON:
|
|
605
|
-
let mjsonBuffer = decoder.decode(payloadBuffer)
|
|
606
|
-
let mjson = JSON.parse(mjsonBuffer)
|
|
607
|
-
if (tag.indexOf('@') === 0) this.emit('@', tag, ...mjson)
|
|
608
|
-
else {
|
|
609
|
-
this.emit(tag, tag, ...mjson)
|
|
610
|
-
this.emit('message', tag, ...mjson)
|
|
611
|
-
}
|
|
612
|
-
break;
|
|
613
|
-
|
|
614
|
-
case PAYLOAD_TYPE.MBA:
|
|
615
|
-
let mbaObject = MBP.unpack(payloadBuffer)
|
|
616
|
-
if (tag.indexOf('@') === 0) this.emit('@', tag, ...mbaObject.args)
|
|
617
|
-
else {
|
|
618
|
-
this.emit(tag, tag, ...mbaObject.args)
|
|
619
|
-
this.emit('message', tag, ...mbaObject.args)
|
|
620
|
-
}
|
|
621
|
-
break;
|
|
622
|
-
|
|
623
|
-
default:
|
|
624
|
-
// console.log('## Unkown payloadtype', payloadType)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
} catch (err) {
|
|
628
|
-
// this.emit('error', new Error('IOCore IOMsg.SIGNAL parser err', err))
|
|
629
|
-
console.log('IOCore IOMsg.SIGNAL parser err', err)
|
|
630
|
-
}
|
|
631
|
-
break;
|
|
632
|
-
|
|
633
|
-
case IOMsg.RESPONSE_MBP:
|
|
634
|
-
this.testPromise(buffer)
|
|
635
|
-
break;
|
|
636
|
-
|
|
637
|
-
case Boho.BohoMsg.AUTH_FAIL:
|
|
638
|
-
this.stateChange('auth_fail', 'auth_fail from server.')
|
|
639
|
-
break;
|
|
640
|
-
|
|
641
|
-
case Boho.BohoMsg.AUTH_RES:
|
|
642
|
-
if (this.boho.verify_auth_res(buffer)) {
|
|
643
|
-
this.stateChange('auth_res', 'server sent auth_res')
|
|
644
|
-
this.send(Buffer.from([IOMsg.CID_REQ]))
|
|
645
|
-
} else {
|
|
646
|
-
this.stateChange('auth_fail', 'verify_auth_res() invalid server_hmac')
|
|
647
|
-
}
|
|
648
|
-
break;
|
|
649
|
-
|
|
650
|
-
default:
|
|
651
|
-
try {
|
|
652
|
-
decoded = decoder.decode(buffer)
|
|
653
|
-
// console.log('text message:', decoded)
|
|
654
|
-
this.emit('text_message', decoded)
|
|
655
|
-
} catch (error) {
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
break;
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Sends an IAM (I Am) message to the server.
|
|
666
|
-
* @param {string} [title] - Optional title for the IAM message.
|
|
667
|
-
*/
|
|
668
|
-
iam(title) {
|
|
669
|
-
// console.log('iam', title)
|
|
670
|
-
if (title) {
|
|
671
|
-
this.send_enc_mode(MBP.pack(
|
|
672
|
-
MBP.MB('#MsgType', '8', IOMsg.IAM),
|
|
673
|
-
MBP.MB('#', title)
|
|
674
|
-
))
|
|
675
|
-
} else {
|
|
676
|
-
this.send_enc_mode(MBP.pack(
|
|
677
|
-
MBP.MB('#MsgType', '8', IOMsg.IAM)
|
|
678
|
-
))
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* Sends a PING message to the server.
|
|
685
|
-
*/
|
|
686
|
-
ping() {
|
|
687
|
-
this.send(Buffer.from([IOMsg.PING]))
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Sends a PONG message to the server.
|
|
692
|
-
*/
|
|
693
|
-
pong() {
|
|
694
|
-
this.send(Buffer.from([IOMsg.PONG]))
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Sends an ECHO message to the server.
|
|
700
|
-
* @param {*} [args] - Optional arguments to echo.
|
|
701
|
-
*/
|
|
702
|
-
echo(args) {
|
|
703
|
-
if (args) {
|
|
704
|
-
// console.log('send echo args:', args)
|
|
705
|
-
this.send_enc_mode(MBP.pack(
|
|
706
|
-
MBP.MB('#MsgType', '8', IOMsg.ECHO),
|
|
707
|
-
MBP.MB('#msg', args)
|
|
708
|
-
))
|
|
709
|
-
} else {
|
|
710
|
-
// # do not encrypt blank echo #
|
|
711
|
-
this.send(Buffer.from([IOMsg.ECHO]))
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Sends binary data.
|
|
718
|
-
* @param {...any} data - Data to send.
|
|
719
|
-
*/
|
|
720
|
-
bin(...data) {
|
|
721
|
-
this.send(MBP.U8pack(...data))
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Sends data over the WebSocket.
|
|
726
|
-
* @param {Buffer} data - The data buffer to send.
|
|
727
|
-
*/
|
|
728
|
-
send(data) {
|
|
729
|
-
if (data.byteLength > this.quota.signalSize) {
|
|
730
|
-
this.emit('over_size')
|
|
731
|
-
console.log('## QUOTA LIMIT OVER!! \nsignal message.byteLength: ', data.byteLength)
|
|
732
|
-
console.log('## your maximum signalSize(bytes) is:', this.quota.signalSize)
|
|
733
|
-
return
|
|
734
|
-
}
|
|
735
|
-
// console.log(`C->[${IOMsg[ data[0]]}]`)
|
|
736
|
-
this.socket_send(data);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Determines if encryption should be used based on current mode and TLS status.
|
|
741
|
-
* @returns {boolean}
|
|
742
|
-
*/
|
|
743
|
-
getEncryptionMode() {
|
|
744
|
-
if (this.encMode === ENC_MODE.YES ||
|
|
745
|
-
this.encMode === ENC_MODE.AUTO &&
|
|
746
|
-
!this.TLS && this.boho.isAuthorized
|
|
747
|
-
) {
|
|
748
|
-
return true;
|
|
749
|
-
} else {
|
|
750
|
-
return false
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Sends data with encryption based on the encryption mode.
|
|
756
|
-
* @param {Buffer} data - The data buffer to send.
|
|
757
|
-
* @param {boolean} [useEncryption] - Optional. Force encryption or not. If undefined, uses default policy.
|
|
758
|
-
*/
|
|
759
|
-
send_enc_mode(data, useEncryption) {
|
|
760
|
-
|
|
761
|
-
// use default policy.
|
|
762
|
-
if (useEncryption === undefined) {
|
|
763
|
-
useEncryption = this.getEncryptionMode()
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (data[0] == IOMsg.SIGNAL_E2E && useEncryption) {
|
|
767
|
-
// input data: signal_header + e2ePayload
|
|
768
|
-
// encrypt signal_header area only. payload is encrypted with e2e key already.
|
|
769
|
-
let tagLen = data[1]
|
|
770
|
-
let encHeader = this.boho.encrypt_488(data.subarray(0, 3 + tagLen))
|
|
771
|
-
encHeader[0] = Boho.BohoMsg.ENC_E2E
|
|
772
|
-
this.send(Buffer.concat([encHeader, data.subarray(3 + tagLen)]))
|
|
773
|
-
// console.log('<< send_enc_mode [ ENC_E2E ]')
|
|
774
|
-
|
|
775
|
-
} else if (useEncryption) {
|
|
776
|
-
// console.log('<< send_enc_mode [ ENC_488 ]')
|
|
777
|
-
let encPack = this.boho.encrypt_488(data)
|
|
778
|
-
this.send(encPack)
|
|
779
|
-
} else {
|
|
780
|
-
// console.log('<< send_enc_mode [ PLAIN ]' )
|
|
781
|
-
this.send(data)
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
/**
|
|
788
|
-
* Sets a message promise for a given message ID.
|
|
789
|
-
* @param {number} mid - The message ID.
|
|
790
|
-
* @returns {Promise<any>}
|
|
791
|
-
*/
|
|
792
|
-
setMsgPromise(mid) {
|
|
793
|
-
return new Promise((resolve, reject) => {
|
|
794
|
-
const timeoutId = setTimeout(e => {
|
|
795
|
-
if (this.promiseMap.has(mid)) {
|
|
796
|
-
reject('timeout');
|
|
797
|
-
this.promiseMap.delete(mid)
|
|
798
|
-
}
|
|
799
|
-
}, this.promiseTimeOut);
|
|
800
|
-
this.promiseMap.set(mid, [resolve, reject, timeoutId]);
|
|
801
|
-
})
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
* Tests and resolves/rejects a promise based on the incoming buffer.
|
|
806
|
-
* @param {Buffer} buffer - The incoming data buffer.
|
|
807
|
-
*/
|
|
808
|
-
testPromise(buffer) {
|
|
809
|
-
|
|
810
|
-
let res = MBP.unpack(buffer)
|
|
811
|
-
if (!res) return
|
|
812
|
-
|
|
813
|
-
if (this.promiseMap.has(res.mid)) {
|
|
814
|
-
let [resolve, reject, timeoutId] = this.promiseMap.get(res.mid)
|
|
815
|
-
clearTimeout(timeoutId)
|
|
816
|
-
this.promiseMap.delete(res.mid)
|
|
817
|
-
|
|
818
|
-
if (res.status < 128) {
|
|
819
|
-
res.ok = true;
|
|
820
|
-
resolve(res)
|
|
821
|
-
} else {
|
|
822
|
-
res.ok = false;
|
|
823
|
-
reject(res)
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
} else {
|
|
828
|
-
console.log('no promise id')
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* alias of signal()
|
|
835
|
-
* Sends a signal with a tag and arguments.
|
|
836
|
-
* @param {string} tag - The signal tag.
|
|
837
|
-
* @param {...any} args - Arguments for the signal.
|
|
838
|
-
*/
|
|
839
|
-
publish(tag, ...args) {
|
|
840
|
-
this.signal(tag, ...args)
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* Sends a signal with a tag and arguments.
|
|
845
|
-
* @param {string} tag - The signal tag.
|
|
846
|
-
* @param {...any} args - Arguments for the signal.
|
|
847
|
-
* @throws {TypeError} If tag is not a string.
|
|
848
|
-
*/
|
|
849
|
-
signal(tag, ...args) {
|
|
850
|
-
if (typeof tag !== 'string') throw TypeError('tag should be string.')
|
|
851
|
-
let signalPack = getSignalPack(tag, ...args)
|
|
852
|
-
this.send_enc_mode(signalPack)
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Decrypts E2E data.
|
|
857
|
-
* @param {Buffer} data - The encrypted data.
|
|
858
|
-
* @param {string} key - The decryption key.
|
|
859
|
-
* @returns {Buffer}
|
|
860
|
-
*/
|
|
861
|
-
decrypt_e2e(data, key) {
|
|
862
|
-
return this.boho.decrypt_e2e(data, key)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Sends an E2E (End-to-End) encrypted signal.
|
|
867
|
-
* @param {string} tag - The signal tag.
|
|
868
|
-
* @param {Buffer} data - The data to encrypt and send.
|
|
869
|
-
* @param {string} key - The encryption key.
|
|
870
|
-
* @throws {TypeError} If tag is not a string.
|
|
871
|
-
*/
|
|
872
|
-
signal_e2e(tag, data, key) {
|
|
873
|
-
|
|
874
|
-
if( !this.boho.isAuthorized ) return;
|
|
875
|
-
if (typeof tag !== 'string') throw TypeError('tag should be string.')
|
|
876
|
-
let tagEncoded = encoder.encode(tag)
|
|
877
|
-
let dataPack = MBP.B8(data)
|
|
878
|
-
|
|
879
|
-
//encrypt payload area with key
|
|
880
|
-
let sercretPack = this.boho.encrypt_e2e(dataPack, key)
|
|
881
|
-
|
|
882
|
-
//change signal MsgType header into SIGNAL_E2E
|
|
883
|
-
let signalPack = MBP.pack(
|
|
884
|
-
MBP.MB('#MsgType', '8', IOMsg.SIGNAL_E2E),
|
|
885
|
-
MBP.MB('#tagLen', '8', tagEncoded.byteLength),
|
|
886
|
-
MBP.MB('#tag', tagEncoded),
|
|
887
|
-
MBP.MB('#payloadType', '8', PAYLOAD_TYPE.BINARY),
|
|
888
|
-
MBP.MB('#payload', sercretPack)
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
this.send_enc_mode(signalPack)
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
/**
|
|
896
|
-
* Sets a value in the store.
|
|
897
|
-
* @param {string} storeName - The name of the store.
|
|
898
|
-
* @param {...any} args - Arguments to set.
|
|
899
|
-
* @returns {Promise<any>}
|
|
900
|
-
*/
|
|
901
|
-
set(storeName, ...args) {
|
|
902
|
-
if (!storeName || args.length == 0) {
|
|
903
|
-
return Promise.reject(new Error('set need storeName and value)'))
|
|
904
|
-
}
|
|
905
|
-
return this.call('store', 'set', storeName, ...args)
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
/**
|
|
909
|
-
* Gets a value from the store.
|
|
910
|
-
* @param {string} storeName - The name of the store.
|
|
911
|
-
* @returns {Promise<any>}
|
|
912
|
-
*/
|
|
913
|
-
async get(storeName) {
|
|
914
|
-
if (!storeName) {
|
|
915
|
-
return Promise.reject(new Error('store get need storeName)'))
|
|
916
|
-
}
|
|
917
|
-
let pack = await this.call('store', 'get', storeName)
|
|
918
|
-
let { $ } = MBP.unpack(pack.body)
|
|
919
|
-
return $
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Sends a request to a target and topic.(remote service call)
|
|
925
|
-
* @param {string} target - The target(service name) of the request.
|
|
926
|
-
* @param {string} topic - The topic(service function name) of the request.
|
|
927
|
-
* @param {...any} args - Optional arguments for the request.
|
|
928
|
-
* @returns {Promise<any>}
|
|
929
|
-
*/
|
|
930
|
-
call(target, topic, ...args) {
|
|
931
|
-
if (!target || !topic)
|
|
932
|
-
return Promise.reject(new Error('request need target and topic)'))
|
|
933
|
-
let sigPack;
|
|
934
|
-
if (args.length > 0) {
|
|
935
|
-
sigPack = MBP.pack(
|
|
936
|
-
MBP.MB('#MsgType', '8', IOMsg.CALL),
|
|
937
|
-
MBP.MB('mid', '16', ++this.mid),
|
|
938
|
-
MBP.MB('target', target),
|
|
939
|
-
MBP.MB('topic', topic),
|
|
940
|
-
MBP.MBA(...args)
|
|
941
|
-
)
|
|
942
|
-
} else {
|
|
943
|
-
sigPack = MBP.pack(
|
|
944
|
-
MBP.MB('#MsgType', '8', IOMsg.CALL),
|
|
945
|
-
MBP.MB('mid', '16', ++this.mid),
|
|
946
|
-
MBP.MB('target', target),
|
|
947
|
-
MBP.MB('topic', topic)
|
|
948
|
-
)
|
|
949
|
-
}
|
|
950
|
-
this.send_enc_mode(sigPack)
|
|
951
|
-
return this.setMsgPromise(this.mid)
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* Subscribes to a channel or channels.
|
|
957
|
-
* @param {string} tag - The tag(s) of the channel(s) to subscribe to (comma-separated).
|
|
958
|
-
* @throws {TypeError} If tag is not a string or exceeds length limit.
|
|
959
|
-
*/
|
|
960
|
-
subscribe(tag) {
|
|
961
|
-
if (typeof tag !== 'string') throw TypeError('tag should be string.')
|
|
962
|
-
if (tag.length > SIZE_LIMIT.TAG_LEN1) throw TypeError('please check tag string length limit:' + SIZE_LIMIT.TAG_LEN1)
|
|
963
|
-
|
|
964
|
-
try {
|
|
965
|
-
let tagEncoded = encoder.encode(tag)
|
|
966
|
-
this.send_enc_mode(
|
|
967
|
-
Buffer.concat([
|
|
968
|
-
MBP.NB('8', IOMsg.SUBSCRIBE),
|
|
969
|
-
MBP.NB('8', tagEncoded.byteLength),
|
|
970
|
-
tagEncoded]))
|
|
971
|
-
} catch (error) { }
|
|
972
|
-
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
/**
|
|
977
|
-
* Subscribes stored channels.
|
|
978
|
-
* called client state become 'ready'
|
|
979
|
-
*/
|
|
980
|
-
subscribe_channels() {
|
|
981
|
-
if (this.state !== STATE.READY) return
|
|
982
|
-
if (this.channels.size == 0) return
|
|
983
|
-
let tag = Array.from(this.channels).join(',')
|
|
984
|
-
|
|
985
|
-
try {
|
|
986
|
-
let tagEncoded = encoder.encode(tag)
|
|
987
|
-
this.send_enc_mode(
|
|
988
|
-
Buffer.concat([
|
|
989
|
-
MBP.NB('8', IOMsg.SUBSCRIBE),
|
|
990
|
-
MBP.NB('8', tagEncoded.byteLength),
|
|
991
|
-
tagEncoded]))
|
|
992
|
-
} catch (error) { }
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/**
|
|
997
|
-
* Unsubscribes from a channel or channels.
|
|
998
|
-
* @param {string} [tag=""] - The tag(s) of the channel(s) to unsubscribe from (comma-separated). If empty, unsubscribes from all.
|
|
999
|
-
* @throws {TypeError} If tag is not a string or exceeds length limit.
|
|
1000
|
-
*/
|
|
1001
|
-
unsubscribe(tag = "") {
|
|
1002
|
-
if (typeof tag !== 'string') throw TypeError('tag should be string.')
|
|
1003
|
-
|
|
1004
|
-
if (tag == "") { // blank tag means unsubscribe all
|
|
1005
|
-
this.channels.clear();
|
|
1006
|
-
} else {
|
|
1007
|
-
let tagList = tag.split(',')
|
|
1008
|
-
tagList.forEach(tag => {
|
|
1009
|
-
this.channels.delete(tag)
|
|
1010
|
-
})
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
let tagEncoded = encoder.encode(tag)
|
|
1014
|
-
if (tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1)
|
|
1015
|
-
|
|
1016
|
-
this.send_enc_mode(Buffer.concat([
|
|
1017
|
-
MBP.NB('8', IOMsg.UNSUBSCRIBE),
|
|
1018
|
-
MBP.NB('8', tagEncoded.byteLength),
|
|
1019
|
-
tagEncoded]))
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Listens for signals on a specific tag.
|
|
1025
|
-
* @param {string} tag - The tag to listen on.
|
|
1026
|
-
* @param {Function} handler - The callback function to handle the signal.
|
|
1027
|
-
* @throws {TypeError} If tag is not a string, handler is not a function, or tag length is invalid.
|
|
1028
|
-
*/
|
|
1029
|
-
listen(tag, handler) {
|
|
1030
|
-
if (typeof tag !== 'string') throw TypeError('tag should be string.')
|
|
1031
|
-
if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
|
|
1032
|
-
if (typeof handler !== 'function') throw TypeError('handler is not a function.')
|
|
1033
|
-
|
|
1034
|
-
if (tag.indexOf('@') !== 0) {
|
|
1035
|
-
this.channels.add(tag)
|
|
1036
|
-
}
|
|
1037
|
-
this.on(tag, handler)
|
|
1038
|
-
// do not subscribe now.
|
|
1039
|
-
// will subscribe when io state is 'ready'. (receive CID_RES from server)
|
|
1040
|
-
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
/**
|
|
1046
|
-
* Links a local target to a remote tag and sets up a handler.
|
|
1047
|
-
* @param {string} to - The local link target.
|
|
1048
|
-
* @param {string} tag - The remote tag.
|
|
1049
|
-
* @param {Function} handler - The callback function to handle the signal.
|
|
1050
|
-
* @throws {TypeError} If 'to' or 'tag' are not strings, handler is not a function, or tag length is invalid.
|
|
1051
|
-
*/
|
|
1052
|
-
link(to, tag, handler) {
|
|
1053
|
-
if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
|
|
1054
|
-
if (typeof tag !== 'string') throw TypeError('tag is not a string.')
|
|
1055
|
-
if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
|
|
1056
|
-
if (typeof handler !== 'function') throw TypeError('handler is not a function.')
|
|
1057
|
-
|
|
1058
|
-
if (tag.indexOf('@') !== 0) {
|
|
1059
|
-
this.channels.add(tag)
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
let linkSet;
|
|
1063
|
-
if (this.linkMap.has(to)) {
|
|
1064
|
-
linkSet = this.linkMap.get(to)
|
|
1065
|
-
} else {
|
|
1066
|
-
linkSet = new Set()
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
linkSet.add(tag)
|
|
1070
|
-
this.linkMap.set(to, linkSet)
|
|
1071
|
-
this.on(tag, handler)
|
|
1072
|
-
this.subscribe(tag)
|
|
1073
|
-
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
/**
|
|
1078
|
-
* Unlinks a specific tag from a local target.
|
|
1079
|
-
* @param {string} to - The local link target.
|
|
1080
|
-
* @param {string} tag - The tag to unlink.
|
|
1081
|
-
* @throws {TypeError} If 'to' or 'tag' are not strings or tag length is invalid.
|
|
1082
|
-
*/
|
|
1083
|
-
unlink(to, tag) {
|
|
1084
|
-
if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
|
|
1085
|
-
if (typeof tag !== 'string') throw TypeError('tag is not a string.')
|
|
1086
|
-
if (tag.length > 255 || tag.length == 0) throw TypeError('tag string length range: 1~255')
|
|
1087
|
-
|
|
1088
|
-
const linkSet = this.linkMap.get(to);
|
|
1089
|
-
if (!linkSet || !linkSet.has(tag)) return;
|
|
1090
|
-
|
|
1091
|
-
this.unsubscribe(tag);
|
|
1092
|
-
this.removeAllListeners(tag);
|
|
1093
|
-
linkSet.delete(tag);
|
|
1094
|
-
|
|
1095
|
-
if (linkSet.size === 0) {
|
|
1096
|
-
this.linkMap.delete(to);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Unlinks all tags from a local target.
|
|
1102
|
-
* @param {string} to - The local link target.
|
|
1103
|
-
* @throws {TypeError} If 'to' is not a string.
|
|
1104
|
-
*/
|
|
1105
|
-
unlinkAll(to) {
|
|
1106
|
-
if (typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
|
|
1107
|
-
|
|
1108
|
-
const linkSet = this.linkMap.get(to);
|
|
1109
|
-
if (!linkSet) return;
|
|
1110
|
-
|
|
1111
|
-
for (const tag of linkSet) {
|
|
1112
|
-
this.unsubscribe(tag);
|
|
1113
|
-
this.removeAllListeners(tag);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
this.linkMap.delete(to);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
/**
|
|
1122
|
-
* Gets connection metrics.
|
|
1123
|
-
* @returns {{tx: number, rx: number, txb: number, rxb: number, last: number}}
|
|
1124
|
-
*/
|
|
1125
|
-
getMetric() {
|
|
1126
|
-
return {
|
|
1127
|
-
tx: this.txCounter,
|
|
1128
|
-
rx: this.rxCounter,
|
|
1129
|
-
txb: this.txBytes,
|
|
1130
|
-
rxb: this.rxBytes,
|
|
1131
|
-
last: (Date.now() - this.lastTxRxTime) / 1000
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Gets the current connection state.
|
|
1138
|
-
* @returns {number}
|
|
1139
|
-
*/
|
|
1140
|
-
getState() {
|
|
1141
|
-
return this.state
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Gets the current connection state name.
|
|
1146
|
-
* @returns {string}
|
|
1147
|
-
*/
|
|
1148
|
-
getStateName() {
|
|
1149
|
-
//state <number>
|
|
1150
|
-
//value of constant STATE.NAME < number >
|
|
1151
|
-
//type of constant STATE.NAME name < string uppercase >
|
|
1152
|
-
//stateName,eventName <string lowercase>
|
|
1153
|
-
return (STATE[this.state]).toLowerCase()
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
/**
|
|
1157
|
-
* Gets security-related information.
|
|
1158
|
-
* @returns {{useAuth: boolean, isTLS: boolean, isAuthorized: boolean, encMode: number, usingEncryption: boolean}}
|
|
1159
|
-
*/
|
|
1160
|
-
getSecurity() {
|
|
1161
|
-
return {
|
|
1162
|
-
useAuth: this.useAuth,
|
|
1163
|
-
isTLS: this.TLS,
|
|
1164
|
-
isAuthorized: this.boho.isAuthorized,
|
|
1165
|
-
encMode: this.encMode,
|
|
1166
|
-
usingEncryption: this.getEncryptionMode()
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
/**
|
|
1171
|
-
* Changes the connection state and emits events.
|
|
1172
|
-
* @param {string} state - The new state name (e.g., 'ready', 'closed').
|
|
1173
|
-
* @param {string} [emitEventAndMessage] - Optional message to emit with the state change event.
|
|
1174
|
-
*
|
|
1175
|
-
* 주의.
|
|
1176
|
-
* 1. 상태가 변경 될 때만 'change' 이벤트 호출된다.
|
|
1177
|
-
* 2. emitEventAndMessage 옵션 값이 지정되야 해당 이벤트 이름이 호출된다.
|
|
1178
|
-
* 보통 이벤트 이름과 동일하게 적거나 이벤트 상황 안내문을 넣는다.
|
|
1179
|
-
*/
|
|
1180
|
-
stateChange(state, emitEventAndMessage) {
|
|
1181
|
-
// STATE constant name <string> upperCase
|
|
1182
|
-
// eventName and .stateName <string> lowerCase
|
|
1183
|
-
// .state <number>
|
|
1184
|
-
// console.log('### stateChange reason:', emitEventAndMessage )
|
|
1185
|
-
let eventName = state.toLowerCase()
|
|
1186
|
-
this.state = STATE[state.toUpperCase()] // state: number
|
|
1187
|
-
|
|
1188
|
-
if (emitEventAndMessage) {
|
|
1189
|
-
this.emit(eventName, emitEventAndMessage)
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
if (this.stateName !== eventName) {
|
|
1193
|
-
this.stateName = eventName
|
|
1194
|
-
this.emit('change', eventName)
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|