@subiz/wsclient 4.0.30 → 4.0.32

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 CHANGED
@@ -1,27 +1,30 @@
1
- # Subiz rtclient
1
+ # Subiz wsclient
2
2
 
3
3
  This library implements subiz long polling protocol
4
4
 
5
5
  ## Method
6
+
6
7
  ### subscribe
7
- ### onEvents
8
- ### onInterrupted
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
- account_id: 'ac1234',
16
- getAccessToken:()=> '333344',
17
- user_mask: 'mask123'
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([ev1, ev2, ev3])
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 exeption
68
- // return undefined when parsing invalid JSON string
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subiz/wsclient",
3
- "version": "4.0.30",
3
+ "version": "4.0.32",
4
4
  "main": "realtime.js",
5
5
  "scripts": {
6
6
  "dev": "vite",
package/realtime.js CHANGED
@@ -17,174 +17,163 @@ 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
-
22
20
  var lastToken = ''
23
21
  if (!accid) accid = ''
24
-
25
- var status = 'active' // active | polling (trouble while polling) | subbing (trouble while subscribing) | dead
22
+ var is_dead = false // tell if the connection is dead
23
+ var is_polling_stuck = false // tell if we've having trouble doing /poll
24
+ var is_sub_stuck = true // tell if we've having trouble doing /subscribe
26
25
 
27
26
  // The long polling loop runs sequentially, with each poll starting immediately
28
27
  // after the previous one completes. If an error occurs, the loop will pause
29
28
  // 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
29
+ // error is encountered.
30
+ // The first loop begins after the first subscription call completes successfully
32
31
  var polling = function (backoff) {
33
- if (status == 'dead') return
32
+ if (is_dead) return
34
33
  callAPI('get', apiUrl + 'poll?token=' + lastToken + '&account-id=' + accid, undefined, function (body, code) {
35
- if (status == 'dead') return
36
-
34
+ if (is_dead) return
37
35
  if (retryable(code)) {
38
- if (status == 'active') status = 'polling'
36
+ is_polling_stuck = true
39
37
  return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
40
38
  }
41
39
 
42
40
  if (code !== 200) {
43
41
  // unretryable error, should kill
44
- status = 'dead'
45
- return onDead('poll')
42
+ is_polling_stuck = true
43
+ return kill('poll')
46
44
  }
47
45
 
48
- if (status == 'polling') status = 'active'
49
46
  // 200, success
47
+ is_polling_stuck = false
50
48
  body = parseJSON(body) || {}
51
49
  if (body.host) apiUrl = absUrl(body.host)
52
50
 
53
51
  var seqToken = body.sequential_token
54
52
  // the server returns a malform payload. We should end
55
- if (!seqToken) {
56
- status = 'dead'
57
- return onDead('poll')
58
- }
53
+ if (!seqToken) return kill('poll')
59
54
  lastToken = seqToken
60
55
  onEvents(body.events || [])
61
56
  return polling(0)
62
57
  })
63
58
  }
64
59
 
65
- var subQueue = flow.batch(50, 100, function (events) {
66
- if (status == 'dead') return repeat('dead', events.length)
67
- if (events.length <= 0) return []
60
+ var subQueue = flow.batch(50, 100, function (topics) {
61
+ 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
68
 
69
69
  var done = false
70
- setTimeout(() => {
71
- if (done) return
72
- if (status == 'dead') return
73
- if (status == 'active') status = 'subbing'
70
+ setTimeout(function () {
71
+ if (!done && !is_dead) is_sub_stuck = true
74
72
  }, 4000)
75
73
 
76
74
  setTimeout(() => {
77
75
  // too long, kill
78
76
  if (done) return
79
77
  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
78
+ is_sub_stuck = true
79
+ resolve(repeat('dead', topics.length))
80
+ if (is_dead) return
81
+ kill('subs')
85
82
  }, 60000)
86
83
 
87
- var resolve
88
- var promise = new Promise((rs) => {
89
- resolve = rs
90
- })
91
- var out = []
92
- flow
93
- .loop(function () {
94
- return new Promise(function (rs) {
95
- if (status == 'dead') {
96
- out = repeat('dead', events.length)
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))
97
95
  return rs(false) // break loop
98
96
  }
99
97
 
100
- var query = '?token=' + lastToken
101
- credential.getAccessToken().then(function (access_token) {
102
- if (status == 'dead') {
103
- out = repeat('dead', events.length)
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 = apiUrl + 'subs' + query + '&account-id=' + encodeURIComponent(accid || credential.account_id)
103
+ callAPI('post', fullurl, JSON.stringify({events: topics}), function (body, code) {
104
+ if (done) return
105
+ if (is_dead) {
106
+ resolve(repeat('dead', topics.length))
104
107
  return rs(false) // break loop
105
108
  }
106
109
 
107
- if (credential.user_ref) query += '&user_ref=' + encodeURIComponent(credential.user_ref)
108
- else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
109
- else if (access_token) query += '&access-token=' + access_token
110
-
111
- var fullurl = apiUrl + 'subs' + query + '&account-id=' + encodeURIComponent(accid || credential.account_id)
112
- callAPI('post', fullurl, JSON.stringify({events: events}), function (body, code) {
113
- if (done) return
114
- if (status == 'subbing') status = 'active'
115
-
116
- if (status == 'dead') {
117
- out = repeat('dead', events.length)
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)
110
+ if (retryable(code)) {
111
+ is_sub_stuck = true
112
+ return setTimeout(rs, 3000, true)
113
+ }
114
+
115
+ // unretryable error
116
+ if (code !== 200) {
117
+ // we are unable to start the communication with the realtime server.
118
+ // Must notify the user by killing the connection
119
+ kill('subs')
120
+ resolve(repeat('dead', topics.length))
154
121
  return rs(false)
155
- })
122
+ }
123
+ is_sub_stuck = false
124
+ done = true
125
+ if (lastToken) {
126
+ resolve(topics)
127
+ return rs(false)
128
+ }
129
+
130
+ // first time sub, should grab the initial token
131
+ body = parseJSON(body) || {}
132
+ var initialToken = body.initial_token
133
+ // the server returns a malform payload. We should end
134
+ if (!initialToken) {
135
+ kill('subs')
136
+ resolve(repeat('dead', topics.length))
137
+ return rs(false) // break loop
138
+ }
139
+
140
+ // learn new host
141
+ if (body.host) apiUrl = absUrl(body.host)
142
+
143
+ lastToken = initialToken
144
+ polling(0)
145
+ resolve(topics)
146
+ return rs(false) // to break the loop
156
147
  })
157
148
  })
158
149
  })
159
- .then(function () {
160
- resolve(out)
161
- })
150
+ })
151
+
162
152
  return promise
163
153
  })
164
154
 
165
155
  // subscribe call /subs API to tell server that we are listening for those events
166
156
  // this function may start polling loop if it hasn't been started yet
167
- // Note: this function is not design to be thread safed so, do not call this
168
- // function multiple times at once
157
+ // its safe to call this function multiple times
169
158
  this.subscribe = subQueue.push.bind(subQueue)
170
159
 
171
160
  this.getStatus = function () {
172
- if (status == 'dead' || status == 'active') return status
173
- return 'connecting'
161
+ if (is_dead) return 'dead'
162
+ if (is_sub_stuck || is_polling_stuck) return 'connecting'
163
+ return 'active'
174
164
  }
175
165
 
176
166
  // kill force the connection to dead state
177
- this.kill = function () {
178
- status = 'dead'
167
+ var kill = function (reason) {
168
+ is_dead = true
169
+ onDead(reason)
179
170
  }
171
+ this.kill = kill
180
172
  }
181
173
 
182
- // Realtime is just a stronger Conn.
183
- // This class helps you subscribe and listen realtime event from the
174
+ // Realtime helps you subscribe and listen realtime event from th
184
175
  // realtime server
185
- // additional features compare to conn:
186
- // + auto recreate and resub if the last conn is dead
187
- // + don't subscribe already subscribed events
176
+ // Realtime is just a stronger Conn.
188
177
  function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
189
178
  if (typeof apiUrls === 'string' || apiUrls instanceof String) apiUrls = [apiUrls]
190
179
  credential = credential || {}
@@ -197,19 +186,13 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
197
186
  var pubsub = new Pubsub()
198
187
 
199
188
  // holds all topics that realtime must subscribed
200
- // {
201
- // a: 'online',
202
- // b: 'offline',
203
- // c: 'connecting',
204
- // }
205
189
  var topics = {}
206
190
 
207
191
  // stop the connection
208
- var stop = false
209
192
  this.stop = function () {
210
- if (!conn || stop) return
211
- stop = true
193
+ if (!conn) return
212
194
  conn.kill()
195
+ conn = undefined
213
196
  pubsub.emit('interrupted')
214
197
  }
215
198
 
@@ -226,9 +209,8 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
226
209
  return conn.getStatus()
227
210
  }
228
211
 
229
- var conn
230
- this.subscribe = function (events) {
231
- if (stop) return Promise.resolve({})
212
+ var subscribe = function (events) {
213
+ if (!conn) return Promise.resolve({error: 'dead'})
232
214
  if (typeof events === 'string') events = [events]
233
215
  if (!Array.isArray(events)) return Promise.resolve({error: 'param should be an array or string'})
234
216
 
@@ -237,50 +219,48 @@ function Realtime(apiUrls, credential, callAPI, accid, skipautoreconnect) {
237
219
  for (var i = 0; i < events.length; i++) {
238
220
  var topic = events[i]
239
221
  if (!topic) continue
240
- if (!topics[topic]) {
241
- topics[topic] = true
242
- all.push(conn.subscribe(topic))
243
- }
222
+ if (topics[topic]) continue
223
+ topics[topic] = true // mark so next time we wont subscribe this event (topic) again
224
+ all.push(conn.subscribe(topic))
244
225
  }
245
226
  if (all.length === 0) return Promise.resolve({})
246
-
247
- return Promise.all(all).then(function (errs) {
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
227
+ return Promise.all(all).then(function () {
228
+ if (!conn || conn.getStatus() == 'dead') return {error: 'dead'}
250
229
  return {}
251
230
  })
252
231
  }
232
+ this.subscribe = subscribe
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
256
- 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
236
+ var conn
237
+ let reconnect = function () {
261
238
  var randomUrl = apiUrls[Math.floor(Math.random() * apiUrls.length)]
262
239
  if (conn) conn.kill() // kill old connection
263
- conn = new Conn(
240
+ let myconn = new Conn(
264
241
  randomUrl,
265
242
  credential,
266
243
  function (code, body, status) {
267
- if (stop) return
244
+ if (myconn != conn) return // outdated
245
+ conn = undefined // prevent dupplicate dead fire
268
246
  pubsub.emit('interrupted', code, body, status)
269
- if (!skipautoreconnect) setTimeout(this.reconnect, 2000) // reconnect and resubscribe after 2 sec
247
+ if (!skipautoreconnect) setTimeout(reconnect, 2000) // reconnect and resubscribe after 2 sec
270
248
  },
271
249
  function (events) {
272
- if (stop) return
250
+ if (myconn != conn) return
273
251
  for (var i = 0; i < events.length; i++) pubsub.emit('event', events[i])
274
252
  },
275
253
  callAPI,
276
254
  accid,
277
255
  )
278
-
256
+ conn = myconn
257
+ var copyTopics = Object.keys(topics)
258
+ topics = {}
279
259
  // resubscribe all subscribed events
280
- var topicKeys = Object.keys(topics)
281
- return this.subscribe(topicKeys)
260
+ return subscribe(copyTopics)
282
261
  }
283
- this.reconnect()
262
+ this.reconnect = reconnect
263
+ reconnect()
284
264
  }
285
265
 
286
266
  // calcNextBackoff returns number of seconds we must wait
package/realtime.test.js CHANGED
@@ -11,7 +11,7 @@ async function testScript(t, script) {
11
11
  calls.push({method, url, body, cb})
12
12
  },
13
13
  'acctest',
14
- true,
14
+ !script.auto_reconnect,
15
15
  )
16
16
  realtime.onEvent((ev) => events.push(ev))
17
17
 
@@ -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
 
@@ -100,7 +100,6 @@ async function testScript(t, script) {
100
100
  }
101
101
  t.true(false, script.name + '[' + stepi + ']=' + step.type)
102
102
  }
103
-
104
103
  // check events
105
104
  let scriptevents = script.events || []
106
105
  t.equal(events.length, scriptevents.length)
@@ -119,7 +118,6 @@ test.only('realtime', async (t) => {
119
118
  {type: 'subscribe', topics: ['message_sent']},
120
119
  {type: 'status', status: 'active'},
121
120
  {type: 'call', call_id: '1', path: '/subs', method: 'post', body: {events: ['message_sent']}},
122
-
123
121
  {type: 'wait', timeout: 5000},
124
122
  {type: 'status', status: 'connecting'},
125
123
  {type: 'wait', timeout: 60000},
@@ -279,11 +277,59 @@ test.only('realtime', async (t) => {
279
277
  {type: 'status', status: 'active'},
280
278
  ],
281
279
  },
280
+ {
281
+ name: 'reconect while poll',
282
+ steps: [
283
+ {type: 'subscribe', topics: ['message_sent']},
284
+ {type: 'call', call_id: 1, path: '/subs'},
285
+ {type: 'response', call_id: '1', body: {initial_token: 'token1'}, code: 200},
286
+ {type: 'call', call_id: '2', path: '/poll', query: {token: 'token1'}, method: 'get'},
287
+ {type: 'status', status: 'active'},
288
+ {type: 'wait', timeout: 1000},
289
+ {type: 'response', call_id: '2', body: {events: [{text: 'xin chao'}], sequential_token: 'token2'}, code: 200},
290
+ {type: 'call', call_id: '3', path: '/poll', query: {token: 'token2'}, method: 'get'},
291
+ {type: 'response', call_id: '3', body: '400 dead', code: 400},
292
+ {type: 'status', status: 'dead'},
293
+ {type: 'reconnect'},
294
+ {type: 'wait', timeout: 1000},
295
+ {type: 'call', call_id: 4, path: '/subs'},
296
+ {type: 'wait', timeout: 1000},
297
+ {type: 'response', call_id: 4, body: {initial_token: 'token1'}, code: 200},
298
+ {type: 'status', status: 'active'},
299
+ ],
300
+ events: [{text: 'xin chao'}],
301
+ },
302
+ {
303
+ name: 'resub',
304
+ auto_reconnect: true,
305
+ steps: [
306
+ {type: 'subscribe', topics: ['message_sent']},
307
+ {type: 'call', call_id: 1, path: '/subs'},
308
+ {type: 'response', call_id: '1', body: 'wait a sec', code: 500},
309
+ {type: 'status', status: 'connecting'},
310
+ {type: 'wait', timeout: 1000},
311
+ {type: 'call', call_id: 2, path: '/subs'},
312
+ {type: 'wait', timeout: 61000},
313
+ {type: 'status', status: 'connecting'},
314
+ {type: 'call', call_id: 3, path: '/subs'},
315
+ {type: 'wait', timeout: 1000},
316
+ {type: 'response', call_id: '3', body: 'wait a sec', code: 500},
317
+ {type: 'status', status: 'connecting'},
318
+ {type: 'wait', timeout: 1000},
319
+ {type: 'call', call_id: 4, path: '/subs'},
320
+ {type: 'wait', timeout: 1000},
321
+ {type: 'response', call_id: '4', body: {initial_token: 'token1'}, code: 200},
322
+ {type: 'call', call_id: '5', path: '/poll', query: {token: 'token1'}, method: 'get'},
323
+ {type: 'status', status: 'active'},
324
+ {type: 'response', call_id: '5', body: {events: [{text: 'xin chao'}], sequential_token: 'token2'}, code: 200},
325
+ ],
326
+ events: [{text: 'xin chao'}],
327
+ },
282
328
  ]
283
329
 
284
330
  let ps = []
285
331
  for (const script of scripts) {
286
- // if (script.name != 'double subscribe -> do nothing') continue
332
+ if (script.name != 'resub') continue
287
333
  ps.push(testScript(t, script))
288
334
  }
289
335
  await Promise.all(ps)