iosignal 0.0.1 → 1.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.
Files changed (47) hide show
  1. package/README.md +170 -0
  2. package/auth_file.json +13 -0
  3. package/auth_file.mjs +23 -0
  4. package/dist/iosignal.cjs +2 -0
  5. package/dist/iosignal.esm.js +3 -0
  6. package/dist/iosignal.esm.js.map +1 -0
  7. package/dist/iosignal.min.js +3 -0
  8. package/dist/iosignal.min.js.map +1 -0
  9. package/img/iosignal_stack.png +0 -0
  10. package/index.js +19 -0
  11. package/indexWebBrowser.js +13 -0
  12. package/package.json +48 -6
  13. package/rollup.config.js +69 -0
  14. package/src/api/RedisAPI.js +68 -0
  15. package/src/api/api_constant.js +7 -0
  16. package/src/api/api_reply.js +34 -0
  17. package/src/api/api_sudo.js +96 -0
  18. package/src/auth/Auth_Env.js +78 -0
  19. package/src/auth/Auth_File.js +75 -0
  20. package/src/auth/Auth_Redis.js +50 -0
  21. package/src/auth/BohoAuth.js +168 -0
  22. package/src/client/CongPacket.js +150 -0
  23. package/src/client/IOCongSocket.js +88 -0
  24. package/src/client/IOCore.js +959 -0
  25. package/src/client/IOWS.js +81 -0
  26. package/src/client/IOWebSocket.js +96 -0
  27. package/src/common/constants.js +159 -0
  28. package/src/common/quotaTable.js +61 -0
  29. package/src/common/util.js +61 -0
  30. package/src/server/FileLogger.js +30 -0
  31. package/src/server/Manager.js +521 -0
  32. package/src/server/Metric.js +154 -0
  33. package/src/server/Server.js +136 -0
  34. package/src/server/ServerRemote.js +235 -0
  35. package/src/server/ServerRemoteCore.js +467 -0
  36. package/src/server/serverOption.js +55 -0
  37. package/test-browser/favicon.ico +0 -0
  38. package/test-browser/index.html +93 -0
  39. package/test-browser/server.js +8 -0
  40. package/test-client-nodejs/client.cjs +17 -0
  41. package/test-client-nodejs/client.mjs +17 -0
  42. package/test-server/server-commonjs.cjs +8 -0
  43. package/test-server/server-esm.mjs +19 -0
  44. package/test-server/test-Auth_Env.js +17 -0
  45. package/test-server/test-Auth_File.js +9 -0
  46. package/test-server/test-api_reply.js +10 -0
  47. package/test-server/test-api_sudo.js +11 -0
