@subiz/wsclient 4.0.35 → 4.0.37

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/realtime.js CHANGED
@@ -1,4 +1,3 @@
1
- var flow = require('@subiz/flow')
2
1
  var {xhrsend, parseJSON, Pubsub} = require('./common.js')
3
2
 
4
3
  // Conn represents a realtime connection between an user and the realtime
@@ -18,145 +17,215 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
18
17
  callAPI = callAPI || xhrsend // allow hook
19
18
  credential = credential || {}
20
19
  var lastToken = ''
20
+ var lastAccessToken = ''
21
21
  if (!accid) accid = ''
22
22
  var is_dead = false // tell if the connection is dead
23
23
  var is_polling_stuck = false // tell if we've having trouble doing /poll
24
24
  var is_sub_stuck = true // tell if we've having trouble doing /subscribe
25
25
 
26
- // The long polling loop runs sequentially, with each poll starting immediately
27
- // after the previous one completes. If an error occurs, the loop will pause
28
- // using an exponential backoff strategy. It will terminate if an unretryable
29
- // error is encountered.
30
- // The first loop begins after the first subscription call completes successfully
31
- var polling = function (backoff) {
32
- if (is_dead) return
33
- callAPI('get', apiUrl + 'poll?token=' + lastToken + '&v=5&account-id=' + accid, undefined, function (body, code) {
34
- if (is_dead) return
35
- if (retryable(code)) {
36
- is_polling_stuck = true
37
- return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
38
- }
39
-
40
- if (code !== 200) {
41
- // unretryable error, should kill
42
- is_polling_stuck = true
43
- return kill('poll')
26
+ // Server holds polls for at most 90s. Allow transport overhead, but recover
27
+ // before a request can hang forever. Custom transports may return an abort handle.
28
+ var POLL_TIMEOUT = 105000
29
+ var timers = new Set()
30
+ var requests = new Set()
31
+ var subscriptions = new Set()
32
+ function later(fn, delay) {
33
+ var timer = setTimeout(function () {
34
+ timers.delete(timer)
35
+ if (!is_dead) fn()
36
+ }, delay)
37
+ timers.add(timer)
38
+ return timer
39
+ }
40
+ function cancelTimer(timer) {
41
+ clearTimeout(timer)
42
+ timers.delete(timer)
43
+ }
44
+ function request(method, url, body, timeout, cb) {
45
+ var settled = false
46
+ var transport
47
+ function abort() {
48
+ try {
49
+ if (typeof transport === 'function') transport()
50
+ else if (transport && typeof transport.abort === 'function') transport.abort()
51
+ } catch (err) {
52
+ reportCallbackError(err)
44
53
  }
54
+ }
55
+ function cancel() {
56
+ if (settled) return
57
+ settled = true
58
+ cancelTimer(timer)
59
+ requests.delete(cancel)
60
+ abort()
61
+ }
62
+ function complete(body, code) {
63
+ if (settled || is_dead) return
64
+ settled = true
65
+ cancelTimer(timer)
66
+ requests.delete(cancel)
67
+ cb(body, code)
68
+ }
69
+ requests.add(cancel)
70
+ var timer = later(function () {
71
+ cancel()
72
+ if (!is_dead) cb(undefined, -1)
73
+ }, timeout)
74
+ try {
75
+ transport = callAPI(method, url, body, complete)
76
+ } catch (err) {
77
+ complete(undefined, -1)
78
+ }
79
+ // A synchronous custom transport can stop the connection before returning.
80
+ if (is_dead) abort()
81
+ }
45
82
 
46
- // 200, success
47
- is_polling_stuck = false
48
- body = parseJSON(body) || {}
49
- if (body.host) apiUrl = absUrl(body.host)
83
+ var MIN_POLL_INTERVAL = 250
84
+ var polling_started = false
85
+ function startPolling() {
86
+ if (polling_started) return
87
+ polling_started = true
88
+ polling(0)
89
+ }
90
+ function polling(backoff) {
91
+ if (is_dead) return
92
+ var started = Date.now()
93
+ request(
94
+ 'get',
95
+ apiUrl + 'poll?token=' + encodeURIComponent(lastToken) + '&v=5&account-id=' + encodeURIComponent(accid),
96
+ undefined,
97
+ POLL_TIMEOUT,
98
+ function (body, code) {
99
+ var wait = Math.max(0, MIN_POLL_INTERVAL - (Date.now() - started))
100
+ if (retryable(code)) {
101
+ is_polling_stuck = true
102
+ return later(
103
+ function () {
104
+ polling(backoff + 1)
105
+ },
106
+ Math.max(calcNextBackoff(backoff), wait),
107
+ )
108
+ }
109
+ if (code !== 200) {
110
+ is_polling_stuck = true
111
+ return kill('poll')
112
+ }
113
+ body = parseJSON(body) || {}
114
+ if (!body.sequential_token) return kill('poll')
115
+ is_polling_stuck = false
116
+ if (body.host) apiUrl = absUrl(body.host)
117
+ lastToken = body.sequential_token
118
+ onEvents(body.events || [])
119
+ if (!is_dead)
120
+ later(function () {
121
+ polling(0)
122
+ }, wait)
123
+ },
124
+ )
125
+ }
50
126
 
51
- var seqToken = body.sequential_token
52
- // the server returns a malform payload. We should end
53
- if (!seqToken) return kill('poll')
54
- lastToken = seqToken
55
- onEvents(body.events || [])
56
- return polling(0)
127
+ var pendingTopics = []
128
+ var batchTimer
129
+ var batchRunning = false
130
+ function scheduleBatch() {
131
+ if (is_dead || batchRunning || !pendingTopics.length) return
132
+ cancelTimer(batchTimer)
133
+ // Flush full queues early; smaller batches wait from their oldest topic.
134
+ var delay = pendingTopics.length > 50 ? 0 : Math.max(0, 100 - (Date.now() - pendingTopics[0].created))
135
+ batchTimer = later(flushBatch, delay)
136
+ }
137
+ function flushBatch() {
138
+ batchTimer = undefined
139
+ if (is_dead || batchRunning || !pendingTopics.length) return
140
+ batchRunning = true
141
+ var batch = pendingTopics.splice(0, 50)
142
+ function finish(results) {
143
+ batch.forEach(function (item, i) { item.resolve(results[i]) })
144
+ batchRunning = false
145
+ scheduleBatch()
146
+ }
147
+ subscribeBatch(batch.map(function (item) { return item.topic })).then(finish, function (err) {
148
+ reportCallbackError(err)
149
+ kill('subs')
150
+ finish(repeat('dead', batch.length))
57
151
  })
58
152
  }
59
-
60
- var subQueue = flow.batch(50, 100, function (topics) {
153
+ function subscribeBatch(topics) {
61
154
  if (is_dead) return Promise.resolve(repeat('dead', topics.length))
62
- if (topics.length <= 0) return Promise.resolve([])
63
-
64
- var resolve
65
- var promise = new Promise((rs) => {
66
- resolve = rs
67
- })
68
-
69
- var done = false
70
- setTimeout(function () {
71
- if (!done && !is_dead) is_sub_stuck = true
72
- }, 4000)
73
-
74
- setTimeout(() => {
75
- // too long, kill
76
- if (done) return
77
- done = true
78
- is_sub_stuck = true
79
- resolve(repeat('dead', topics.length))
80
- if (is_dead) return
81
- kill('subs')
82
- }, 60000)
83
-
84
- flow.loop(function () {
85
- return new Promise(function (rs) {
86
- if (is_dead) {
87
- resolve(repeat('dead', topics.length))
88
- return rs(false) // break loop
89
- }
90
-
91
- var query = '?token=' + lastToken
92
- credential.getAccessToken().then(function (access_token) {
93
- if (is_dead) {
94
- resolve(repeat('dead', topics.length))
95
- return rs(false) // break loop
96
- }
97
-
98
- if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
99
- else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
100
- else if (access_token) query += '&access-token=' + access_token
101
-
102
- var fullurl =
103
- apiUrl + 'subs' + query + '&v=5&account-id=' + encodeURIComponent(accid || credential.account_id)
104
- callAPI('post', fullurl, JSON.stringify({events: topics}), function (body, code) {
105
- if (done) return
106
- if (is_dead) {
107
- resolve(repeat('dead', topics.length))
108
- return rs(false) // break loop
109
- }
110
-
111
- if (retryable(code)) {
112
- is_sub_stuck = true
113
- return setTimeout(rs, 3000, true)
114
- }
115
-
116
- // unretryable error
117
- if (code !== 200) {
118
- // we are unable to start the communication with the realtime server.
119
- // Must notify the user by killing the connection
120
- kill('subs')
121
- resolve(repeat('dead', topics.length))
122
- return rs(false)
123
- }
124
- is_sub_stuck = false
125
- done = true
126
- if (lastToken) {
127
- resolve(topics)
128
- return rs(false)
129
- }
130
-
131
- // first time sub, should grab the initial token
132
- body = parseJSON(body) || {}
133
- var initialToken = body.initial_token
134
- // the server returns a malform payload. We should end
135
- if (!initialToken) {
136
- kill('subs')
137
- resolve(repeat('dead', topics.length))
138
- return rs(false) // break loop
139
- }
140
-
141
- // learn new host
142
- if (body.host) apiUrl = absUrl(body.host)
143
-
144
- lastToken = initialToken
145
- polling(0)
146
- resolve(topics)
147
- return rs(false) // to break the loop
155
+ if (!topics.length) return Promise.resolve([])
156
+ return new Promise(function (resolve) {
157
+ var done = false
158
+ function finish(result) {
159
+ if (done) return
160
+ done = true
161
+ cancelTimer(stuckTimer)
162
+ cancelTimer(deadline)
163
+ cancelTimer(retryTimer)
164
+ subscriptions.delete(cancel)
165
+ resolve(result)
166
+ }
167
+ function cancel() {
168
+ finish(repeat('dead', topics.length))
169
+ }
170
+ subscriptions.add(cancel)
171
+ var stuckTimer = later(function () {
172
+ is_sub_stuck = true
173
+ }, 4000)
174
+ var deadline = later(function () {
175
+ is_sub_stuck = true
176
+ kill('subs')
177
+ }, 60000)
178
+ var retryTimer
179
+ function retry() {
180
+ if (done || is_dead) return
181
+ is_sub_stuck = true
182
+ retryTimer = later(attempt, 3000)
183
+ }
184
+ function attempt() {
185
+ if (done || is_dead) return
186
+ Promise.resolve()
187
+ .then(function () {
188
+ return credential.getAccessToken()
148
189
  })
149
- })
150
- })
190
+ .then(function (accessToken) {
191
+ if (done || is_dead) return
192
+ lastAccessToken = accessToken || ''
193
+ var query = '?token=' + encodeURIComponent(lastToken)
194
+ if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
195
+ else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
196
+ else if (accessToken) query += '&access-token=' + encodeURIComponent(accessToken)
197
+ var url = apiUrl + 'subs' + query + '&v=5&account-id=' + encodeURIComponent(accid || credential.account_id)
198
+ request('post', url, JSON.stringify({events: topics}), 60000, function (body, code) {
199
+ if (done || is_dead) return
200
+ if (retryable(code)) return retry()
201
+ if (code !== 200) return kill('subs')
202
+ if (!lastToken) {
203
+ body = parseJSON(body) || {}
204
+ if (!body.initial_token) return kill('subs')
205
+ if (body.host) apiUrl = absUrl(body.host)
206
+ lastToken = body.initial_token
207
+ }
208
+ is_sub_stuck = false
209
+ finish(topics)
210
+ startPolling()
211
+ })
212
+ })
213
+ .catch(retry)
214
+ }
215
+ attempt()
151
216
  })
152
-
153
- return promise
154
- })
217
+ }
155
218
 
156
219
  // subscribe call /subs API to tell server that we are listening for those events
157
220
  // this function may start polling loop if it hasn't been started yet
158
221
  // its safe to call this function multiple times
159
- this.subscribe = subQueue.push.bind(subQueue)
222
+ this.subscribe = function (topic) {
223
+ if (is_dead) return Promise.resolve('dead')
224
+ return new Promise(function (resolve) {
225
+ pendingTopics.push({topic, resolve, created: Date.now()})
226
+ scheduleBatch()
227
+ })
228
+ }
160
229
 
161
230
  this.getStatus = function () {
162
231
  if (is_dead) return 'dead'
@@ -164,9 +233,38 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
164
233
  return 'active'
165
234
  }
166
235
 
236
+ // Page exit cannot wait for credential refresh or a normal XHR response.
237
+ this.cleanup = function (browserWindow) {
238
+ if (is_dead || !lastToken) return
239
+ var url = apiUrl + 'cleanup?token=' + encodeURIComponent(lastToken) + '&account-id=' + encodeURIComponent(accid)
240
+ // Also support hosts where /rt goes through the standard API auth wrapper.
241
+ if (credential.user_ref) url += '&user_ref=' + encodeURIComponent(credential.user_ref)
242
+ else if (credential.user_mask) url += '&user-mask=' + encodeURIComponent(credential.user_mask)
243
+ else if (lastAccessToken) url += '&access-token=' + encodeURIComponent(lastAccessToken)
244
+ try {
245
+ if (browserWindow.navigator && typeof browserWindow.navigator.sendBeacon === 'function') {
246
+ if (browserWindow.navigator.sendBeacon(url)) return
247
+ }
248
+ } catch (e) {}
249
+ try {
250
+ if (typeof browserWindow.fetch === 'function') {
251
+ Promise.resolve(browserWindow.fetch(url, {method: 'POST', keepalive: true, credentials: 'include'})).catch(
252
+ function () {},
253
+ )
254
+ }
255
+ } catch (e) {}
256
+ }
257
+
167
258
  // kill force the connection to dead state
168
259
  var kill = function (reason) {
260
+ if (is_dead) return
169
261
  is_dead = true
262
+ for (var timer of timers) clearTimeout(timer)
263
+ timers.clear()
264
+ for (var cancel of requests) cancel()
265
+ for (var finish of subscriptions) finish()
266
+ for (var item of pendingTopics) item.resolve('dead')
267
+ pendingTopics = []
170
268
  onDead(reason)
171
269
  }
172
270
  this.kill = kill
@@ -184,16 +282,47 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
184
282
  }
185
283
  }
186
284
  if (!accid) accid = credential.account_id || ''
187
- var pubsub = new Pubsub()
285
+ var pubsub = new Pubsub(reportCallbackError)
286
+ var reconnectTimer
287
+ var stopped = false
288
+ var leavingPage = false
289
+ var browserWindow =
290
+ typeof window !== 'undefined' &&
291
+ window.document &&
292
+ typeof window.addEventListener === 'function' &&
293
+ typeof window.removeEventListener === 'function'
294
+ ? window
295
+ : undefined
296
+
297
+ var onPageHide = function (event) {
298
+ // A page saved in the back/forward cache may resume this connection.
299
+ if (event.persisted || leavingPage) return
300
+ leavingPage = true
301
+ clearTimeout(reconnectTimer)
302
+ reconnectTimer = undefined
303
+ browserWindow.removeEventListener('pagehide', onPageHide)
304
+ if (!conn) return
305
+ var oldconn = conn
306
+ conn = undefined
307
+ oldconn.cleanup(browserWindow)
308
+ oldconn.kill('pagehide')
309
+ }
188
310
 
189
311
  // holds all topics that realtime must subscribed
190
312
  var topics = {}
191
313
 
192
314
  // stop the connection
193
315
  this.stop = function () {
316
+ stopped = true
317
+ clearTimeout(reconnectTimer)
318
+ reconnectTimer = undefined
319
+ if (browserWindow) browserWindow.removeEventListener('pagehide', onPageHide)
194
320
  if (!conn) return
195
- conn.kill()
321
+ // detach before killing so onDead knows this is intentional and
322
+ // doesn't schedule a reconnect
323
+ var oldconn = conn
196
324
  conn = undefined
325
+ oldconn.kill()
197
326
  pubsub.emit('interrupted')
198
327
  }
199
328
 
@@ -236,8 +365,20 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
236
365
  // if the Conn is dead, it recreate a new one
237
366
  var conn
238
367
  var reconnect = function () {
368
+ if (leavingPage) return Promise.resolve({error: 'dead'})
369
+ stopped = false
370
+ clearTimeout(reconnectTimer)
371
+ reconnectTimer = undefined
372
+ if (browserWindow) browserWindow.addEventListener('pagehide', onPageHide)
239
373
  var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
240
- if (conn) conn.kill() // kill old connection
374
+ if (conn) {
375
+ // detach before killing so the old conn's onDead doesn't
376
+ // schedule another reconnect (which would kill this new conn,
377
+ // schedule another one, and churn forever)
378
+ var oldconn = conn
379
+ conn = undefined
380
+ oldconn.kill()
381
+ }
241
382
  var myconn = new Conn(
242
383
  randomUrl,
243
384
  credential,
@@ -245,7 +386,7 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
245
386
  if (conn === undefined || myconn != conn) return // outdated
246
387
  conn = undefined // prevent dupplicate dead fire
247
388
  pubsub.emit('interrupted', code, body, status)
248
- if (!skipautoreconnect) setTimeout(reconnect, 2000) // reconnect and resubscribe after 2 sec
389
+ if (!skipautoreconnect && !stopped && !leavingPage) reconnectTimer = setTimeout(reconnect, 2000)
249
390
  },
250
391
  function (events) {
251
392
  if (myconn != conn) return
@@ -298,4 +439,10 @@ function absUrl(url) {
298
439
  return url
299
440
  }
300
441
 
442
+ function reportCallbackError(err) {
443
+ try {
444
+ if (typeof console !== 'undefined' && console.error) console.error('Realtime callback failed:', err)
445
+ } catch (ignored) {}
446
+ }
447
+
301
448
  module.exports = Realtime
package/webphone.js CHANGED
@@ -1,9 +1,9 @@
1
- var {xhrsend, parseJSON, Pubsub, randomString} = require('./common.js')
1
+ var {xhrsend, parseJSON, Pubsub, randomString, reportCallbackError} = require('./common.js')
2
2
  const WebRTCConn = require('./webrtc.js')
3
3
  var Realtime = require('./realtime.js')
4
4
 
5
5
  function WebPhone(access_token, realtime) {
6
- var pubsub = new Pubsub()
6
+ var pubsub = new Pubsub(reportCallbackError)
7
7
 
8
8
  var $audio = null
9
9
  // chuẩn bị thẻ audio để nghe được
@@ -28,13 +28,18 @@ function WebPhone(access_token, realtime) {
28
28
  if (!agid.startsWith('ag')) throw 'invalid_access_token'
29
29
 
30
30
  if (!realtime)
31
- realtime = new Realtime('https://api.subiz.com.vn/rt/0/', {
32
- getAccessToken: function () {
33
- return new Promise(function (rs) {
34
- rs(access_token)
35
- })
31
+ realtime = new Realtime(
32
+ 'https://api.subiz.com.vn/rt/0/',
33
+ {
34
+ getAccessToken: function () {
35
+ return new Promise(function (rs) {
36
+ rs(access_token)
37
+ })
38
+ },
36
39
  },
37
- }, undefined, accid)
40
+ undefined,
41
+ accid,
42
+ )
38
43
 
39
44
  var webrtcconn = new WebRTCConn({
40
45
  accid: accid,
@@ -52,6 +57,7 @@ function WebPhone(access_token, realtime) {
52
57
  },
53
58
  onTrack: function (event) {
54
59
  if (event.track.kind != 'audio') return
60
+ if (!$audio) return
55
61
  $audio.srcObject = event.streams[0]
56
62
  },
57
63
  })
@@ -100,8 +106,8 @@ function WebPhone(access_token, realtime) {
100
106
 
101
107
  var me = this
102
108
  this.isMicAllowed = function () {
103
- return me.checkMic().then(function (out) {
104
- return out.result
109
+ return me.checkMic().result.then(function (stream) {
110
+ return !!stream
105
111
  })
106
112
  }
107
113
 
@@ -117,7 +123,7 @@ function WebPhone(access_token, realtime) {
117
123
  var microPermission = getMicroPermissions()
118
124
  return {
119
125
  timeout: Promise.race([timeout, microPermission]),
120
- result: getMicroPermissions(),
126
+ result: microPermission,
121
127
  }
122
128
  }
123
129
 
@@ -138,14 +144,9 @@ function WebPhone(access_token, realtime) {
138
144
  }
139
145
 
140
146
  this.answerWebCall = function (callid) {
141
- return new Promise(function (rs) {
142
- getMicroPermissions().then(function (stream) {
143
- current_call_id = callid
144
- webrtcconn.answerCall(callid, stream).then(function (out) {
145
- if (out.error) return rs({error})
146
- rs(body)
147
- })
148
- })
147
+ current_call_id = callid
148
+ return webrtcconn.answerCall(callid, getMicroPermissions()).then(function (out) {
149
+ return out.error ? {error: out.error} : out.body
149
150
  })
150
151
  }
151
152
 
@@ -158,12 +159,13 @@ function WebPhone(access_token, realtime) {
158
159
  // makeCall(number, ['a','b','c'], streamPm) // fallback specific number
159
160
  // makeCall(number, function() {}, streamPm) //
160
161
  this.makeCall = function (number, fromnumbers, streamPm, _a, _b, _state) {
162
+ var ownsSource = !_state
161
163
  if (!_state) _state = {}
162
164
  if (!fromnumbers || (Array.isArray(fromnumbers) && fromnumbers.length == 0)) {
163
165
  fromnumbers = _numbers
164
166
  }
165
167
 
166
- if (typeof fromnumbers === 'string' || fromnumbers instanceof String || typeof value === 'number') {
168
+ if (typeof fromnumbers === 'string' || fromnumbers instanceof String || typeof fromnumbers === 'number') {
167
169
  fromnumbers = [fromnumbers + '']
168
170
  }
169
171
 
@@ -172,11 +174,9 @@ function WebPhone(access_token, realtime) {
172
174
  callid += `-${randomString(4, base)}-${randomString(12, base)}`
173
175
  current_call_id = callid
174
176
 
175
- if (!streamPm) streamPm = getMicroPermissions()
176
-
177
177
  let callnumber = chooseCallNumber(fromnumbers, _state.called)
178
178
  if (!callnumber)
179
- return (
179
+ return Promise.resolve(
180
180
  _state.last_result || {
181
181
  start: Date.now(),
182
182
  ended: Date.now(),
@@ -187,9 +187,13 @@ function WebPhone(access_token, realtime) {
187
187
  id: callid,
188
188
  hangup_code: 'failure',
189
189
  status: 'ended',
190
- }
190
+ },
191
191
  )
192
- return webrtcconn.makeCall(number, callnumber, streamPm, callid).then((out) => {
192
+ // Each attempt owns its clone. The source stays live across number fallback
193
+ // and is released once the complete dialing sequence finishes.
194
+ if (!_state.source) _state.source = Promise.resolve(streamPm || getMicroPermissions())
195
+ var attemptStream = _state.source.then((stream) => stream && stream.clone())
196
+ var result = webrtcconn.makeCall(number, callnumber, attemptStream, callid).then((out) => {
193
197
  if (out.error) return out
194
198
  if (out.hangup_code == 'cancel') return out
195
199
  if (out.answered > 0) return out
@@ -198,10 +202,18 @@ function WebPhone(access_token, realtime) {
198
202
  if (!_state.called) _state.called = {}
199
203
  _state.called[callnumber] = true
200
204
  _state.last_result = out
201
- return this.makeCall(number, fromnumbers, streamPm, _a, _b, _state)
205
+ return me.makeCall(number, fromnumbers, streamPm, _a, _b, _state)
202
206
  }
203
207
  return out
204
208
  })
209
+ if (!ownsSource) return result
210
+ return result.finally(() => {
211
+ _state.source
212
+ .then((stream) => {
213
+ if (stream) stream.getTracks().forEach((track) => track.stop())
214
+ }, reportCallbackError)
215
+ .catch(reportCallbackError)
216
+ })
205
217
  }
206
218
 
207
219
  var current_call_id = ''