@subiz/wsclient 4.0.36 → 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/README.md +363 -92
- package/common.js +54 -53
- package/package.json +11 -10
- package/realtime.js +249 -133
- package/webphone.js +31 -20
- package/webrtc.js +340 -277
- package/.claude/settings.local.json +0 -12
- package/.prettierrc.js +0 -19
- package/realtime.test.js +0 -338
- package/vite.config.js +0 -16
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@subiz/wsclient",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.37",
|
|
4
4
|
"main": "realtime.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"realtime.js",
|
|
7
|
+
"webphone.js",
|
|
8
|
+
"webrtc.js",
|
|
9
|
+
"common.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
5
12
|
"scripts": {
|
|
6
|
-
"
|
|
7
|
-
"test": "node realtime.test.js"
|
|
13
|
+
"test": "node --test realtime.lifecycle.test.js common.transport.test.js webrtc.lifecycle.test.js && node realtime.test.js"
|
|
8
14
|
},
|
|
9
15
|
"repository": {
|
|
10
16
|
"type": "git",
|
|
@@ -13,13 +19,8 @@
|
|
|
13
19
|
"author": "Thanh Pham <thanhpham@vietnambiz.com>",
|
|
14
20
|
"license": "MIT",
|
|
15
21
|
"homepage": "https://github.com/subiz/wsclient#readme",
|
|
16
|
-
"description": "",
|
|
22
|
+
"description": "Subiz realtime subscriptions over HTTP long polling and browser WebRTC calling.",
|
|
17
23
|
"devDependencies": {
|
|
18
|
-
"tape": "^5.10.
|
|
19
|
-
"vite": "^8.2.1",
|
|
20
|
-
"vite-plugin-commonjs": "^0.10.4"
|
|
21
|
-
},
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@subiz/flow": "^1.1.0"
|
|
24
|
+
"tape": "^5.10.3"
|
|
24
25
|
}
|
|
25
26
|
}
|
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,166 +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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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)
|
|
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
|
+
}
|
|
82
|
+
|
|
83
|
+
var MIN_POLL_INTERVAL = 250
|
|
84
|
+
var polling_started = false
|
|
85
|
+
function startPolling() {
|
|
35
86
|
if (polling_started) return
|
|
36
87
|
polling_started = true
|
|
37
88
|
polling(0)
|
|
38
89
|
}
|
|
39
|
-
|
|
90
|
+
function polling(backoff) {
|
|
40
91
|
if (is_dead) return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
}
|
|
61
126
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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))
|
|
69
151
|
})
|
|
70
152
|
}
|
|
71
|
-
|
|
72
|
-
var subQueue = flow.batch(50, 100, function (topics) {
|
|
153
|
+
function subscribeBatch(topics) {
|
|
73
154
|
if (is_dead) return Promise.resolve(repeat('dead', topics.length))
|
|
74
|
-
if (topics.length
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
104
186
|
Promise.resolve()
|
|
105
187
|
.then(function () {
|
|
106
188
|
return credential.getAccessToken()
|
|
107
189
|
})
|
|
108
|
-
.then(function (
|
|
109
|
-
if (is_dead)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
190
|
+
.then(function (accessToken) {
|
|
191
|
+
if (done || is_dead) return
|
|
192
|
+
lastAccessToken = accessToken || ''
|
|
193
|
+
var query = '?token=' + encodeURIComponent(lastToken)
|
|
114
194
|
if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
|
|
115
195
|
else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
|
|
116
|
-
else if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (retryable(code)) {
|
|
128
|
-
is_sub_stuck = true
|
|
129
|
-
return setTimeout(rs, 3000, true)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// unretryable error
|
|
133
|
-
if (code !== 200) {
|
|
134
|
-
// we are unable to start the communication with the realtime server.
|
|
135
|
-
// Must notify the user by killing the connection
|
|
136
|
-
kill('subs')
|
|
137
|
-
resolve(repeat('dead', topics.length))
|
|
138
|
-
return rs(false)
|
|
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
|
|
139
207
|
}
|
|
140
208
|
is_sub_stuck = false
|
|
141
|
-
|
|
142
|
-
if (lastToken) {
|
|
143
|
-
resolve(topics)
|
|
144
|
-
return rs(false)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// first time sub, should grab the initial token
|
|
148
|
-
body = parseJSON(body) || {}
|
|
149
|
-
var initialToken = body.initial_token
|
|
150
|
-
// the server returns a malform payload. We should end
|
|
151
|
-
if (!initialToken) {
|
|
152
|
-
kill('subs')
|
|
153
|
-
resolve(repeat('dead', topics.length))
|
|
154
|
-
return rs(false) // break loop
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// learn new host
|
|
158
|
-
if (body.host) apiUrl = absUrl(body.host)
|
|
159
|
-
|
|
160
|
-
lastToken = initialToken
|
|
209
|
+
finish(topics)
|
|
161
210
|
startPolling()
|
|
162
|
-
resolve(topics)
|
|
163
|
-
return rs(false) // to break the loop
|
|
164
211
|
})
|
|
165
212
|
})
|
|
166
|
-
.catch(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
setTimeout(rs, 3000, true)
|
|
170
|
-
})
|
|
171
|
-
})
|
|
213
|
+
.catch(retry)
|
|
214
|
+
}
|
|
215
|
+
attempt()
|
|
172
216
|
})
|
|
173
|
-
|
|
174
|
-
return promise
|
|
175
|
-
})
|
|
217
|
+
}
|
|
176
218
|
|
|
177
219
|
// subscribe call /subs API to tell server that we are listening for those events
|
|
178
220
|
// this function may start polling loop if it hasn't been started yet
|
|
179
221
|
// its safe to call this function multiple times
|
|
180
|
-
this.subscribe =
|
|
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
|
+
}
|
|
181
229
|
|
|
182
230
|
this.getStatus = function () {
|
|
183
231
|
if (is_dead) return 'dead'
|
|
@@ -185,9 +233,38 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
|
185
233
|
return 'active'
|
|
186
234
|
}
|
|
187
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
|
+
|
|
188
258
|
// kill force the connection to dead state
|
|
189
259
|
var kill = function (reason) {
|
|
260
|
+
if (is_dead) return
|
|
190
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 = []
|
|
191
268
|
onDead(reason)
|
|
192
269
|
}
|
|
193
270
|
this.kill = kill
|
|
@@ -205,13 +282,41 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
205
282
|
}
|
|
206
283
|
}
|
|
207
284
|
if (!accid) accid = credential.account_id || ''
|
|
208
|
-
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
|
+
}
|
|
209
310
|
|
|
210
311
|
// holds all topics that realtime must subscribed
|
|
211
312
|
var topics = {}
|
|
212
313
|
|
|
213
314
|
// stop the connection
|
|
214
315
|
this.stop = function () {
|
|
316
|
+
stopped = true
|
|
317
|
+
clearTimeout(reconnectTimer)
|
|
318
|
+
reconnectTimer = undefined
|
|
319
|
+
if (browserWindow) browserWindow.removeEventListener('pagehide', onPageHide)
|
|
215
320
|
if (!conn) return
|
|
216
321
|
// detach before killing so onDead knows this is intentional and
|
|
217
322
|
// doesn't schedule a reconnect
|
|
@@ -260,6 +365,11 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
260
365
|
// if the Conn is dead, it recreate a new one
|
|
261
366
|
var conn
|
|
262
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)
|
|
263
373
|
var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
|
|
264
374
|
if (conn) {
|
|
265
375
|
// detach before killing so the old conn's onDead doesn't
|
|
@@ -276,7 +386,7 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
276
386
|
if (conn === undefined || myconn != conn) return // outdated
|
|
277
387
|
conn = undefined // prevent dupplicate dead fire
|
|
278
388
|
pubsub.emit('interrupted', code, body, status)
|
|
279
|
-
if (!skipautoreconnect) setTimeout(reconnect, 2000)
|
|
389
|
+
if (!skipautoreconnect && !stopped && !leavingPage) reconnectTimer = setTimeout(reconnect, 2000)
|
|
280
390
|
},
|
|
281
391
|
function (events) {
|
|
282
392
|
if (myconn != conn) return
|
|
@@ -329,4 +439,10 @@ function absUrl(url) {
|
|
|
329
439
|
return url
|
|
330
440
|
}
|
|
331
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
|
+
|
|
332
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(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
+
undefined,
|
|
41
|
+
accid,
|
|
42
|
+
)
|
|
38
43
|
|
|
39
44
|
var webrtcconn = new WebRTCConn({
|
|
40
45
|
accid: accid,
|
|
@@ -139,14 +144,9 @@ function WebPhone(access_token, realtime) {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
this.answerWebCall = function (callid) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
webrtcconn.answerCall(callid, stream).then(function (out) {
|
|
146
|
-
if (out.error) return rs({error: out.error})
|
|
147
|
-
rs(out.body)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
147
|
+
current_call_id = callid
|
|
148
|
+
return webrtcconn.answerCall(callid, getMicroPermissions()).then(function (out) {
|
|
149
|
+
return out.error ? {error: out.error} : out.body
|
|
150
150
|
})
|
|
151
151
|
}
|
|
152
152
|
|
|
@@ -159,6 +159,7 @@ function WebPhone(access_token, realtime) {
|
|
|
159
159
|
// makeCall(number, ['a','b','c'], streamPm) // fallback specific number
|
|
160
160
|
// makeCall(number, function() {}, streamPm) //
|
|
161
161
|
this.makeCall = function (number, fromnumbers, streamPm, _a, _b, _state) {
|
|
162
|
+
var ownsSource = !_state
|
|
162
163
|
if (!_state) _state = {}
|
|
163
164
|
if (!fromnumbers || (Array.isArray(fromnumbers) && fromnumbers.length == 0)) {
|
|
164
165
|
fromnumbers = _numbers
|
|
@@ -173,8 +174,6 @@ function WebPhone(access_token, realtime) {
|
|
|
173
174
|
callid += `-${randomString(4, base)}-${randomString(12, base)}`
|
|
174
175
|
current_call_id = callid
|
|
175
176
|
|
|
176
|
-
if (!streamPm) streamPm = getMicroPermissions()
|
|
177
|
-
|
|
178
177
|
let callnumber = chooseCallNumber(fromnumbers, _state.called)
|
|
179
178
|
if (!callnumber)
|
|
180
179
|
return Promise.resolve(
|
|
@@ -188,9 +187,13 @@ function WebPhone(access_token, realtime) {
|
|
|
188
187
|
id: callid,
|
|
189
188
|
hangup_code: 'failure',
|
|
190
189
|
status: 'ended',
|
|
191
|
-
}
|
|
190
|
+
},
|
|
192
191
|
)
|
|
193
|
-
|
|
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) => {
|
|
194
197
|
if (out.error) return out
|
|
195
198
|
if (out.hangup_code == 'cancel') return out
|
|
196
199
|
if (out.answered > 0) return out
|
|
@@ -203,6 +206,14 @@ function WebPhone(access_token, realtime) {
|
|
|
203
206
|
}
|
|
204
207
|
return out
|
|
205
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
|
+
})
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
var current_call_id = ''
|