@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 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.31",
4
4
  "main": "realtime.js",
5
5
  "scripts": {
6
6
  "dev": "vite",
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 status = 'active' // active | polling (trouble while polling) | subbing (trouble while subscribing) | dead
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. The polling loop begins after the first subscription
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 (status == 'dead') return
34
+ if (is_dead) return
34
35
  callAPI('get', apiUrl + 'poll?token=' + lastToken + '&account-id=' + accid, undefined, function (body, code) {
35
- if (status == 'dead') return
36
-
36
+ if (is_dead) return
37
37
  if (retryable(code)) {
38
- if (status == 'active') status = 'polling'
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
- status = 'dead'
45
- return onDead('poll')
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 (events) {
66
- if (status == 'dead') return repeat('dead', events.length)
67
- if (events.length <= 0) return []
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) return
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
- if (status == 'dead') return
81
- status = 'dead' // mark as dead
82
- onDead('poll')
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
- 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)
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
- var query = '?token=' + lastToken
101
- credential.getAccessToken().then(function (access_token) {
102
- if (status == 'dead') {
103
- out = repeat('dead', events.length)
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 (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)
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
- .then(function () {
160
- resolve(out)
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
- // Note: this function is not design to be thread safed so, do not call this
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 (status == 'dead' || status == 'active') return status
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
- this.kill = function () {
178
- status = 'dead'
168
+ var kill = function (reason) {
169
+ is_dead = true
170
+ onDead(reason)
179
171
  }
172
+ this.kill = kill
180
173
  }
181
174
 
182
- // Realtime is just a stronger Conn.
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
- // additional features compare to conn:
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 || stop) return
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 (stop) return Promise.resolve({})
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 (!topics[topic]) {
241
- topics[topic] = true
242
- all.push(conn.subscribe(topic))
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
- 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
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
- 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
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 (stop) return
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
- var topicKeys = Object.keys(topics)
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},