Created
January 23, 2020 20:36
-
-
Save crrobinson14/fe73453b6aa996fdcdcb2de5e2adc82f to your computer and use it in GitHub Desktop.
WS/REST capable Chat / Action Handling client for ActionHero
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const ReconnectingWebSocket = require('reconnecting-websocket'); | |
const axios = require('axios'); | |
const WS = require('ws'); | |
// Written with ES5 metaphors to eliminate the need for Babel in test. | |
const KEY_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); | |
const generateRequestId = () => { | |
const uuid = []; | |
let i; | |
for (i = 0; i < 10; i++) { | |
// eslint-disable-next-line no-bitwise | |
uuid.push(KEY_CHARS[0 | Math.random() * KEY_CHARS.length]); | |
} | |
return uuid.join(''); | |
}; | |
class ChatClient { | |
constructor(apiUrl) { | |
this.apiUrl = apiUrl; | |
this.wsUrl = `${apiUrl.replace('http', 'ws')}/primus`; | |
// Set this to true after creation to enable logging | |
this.debug = false; | |
// Register a callback for incoming chat messages | |
this.onLiveChatMessage = null; | |
// Will be set post-connection upon a successful auth cycle | |
this.isAuthenticated = false; | |
this.user = {}; | |
// Settable in Node to force a session token for automated tests. Only works in dev/test environnents. | |
this.sessionId = null; | |
// Private, do not touch | |
this.pendingActions = {}; | |
this.socket = null; | |
} | |
async connect() { | |
this.user = {}; | |
this.isAuthenticated = false; | |
this.debug && console.log('[CLIENT] Connecting...', this.apiUrl); | |
this.socket = new ReconnectingWebSocket(this.getEndpoint.bind(this), null, { | |
WebSocket: WS, | |
timeoutInterval: 3000, | |
minReconnectionDelay: 1, | |
}); | |
this.socket.addEventListener('open', this.onOpen.bind(this)); | |
this.socket.addEventListener('message', this.onMessage.bind(this)); | |
this.socket.addEventListener('close', this.onClose.bind(this)); | |
this.socket.addEventListener('error', this.onError.bind(this)); | |
} | |
async getEndpoint() { | |
const { apiUrl } = this; | |
const wsPrefix = apiUrl.replace('http', 'ws'); | |
const options = this.sessionId | |
? { headers: { Authorization: `Bearer: ${this.sessionId}` }, withCredentials: true } | |
: undefined; | |
this.debug && console.log('[CLIENT] Getting connection...', { apiUrl, options }); | |
const { token } = await axios.post(`${apiUrl}/v1/connections`, options); | |
console.log('got connection token', token); | |
this.token = token; | |
console.log('wsUrl', `${wsPrefix}/primus?token=${token}`); | |
return `${wsPrefix}/primus?token=${token}`; | |
} | |
close() { | |
if (this.socket) { | |
this.socket.close(); | |
} | |
this.clearProperties(); | |
} | |
clearProperties() { | |
this.user = {}; | |
this.socket = null; | |
} | |
onOpen() { | |
this.debug && console.log('[CLIENT] Connected'); | |
} | |
onClose() { | |
this.clearProperties(); | |
} | |
onError(e) { | |
this.clearProperties(); | |
this.debug && console.error(e); | |
} | |
onMessage(e) { | |
this.debug && console.log('[CLIENT] onMessage', e.data); | |
if ((e.data || '').substr(0, 15) === '"primus::ping::') { | |
this.socket.send(e.data.replace('ping', 'pong')); | |
return; | |
} | |
if (e.data === 'primus::server::close') { | |
this.clearProperties(); | |
return; | |
} | |
const message = JSON.parse(e.data); | |
const { error, apiRequestId, context } = message; | |
switch (context) { | |
case 'response': | |
if (apiRequestId && this.pendingActions[apiRequestId]) { | |
if (error) { | |
this.pendingActions[apiRequestId].reject(message); | |
} else { | |
this.pendingActions[apiRequestId].resolve(message); | |
} | |
delete this.pendingActions[apiRequestId]; | |
} | |
break; | |
case 'user': | |
// console.debug('[CLIENT] Got user message', message); | |
break; | |
case 'api': | |
// console.debug('[CLIENT] Got generic API message', message); | |
break; | |
case 'liveChat': | |
if (this.onLiveChatMessage) {qz | |
this.onLiveChatMessage(message); | |
} | |
break; | |
default: | |
this.debug && console.log('[CLIENT] Unhandled message from server', message); | |
break; | |
} | |
} | |
/** | |
* Execute an action via the socket. | |
* | |
* @param {String} action The name of the action to call. | |
* @param {Object} [params] Optional. Key:Value pairs to include as parameters for the request. | |
*/ | |
runAction(action, params) { | |
const pendingAction = {}; | |
const apiRequestId = generateRequestId(); | |
pendingAction.promise = new Promise((resolve, reject) => { | |
pendingAction.resolve = resolve; | |
pendingAction.reject = reject; | |
}); | |
params = params || {}; | |
params.action = action; | |
params.appId = this.appId; | |
params.apiRequestId = apiRequestId; | |
pendingAction.action = action; | |
pendingAction.params = params; | |
this.pendingActions[apiRequestId] = pendingAction; | |
if (this.socket) { | |
this.socket.send(JSON.stringify({ event: 'action', params })); | |
this.debug && console.log(`[CLIENT] runAction(${action})`); | |
} else { | |
throw new Error('[CLIENT] runAction failed: socket not connected'); | |
} | |
return pendingAction.promise; | |
} | |
} | |
module.exports = ChatClient; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment