Skip to content

Instantly share code, notes, and snippets.

@crrobinson14
Created January 23, 2020 20:36
Show Gist options
  • Save crrobinson14/fe73453b6aa996fdcdcb2de5e2adc82f to your computer and use it in GitHub Desktop.
Save crrobinson14/fe73453b6aa996fdcdcb2de5e2adc82f to your computer and use it in GitHub Desktop.
WS/REST capable Chat / Action Handling client for ActionHero
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