@subiz/wsclient 3.1.8 → 4.0.0

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/common.js ADDED
@@ -0,0 +1,66 @@
1
+ function dofetch(method, url, body, cb) {
2
+ let headers = {}
3
+ if (body) headers['content-type'] = 'text/plain'
4
+ fetch(url, {method: method, headers: headers, body: body})
5
+ .then((res) => res.text().then((text) => cb && cb(text, res.status)))
6
+ .catch((err) => cb && cb(err, -1))
7
+ }
8
+
9
+ // xhrsend sends an HTTP request
10
+ var xhrsend = function (method, url, body, cb) {
11
+ if (typeof XMLHttpRequest === 'undefined') return dofetch(method, url, body, cb)
12
+ var request = new XMLHttpRequest()
13
+ request.onreadystatechange = function (e) {
14
+ if (request.readyState !== 4) return
15
+ cb && cb(request.responseText, request.status)
16
+ cb = undefined // dont call cb anymore
17
+ }
18
+
19
+ request.onerror = function () {
20
+ cb && cb(request.responseText, -1) // network error
21
+ cb = undefined // dont call cb anymore
22
+ }
23
+
24
+ request.open(method, url)
25
+ if (body) request.setRequestHeader('content-type', 'text/plain')
26
+ request.send(body)
27
+ }
28
+
29
+ // JSON.parse without exeption
30
+ // return undefined when parsing invalid JSON string
31
+ function parseJSON(str) {
32
+ try {
33
+ return JSON.parse(str)
34
+ } catch (e) {}
35
+ }
36
+
37
+ function Pubsub() {
38
+ // holds all subscriptions for all topics
39
+ // example {
40
+ // topic1: [cb1, cb2],
41
+ // topic2: [cb3, cb4],
42
+ // }
43
+ var listeners = {}
44
+
45
+ // emit notifies any callback functions that previously
46
+ // subscribed to the topic (by the on function)
47
+ this.emit = function (topic) {
48
+ // skip the first item in arguments, which is the topic
49
+ var args = []
50
+ for (var i = 1; i < arguments.length; i++) args.push(arguments[i])
51
+
52
+ var cbs = listeners[topic]
53
+ if (!cbs) return
54
+ for (var i = 0; i < cbs.length; i++) cbs[i].apply(undefined, args)
55
+ }
56
+
57
+ // register a callback function for a topic
58
+ // when something is sent to the topic (by the emit function),
59
+ // the callback function will be called
60
+ this.on = function (topic, cb) {
61
+ if (!listeners[topic]) listeners[topic] = []
62
+ listeners[topic].push(cb)
63
+ }
64
+ }
65
+
66
+ module.exports = {xhrsend, parseJSON, Pubsub}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subiz/wsclient",
3
- "version": "3.1.8",
4
- "main": "index.js",
3
+ "version": "4.0.0",
4
+ "main": "realtime.js",
5
5
  "scripts": {
6
6
  "test": "node realtime.test.js"
7
7
  },
package/realtime.js CHANGED
@@ -1,4 +1,5 @@
1
1
  var flow = require('@subiz/flow')
2
+ var {xhrsend, parseJSON, Pubsub} = require('./common.js')
2
3
 
3
4
  // Conn represents a realtime connection between an user and the realtime
4
5
  // server (https://rt-0.subiz.com)
