@subiz/wsclient 4.0.27 → 4.0.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subiz/wsclient",
3
- "version": "4.0.27",
3
+ "version": "4.0.30",
4
4
  "main": "realtime.js",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -15,9 +15,9 @@
15
15
  "homepage": "https://github.com/subiz/wsclient#readme",
16
16
  "description": "",
17
17
  "devDependencies": {
18
- "tape": "^5.8.1",
19
- "vite": "^5.3.4",
20
- "vite-plugin-commonjs": "^0.10.1"
18
+ "tape": "^5.9.0",
19
+ "vite": "^6.3.3",
20
+ "vite-plugin-commonjs": "^0.10.4"
21
21
  },
22
22
  "dependencies": {
23
23
  "@subiz/flow": "^1.1.0"
package/realtime.js CHANGED
@@ -8,9 +8,9 @@ var {xhrsend, parseJSON, Pubsub} = require('./common.js')
8
8
  // as fast as possible through onEvents callback.
9
9
  //
10
10
  // It will try it best to keep the communication with the realtime
11
- // server. But if the user is offline for too long or her network can't
12
- // keep up with the incomming events (rarely happend with today
13
- // internet speed). The connection will be halt. Dead.
11
+ // server. But if the user is offline for too long or the network can't
12
+ // keep up with incomming events (rarely happend with today internet speed).
13
+ // The connection will be killed. Dead.
14
14
  //
15
15
  // Once dead, onDead will be called. After that, the connection is
16
16
  // useless, all resources will be released.
@@ -18,38 +18,44 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
18
18
  callAPI = callAPI || xhrsend // allow hook
19
19
  credential = credential || {}
20
20
  // tell if connection is dead
21
- var dead = false
22
21
 
23
22
  var lastToken = ''
24
- // kill force the connection to dead state
25
- this.kill = function () {
26
- dead = true
27
- }
28
23
  if (!accid) accid = ''
29
24
 
30
- // long polling loop, polling will run sequentialy. Each polling will start right
31
- // after the previous returned. In case of error, polling loop will pause based on
32
- // exponential backoff algorithm. Polling loop will terminate if it encounter an
33
- // un-retryable error
34
- // the polling loop starts after the first subscribe call finished successfully
25
+ var status = 'active' // active | polling (trouble while polling) | subbing (trouble while subscribing) | dead
26
+
27
+ // The long polling loop runs sequentially, with each poll starting immediately
28
+ // after the previous one completes. If an error occurs, the loop will pause
29
+ // using an exponential backoff strategy. It will terminate if an unretryable
30
+ // error is encountered. The polling loop begins after the first subscription
31
+ // call completes successfully
35
32
  var polling = function (backoff) {
36
- if (dead) return
37
- callAPI('get', apiUrl + 'poll?token=' + lastToken + '&account_id=' + accid, undefined, function (body, code) {
38
- if (dead) return
39
- if (retryable(code)) return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
33
+ if (status == 'dead') return
34
+ callAPI('get', apiUrl + 'poll?token=' + lastToken + '&account-id=' + accid, undefined, function (body, code) {
35
+ if (status == 'dead') return
36
+
37
+ if (retryable(code)) {
38
+ if (status == 'active') status = 'polling'
39
+ return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
40
+ }
41
+
40
42
  if (code !== 200) {
41
43
  // unretryable error, should kill
42
- dead = true
44
+ status = 'dead'
43
45
  return onDead('poll')
44
46
  }
45
47
 
48
+ if (status == 'polling') status = 'active'
46
49
  // 200, success
47
50
  body = parseJSON(body) || {}
48
51
  if (body.host) apiUrl = absUrl(body.host)
49
52
 
50
53
  var seqToken = body.sequential_token
51
- // the server returns a malform payload. We should retry and hope it heal soon
52
- if (!seqToken) return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
54
+ // the server returns a malform payload. We should end
55
+ if (!seqToken) {
56
+ status = 'dead'
57
+ return onDead('poll')
58
+ }
53
59
  lastToken = seqToken
54
60
  onEvents(body.events || [])
55
61
  return polling(0)
@@ -57,44 +63,90 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
57
63
  }
58
64
 
59
65
  var subQueue = flow.batch(50, 100, function (events) {
60
- if (dead) return ['dead']
66
+ if (status == 'dead') return repeat('dead', events.length)
61
67
  if (events.length <= 0) return []
62
68
 
69
+ var done = false
70
+ setTimeout(() => {
71
+ if (done) return
72
+ if (status == 'dead') return
73
+ if (status == 'active') status = 'subbing'
74
+ }, 4000)
75
+
76
+ setTimeout(() => {
77
+ // too long, kill
78
+ if (done) return
79
+ done = true
80
+ if (status == 'dead') return
81
+ status = 'dead' // mark as dead
82
+ onDead('poll')
83
+ out = repeat('dead', events.length)
84
+ resolve(false) // break loop
85
+ }, 60000)
86
+
87
+ var resolve
88
+ var promise = new Promise((rs) => {
89
+ resolve = rs
90
+ })
63
91
  var out = []
64
- return flow
92
+ flow
65
93
  .loop(function () {
66
94
  return new Promise(function (rs) {
95
+ if (status == 'dead') {
96
+ out = repeat('dead', events.length)
97
+ return rs(false) // break loop
98
+ }
99
+
67
100
  var query = '?token=' + lastToken
68
101
  credential.getAccessToken().then(function (access_token) {
102
+ if (status == 'dead') {
103
+ out = repeat('dead', events.length)
104
+ return rs(false) // break loop
105
+ }
106
+
69
107
  if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
70
108
  else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
71
109
  else if (access_token) query += '&access-token=' + access_token
72
110
 
73
- let fullurl = apiUrl + 'subs' + query + '&account_id=' + encodeURIComponent(accid || credential.account_id)
111
+ var fullurl = apiUrl + 'subs' + query + '&account-id=' + encodeURIComponent(accid || credential.account_id)
74
112
  callAPI('post', fullurl, JSON.stringify({events: events}), function (body, code) {
75
- if (dead) {
113
+ if (done) return
114
+ if (status == 'subbing') status = 'active'
115
+
116
+ if (status == 'dead') {
76
117
  out = repeat('dead', events.length)
77
118
  return rs(false) // break loop
78
119
  }
79
120
 
80
- if (retryable(code)) return setTimeout(rs, 3000, true)
121
+ if (retryable(code)) {
122
+ status = 'subbing'
123
+ return setTimeout(rs, 3000, true)
124
+ }
125
+
81
126
  // unretryable error
82
127
  if (code !== 200) {
83
- // we are unable to start the communication with realtime server.
128
+ // we are unable to start the communication with the realtime server.
84
129
  // Must notify the user by killing the connection
85
- dead = true
130
+ status = 'dead'
86
131
  onDead('subscribe', body, code)
87
132
  out = repeat('dead', events.length)
88
133
  return rs(false)
89
134
  }
90
135
 
136
+ done = true
91
137
  if (lastToken) return rs(false)
92
138
 
93
139
  // first time sub, should grab the initial token
94
140
  body = parseJSON(body) || {}
95
141
  var initialToken = body.initial_token
96
- // the server returns a malform payload. We should retry and hope it heal soon
97
- if (!initialToken) return setTimeout(rs, 3000, true)
142
+ // the server returns a malform payload. We should end
143
+ if (!initialToken) {
144
+ status = 'dead'
145
+ onDead('subscribe', body, code)
146
+ out = repeat('dead', events.length)
147
+ return rs(false) // break loop
148
+ }
149
+
98
150
  if (body.host) apiUrl = absUrl(body.host)
99
151
 
100
152
  lastToken = initialToken
@@ -105,8 +157,9 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
105
157
  })
106
158
  })
107
159
  .then(function () {
108
- return out
160
+ resolve(out)
109
161
  })
162
+ return promise
110
163
  })
111
164
 
112
165
  // subscribe call /subs API to tell server that we are listening for those events
@@ -114,6 +167,16 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
114
167
  // Note: this function is not design to be thread safed so, do not call this
115
168
  // function multiple times at once
116
169
  this.subscribe = subQueue.push.bind(subQueue)
170
+
171
+ this.getStatus = function () {
172
+ if (status == 'dead' || status == 'active') return status
173
+ return 'connecting'
174
+ }
175
+
176
+ // kill force the connection to dead state
177
+ this.kill = function () {
178
+ status = 'dead'
179
+ }
117
180
  }
118
181
 
119
182
  // Realtime is just a stronger Conn.
@@ -122,7 +185,7 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
122
185
  // additional features compare to conn:
123
186
  // + auto recreate and resub if the last conn is dead
124
187
  // + don't subscribe already subscribed events
125
- function Realtime(apiUrls, credential, callAPI, accid) {
188
+ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
126
189
  if (typeof apiUrls === 'string' || apiUrls instanceof String) apiUrls = [apiUrls]
127
190
  credential = credential || {}
128
191
  if (!credential.getAccessToken) {
@@ -158,6 +221,11 @@ function Realtime(apiUrls, credential, callAPI, accid) {
158
221
  return pubsub.on('interrupted', cb)
159
222
  }
160
223
 
224
+ this.getStatus = function () {
225
+ if (!conn) return 'dead'
226
+ return conn.getStatus()
227
+ }
228
+
161
229
  var conn
162
230
  this.subscribe = function (events) {
163
231
  if (stop) return Promise.resolve({})
@@ -169,9 +237,13 @@ function Realtime(apiUrls, credential, callAPI, accid) {
169
237
  for (var i = 0; i < events.length; i++) {
170
238
  var topic = events[i]
171
239
  if (!topic) continue
172
- if (!topics[topic]) all.push(conn.subscribe(topic))
240
+ if (!topics[topic]) {
241
+ topics[topic] = true
242
+ all.push(conn.subscribe(topic))
243
+ }
173
244
  }
174
245
  if (all.length === 0) return Promise.resolve({})
246
+
175
247
  return Promise.all(all).then(function (errs) {
176
248
  for (var i = 0; i < errs.length; i++) if (errs[i]) return {error: errs[i]}
177
249
  for (var j = 0; j < events.length; j++) topics[events[j]] = true
@@ -181,24 +253,24 @@ function Realtime(apiUrls, credential, callAPI, accid) {
181
253
 
182
254
  // reconnect make sure there is alway a Conn running in the background
183
255
  // if the Conn is dead, it recreate a new one
184
- var reconnect = function () {
185
- if (stop) return
256
+ this.reconnect = function () {
257
+ if (stop) return Promise.resolve(false)
186
258
  // reset subscribed topic
187
259
  var allTopics = Object.keys(topics)
188
260
  for (var i = 0; i < allTopics.length; i++) topics[allTopics[i]] = false
189
-
190
261
  var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
262
+ if (conn) conn.kill() // kill old connection
191
263
  conn = new Conn(
192
264
  randomUrl,
193
265
  credential,
194
266
  function (code, body, status) {
195
267
  if (stop) return
196
268
  pubsub.emit('interrupted', code, body, status)
197
- setTimeout(reconnect, 2000) // reconnect and resubscribe after 2 sec
269
+ if (!skipautoreconnect) setTimeout(this.reconnect, 2000) // reconnect and resubscribe after 2 sec
198
270
  },
199
- function (topics) {
271
+ function (events) {
200
272
  if (stop) return
201
- for (var i = 0; i < topics.length; i++) pubsub.emit('event', topics[i])
273
+ for (var i = 0; i < events.length; i++) pubsub.emit('event', events[i])
202
274
  },
203
275
  callAPI,
204
276
  accid,
@@ -206,9 +278,9 @@ function Realtime(apiUrls, credential, callAPI, accid) {
206
278
 
207
279
  // resubscribe all subscribed events
208
280
  var topicKeys = Object.keys(topics)
209
- for (var i = 0; i < topicKeys.length; i++) conn.subscribe(topicKeys[i])
281
+ return this.subscribe(topicKeys)
210
282
  }
211
- reconnect()
283
+ this.reconnect()
212
284
  }
213
285
 
214
286
  // calcNextBackoff returns number of seconds we must wait
package/realtime.test.js CHANGED
@@ -1,99 +1,292 @@
1
1
  var test = require('tape')
2
2
  var Realtime = require('./realtime.js')
3
3
 
4
- function makeMockServer(script) {
5
- var i = -1
6
- return (method, url, body, cb) => {
7
- i++
8
- script[i](method, url, body, cb)
4
+ async function testScript(t, script) {
5
+ let events = []
6
+ let calls = []
7
+ var realtime = new Realtime(
8
+ 'https://api5.subiz.com.vn/',
9
+ {user_mask: 'mask123'},
10
+ (method, url, body, cb) => {
11
+ calls.push({method, url, body, cb})
12
+ },
13
+ 'acctest',
14
+ true,
15
+ )
16
+ realtime.onEvent((ev) => events.push(ev))
17
+
18
+ for (let stepi in script.steps) {
19
+ let step = script.steps[stepi]
20
+ if (step.type == 'status') {
21
+ t.equal(realtime.getStatus(), step.status, script.name + '[' + stepi + ']')
22
+ continue
23
+ }
24
+
25
+ if (step.type == 'subscribe') {
26
+ realtime.subscribe(step.topics)
27
+ continue
28
+ }
29
+
30
+ if (step.type == 'reconnect') {
31
+ realtime.reconnect()
32
+ continue
33
+ }
34
+
35
+ if (step.type == 'wait') {
36
+ await sleep(step.timeout)
37
+ continue
38
+ }
39
+
40
+ if (step.type == 'call') {
41
+ t.true(step.call_id != undefined) // must define call id in the script
42
+ let start = Date.now()
43
+ let timeout = step.timeout || 10_000
44
+ let istimeout
45
+ for (;;) {
46
+ if (Date.now() - start > timeout) {
47
+ istimeout = true
48
+ break
49
+ }
50
+ // wait util api is called
51
+ await sleep(100)
52
+ let call
53
+ for (let c = 0; c < calls.length; c++) {
54
+ if (!calls[c].call_id) {
55
+ call = calls[c]
56
+ break
57
+ }
58
+ }
59
+ if (!call) continue
60
+ call.call_id = step.call_id
61
+ const myUrl = new URL(call.url)
62
+
63
+ if (step.path) t.equal(myUrl.pathname, step.path)
64
+
65
+ if (step.method) t.equal(step.method, call.method)
66
+
67
+ if (step.query) {
68
+ for (var key of Object.keys(step.query)) {
69
+ t.equal(step.query[key], myUrl.searchParams.get(key), script.name)
70
+ }
71
+ }
72
+
73
+ if (step.body) {
74
+ let keys = Object.keys(step.body)
75
+ let callbody
76
+ if (call.body) callbody = JSON.parse(call.body)
77
+ for (var key of keys) {
78
+ t.equal(JSON.stringify(step.body[key]), JSON.stringify(callbody[key]))
79
+ }
80
+ }
81
+ break
82
+ }
83
+
84
+ t.true(!istimeout, JSON.stringify(step))
85
+ continue
86
+ }
87
+
88
+ if (step.type == 'response') {
89
+ let call
90
+ for (var c = 0; c < calls.length; c++) {
91
+ if (calls[c].call_id == step.call_id) {
92
+ call = calls[c]
93
+ break
94
+ }
95
+ }
96
+
97
+ t.true(call != undefined)
98
+ call.cb(JSON.stringify(step.body), step.code)
99
+ continue
100
+ }
101
+ t.true(false, script.name + '[' + stepi + ']=' + step.type)
102
+ }
103
+
104
+ // check events
105
+ let scriptevents = script.events || []
106
+ t.equal(events.length, scriptevents.length)
107
+ for (let i in events) {
108
+ t.equal(JSON.stringify(events[i]), JSON.stringify(scriptevents[i]))
9
109
  }
110
+ realtime.stop()
10
111
  }
11
112
 
12
- test('realtime', async (t) => {
13
- var callapi = makeMockServer([
14
- async (method, url, body, cb) => {
15
- // must first call sub and must attach user-mask in the query
16
- t.equal(method, 'post')
17
- t.true(url.indexOf('subs?') !== -1)
18
- t.true(url.indexOf('user-mask=mask123') !== -1)
19
- cb(`{"initial_token": "token0"}`, 200)
113
+ test.only('realtime', async (t) => {
114
+ let scripts = [
115
+ {
116
+ name: 'starting status',
117
+ steps: [
118
+ {type: 'status', status: 'active'},
119
+ {type: 'subscribe', topics: ['message_sent']},
120
+ {type: 'status', status: 'active'},
121
+ {type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
122
+
123
+ {type: 'wait', timeout: 5000},
124
+ {type: 'status', status: 'connecting'},
125
+ {type: 'wait', timeout: 60000},
126
+ {type: 'status', status: 'dead'},
127
+ {type: 'response', call_id: '1', body: {initial_token: 'aaaaaa'}, code: 200},
128
+ {type: 'status', status: 'dead'}, // dead must be dead
129
+ ],
20
130
  },
21
- async (method, url, body, cb) => {
22
- // start the poll and must attach initial token in the query
23
- t.equal(method, 'get')
24
- t.true(url.indexOf('poll?') !== -1)
25
- t.true(url.indexOf('token=token0') !== -1)
26
- cb(`{"events": [], "sequential_token": "token1"}`, 200)
131
+ {
132
+ name: 'long subscribe',
133
+ steps: [
134
+ {type: 'subscribe', topics: ['message_sent']},
135
+ {type: 'status', status: 'active'},
136
+ {type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
137
+ {type: 'wait', timeout: 5000},
138
+ {type: 'status', status: 'connecting'},
139
+ {type: 'wait', timeout: 3000},
140
+ {type: 'status', status: 'connecting'},
141
+ {type: 'response', call_id: '1', body: {initial_token: 'aaaaaa'}, code: 200},
142
+ {type: 'status', status: 'active'},
143
+ ],
144
+ events: [],
27
145
  },
28
- async (method, url, body, cb) => {
29
- // next poll must attach last received token
30
- t.equal(method, 'get')
31
- t.true(url.indexOf('poll?') !== -1)
32
- t.true(url.indexOf('token=token1') !== -1)
33
- realtime.stop()
34
- cb(`{"events": [], "token": "-"}`, 200)
146
+ {
147
+ name: 'double subscribe -> do nothing',
148
+ steps: [
149
+ {type: 'subscribe', topics: ['message_sent']},
150
+ {type: 'status', status: 'active'},
151
+ {type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
152
+ {type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
153
+ {type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
154
+ {type: 'subscribe', topics: ['message_sent', 'message_pong']},
155
+ {type: 'call', call_id: '3', path: '/subs', method: 'post', body: {events: ['message_pong']}},
156
+ ],
35
157
  },
36
- ])
37
-
38
- var realtime = new Realtime('', {user_mask: 'mask123'}, callapi)
39
- var err = await realtime.subscribe(['message_pong'])
40
- t.true(Object.keys(err).length === 0)
41
- })
42
-
43
- test('realtime subs when server down and then backup', async (t) => {
44
- var callapi = makeMockServer([
45
- async (method, url, body, cb) => {
46
- // must first call sub and must attach user-mask in the query
47
- t.equal(method, 'post')
48
- t.true(url.indexOf('subs?') !== -1)
49
- t.true(url.indexOf('user-mask=mask123') !== -1)
50
- cb(`502 too busy`, 502)
158
+ {
159
+ name: 'polling unstable',
160
+ steps: [
161
+ {type: 'subscribe', topics: ['message_sent']},
162
+ {type: 'status', status: 'active'},
163
+ {type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
164
+ {type: 'wait', timeout: 1000},
165
+ {type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
166
+ {type: 'status', status: 'active'},
167
+ {type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
168
+ {type: 'wait', timeout: 30000},
169
+ {type: 'response', call_id: '2', body: 'network disconnected', code: 0},
170
+ {type: 'status', status: 'connecting'},
171
+ {type: 'call', call_id: '3', path: '/poll', query: {token: 'token1'}, method: 'get'},
172
+ {type: 'wait', timeout: 30000},
173
+ {type: 'response', call_id: '3', body: '500 keep internal', code: 500},
174
+ // more than 60 sec without any data => dead
175
+ {type: 'status', status: 'connecting'},
176
+ ],
177
+ },
178
+ {
179
+ name: 'polling dead',
180
+ steps: [
181
+ {type: 'subscribe', topics: ['message_sent']},
182
+ {type: 'status', status: 'active'},
183
+ {type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
184
+ {type: 'wait', timeout: 1000},
185
+ {type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
186
+ {type: 'status', status: 'active'},
187
+ {type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
188
+ {type: 'response', call_id: '2', body: '400 dead', code: 400},
189
+ {type: 'status', status: 'dead'},
190
+ ],
191
+ },
192
+ {
193
+ name: 'basic',
194
+ steps: [
195
+ {type: 'subscribe', topics: ['message_sent']},
196
+ {
197
+ type: 'call',
198
+ call_id: '1',
199
+ path: '/subs',
200
+ query: {'user-mask': 'mask123'},
201
+ method: 'post',
202
+ body: {events: ['message_sent']},
203
+ },
204
+ {type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
205
+ {type: 'wait', timeout: 1000},
206
+ {type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
207
+ {type: 'status', status: 'active'},
208
+ {type: 'wait', timeout: 3000},
209
+ {type: 'response', call_id: '2', body: {events: [{text: 'xin chao'}], sequential_token: 'token2'}, code: 200},
210
+ {type: 'status', status: 'active'},
211
+ {type: 'call', call_id: '3', path: '/poll', query: {token: 'token2'}, method: 'get'},
212
+ ],
213
+ events: [{text: 'xin chao'}],
51
214
  },
52
- async (method, url, body, cb) => {
53
- // should retry
54
- t.equal(method, 'post')
55
- t.true(url.indexOf('subs?') !== -1)
56
- t.true(url.indexOf('user-mask=mask123') !== -1)
57
- cb(`502 still busy`, 502)
215
+ {
216
+ name: 'realtime subs when server down and then backup',
217
+ steps: [
218
+ {type: 'subscribe', topics: ['message_sent']},
219
+ {
220
+ type: 'call',
221
+ call_id: '1',
222
+ path: '/subs',
223
+ method: 'post',
224
+ body: {events: ['message_sent']},
225
+ },
226
+ {type: 'wait', timeout: 1000},
227
+ {type: 'response', call_id: '1', body: '502 too busy', code: 502},
228
+ {
229
+ type: 'call',
230
+ call_id: '1.1',
231
+ path: '/subs',
232
+ method: 'post',
233
+ body: {events: ['message_sent']},
234
+ },
235
+ {type: 'status', status: 'connecting'},
236
+ {type: 'wait', timeout: 1000},
237
+ {type: 'response', call_id: '1.1', body: '502 still busy', code: 502},
238
+ {type: 'status', status: 'connecting'},
239
+ {type: 'wait', timeout: 1000},
240
+ {
241
+ type: 'call',
242
+ call_id: '1.2',
243
+ path: '/subs',
244
+ method: 'post',
245
+ body: {events: ['message_sent']},
246
+ },
247
+ {type: 'status', status: 'connecting'},
248
+ {type: 'response', call_id: '1.2', body: {initial_token: 'token1'}, code: 200},
249
+ {type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
250
+ {type: 'status', status: 'active'},
251
+ {type: 'wait', timeout: 3000},
252
+ {type: 'response', call_id: '2', body: {events: [{text: 'xin chao'}], sequential_token: 'token2'}, code: 200},
253
+ {type: 'status', status: 'active'},
254
+ {type: 'call', call_id: '3', path: '/poll', query: {token: 'token2'}, method: 'get'},
255
+ ],
256
+ events: [{text: 'xin chao'}],
58
257
  },
59
- async (method, url, body, cb) => {
60
- // should retry
61
- t.equal(method, 'post')
62
- t.true(url.indexOf('subs?') !== -1)
63
- t.true(url.indexOf('user-mask=mask123') !== -1)
64
- cb(`{"initial_token": "token0"}`, 200)
258
+ {
259
+ name: 'realtime must stop if there is something wrong with our credential or our code',
260
+ steps: [
261
+ {type: 'subscribe', topics: ['message_sent']},
262
+ {type: 'call', call_id: 1, path: '/subs'},
263
+ {type: 'response', call_id: 1, body: '400 invalid mask', code: 400},
264
+ {type: 'status', status: 'dead'},
265
+ ],
65
266
  },
66
- async (method, url, body, cb) => {
67
- t.equal(method, 'get')
68
- t.true(url.indexOf('poll?') !== -1)
69
- t.true(url.indexOf('token=token0') !== -1)
70
- realtime.stop()
71
- cb(`{"events": [], "token": "-"}`, 200)
267
+ {
268
+ name: 'reconect',
269
+ steps: [
270
+ {type: 'subscribe', topics: ['message_sent']},
271
+ {type: 'call', call_id: 1, path: '/subs'},
272
+ {type: 'response', call_id: 1, body: '400 invalid mask', code: 400},
273
+ {type: 'status', status: 'dead'},
274
+ {type: 'reconnect'},
275
+ {type: 'wait', timeout: 1000},
276
+ {type: 'call', call_id: 2, path: '/subs'},
277
+ {type: 'wait', timeout: 1000},
278
+ {type: 'response', call_id: 2, body: {initial_token: 'token1'}, code: 200},
279
+ {type: 'status', status: 'active'},
280
+ ],
72
281
  },
73
- ])
282
+ ]
74
283
 
75
- var realtime = new Realtime('', {user_mask: 'mask123'}, callapi)
76
- var err = await realtime.subscribe(['message_pong'])
77
- t.true(Object.keys(err).length === 0)
284
+ let ps = []
285
+ for (const script of scripts) {
286
+ // if (script.name != 'double subscribe -> do nothing') continue
287
+ ps.push(testScript(t, script))
288
+ }
289
+ await Promise.all(ps)
78
290
  })
79
291
 
80
- test('realtime must stop if there is something wrong with our credential or our code', async (t) => {
81
- var subcode
82
- var callapi = makeMockServer([
83
- async (method, url, bkody, cb) => {
84
- // must first call sub and must attach user-mask in the query
85
- t.equal(method, 'post')
86
- t.true(url.indexOf('subs?') !== -1)
87
- t.true(url.indexOf('user-mask=mask123') !== -1)
88
- cb(`400 invalid mask`, 400)
89
- realtime.stop()
90
- },
91
- ])
92
-
93
- var realtime = new Realtime('', {user_mask: 'mask123'}, callapi)
94
- realtime.onInterrupted((_, _1, code) => {
95
- subcode = code
96
- })
97
- var err = await realtime.subscribe(['message_pong'])
98
- t.false(Object.keys(err).length === 0)
99
- })
292
+ let sleep = (ms) => new Promise((rs) => setTimeout(rs, ms))
package/demo.html DELETED
@@ -1,129 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link
6
- rel="stylesheet"
7
- href="https://cdn.jsdelivr.net/npm/bootstrap@4.3.1/dist/css/bootstrap.min.css"
8
- />
9
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
- <title>Subiz WebPhone Demo</title>
11
- <style>
12
- #info {
13
- background: whitesmoke;
14
- border-radius: 5px;
15
- }
16
- label {
17
- font-weight: 600;
18
- margin-right: 10px;
19
- }
20
- .info_line {
21
- display: flex;
22
- align-items: flex-start;
23
- }
24
- </style>
25
- </head>
26
- <body class="container pt-3">
27
- <img
28
- width="80"
29
- src="https://storage.googleapis.com/sbz-email/images/d009afe944c1948998544e103ab0fef2_logo.png"
30
- />
31
- <h1 class="mt-4">Subiz call demo</h1>
32
- <button
33
- class="btn btn-primary"
34
- style="width: 120px"
35
- id="btnHangup"
36
- onClick="inputToken()"
37
- >
38
- Nhập token
39
- </button>
40
- <div id="tokenStatus" class="mt-2 text-danger">Chưa nhập token</div>
41
-
42
- <div class="mt-3" style="width: 250px">
43
- <div>
44
- <label>Đầu số</label>
45
- <input
46
- class="form-control"
47
- id="fromNumber"
48
- type="text"
49
- value="842473021368"
50
- />
51
- </div>
52
- <div class="mt-3">
53
- <label>Số khách</label>
54
- <input
55
- class="form-control"
56
- placeholder="036..."
57
- id="toNumber"
58
- type="text"
59
- />
60
- </div>
61
- <div class="mt-3">
62
- <button
63
- class="btn btn-success mr-3"
64
- style="width: 150px"
65
- id="btnCall"
66
- disabled
67
- onClick="makeCall()"
68
- >
69
- Gọi
70
- </button>
71
- <button
72
- class="btn btn-outline-danger"
73
- style="width: 79px"
74
- id="btnHangup"
75
- onClick="hangupCall()"
76
- >
77
- Ngắt
78
- </button>
79
- </div>
80
- </div>
81
- <h3 class="mt-4">Thông tin cuộc gọi</h3>
82
- <div class="p-2" id="info">
83
- <div class="info_line">
84
- <label>Call ID:</label>
85
- <div id="infoCallId"></div>
86
- </div>
87
- <div class="info_line">
88
- <label>Conversation ID:</label>
89
- <div id="infoConversationId"></div>
90
- </div>
91
- <div class="info_line">
92
- <label>Ghi âm:</label>
93
- <a href="#" id="infoRecordedUrl" target="_blank"></a>
94
- </div>
95
- <div class="info_line">
96
- <label>Gọi từ số:</label>
97
- <div id="infoFromNumber"></div>
98
- </div>
99
- <div class="info_line">
100
- <label>Gọi đến số:</label>
101
- <div id="infoToNumber"></div>
102
- </div>
103
- <div class="info_line">
104
- <label>Trạng thái:</label>
105
- <div id="infoStatus"></div>
106
- </div>
107
- <div class="info_line">
108
- <label>Gọi lúc:</label>
109
- <div id="infoCreated"></div>
110
- </div>
111
- <div class="info_line">
112
- <label>Thời lượng (giây):</label>
113
- <div id="infoDuration"></div>
114
- </div>
115
- <div class="info_line">
116
- <label>Mã ngắt máy:</label>
117
- <div id="infoHangupCode"></div>
118
- </div>
119
- </div>
120
- <div class="mt-4">
121
- <h3>Sự kiện realtime</h3>
122
- <pre
123
- style="padding: 10px 15px; border-radius: 5px; background: whitesmoke"
124
- id="infoLog"
125
- ></pre>
126
- </div>
127
- <script type="module" src="./demo.js"></script>
128
- </body>
129
- </html>
package/demo.js DELETED
@@ -1,207 +0,0 @@
1
- const SubizWebPhone = require('./webphone.js')
2
-
3
- let webphone
4
- var mycallid = 'x'
5
- let accid
6
- let eventLogs = {}
7
-
8
- // setTokenStatus renders whether token is valid or invalid
9
- function setTokenStatus(me) {
10
- let $tokenStatus = document.getElementById('tokenStatus')
11
- if (me == 'loading') {
12
- $tokenStatus.innerHTML = 'Đang kiểm tra'
13
- $tokenStatus.classList.add('text-muted')
14
- $tokenStatus.classList.remove('text-danger')
15
- $tokenStatus.classList.remove('text-success')
16
- return
17
- }
18
-
19
- if (!me || !me.account_id) {
20
- $tokenStatus.innerHTML = 'Token rỗng hoặc không hợp lệ. Vui lòng nhập lại'
21
- $tokenStatus.classList.remove('text-danger')
22
- $tokenStatus.classList.remove('text-success')
23
- $tokenStatus.classList.add('text-muted')
24
- return
25
- }
26
-
27
- $tokenStatus.innerHTML = 'Token hợp lệ. Xin chào ' + me.fullname
28
- $tokenStatus.classList.add('text-success')
29
- $tokenStatus.classList.remove('text-danger')
30
- $tokenStatus.classList.remove('text-muted')
31
- }
32
-
33
- function init() {
34
- setTokenStatus('loading')
35
- fetch(`https://api.subiz.com.vn/4.0/me?x-access-token=${access_token}`)
36
- .then((response) => {
37
- return response.json()
38
- })
39
- .then((me) => {
40
- setTokenStatus(me)
41
- if (!me || !me.account_id) return null
42
- return me
43
- })
44
- .then((me) => {
45
- if (!me) return
46
- // save token
47
- try {
48
- webphone = new SubizWebPhone(access_token)
49
- accid = access_token.split('_')[0]
50
- } catch (e) {
51
- alert('ERROR2: ' + e)
52
- // setTimeout(main)
53
- return
54
- }
55
-
56
- document.getElementById('btnCall').disabled = false
57
-
58
- webphone.onEvent(function (ev) {
59
- let call = ev.data.call_info
60
- let callid = call.call_id
61
- eventLogs[callid] = eventLogs[callid] || []
62
- if (callid != mycallid) return
63
- eventLogs[callid].push(ev)
64
-
65
- // render log
66
- let logM = eventLogs[callid]
67
- if (logM) {
68
- let log = logM
69
- .map((ev) => {
70
- return `${new Date(ev.created).toLocaleString()} | ${ev.type} | ${JSON.stringify(ev.data.call_info)}`
71
- })
72
- .join('\n')
73
- document.getElementById('infoLog').innerHTML = log
74
- }
75
-
76
- if (call.status == 'dialing') {
77
- document.getElementById('infoRecordedUrl').innerHTML = ''
78
- document.getElementById('infoRecordedUrl').href = undefined
79
- document.getElementById('info').style['background-color'] = '#fffed3'
80
- }
81
-
82
- if (call.status == 'active') {
83
- document.getElementById('info').style['background-color'] = '#8cff6b'
84
- }
85
-
86
- if (call.status == 'ended' || !call.status) {
87
- document.getElementById('info').style['background-color'] = '#ffc6c6'
88
- }
89
-
90
- if (call.status == 'ended' && ev.id) {
91
- // from server, not fake
92
- document.getElementById('infoRecordedUrl').innerHTML = 'Đang tải...'
93
- document.getElementById('infoRecordedUrl').href = undefined
94
-
95
- fetchRecordedUrl(callid).then((url) => {
96
- if (url == 'unavailable') {
97
- document.getElementById('infoRecordedUrl').innerHTML = 'Không khả dụng'
98
- document.getElementById('infoRecordedUrl').href = undefined
99
- return
100
- }
101
- document.getElementById('infoRecordedUrl').innerHTML = url
102
- document.getElementById('infoRecordedUrl').href = url
103
- })
104
- }
105
- })
106
- })
107
- }
108
-
109
- // inputToken asks access token from the agent
110
- function inputToken() {
111
- access_token = prompt('Vui lòng nhập access token')
112
- localStorage.setItem('access_token', access_token)
113
- if (access_token) init()
114
- }
115
-
116
- // fetchRecordedUrl attemps to get the recorded audio url
117
- // since we dont know when the file is uploaded, we must retry multiple times.
118
- // this code is only for demo purpose
119
- function fetchRecordedUrl(callid) {
120
- // try maximum 4 times
121
- return new Promise((rs) => {
122
- fetchConversation(callid).then((convo) => {
123
- if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
124
- return rs(convo.call.recorded_audio.url)
125
- }
126
- setTimeout(() => {
127
- fetchConversation(callid).then((convo) => {
128
- if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
129
- return rs(convo.call.recorded_audio.url)
130
- }
131
-
132
- setTimeout(() => {
133
- fetchConversation(callid).then((convo) => {
134
- if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
135
- return rs(convo.call.recorded_audio.url)
136
- }
137
-
138
- setTimeout(() => {
139
- fetchConversation(callid).then((convo) => {
140
- if (convo.call.recorded_audio && convo.call.recorded_audio.url) {
141
- return rs(convo.call.recorded_audio.url)
142
- }
143
- rs('unavailable')
144
- })
145
- }, 4000)
146
- })
147
- }, 4000)
148
- })
149
- }, 4000)
150
- })
151
- })
152
- }
153
-
154
- // fetchConversation sends a request API to get the latest conversation
155
- function fetchConversation(callid) {
156
- let call = webphone.getCall(callid)
157
- if (!call) return Promise.resolve(null)
158
- let convoid = call.conversation_id
159
- if (!convoid) return Promise.resolve(null)
160
- return fetch(
161
- `https://api.subiz.com.vn/4.0/accounts/${accid}/conversations/${convoid}?x-access-token=${access_token}`,
162
- ).then((response) => {
163
- return response.json()
164
- })
165
- }
166
-
167
- function makeCall() {
168
- if (!webphone) return
169
- let fromNumber = document.getElementById('fromNumber').value
170
- let toNumber = document.getElementById('toNumber').value
171
- //webphone.makeCall(toNumber, fromNumber);
172
-
173
- webphone.makeCall('0364821895', ['935022139843821727', '842473021368'])
174
- mycallid = webphone.getCurrentCallId()
175
- }
176
-
177
- let access_token = localStorage.getItem('access_token')
178
- if (access_token) init()
179
-
180
- // update loop
181
- setInterval(function () {
182
- if (!webphone) return
183
- let call = webphone.getCall(mycallid)
184
- if (!call) return
185
- document.getElementById('infoCallId').innerHTML = call.call_id
186
- document.getElementById('infoFromNumber').innerHTML = call.from_number
187
- document.getElementById('infoConversationId').innerHTML = call.conversation_id
188
- document.getElementById('infoToNumber').innerHTML = call.to_number
189
- document.getElementById('infoStatus').innerHTML = call.status
190
- document.getElementById('infoCreated').innerHTML = new Date(call.started).toLocaleString()
191
- if (call.status == 'active')
192
- document.getElementById('infoDuration').innerHTML = Math.floor((Date.now() - call.started) / 1000) || 0
193
- else {
194
- document.getElementById('infoDuration').innerHTML = Math.floor((call.ended - call.started) / 1000) || 0
195
- }
196
- if (call.hangup_code) document.getElementById('infoHangupCode').innerHTML = call.hangup_code
197
- }, 500)
198
-
199
- function hangupCall() {
200
- if (!webphone) return
201
- webphone.hangupCall(mycallid)
202
- }
203
-
204
- // expose to window so we can call it in HTML
205
- window.makeCall = makeCall
206
- window.hangupCall = hangupCall
207
- window.inputToken = inputToken