@subiz/wsclient 4.0.30 → 4.0.31
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 +13 -10
- package/common.js +2 -2
- package/package.json +1 -1
- package/realtime.js +109 -130
- package/realtime.test.js +1 -2
package/README.md
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
# Subiz
|
|
1
|
+
# Subiz wsclient
|
|
2
2
|
|
|
3
3
|
This library implements subiz long polling protocol
|
|
4
4
|
|
|
5
5
|
## Method
|
|
6
|
+
|
|
6
7
|
### subscribe
|
|
7
|
-
|
|
8
|
-
###
|
|
8
|
+
|
|
9
|
+
### onEvent
|
|
10
|
+
|
|
9
11
|
### stop
|
|
10
12
|
|
|
11
13
|
## Example
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
```js
|
|
13
16
|
var Realtime = require('@subiz/wsclient')
|
|
14
17
|
var realtime = new Realtime('https://realtime-0.subiz.net/', {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
account_id: 'ac1234',
|
|
19
|
+
getAccessToken: () => '333344',
|
|
20
|
+
user_mask: 'mask123',
|
|
18
21
|
})
|
|
19
22
|
|
|
20
|
-
realtime.onEvent(ev => console.log(ev))
|
|
21
|
-
realtime.onInterrupted(() => console.log('some message may have been losted'))
|
|
23
|
+
realtime.onEvent((ev) => console.log(ev))
|
|
22
24
|
|
|
23
25
|
// subscribe events
|
|
24
|
-
realtime.subscribe([
|
|
26
|
+
await realtime.subscribe(['message_sent', 'user_info_updated'])
|
|
27
|
+
console.log(realtime.getStatus()) // 'active'
|
|
25
28
|
|
|
26
29
|
realtime.stop()
|
|
27
30
|
```
|
package/common.js
CHANGED
|
@@ -64,8 +64,8 @@ var xhrsend = function (method, url, body, cb) {
|
|
|
64
64
|
request.send(body)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// JSON.parse without
|
|
68
|
-
// return undefined when
|
|
67
|
+
// JSON.parse without exception
|
|
68
|
+
// return undefined when input is an invalid JSON
|
|
69
69
|
function parseJSON(str) {
|
|
70
70
|
try {
|
|
71
71
|
return JSON.parse(str)
|
package/package.json
CHANGED
package/realtime.js
CHANGED
|
@@ -17,174 +17,164 @@ var {xhrsend, parseJSON, Pubsub} = require('./common.js')
|
|
|
17
17
|
function Conn(apiUrl, credential, onDead, onEvents, callAPI, accid) {
|
|
18
18
|
callAPI = callAPI || xhrsend // allow hook
|
|
19
19
|
credential = credential || {}
|
|
20
|
-
// tell if connection is dead
|
|
21
20
|
|
|
22
21
|
var lastToken = ''
|
|
23
22
|
if (!accid) accid = ''
|
|
24
23
|
|
|
25
|
-
var
|
|
24
|
+
var is_dead = false // tell if the connection is dead
|
|
25
|
+
var is_polling_stuck = false // tell if we've having trouble doing /poll
|
|
26
|
+
var is_sub_stuck = false // tell if we've having trouble doing /subscribe
|
|
26
27
|
|
|
27
28
|
// The long polling loop runs sequentially, with each poll starting immediately
|
|
28
29
|
// after the previous one completes. If an error occurs, the loop will pause
|
|
29
30
|
// using an exponential backoff strategy. It will terminate if an unretryable
|
|
30
|
-
// error is encountered.
|
|
31
|
-
// call completes successfully
|
|
31
|
+
// error is encountered.
|
|
32
|
+
// The first loop begins after the first subscription call completes successfully
|
|
32
33
|
var polling = function (backoff) {
|
|
33
|
-
if (
|
|
34
|
+
if (is_dead) return
|
|
34
35
|
callAPI('get', apiUrl + 'poll?token=' + lastToken + '&account-id=' + accid, undefined, function (body, code) {
|
|
35
|
-
if (
|
|
36
|
-
|
|
36
|
+
if (is_dead) return
|
|
37
37
|
if (retryable(code)) {
|
|
38
|
-
|
|
38
|
+
is_polling_stuck = true
|
|
39
39
|
return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (code !== 200) {
|
|
43
43
|
// unretryable error, should kill
|
|
44
|
-
|
|
45
|
-
return
|
|
44
|
+
is_polling_stuck = true
|
|
45
|
+
return kill('poll')
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
if (status == 'polling') status = 'active'
|
|
49
48
|
// 200, success
|
|
49
|
+
is_polling_stuck = false
|
|
50
50
|
body = parseJSON(body) || {}
|
|
51
51
|
if (body.host) apiUrl = absUrl(body.host)
|
|
52
52
|
|
|
53
53
|
var seqToken = body.sequential_token
|
|
54
54
|
// the server returns a malform payload. We should end
|
|
55
|
-
if (!seqToken)
|
|
56
|
-
status = 'dead'
|
|
57
|
-
return onDead('poll')
|
|
58
|
-
}
|
|
55
|
+
if (!seqToken) return kill('poll')
|
|
59
56
|
lastToken = seqToken
|
|
60
57
|
onEvents(body.events || [])
|
|
61
58
|
return polling(0)
|
|
62
59
|
})
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
var subQueue = flow.batch(50, 100, function (
|
|
66
|
-
if (
|
|
67
|
-
if (
|
|
62
|
+
var subQueue = flow.batch(50, 100, function (topics) {
|
|
63
|
+
if (is_dead) return Promise.resolve(repeat('dead', topics.length))
|
|
64
|
+
if (topics.length <= 0) return Promise.resolve([])
|
|
65
|
+
|
|
66
|
+
var resolve
|
|
67
|
+
var promise = new Promise((rs) => {
|
|
68
|
+
resolve = rs
|
|
69
|
+
})
|
|
68
70
|
|
|
69
71
|
var done = false
|
|
70
|
-
setTimeout(()
|
|
71
|
-
if (done)
|
|
72
|
-
if (status == 'dead') return
|
|
73
|
-
if (status == 'active') status = 'subbing'
|
|
72
|
+
setTimeout(function () {
|
|
73
|
+
if (!done && !is_dead) is_sub_stuck = true
|
|
74
74
|
}, 4000)
|
|
75
75
|
|
|
76
76
|
setTimeout(() => {
|
|
77
77
|
// too long, kill
|
|
78
78
|
if (done) return
|
|
79
79
|
done = true
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
out = repeat('dead', events.length)
|
|
84
|
-
resolve(false) // break loop
|
|
80
|
+
resolve(repeat('dead', topics.length))
|
|
81
|
+
if (is_dead) return
|
|
82
|
+
kill('subs')
|
|
85
83
|
}, 60000)
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
flow.loop(function () {
|
|
86
|
+
return new Promise(function (rs) {
|
|
87
|
+
if (is_dead) {
|
|
88
|
+
resolve(repeat('dead', topics.length))
|
|
89
|
+
return rs(false) // break loop
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var query = '?token=' + lastToken
|
|
93
|
+
credential.getAccessToken().then(function (access_token) {
|
|
94
|
+
if (is_dead) {
|
|
95
|
+
resolve(repeat('dead', topics.length))
|
|
97
96
|
return rs(false) // break loop
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
credential.
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
|
|
100
|
+
else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
|
|
101
|
+
else if (access_token) query += '&access-token=' + access_token
|
|
102
|
+
|
|
103
|
+
var fullurl = apiUrl + 'subs' + query + '&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))
|
|
104
108
|
return rs(false) // break loop
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return rs(false) // break loop
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (retryable(code)) {
|
|
122
|
-
status = 'subbing'
|
|
123
|
-
return setTimeout(rs, 3000, true)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// unretryable error
|
|
127
|
-
if (code !== 200) {
|
|
128
|
-
// we are unable to start the communication with the realtime server.
|
|
129
|
-
// Must notify the user by killing the connection
|
|
130
|
-
status = 'dead'
|
|
131
|
-
onDead('subscribe', body, code)
|
|
132
|
-
out = repeat('dead', events.length)
|
|
133
|
-
return rs(false)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
done = true
|
|
137
|
-
if (lastToken) return rs(false)
|
|
138
|
-
|
|
139
|
-
// first time sub, should grab the initial token
|
|
140
|
-
body = parseJSON(body) || {}
|
|
141
|
-
var initialToken = body.initial_token
|
|
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
|
-
|
|
150
|
-
if (body.host) apiUrl = absUrl(body.host)
|
|
151
|
-
|
|
152
|
-
lastToken = initialToken
|
|
153
|
-
polling(0)
|
|
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))
|
|
154
122
|
return rs(false)
|
|
155
|
-
}
|
|
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
|
|
156
148
|
})
|
|
157
149
|
})
|
|
158
150
|
})
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
})
|
|
151
|
+
})
|
|
152
|
+
|
|
162
153
|
return promise
|
|
163
154
|
})
|
|
164
155
|
|
|
165
156
|
// subscribe call /subs API to tell server that we are listening for those events
|
|
166
157
|
// this function may start polling loop if it hasn't been started yet
|
|
167
|
-
//
|
|
168
|
-
// function multiple times at once
|
|
158
|
+
// its safe to call this function multiple times
|
|
169
159
|
this.subscribe = subQueue.push.bind(subQueue)
|
|
170
160
|
|
|
171
161
|
this.getStatus = function () {
|
|
172
|
-
if (
|
|
173
|
-
return 'connecting'
|
|
162
|
+
if (is_dead) return 'dead'
|
|
163
|
+
if (is_sub_stuck || is_polling_stuck) return 'connecting'
|
|
164
|
+
return 'active'
|
|
174
165
|
}
|
|
175
166
|
|
|
176
167
|
// kill force the connection to dead state
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
var kill = function (reason) {
|
|
169
|
+
is_dead = true
|
|
170
|
+
onDead(reason)
|
|
179
171
|
}
|
|
172
|
+
this.kill = kill
|
|
180
173
|
}
|
|
181
174
|
|
|
182
|
-
// Realtime
|
|
183
|
-
// This class helps you subscribe and listen realtime event from the
|
|
175
|
+
// Realtime helps you subscribe and listen realtime event from th
|
|
184
176
|
// realtime server
|
|
185
|
-
//
|
|
186
|
-
// + auto recreate and resub if the last conn is dead
|
|
187
|
-
// + don't subscribe already subscribed events
|
|
177
|
+
// Realtime is just a stronger Conn.
|
|
188
178
|
function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
189
179
|
if (typeof apiUrls === 'string' || apiUrls instanceof String) apiUrls = [apiUrls]
|
|
190
180
|
credential = credential || {}
|
|
@@ -197,19 +187,13 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
197
187
|
var pubsub = new Pubsub()
|
|
198
188
|
|
|
199
189
|
// holds all topics that realtime must subscribed
|
|
200
|
-
// {
|
|
201
|
-
// a: 'online',
|
|
202
|
-
// b: 'offline',
|
|
203
|
-
// c: 'connecting',
|
|
204
|
-
// }
|
|
205
190
|
var topics = {}
|
|
206
191
|
|
|
207
192
|
// stop the connection
|
|
208
|
-
var stop = false
|
|
209
193
|
this.stop = function () {
|
|
210
|
-
if (!conn
|
|
211
|
-
stop = true
|
|
194
|
+
if (!conn) return
|
|
212
195
|
conn.kill()
|
|
196
|
+
conn = undefined
|
|
213
197
|
pubsub.emit('interrupted')
|
|
214
198
|
}
|
|
215
199
|
|
|
@@ -226,9 +210,8 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
226
210
|
return conn.getStatus()
|
|
227
211
|
}
|
|
228
212
|
|
|
229
|
-
var conn
|
|
230
213
|
this.subscribe = function (events) {
|
|
231
|
-
if (
|
|
214
|
+
if (!conn) return Promise.resolve({error: 'dead'})
|
|
232
215
|
if (typeof events === 'string') events = [events]
|
|
233
216
|
if (!Array.isArray(events)) return Promise.resolve({error: 'param should be an array or string'})
|
|
234
217
|
|
|
@@ -237,48 +220,44 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
|
|
|
237
220
|
for (var i = 0; i < events.length; i++) {
|
|
238
221
|
var topic = events[i]
|
|
239
222
|
if (!topic) continue
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
223
|
+
if (topics[topic]) continue
|
|
224
|
+
topics[topic] = true // mark so next time we wont subscribe this event (topic) again
|
|
225
|
+
all.push(conn.subscribe(topic))
|
|
244
226
|
}
|
|
245
227
|
if (all.length === 0) return Promise.resolve({})
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
for (var i = 0; i < errs.length; i++) if (errs[i]) return {error: errs[i]}
|
|
249
|
-
for (var j = 0; j < events.length; j++) topics[events[j]] = true
|
|
228
|
+
return Promise.all(all).then(function () {
|
|
229
|
+
if (!conn || conn.getStatus() == 'dead') return {error: 'dead'}
|
|
250
230
|
return {}
|
|
251
231
|
})
|
|
252
232
|
}
|
|
253
233
|
|
|
254
234
|
// reconnect make sure there is alway a Conn running in the background
|
|
255
235
|
// if the Conn is dead, it recreate a new one
|
|
236
|
+
var conn
|
|
256
237
|
this.reconnect = function () {
|
|
257
|
-
if (stop) return Promise.resolve(false)
|
|
258
|
-
// reset subscribed topic
|
|
259
|
-
var allTopics = Object.keys(topics)
|
|
260
|
-
for (var i = 0; i < allTopics.length; i++) topics[allTopics[i]] = false
|
|
261
238
|
var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
|
|
262
239
|
if (conn) conn.kill() // kill old connection
|
|
263
|
-
|
|
240
|
+
let myconn = new Conn(
|
|
264
241
|
randomUrl,
|
|
265
242
|
credential,
|
|
266
243
|
function (code, body, status) {
|
|
267
|
-
if (
|
|
244
|
+
if (myconn != conn) return // outdated
|
|
268
245
|
pubsub.emit('interrupted', code, body, status)
|
|
269
246
|
if (!skipautoreconnect) setTimeout(this.reconnect, 2000) // reconnect and resubscribe after 2 sec
|
|
270
247
|
},
|
|
271
248
|
function (events) {
|
|
272
|
-
if (
|
|
249
|
+
if (myconn != conn) return
|
|
273
250
|
for (var i = 0; i < events.length; i++) pubsub.emit('event', events[i])
|
|
274
251
|
},
|
|
275
252
|
callAPI,
|
|
276
253
|
accid,
|
|
277
254
|
)
|
|
255
|
+
conn = myconn
|
|
278
256
|
|
|
257
|
+
var copyTopics = Object.keys(topics)
|
|
258
|
+
topics = {}
|
|
279
259
|
// resubscribe all subscribed events
|
|
280
|
-
|
|
281
|
-
return this.subscribe(topicKeys)
|
|
260
|
+
return this.subscribe(copyTopics)
|
|
282
261
|
}
|
|
283
262
|
this.reconnect()
|
|
284
263
|
}
|
package/realtime.test.js
CHANGED
|
@@ -81,7 +81,7 @@ async function testScript(t, script) {
|
|
|
81
81
|
break
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
t.true(!istimeout, JSON.stringify(step))
|
|
84
|
+
t.true(!istimeout, script.name + ' ' + stepi + ' ' + JSON.stringify(step))
|
|
85
85
|
continue
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -119,7 +119,6 @@ test.only('realtime', async (t) => {
|
|
|
119
119
|
{type: 'subscribe', topics: ['message_sent']},
|
|
120
120
|
{type: 'status', status: 'active'},
|
|
121
121
|
{type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
|
|
122
|
-
|
|
123
122
|
{type: 'wait', timeout: 5000},
|
|
124
123
|
{type: 'status', status: 'connecting'},
|
|
125
124
|
{type: 'wait', timeout: 60000},
|