@@ -22,7 +23,7 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI) {
22
23
  var lastToken = ''
23
24
 
24
25
  // kill force the connection to dead state
25
- this.kill = function() {
26
+ this.kill = function () {
26
27
  dead = true
27
28
  }
28
29
 
@@ -31,9 +32,9 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI) {
31
32
  // exponential backoff algorithm. Polling loop will terminate if it encounter an
32
33
  // un-retryable error
33
34
  // the polling loop starts after the first subscribe call finished successfully
34
- var polling = function(backoff) {
35
+ var polling = function (backoff) {
35
36
  if (dead) return
36
- callAPI('get', apiUrl + 'poll?token=' + lastToken, undefined, function(body, code) {
37
+ callAPI('get', apiUrl + 'poll?token=' + lastToken, undefined, function (body, code) {
37
38
  if (dead) return
38
39
  if (retryable(code)) return setTimeout(polling, calcNextBackoff(backoff), backoff + 1)
39
40
  if (code !== 200) {
@@ -55,21 +56,26 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI) {
55
56
  })
56
57
  }
57
58
 
58
- var subQueue = flow.batch(50, 100, function(events) {
59
+ var subQueue = flow.batch(50, 100, function (events) {
59
60
  if (dead) return ['dead']
60
61
  if (events.length <= 0) return []
61
62
 
62
63
  var out = []
63
64
  return flow
64
- .loop(function() {
65
- return new Promise(function(rs) {
65
+ .loop(function () {
66
+ return new Promise(function (rs) {
66
67
  var query = '?token=' + lastToken
67
- credential.getAccessToken().then(function(access_token) {
68
- if (credential.user_ref) query += '&user-ref=' + encodeURIComponent(credential.user_ref)
68
+ credential.getAccessToken().then(function (access_token) {
69
+ if (credential.user_ref)
70
+ query +=
71
+ '&user_ref=' +
72
+ encodeURIComponent(credential.user_ref) +
73
+ '&account_id=' +
74
+ encodeURIComponent(credential.account_id)
69
75
  else if (credential.user_mask) query += '&user-mask=' + encodeURIComponent(credential.user_mask)
70
76
  else if (access_token) query += '&access-token=' + access_token
71
77
 
72
- callAPI('post', apiUrl + 'subs' + query, JSON.stringify({events: events}), function(body, code) {
78
+ callAPI('post', apiUrl + 'subs' + query, JSON.stringify({events: events}), function (body, code) {
73
79
  if (dead) {
74
80
  out = repeat('dead', events.length)
75
81
  return rs(false) // break loop
@@ -102,7 +108,7 @@ function Conn(apiUrl, credential, onDead, onEvents, callAPI) {
102
108
  })
103
109
  })
104
110
  })
105
- .then(function() {
111
+ .then(function () {
106
112
  return out
107
113
  })
108
114
  })
@@ -124,7 +130,7 @@ function Realtime(apiUrls, credential, callAPI) {
124
130
  if (typeof apiUrls === 'string' || apiUrls instanceof String) apiUrls = [apiUrls]
125
131
  credential = credential || {}
126
132
  if (!credential.getAccessToken) {
127
- credential.getAccessToken = function() {
133
+ credential.getAccessToken = function () {
128
134
  return Promise.resolve('')
129
135
  }
130
136
  }
@@ -141,22 +147,22 @@ function Realtime(apiUrls, credential, callAPI) {
141
147
 
142
148
  // stop the connection
143
149
  var stop = false
144
- this.stop = function() {
150
+ this.stop = function () {
145
151
  if (stop) return
146
152
  stop = true
147
153
  conn.kill()
148
154
  }
149
155
 
150
- this.onEvent = function(cb) {
156
+ this.onEvent = function (cb) {
151
157
  return pubsub.on('event', cb)
152
158
  }
153
159
 
154
- this.onInterrupted = function(cb) {
160
+ this.onInterrupted = function (cb) {
155
161
  return pubsub.on('interrupted', cb)
156
162
  }
157
163
 
158
164
  var conn
159
- this.subscribe = function(events) {
165
+ this.subscribe = function (events) {
160
166
  if (stop) return Promise.resolve({})
161
167
  if (typeof events === 'string') events = [events]
162
168
  if (!Array.isArray(events)) return Promise.resolve({error: 'param should be an array or string'})
@@ -169,7 +175,7 @@ function Realtime(apiUrls, credential, callAPI) {
169
175
  if (!topics[topic]) all.push(conn.subscribe(topic))
170
176
  }
171
177
  if (all.length === 0) return Promise.resolve({})
172
- return Promise.all(all).then(function(errs) {
178
+ return Promise.all(all).then(function (errs) {
173
179
  for (var i = 0; i < errs.length; i++) if (errs[i]) return {error: errs[i]}
174
180
  for (var j = 0; j < events.length; j++) topics[events[j]] = true
175
181
  return {}
@@ -178,7 +184,7 @@ function Realtime(apiUrls, credential, callAPI) {
178
184
 
179
185
  // reconnect make sure there is alway a Conn running in the background
180
186
  // if the Conn is dead, it recreate a new one
181
- var reconnect = function() {
187
+ var reconnect = function () {
182
188
  if (stop) return
183
189
  // reset subscribed topic
184
190
  var allTopics = Object.keys(topics)
@@ -188,12 +194,12 @@ function Realtime(apiUrls, credential, callAPI) {
188
194
  conn = new Conn(
189
195
  randomUrl,
190
196
  credential,
191
- function(code, body, status) {
197
+ function (code, body, status) {
192
198
  if (stop) return
193
199
  pubsub.emit('interrupted', code, body, status)
194
200
  setTimeout(reconnect, 2000) // reconnect and resubscribe after 2 sec
195
201
  },
196
- function(topics) {
202
+ function (topics) {
197
203
  if (stop) return
198
204
  for (var i = 0; i < topics.length; i++) pubsub.emit('event', topics[i])
199
205
  },
@@ -207,74 +213,6 @@ function Realtime(apiUrls, credential, callAPI) {
207
213
  reconnect()
208
214
  }
209
215
 
210
- var dofetch = function(method, url, body, cb) {
211
- let headers = {}
212
- if (body) headers['content-type'] = 'text/plain'
213
- fetch(url, {
214
- method: method,
215
- headers: headers,
216
- body: body,
217
- })
218
- .then((res) => res.text().then((text) => cb && cb(text, res.status)))
219
- .catch((err) => cb && cb(err, -1))
220
- }
221
- // xhrsend sends an HTTP request
222
- var xhrsend = function(method, url, body, cb) {
223
- if (typeof XMLHttpRequest === 'undefined') return dofetch(method, url, body, cb)
224
- var request = new XMLHttpRequest()
225
- request.onreadystatechange = function(e) {
226
- if (request.readyState !== 4) return
227
- cb && cb(request.responseText, request.status)
228
- cb = undefined // dont call cb anymore
229
- }
230
-
231
- request.onerror = function() {
232
- cb && cb(request.responseText, -1) // network error
233
- cb = undefined // dont call cb anymore
234
- }
235
-
236
- request.open(method, url)
237
- if (body) request.setRequestHeader('content-type', 'text/plain')
238
- request.send(body)
239
- }
240
-
241
- // JSON.parse without exeption
242
- // return undefined when parsing invalid JSON string
243
- function parseJSON(str) {
244
- try {
245
- return JSON.parse(str)
246
- } catch (e) {}
247
- }
248
-
249
- function Pubsub() {
250
- // holds all subscriptions for all topics
251
- // example {
252
- // topic1: [cb1, cb2],
253
- // topic2: [cb3, cb4],
254
- // }
255
- var listeners = {}
256
-
257
- // emit notifies any callback functions that previously
258
- // subscribed to the topic (by the on function)
259
- this.emit = function(topic) {
260
- // skip the first item in arguments, which is the topic
261
- var args = []
262
- for (var i = 1; i < arguments.length; i++) args.push(arguments[i])
263
-
264
- var cbs = listeners[topic]
265
- if (!cbs) return
266
- for (var i = 0; i < cbs.length; i++) cbs[i].apply(undefined, args)
267
- }
268
-
269
- // register a callback function for a topic
270
- // when something is sent to the topic (by the emit function),
271
- // the callback function will be called
272
- this.on = function(topic, cb) {
273
- if (!listeners[topic]) listeners[topic] = []
274
- listeners[topic].push(cb)
275
- }
276
- }
277
-
278
216
  // calcNextBackoff returns number of seconds we must wait
279
217
  // before sending a next request
280
218
  // the results is determind based on exponential backoff algorithm
package/webrtc.js ADDED
@@ -0,0 +1,302 @@
1
+ var flow = require('@subiz/flow')
2
+ var {xhrsend} = require('./common.js')
3
+ const servers = {
4
+ iceServers: [{urls: ['stun:stun.l.google.com:19302?transport=tcp']}],
5
+ }
6
+
7
+ // import {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, mediaDevices} from 'react-native-webrtc'
8
+ // let connId = 'webrtc' + sb.randomString(20)
9
+ // env = {RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, setTimeout, setInterval, jsonify, parseJSON}
10
+ // setting env._is_webrtc_local to true for local testing
11
+ function WebRTCConn(options) {
12
+ let {apiUrl, access_token, accid, agid, connId, realtime, callAPI, env, onTrack, onEvent} = options
13
+ if (!apiUrl) {
14
+ apiUrl = 'https://callcenter.subiz.com.vn/wc/'
15
+ if (env._is_webrtc_local) apiUrl = 'http://localhost:8008/'
16
+ }
17
+
18
+ callAPI = callAPI || xhrsend // allow hook
19
+
20
+ let parseJSON = (str) => {
21
+ try {
22
+ return env.JSON.parse(str)
23
+ } catch (e) {}
24
+ }
25
+ let jsonify = (str) => {
26
+ try {
27
+ return env.JSON.stringify(str)
28
+ } catch (e) {}
29
+ }
30
+ let currentStream
31
+ let pendingStream
32
+ let activeCalls = {}
33
+
34
+ let subscribe = realtime.subscribe([
35
+ `webrtc.account.${accid}.agent.${agid}`,
36
+ `webrtc.account.${accid}.agent.${agid}.webrtc_connection.${connId}`,
37
+ ])
38
+
39
+ let peer
40
+ // makeSure only returns when stream is added and negotiation is finished
41
+ // its safe to call this function multiple times at once
42
+ let makeSure = (cb) => {
43
+ initConnection()
44
+ subscribe.then(cb)
45
+ }
46
+
47
+ let checkIceConnected = (cb) =>
48
+ flow.loop(
49
+ () =>
50
+ new env.Promise((rs) => {
51
+ let pass = peer && (peer.iceConnectionState == 'connected' || peer.iceConnectionState == 'completed')
52
+ if (!pass) return env.setTimeout((_) => rs(true), 100) // keep looping ultil connected
53
+ cb && cb()
54
+ rs(false)
55
+ }),
56
+ )
57
+
58
+ let readyToSendCandidateResolve
59
+ let readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
60
+
61
+ let initConnection = () => {
62
+ let needConnect =
63
+ !peer ||
64
+ peer.connectionState === 'disconnected' ||
65
+ peer.connectionState === 'closed' ||
66
+ peer.connectionState === 'failed'
67
+
68
+ if (!needConnect) return
69
+ peer = new env.RTCPeerConnection(servers)
70
+ let mypeer = peer
71
+ window.peer = peer
72
+
73
+ readyToSendCandidate = new env.Promise((rs) => (readyToSendCandidateResolve = rs))
74
+
75
+ peer.ontrack = (event) => {
76
+ if (peer != mypeer) return // outdated
77
+ onTrack && onTrack(event)
78
+ }
79
+
80
+ peer.onicecandidate = (event) => {
81
+ if (!event.candidate) return
82
+ if (peer != mypeer) return // outdated
83
+ let url = `${apiUrl}ice-candidates?x-access-token=${access_token}&connection_id=${connId}`
84
+ callAPI('post', url, jsonify(event.candidate))
85
+ }
86
+
87
+ peer.onnegotiationneeded = (ev) => console.log('NNNEED RE-NEGOTIATION', ev)
88
+ console.log('NEW PERR')
89
+ }
90
+
91
+ let stopMic = () => {
92
+ console.log('00000000STOP TRACK', currentStream)
93
+ return
94
+ currentStream &&
95
+ currentStream.getTracks().forEach((track) => {
96
+ track.enabled = false
97
+ })
98
+ }
99
+
100
+ // auto tear down when byte received is zero
101
+ // use to clear peer when there's no byte sent around 100 seconds
102
+ let receivedBytes = []
103
+ let check = () => {
104
+ let url = `${apiUrl}calls?x-access-token=${access_token}&connection_id=${connId}`
105
+ callAPI('get', url, undefined, function (body, code) {
106
+ if (code == 200) {
107
+ body = parseJSON(body) || []
108
+ activeCalls = {}
109
+ body.map && body.map((call) => (activeCalls[call.call_id] = call))
110
+ }
111
+
112
+ if (!peer) return
113
+ peer.getStats(null).then((stats) => {
114
+ let statsOutput = ''
115
+ let receivedByte = 0
116
+ stats.forEach((report) => {
117
+ if (report.type === 'inbound-rtp' && report.kind === 'audio') {
118
+ receivedByte += report.bytesReceived || 0
119
+ }
120
+ if (report.type === 'inbound-rtp' && report.kind === 'video') {
121
+ receivedByte += report.bytesReceived || 0
122
+ }
123
+ })
124
+ receivedBytes.push(receivedByte)
125
+ while (receivedBytes.length > 10) receivedBytes.shift()
126
+ let diff = receivedBytes.filter((n) => n != receivedByte)
127
+ if (diff.length == 0 && receivedBytes.length >= 10 && Object.keys(activeCalls).length == 0) {
128
+ if (Object.keys(activeCalls).length == 0) stopMic()
129
+ // could tear down
130
+ peer.close()
131
+ peer = undefined
132
+ }
133
+ })
134
+ })
135
+ }
136
+ check()
137
+ env.setInterval(check, 30000)
138
+
139
+ let publish = (ev) => onEvent && onEvent(ev)
140
+
141
+ this.matchCall = (callid) => (!callid ? activeCalls : activeCalls[callid])
142
+
143
+ realtime.onEvent((ev) => {
144
+ if (!ev || !ev.type || !ev.data) return
145
+ // if (ev.type.startsWith('call')) console.log('EEEEEEE', ev)
146
+ if (env._is_webrtc_local && ev.user_id != '8') return // local
147
+ if (!env._is_webrtc_local && ev.user_id != '9') return // prod
148
+
149
+ if (ev.type == 'webrtc_candidate_added') {
150
+ let ice_candidate = ev.data.ice_candidate
151
+ if (!ice_candidate) return
152
+ return readyToSendCandidate.then(() => peer && peer.addIceCandidate(parseJSON(ice_candidate)))
153
+ }
154
+
155
+ if (ev.type == 'webrtc_negotiation_offered') {
156
+ console.log('RT/RECEIVE OFFERED NEGOTIATION', ev)
157
+ if (!peer) return
158
+ let negid = ev.id
159
+ if (!ev.data.ice_candidate) return
160
+ let offer = parseJSON(ev.data.ice_candidate)
161
+
162
+ peer
163
+ .setRemoteDescription(offer)
164
+ .then(() => {
165
+ readyToSendCandidateResolve()
166
+ if (pendingStream && pendingStream != currentStream) {
167
+ currentStream = pendingStream
168
+ currentStream.getTracks().forEach((track) => peer.addTrack(track, currentStream))
169
+ }
170
+ pendingStream = undefined
171
+ return peer.createAnswer()
172
+ })
173
+ .then((answer) =>
174
+ peer.setLocalDescription(answer).then(() => {
175
+ let url = `${apiUrl}finish?x-access-token=${access_token}&connection_id=${connId}&version=2&negotiation_id=${negid}`
176
+ callAPI('post', url, jsonify(answer), function (body, code) {
177
+ if (code !== 200) return console.error('CANNOT FINISH', body)
178
+ return
179
+ })
180
+ }),
181
+ )
182
+ }
183
+
184
+ let call_info = ev.data.call_info || {}
185
+ let callid = call_info.call_id
186
+ if (!callid) return
187
+
188
+ if (ev.type === 'call_invite_expired') {
189
+ activeCalls[callid] = call_info
190
+ let device_id = call_info.device_id
191
+ if (device_id == connId) return // ignore since we have accepted the call
192
+
193
+ publish(Object.assign({}, ev, {type: 'call_ended'})) // fake ended call
194
+ delete activeCalls[callid]
195
+ }
196
+ let type = ev.type
197
+ if (type == 'call_ended' || type == 'call_ringing' || type == 'call_joined' || type === 'call_invited') {
198
+ if (ev.type === 'call_invited' && agid == call_info.from_number) return // ignore our own invite
199
+ activeCalls[callid] = ev.data.call_info
200
+ return publish(ev)
201
+ }
202
+ })
203
+
204
+ this.makeCall = (number, fromnumber, stream) =>
205
+ new env.Promise((rs) => {
206
+ let callid = 'webcall-' + randomString(20)
207
+ this.joinCall(callid, stream, (err) => {
208
+ if (err) return rs({error: err})
209
+ let url = `${apiUrl}call?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number=${number}&from_number=${fromnumber}`
210
+ callAPI('post', url, undefined, (body, code) => {
211
+ if (code != 200) return rs({error: body})
212
+ body = parseJSON(body)
213
+ if (body) activeCalls[callid] = body
214
+ rs({body})
215
+ })
216
+ })
217
+ })
218
+
219
+ this.hangupCall = (callid) => {
220
+ if (!callid) return
221
+ let call = activeCalls[callid]
222
+ if (!call) {
223
+ call = {call_id: callid}
224
+ activeCalls[callid] = call
225
+ }
226
+ call.status = 'ended'
227
+ call.ended = env.Date.now()
228
+ callAPI('post', `${apiUrl}hangup?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`)
229
+ publish({type: 'call_ended', data: {call_info: call}})
230
+ }
231
+
232
+ this.listenCall = (callid, cb) => {
233
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
234
+ makeSure(() => callAPI('post', url, undefined, (body, code) => (code != 200 ? cb(body) : cb())))
235
+ }
236
+
237
+ this.joinCall = (callid, stream, cb) => {
238
+ console.time('makecall' + callid)
239
+ makeSure(() => {
240
+ pendingStream = stream
241
+ let url = `${apiUrl}listen?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
242
+ callAPI('post', url, undefined, (body, code) => {
243
+ if (code != 200) return cb(body)
244
+ let url = `${apiUrl}join?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
245
+ callAPI('post', url, undefined, (body, code) => {
246
+ console.timeEnd('makecall' + callid)
247
+ if (code != 200) return cb(body)
248
+ checkIceConnected(() => cb())
249
+ })
250
+ })
251
+ })
252
+ }
253
+
254
+ this.answerCall = (callid, stream) =>
255
+ new env.Promise((rs) => {
256
+ this.joinCall(callid, stream, (err) => {
257
+ if (err) return rs({error: err})
258
+ let url = `${apiUrl}answer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}`
259
+ callAPI('post', url, undefined, (body, code) => {
260
+ body = parseJSON(body)
261
+ return code != 200 ? rs({error: body}) : rs({body})
262
+ })
263
+ })
264
+ })
265
+
266
+ this.sendDtmf = (key, callid) =>
267
+ new env.Promise((rs) => {
268
+ if (!callid) return rs({body: {}})
269
+ let url = `${apiUrl}dtml?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&key={key}`
270
+ makeSure(() =>
271
+ callAPI('post', url, undefined, (body, bode) => {
272
+ body = parseJSON(body)
273
+ return code != 200 ? rs({error: body}) : rs({body})
274
+ }),
275
+ )
276
+ })
277
+
278
+ this.transferCall = (tonumber, callid) =>
279
+ new env.Promise((rs) => {
280
+ if (!callid) return rs({})
281
+ let url = `${apiUrl}refer?x-access-token=${access_token}&connection_id=${connId}&call_id=${callid}&number={tonumber}`
282
+ makeSure(() =>
283
+ callAPI('post', url, undefined, (body, code) => {
284
+ body = parseJSON(body)
285
+ return code != 200 ? rs({error: body}) : rs({body})
286
+ }),
287
+ )
288
+ })
289
+ }
290
+
291
+ function randomString(len) {
292
+ var str = ''
293
+ if (!len || len < 1) len = 10
294
+ var asciiKey
295
+ for (var i = 0; i < len; i++) {
296
+ asciiKey = Math.floor(Math.random() * 25 + 97)
297
+ str += String.fromCharCode(asciiKey)
298
+ }
299
+ return str
300
+ }
301
+
302
+ module.exports = WebRTCConn
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./realtime.js')