@@ -0,0 +1,959 @@
1
+ import { MBP ,Buffer} from 'meta-buffer-pack'
2
+ import EventEmitter from "eventemitter3";
3
+ import { Boho, BohoMsg, MetaSize } from "boho";
4
+ import { quotaTable } from '../common/quotaTable.js'
5
+ import { IOMsg , PAYLOAD_TYPE , SIZE_LIMIT , ENC_MODE ,STATES} from '../common/constants.js'
6
+
7
+ const encoder = new TextEncoder()
8
+ const decoder = new TextDecoder()
9
+
10
+ function byteToUrl( buffer){
11
+ //ipv4(4bytes) , port(2bytes)
12
+ if(buffer.byteLength != 6 ) return
13
+ let address = buffer[0].toString() + "."+ buffer[1].toString()
14
+ + "."+ buffer[2].toString() + "."+ buffer[3].toString();
15
+ let port = (buffer[4] << 8) + buffer[5]
16
+
17
+ return address +':'+ port.toString()
18
+ }
19
+
20
+ export class IOCore extends EventEmitter{
21
+ constructor( url) {
22
+ super();
23
+ this.cid = "" // get from the server CID_RES
24
+ this.ip = "" // get from the server IAM_RES message.
25
+ this.socket = null;
26
+ this.url = url; // init default server url
27
+ this.state = STATES.CLOSED; // Number type
28
+ this.stateName = this.getStateName() // String type
29
+
30
+ this.txCounter = 0;
31
+ this.rxCounter = 0;
32
+ this.txBytes = 0;
33
+ this.rxBytes = 0;
34
+
35
+ this.lastTxRxTime = Date.now();
36
+ this.connectionCheckerPeriod = SIZE_LIMIT.CONNECTION_CHECKER_PERIOD ;
37
+ this.connectionCheckerIntervalID = null;
38
+
39
+ this.boho = new Boho()
40
+ this.TLS = false // true if protocol is wss(TLS)
41
+ this.encMode = ENC_MODE.AUTO;
42
+ this.useAuth = false;
43
+
44
+ this.nick = "";
45
+ this.channels = new Set()
46
+ this.promiseMap = new Map()
47
+ this.promiseTimeOut = SIZE_LIMIT.PROMISE_TIMEOUT
48
+ this.mid = 0 // promise message id
49
+
50
+ this.level = 0; // also defaultQuotaLevel
51
+ this.quota = quotaTable[ this.level ];
52
+ this.serverSet = {}
53
+
54
+ this.linkMap = new Map()
55
+
56
+ this.on('open',this.onOpen.bind(this))
57
+ this.on('close',this.onClose.bind(this))
58
+ this.on('socket_data',this.onData.bind(this))
59
+ // this.on('auth_fail',this.onAuthFail.bind(this))
60
+ // this.on('server_signal', this.onServerSignal.bind(this))
61
+ }
62
+
63
+
64
+ // onAuthFail(event, reason){
65
+ // }
66
+
67
+ // onServerSignal(event, data ){
68
+ // console.log('onServerSignal', event, data )
69
+ // }
70
+
71
+ redirect(url2){
72
+ this.close()
73
+ this.stateChange('redirecting')
74
+ this.createConnection( url2)
75
+ }
76
+
77
+ open(url ) {
78
+ if( !url && !this.url ) return;
79
+
80
+ if( url ){
81
+ if( !this.url ){ // default host url
82
+ this.url = url
83
+ }else if( url !== this.url ){ // default host url change
84
+ this.url = url;
85
+ if( this.socket ){
86
+ this.close()
87
+ return
88
+ }
89
+ }
90
+ }
91
+
92
+ this.createConnection( this.url )
93
+
94
+ if(!this.connectionCheckerIntervalID){
95
+ this.connectionCheckerIntervalID = setInterval(this.keepAlive.bind(this), this.connectionCheckerPeriod);
96
+ }
97
+ }
98
+
99
+ onOpen( ){
100
+ if( this.url.includes("wss://" )){
101
+ this.TLS = true;
102
+ }else{
103
+ this.TLS = false;
104
+ }
105
+ this.stateChange('open' )
106
+ }
107
+
108
+ onClose(){
109
+ this.boho.isAuthorized = false;
110
+ this.cid = ""
111
+ this.stateChange('closed' )
112
+ }
113
+
114
+ // manual login
115
+ login( id, key){
116
+ if( !id && !key){
117
+ console.log('no id and key.')
118
+ return
119
+ }
120
+ console.log('manual login: ', id)
121
+
122
+ if( !key && id.includes('.') ){
123
+ this.boho.set_id_key(id)
124
+ }else if( id && key){
125
+ this.boho.set_id8(id)
126
+ this.boho.set_key(key)
127
+ }else{
128
+ console.log('no id or key.')
129
+ return
130
+ }
131
+ this.useAuth = true
132
+ let auth_pack = this.boho.auth_req()
133
+ // console.log('auth_req_pack', auth_pack )
134
+ this.send(auth_pack )
135
+ }
136
+
137
+ // auto login
138
+ auth( id, key){
139
+ if( !id && !key){
140
+ console.log('no id and key.')
141
+ return
142
+ }
143
+ // console.log('set auto auth: ', id)
144
+
145
+ if( !key && id.includes('.') ){
146
+ this.boho.set_id_key(id)
147
+ }else if( id && key){
148
+ this.boho.set_id8(id)
149
+ this.boho.set_key(key)
150
+ }else{
151
+ console.log('no id or key.')
152
+ return
153
+ }
154
+ this.useAuth = true
155
+ }
156
+
157
+ onData( buffer ){
158
+ // console.log('remote rcv socket_message', buffer )
159
+ //check first byte (remote message type)
160
+ let msgType = buffer[0];
161
+ let decoded;
162
+
163
+ if( msgType === BohoMsg.ENC_488 ){
164
+ decoded = this.boho.decrypt_488( buffer )
165
+ if( decoded ){
166
+ // console.log( decoded )
167
+ msgType = decoded[0]
168
+ buffer = decoded
169
+ // console.log('DECODED MsgType:', IOMsg[ msgType ] )
170
+ }else{
171
+ console.log('DEC_FAIL', buffer.byteLength)
172
+ }
173
+ }else if( msgType === BohoMsg.ENC_E2E ){
174
+ // console.log('rcv ENC_E2E' )
175
+
176
+ try{
177
+ decoded = this.boho.decrypt_488( buffer )
178
+ //헤더를 읽고 헤더크기만큼만 해석한다.
179
+ if( decoded ){
180
+ // console.log( 'ENC_E2E decoded ', decoded )
181
+ msgType = decoded[0]
182
+ // decoded has msg_header only.
183
+ buffer.set( decoded ,MetaSize.ENC_488) // set decoded signal_e2e headaer.
184
+ buffer = buffer.subarray( MetaSize.ENC_488 ) // reset offset.
185
+ // console.log('DECODED MsgType:', IOMsg[ msgType ] )
186
+ }else{
187
+ console.log('488 DEC_FAIL', buffer)
188
+ return
189
+ }
190
+
191
+ }catch(err){
192
+ console.log('E2E DEC_FAIL decryption error', err)
193
+ return
194
+ }
195
+
196
+ }
197
+
198
+ let type = IOMsg[ msgType ]
199
+ if( !type ) type = BohoMsg[ msgType ]
200
+
201
+ // console.log( "MsgType: ", type , " LEN ", buffer.byteLength)
202
+
203
+ switch( msgType){
204
+ case IOMsg.OVER_SIZE :
205
+ console.log('## server sent: over_size event.')
206
+ this.emit('over_size','over_size')
207
+ break;
208
+ case IOMsg.PING :
209
+ this.pong();
210
+ break;
211
+
212
+ case IOMsg.PONG :
213
+ break;
214
+
215
+ case IOMsg.IAM_RES:
216
+ try {
217
+ let str = decoder.decode( buffer.subarray(1) )
218
+ let jsonInfo = JSON.parse(str)
219
+ if( jsonInfo.ip ){
220
+ this.ip = jsonInfo.ip;
221
+ }
222
+ console.log('<IAM_RES>', JSON.stringify(jsonInfo))
223
+ // console.log('<IAM_RES>', JSON.stringify(jsonInfo,null,2))
224
+ } catch (error) {
225
+ console.log('<IAM_RES> data error')
226
+ }
227
+ break;
228
+
229
+ case IOMsg.CID_RES :
230
+ let cidStr = decoder.decode( buffer.subarray(1) )
231
+ // console.log( '>> CID_RES: ' ,cidStr )
232
+ this.cid = cidStr;
233
+ this.stateChange('ready','cid_ready' )
234
+ // change state before subscribe.
235
+ this.subscribe_memory_channels()
236
+ break;
237
+
238
+ case IOMsg.QUOTA_LEVEL :
239
+ let quotaLevel = buffer[1]
240
+ this.level = quotaLevel;
241
+ this.quota = quotaTable[ quotaLevel ];
242
+ console.log('## QUOTA:', quotaLevel, JSON.stringify(this.quota) )
243
+ break;
244
+
245
+ case IOMsg.SERVER_CLEAR_AUTH :
246
+ this.useAuth = false;
247
+ this.boho.clearAuth();
248
+ this.stop();
249
+ break;
250
+
251
+ case IOMsg.SERVER_REDIRECT :
252
+ // console.log( buffer.toString('hex'))
253
+ let host_port;
254
+ let url;
255
+ let protocol;
256
+ let addressType;
257
+ if( buffer.byteLength == 7){ // ipv4 ,port
258
+ addressType = 'IPV4:PORT'
259
+ host_port = byteToUrl( buffer.subarray(1))
260
+ protocol = 'cong://'
261
+ }else{ // domain url
262
+ addressType= 'URL'
263
+ host_port = decoder.decode( buffer.subarray(1))
264
+ protocol = '' // must included in url
265
+ }
266
+
267
+ url = protocol + host_port
268
+
269
+ console.log(`REDIRECT TO <${addressType}> : ${url}` )
270
+ this.redirect(url)
271
+ break;
272
+
273
+ case IOMsg.SERVER_READY :
274
+ // console.log('>> SERVER_READY')
275
+ this.stateChange('server_ready','server_ready' )
276
+ if(this.useAuth){
277
+ this.send( this.boho.auth_req() )
278
+ // CID_REQ will be called, after auth_ack.
279
+ }else{
280
+ // CID_REQ here, if not using auth.
281
+ this.send( Buffer.from([IOMsg.CID_REQ]) )
282
+ }
283
+ break;
284
+
285
+ case IOMsg.SERVER_SIGNAL:
286
+ try {
287
+ let str = decoder.decode( buffer.subarray(1) )
288
+ let ss = JSON.parse(str)
289
+ console.log('SERVER_SIGNAL', JSON.stringify(ss))
290
+
291
+ if( ss.event && ss.data ){
292
+ this.serverSet = ss.data;
293
+ this.emit( ss.event , ss.data )
294
+ }
295
+
296
+ } catch (error) {
297
+ console.log('<SERVER_SIGNAL> parsing error')
298
+ }
299
+ break;
300
+
301
+ case IOMsg.SET:
302
+ try {
303
+ let setPack = MBP.unpack(buffer)
304
+ if(setPack ){
305
+ console.log('[SET] topic: ',setPack.topic )
306
+ this.emit( setPack.topic, ...setPack.args )
307
+ }
308
+ } catch (error) {
309
+ console.log('<SET> parsing error')
310
+ }
311
+ break;
312
+
313
+ case IOMsg.SIGNAL_E2E:
314
+ case IOMsg.SIGNAL:
315
+ try{
316
+ let tagLen = buffer.readUint8(1)
317
+ let tagBuf = buffer.subarray(2, 2 + tagLen )
318
+ let tag = decoder.decode(tagBuf)
319
+
320
+ let payloadType = buffer.readUint8( 2 + tagLen )
321
+ let payloadBuffer = buffer.subarray( 3 + tagLen )
322
+
323
+ /* three types of signal message.
324
+ > unicast message to me: tag includes @, no cid: '@*'
325
+ > cid_sub message: tag includes cid and @ both : 'cid@*'
326
+ > ch_sub message: else.
327
+ */
328
+
329
+
330
+ switch( payloadType ){
331
+
332
+ case PAYLOAD_TYPE.EMPTY: // 0
333
+ if( tag.indexOf('@') === 0 ) this.emit( '@', null , tag)
334
+ else this.emit( tag, null , tag )
335
+ break;
336
+
337
+ case PAYLOAD_TYPE.TEXT: // 1
338
+ // !! Must remove null char before decode in JS.
339
+ // string payload contains null char for the c/cpp devices.
340
+ let payloadStringWithoutNull = payloadBuffer.subarray(0,payloadBuffer.byteLength - 1 )
341
+ let oneString = decoder.decode( payloadStringWithoutNull )
342
+ if( tag.indexOf('@') === 0 ) this.emit( '@', oneString , tag )
343
+ if( tag !== '@') this.emit( tag, oneString , tag )
344
+ break;
345
+
346
+ case PAYLOAD_TYPE.BINARY: // 2
347
+ if( tag.indexOf('@') === 0 ) this.emit( '@', payloadBuffer , tag )
348
+ if( tag !== '@') this.emit( tag, payloadBuffer , tag )
349
+ break;
350
+
351
+ case PAYLOAD_TYPE.OBJECT:
352
+ let oneObjectBuffer = decoder.decode( payloadBuffer )
353
+ let oneJSONObject = JSON.parse( oneObjectBuffer )
354
+ if( tag.indexOf('@') === 0 ) this.emit( '@', oneJSONObject , tag )
355
+ if( tag !== '@') this.emit( tag, oneJSONObject , tag )
356
+ break;
357
+
358
+ case PAYLOAD_TYPE.MJSON:
359
+ let mjsonBuffer = decoder.decode( payloadBuffer )
360
+ // console.log('raw mjson tag', tag)
361
+ // console.log('raw mjson', mjsonBuffer)
362
+ let mjson = JSON.parse( mjsonBuffer )
363
+ // console.log('parsed mjson', mjson)
364
+ if( tag.indexOf('@') === 0 ) this.emit( '@', ...mjson , tag )
365
+ if( tag !== '@') this.emit( tag, ...mjson , tag )
366
+ break;
367
+
368
+ case PAYLOAD_TYPE.MBA:
369
+ let mbaObject = MBP.unpack( buffer )
370
+ if( tag.indexOf('@') === 0 ) this.emit( '@', ...mbaObject.args , tag )
371
+ if( tag !== '@') this.emit( tag, ...mbaObject.args , tag )
372
+ break;
373
+
374
+ default:
375
+ console.log('## Unkown payloadtype', payloadType)
376
+
377
+ }
378
+
379
+
380
+ }catch(err){
381
+ console.log('## signal parse err',err)
382
+ }
383
+ break;
384
+
385
+
386
+
387
+ case IOMsg.RESPONSE_MBP:
388
+ this.testPromise( buffer)
389
+ break;
390
+
391
+
392
+
393
+ case BohoMsg.AUTH_NONCE:
394
+ // console.log('auth_nonce', buffer )
395
+ let auth_hmac = this.boho.auth_hmac( buffer )
396
+ if(auth_hmac){
397
+ this.send( auth_hmac )
398
+ }else{
399
+ this.stateChange('auth_fail', 'Invalid local auth_hmac.' )
400
+ console.log('auth_fail', 'Invalid local auth_hmac.' )
401
+ }
402
+ break;
403
+ case BohoMsg.AUTH_FAIL:
404
+ this.stateChange('auth_fail','server reject auth.' )
405
+ console.log('auth_fail', 'server reject auth.' )
406
+ break;
407
+ case BohoMsg.AUTH_ACK:
408
+ if(this.boho.check_auth_ack_hmac( buffer ) ){
409
+ // this.emit('authorized' );
410
+ this.stateChange('auth_ready','server sent auth_ack' )
411
+ this.send( Buffer.from([IOMsg.CID_REQ ]) )
412
+ }else{
413
+ // this.emit('auth_fail','invalid server hmac')
414
+ this.stateChange('auth_fail','invalid server_hmac' )
415
+ console.log('auth_fail', 'Invalid server hmac.' )
416
+ }
417
+ break;
418
+
419
+ default:
420
+ try {
421
+ decoded = decoder.decode( buffer )
422
+ // console.log('text message:', decoded)
423
+ this.emit('text_message', decoded)
424
+ } catch (error) {
425
+
426
+ }
427
+
428
+ break;
429
+
430
+ }
431
+ }
432
+
433
+ iam( title ){
434
+ // console.log('iam', title)
435
+ if(title ){
436
+ this.send_enc_mode( MBP.pack(
437
+ MBP.MB('#MsgType','8', IOMsg.IAM ) ,
438
+ MBP.MB('#', title )
439
+ ))
440
+ }else{
441
+ this.send_enc_mode( MBP.pack(
442
+ MBP.MB('#MsgType','8', IOMsg.IAM )
443
+ ))
444
+ }
445
+ }
446
+
447
+
448
+ ping(){
449
+ this.send( Buffer.from( [ IOMsg.PING ]))
450
+ }
451
+
452
+ pong(){
453
+ this.send( Buffer.from( [ IOMsg.PONG ]))
454
+ }
455
+
456
+
457
+ // application level ping tool.
458
+ // simple message sending and reply.
459
+ echo( args ){
460
+ if(args ){
461
+ console.log( 'echo args:', args )
462
+ this.send_enc_mode( MBP.pack(
463
+ MBP.MB('#MsgType','8', IOMsg.ECHO ) ,
464
+ MBP.MB('#msg', args )
465
+ ))
466
+ }else{
467
+ // # do not encrypt blank echo #
468
+ this.send( Buffer.from([ IOMsg.ECHO ]))
469
+ }
470
+ }
471
+
472
+
473
+ bin(...data){
474
+ this.send( MBP.U8pack( ...data) )
475
+ }
476
+
477
+ send( data ){
478
+ if( data.byteLength > this.quota.signalSize ){
479
+ this.emit('over_size')
480
+ console.log('## QUOTA LIMIT OVER!! \nsignal message.byteLength: ', data.byteLength )
481
+ console.log('## your maximum signalSize(bytes) is:', this.quota.signalSize )
482
+ return
483
+ }
484
+ this.socket_send( data );
485
+ }
486
+
487
+ /*
488
+ Policy. Should message do encrypt?
489
+
490
+ if encMode == auto
491
+ NO. if connection using TLS line.
492
+ // ex. wss://url connection.
493
+ YES. if no TLS line.
494
+ // ex. ws://url connection.
495
+
496
+ if encMode == YES
497
+ YES. encrypt the message.
498
+
499
+ if encMode == NO
500
+ NO. do not ecnrypt message.
501
+
502
+ */
503
+ getEncryptionMode(){
504
+ if( this.encMode === ENC_MODE.YES ||
505
+ this.encMode === ENC_MODE.AUTO &&
506
+ !this.TLS && this.boho.isAuthorized
507
+ ){
508
+ return true;
509
+ }else{
510
+ return false
511
+ }
512
+ }
513
+
514
+ send_enc_mode( data ,useEncryption ){
515
+
516
+ // use default policy.
517
+ if( useEncryption === undefined){
518
+ useEncryption = this.getEncryptionMode()
519
+ }
520
+
521
+ if( data[0] == IOMsg.SIGNAL_E2E && useEncryption){
522
+ // input data: signal_header + e2ePayload
523
+ // encrypt signal_header area only. payload is encrypted with e2e key already.
524
+ let tagLen = data[1]
525
+ let encHeader = this.boho.encrypt_488( data.subarray(0, 3 + tagLen))
526
+ encHeader[0] = BohoMsg.ENC_E2E
527
+ this.send( Buffer.concat([encHeader, data.subarray(3+tagLen) ]))
528
+ // console.log('<< send_enc_mode [ ENC_E2E ]')
529
+
530
+ }else if( useEncryption ){
531
+ // console.log('<< send_enc_mode [ ENC_488 ]')
532
+ let encPack = this.boho.encrypt_488( data )
533
+ this.send( encPack )
534
+ }else{
535
+ // console.log('<< send_enc_mode [ PLAIN ]' )
536
+ this.send( data )
537
+ }
538
+
539
+ }
540
+
541
+
542
+ setMsgPromise(mid ){
543
+ return new Promise( (resolve, reject)=>{
544
+ this.promiseMap.set( mid, [resolve, reject ] )
545
+ // console.log('set promise. mid, size', mid, this.promiseMap.size)
546
+ setTimeout( e=>{
547
+ if(this.promiseMap.has(mid )){
548
+ reject('timeout');
549
+ this.promiseMap.delete( mid )
550
+ // console.log('promise timeout. mid, size:', mid, this.promiseMap.size)
551
+ }
552
+ }, this.promiseTimeOut);
553
+ })
554
+ }
555
+
556
+ testPromise( buffer ){
557
+ // console.log('mbp buffer : ', buffer , buffer.byteLength)
558
+ // let mbp = ( buffer.byteLength > 4 ) ? buffer.subarray(4) : ""
559
+
560
+ let res = MBP.unpack(buffer)
561
+ if( !res ) return
562
+ // console.log( res )
563
+
564
+ // console.log(`RESPONSE_MBP MID: ${mid} status: ${status} ,mbp: ${ buffer.subarray(4)} `)
565
+
566
+ if( this.promiseMap.has(res.mid)){
567
+ // console.log('res promise msg', mid)
568
+ let [ resolve, reject ] = this.promiseMap.get( res.mid )
569
+ this.promiseMap.delete( res.mid )
570
+
571
+ if(res.status < 128){
572
+ res.ok = true;
573
+ // console.log( 'unpack meta:', meta)
574
+ resolve( res )
575
+ } else{
576
+ res.ok = false ;
577
+ reject ( res )
578
+ }
579
+
580
+
581
+ }else{
582
+ console.log('no promise id')
583
+ }
584
+ }
585
+
586
+
587
+ publish( ...args ){
588
+ this.signal( ...args )
589
+ }
590
+
591
+
592
+ parsePayload( args ){
593
+ // console.log( 'parsePayload args', args )
594
+ let type, pack;
595
+ if( args.length == 0){
596
+ type = PAYLOAD_TYPE.EMPTY
597
+ pack = null
598
+ }else if( args.length == 1){
599
+ if( typeof args[0] === 'string' || typeof args[0] === 'number'){
600
+ type = PAYLOAD_TYPE.TEXT
601
+ pack = encoder.encode( args[0] + ".") // add null area.
602
+ pack[pack.byteLength - 1 ] = 0 // set null.
603
+
604
+ }else if( ArrayBuffer.isView( args[0]) || args[0] instanceof ArrayBuffer ){ //one buffer
605
+ type = PAYLOAD_TYPE.BINARY
606
+ pack = MBP.B8( args[0 ] )
607
+ }else if(typeof args[0] === 'object'){
608
+ type = PAYLOAD_TYPE.OBJECT
609
+ pack = encoder.encode( JSON.stringify( args[0]) )
610
+ }else{
611
+ //
612
+ console.log('unknown type payload arguments')
613
+ }
614
+ }else{ // args 2 and more
615
+ let containsBuffer = false
616
+ args.forEach( item =>{
617
+ if( ArrayBuffer.isView( item ) || item instanceof ArrayBuffer ) containsBuffer = true;
618
+ // console.log('payload item', item )
619
+ })
620
+
621
+ if( containsBuffer ){
622
+ type = PAYLOAD_TYPE.MBA;
623
+ // pack
624
+ }else{
625
+ type = PAYLOAD_TYPE.MJSON;
626
+ // args is array
627
+ pack = encoder.encode( JSON.stringify( args ) )
628
+ }
629
+
630
+ }
631
+
632
+ return { type: type, buffer: pack }
633
+
634
+ }
635
+
636
+ get_signal_pack( tag, ...args ){
637
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
638
+ let tagEncoded = encoder.encode( tag)
639
+ let payload = this.parsePayload( args )
640
+
641
+ let sigPack;
642
+ if( payload.type == PAYLOAD_TYPE.EMPTY ){
643
+ sigPack = MBP.pack(
644
+ MBP.MB('#MsgType','8', IOMsg.SIGNAL) ,
645
+ MBP.MB('#tagLen','8', tagEncoded.byteLength),
646
+ MBP.MB('#tag', tagEncoded),
647
+ MBP.MB('#payloadType', '8', payload.type )
648
+ )
649
+ }else if( payload.type == PAYLOAD_TYPE.MBA ){
650
+ sigPack = MBP.pack(
651
+ MBP.MB('#MsgType','8', IOMsg.SIGNAL) ,
652
+ MBP.MB('#tagLen','8', tagEncoded.byteLength),
653
+ MBP.MB('#tag', tagEncoded),
654
+ MBP.MB('#payloadType', '8', payload.type ),
655
+ MBP.MBA(...args)
656
+ )
657
+ }else {
658
+ sigPack = MBP.pack(
659
+ MBP.MB('#MsgType','8', IOMsg.SIGNAL) ,
660
+ MBP.MB('#tagLen','8', tagEncoded.byteLength),
661
+ MBP.MB('#tag', tagEncoded),
662
+ MBP.MB('#payloadType', '8', payload.type ),
663
+ MBP.MB('#payload', payload.buffer )
664
+ )
665
+ }
666
+ return sigPack
667
+ }
668
+
669
+
670
+ signal( tag , ...args ){
671
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
672
+
673
+ let signalPack = this.get_signal_pack(tag, ...args )
674
+ this.send_enc_mode( signalPack )
675
+ }
676
+
677
+ decrypt_e2e( data, key ){
678
+ return this.boho.decrypt_e2e( data, key )
679
+ }
680
+
681
+ signal_e2e( tag , data, key){
682
+
683
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
684
+ let tagEncoded = encoder.encode( tag)
685
+ let dataPack = MBP.B8( data )
686
+
687
+ //encrypt payload area with key
688
+ let sercretPack = this.boho.encrypt_e2e( dataPack, key )
689
+
690
+ //change signal MsgType header into SIGNAL_E2E
691
+ let signalPack = MBP.pack(
692
+ MBP.MB('#MsgType','8', IOMsg.SIGNAL_E2E) ,
693
+ MBP.MB('#tagLen','8', tagEncoded.byteLength),
694
+ MBP.MB('#tag', tagEncoded),
695
+ MBP.MB('#payloadType', '8', PAYLOAD_TYPE.BINARY ),
696
+ MBP.MB('#payload', sercretPack )
697
+ )
698
+
699
+ this.send_enc_mode( signalPack )
700
+ }
701
+
702
+
703
+
704
+ set( storeName, ...args ){
705
+ if( !storeName || args.length == 0 ){
706
+ return Promise.reject(new Error('set need storeName and value)'))
707
+ }
708
+ return this.req('store', 'set', storeName, ...args )
709
+ }
710
+
711
+ async get( storeName ){
712
+ if( !storeName ){
713
+ return Promise.reject(new Error('store get need storeName)'))
714
+ }
715
+ let pack = await this.req('store', 'get', storeName )
716
+ let { $ } = MBP.unpack(pack.body)
717
+ return $
718
+ }
719
+
720
+
721
+ req( target, topic, ...args ){
722
+ // console.log('common_req args', args)
723
+ if( !target || !topic)
724
+ return Promise.reject(new Error('request need target and topic)'))
725
+ let sigPack;
726
+ if(args.length > 0){
727
+ sigPack = MBP.pack(
728
+ MBP.MB('#MsgType','8', IOMsg.REQUEST) ,
729
+ MBP.MB('mid','16',++this.mid),
730
+ MBP.MB('target', target ),
731
+ MBP.MB('topic', topic ),
732
+ MBP.MBA( ...args )
733
+ )
734
+ }else{
735
+ sigPack = MBP.pack(
736
+ MBP.MB('#MsgType','8', IOMsg.REQUEST) ,
737
+ MBP.MB('mid','16',++this.mid),
738
+ MBP.MB('target', target ),
739
+ MBP.MB('topic', topic )
740
+ )
741
+ }
742
+ // console.log('<< adminPack', this.mid, sigPack)
743
+ this.send_enc_mode( sigPack )
744
+ return this.setMsgPromise( this.mid )
745
+ }
746
+
747
+
748
+ subscribe(tag ){
749
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
750
+ if( this.state !== STATES.READY ) return
751
+
752
+ let tagList = tag.split(',')
753
+ tagList.forEach( tag=>{
754
+ this.channels.add(tag)
755
+ })
756
+
757
+ let tagEncoded = encoder.encode( tag)
758
+ if( tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1 ) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1 )
759
+
760
+ this.send_enc_mode(
761
+ Buffer.concat( [
762
+ MBP.NB('8',IOMsg.SUBSCRIBE),
763
+ MBP.NB('8', tagEncoded.byteLength),
764
+ tagEncoded ]) )
765
+ }
766
+
767
+ subscribe_promise(tag){
768
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
769
+ if( this.state !== STATES.READY ){
770
+ // console.log('not ready state:', this.state )
771
+ return Promise.reject('subscribe_promise:: connection is not ready')
772
+ }
773
+
774
+ let tagEncoded = encoder.encode( tag)
775
+ if( tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN2 ) throw TypeError('please use tag string bytelength: ' + SIZE_LIMIT.TAG_LEN2)
776
+
777
+ this.send_enc_mode(
778
+ Buffer.concat( [
779
+ MBP.NB('8',IOMsg.SUBSCRIBE_REQ),
780
+ MBP.NB('16', ++this.mid),
781
+ MBP.NB('16', tagEncoded.byteLength),
782
+ tagEncoded ]) )
783
+ return this.setMsgPromise( this.mid )
784
+ }
785
+
786
+ subscribe_memory_channels( ){ //local cache . auto_resubscribe
787
+ if(this.channels.size == 0) return
788
+ let chList = Array.from( this.channels).join(',')
789
+ // console.log('<< subscibe memory channels by cid', chList , this.cid )
790
+
791
+ this.subscribe_promise( chList)
792
+ .then( (res )=>{
793
+ // console.log('>> SUBSCRIBE_REQ result', res ) // return code == map.size
794
+ }).catch( (e)=>{
795
+ console.log('>> SUBSCRIBE FAIL:', e)
796
+ })
797
+
798
+ }
799
+
800
+ unsubscribe(tag = ""){
801
+ // console.log('unsub', tag)
802
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
803
+
804
+ if(tag == ""){
805
+ // console.log('unsub all')
806
+ this.channels.clear();
807
+ }else{
808
+ let tagList = tag.split(',')
809
+ tagList.forEach( tag=>{
810
+ this.channels.delete(tag)
811
+ })
812
+ }
813
+
814
+ let tagEncoded = encoder.encode( tag)
815
+ if( tagEncoded.byteLength > SIZE_LIMIT.TAG_LEN1 ) throw TypeError('please use tag string bytelength below:' + SIZE_LIMIT.TAG_LEN1 )
816
+
817
+ this.send_enc_mode( Buffer.concat( [
818
+ MBP.NB('8',IOMsg.UNSUBSCRIBE),
819
+ MBP.NB('8', tagEncoded.byteLength),
820
+ tagEncoded ]) )
821
+ }
822
+
823
+
824
+ listen(tag , handler){
825
+ if( typeof tag !== 'string') throw TypeError('tag should be string.')
826
+ if( tag.length > 255 || tag.length == 0 ) throw TypeError('tag string length range: 1~255')
827
+ if( typeof handler !== 'function') throw TypeError('handler is not a function.')
828
+
829
+ if( tag.indexOf('@') !== 0){
830
+ this.channels.add(tag)
831
+ }
832
+ // console.log('channels:', this.channels )
833
+ this.on( tag , handler)
834
+ // do not subscribe now.
835
+ // will subscribe when receive CID_RES signal from server.
836
+
837
+ }
838
+
839
+
840
+
841
+ link( to , tag , handler){
842
+ if( typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
843
+ if( typeof tag !== 'string') throw TypeError('tag is not a string.')
844
+ if( tag.length > 255 || tag.length == 0 ) throw TypeError('tag string length range: 1~255')
845
+ if( typeof handler !== 'function') throw TypeError('handler is not a function.')
846
+
847
+ if( tag.indexOf('@') !== 0){
848
+ this.channels.add(tag)
849
+ }
850
+
851
+ let linkSet;
852
+ if( this.linkMap.has(to) ){
853
+ linkSet = this.linkMap.get(to)
854
+ }else {
855
+ linkSet = new Set()
856
+ }
857
+
858
+ linkSet.add(tag )
859
+ this.linkMap.set( to, linkSet)
860
+ this.on( tag , handler)
861
+ this.subscribe( tag )
862
+ // console.log('link [to] linkMap:', to, this.linkMap )
863
+
864
+ }
865
+
866
+
867
+ unlink( to, tag){
868
+ if( typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
869
+ if( typeof tag !== 'string') throw TypeError('tag is not a string.')
870
+ if( tag.length > 255 || tag.length == 0 ) throw TypeError('tag string length range: 1~255')
871
+
872
+ if( !this.linkMap.has( to ) ) return;
873
+
874
+ let linkSet = this.linkMap.get(to)
875
+ let tags = Array.from(linkSet)
876
+ for(let i=0; i< tags.length; i++){
877
+ if( tags[i] == tag ){
878
+ this.unsubscribe( tag )
879
+ this.removeAllListeners(tag)
880
+ linkSet.delete(tag)
881
+ this.linkMap.set( to , linkSet)
882
+ break;
883
+ }
884
+ }
885
+
886
+ // console.log('unlink linkMap result:', this.linkMap )
887
+ }
888
+
889
+ unlinkAll( to){
890
+ if( typeof to !== 'string') throw TypeError('to(local link target) is not a string.')
891
+ if( !this.linkMap.has( to ) ) return;
892
+
893
+ let linkSet = this.linkMap.get(to)
894
+ let tags = Array.from(linkSet)
895
+ for(let i=0; i< tags.length; i++){
896
+ this.unsubscribe( tags[i] )
897
+ this.removeAllListeners(tags[i])
898
+ linkSet.delete(tags[i])
899
+ }
900
+ this.linkMap.delete( to )
901
+
902
+ // console.log('unlinkAll linkMap result:', this.linkMap )
903
+ }
904
+
905
+
906
+
907
+ getMetric(){
908
+ return {
909
+ tx: this.txCounter,
910
+ rx: this.rxCounter,
911
+ txb: this.txBytes,
912
+ rxb: this.rxBytes,
913
+ last: ( Date.now() - this.lastTxRxTime) / 1000
914
+ }
915
+
916
+ }
917
+
918
+ getState(){
919
+ return this.state
920
+ }
921
+
922
+ getStateName(){
923
+ //state <number>
924
+ //value of constant STATES.NAME < number >
925
+ //type of constant STATES.NAME name < string uppercase >
926
+ //stateName,eventName <string lowercase>
927
+ return ( STATES[ this.state ]).toLowerCase()
928
+ }
929
+
930
+ getSecurity(){
931
+ return {
932
+ useAuth: this.useAuth,
933
+ isTLS: this.TLS,
934
+ isAuthorized: this.boho.isAuthorized,
935
+ encMode: this.encMode,
936
+ usingEncryption: this.getEncryptionMode()
937
+ }
938
+ }
939
+
940
+ stateChange(state, emitEventAndMessage ){
941
+ // STATES constant name : string upperCase
942
+ // eventName, .stateName : string lowerCase
943
+ // .state : number
944
+ let eventName = state.toLowerCase()
945
+ this.state = STATES[ state.toUpperCase() ] // state: number
946
+ if(emitEventAndMessage) this.emit(eventName, emitEventAndMessage)
947
+
948
+ if( this.stateName !== eventName ){
949
+ // console.log(`state: ${this.stateName} => ${eventName}` )
950
+ this.emit('change', eventName)
951
+ this.stateName = eventName
952
+ }
953
+ }
954
+
955
+ }
956
+
957
+
958
+
959
+