flowai-js
Version:
The flow.ai Javascript SDK
929 lines (765 loc) • 28 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _events = _interopRequireDefault(require("events"));
var _backo = _interopRequireDefault(require("backo"));
var _debug = _interopRequireDefault(require("debug"));
var _websocket = require("websocket");
var _message = _interopRequireDefault(require("./message"));
var _reply = _interopRequireDefault(require("./reply"));
var _rest = _interopRequireDefault(require("./rest"));
var _unique = _interopRequireDefault(require("./unique"));
var _exception = _interopRequireDefault(require("./exception"));
var _fileAttachment = _interopRequireDefault(require("./file-attachment"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
(0, _debug.default)('flowai:liveclient');
/**
* Live streaming websocket client extends EventEmitter
* @class
* @example
* // Node.js
* const client = new LiveClient({
* clientId: 'MY CLIENT ID',
* origin: 'https://my.website'
* })
*
* // Web
* const client = new LiveClient({
* clientId: 'MY CLIENT ID',
* storage: 'session'
* })
*
* // Lambda function
* const client = new LiveClient({
* clientId: 'MY CLIENT ID',
* storage: 'memory'
* })
**/
var LiveClient =
/*#__PURE__*/
function (_EventEmitter) {
_inherits(LiveClient, _EventEmitter);
/**
* Constructor
* @constructor
* @param {(object|string)} opts - Configuration options or shorthand for just clientId
* @param {string} opts.clientId - Mandatory Client token
* @param {string} opts.storage - Optional, 'session' for using sessionStorage, 'local' for localStorage or `memory` for a simple memory store
* @param {string} opts.endpoint - Optional, only for testing purposes
* @param {string} opts.origin - When running on Nodejs you MUST set the origin
* @returns {LiveClient}
*
*/
function LiveClient(opts) {
var _this;
_classCallCheck(this, LiveClient);
_this = _possibleConstructorReturn(this, _getPrototypeOf(LiveClient).call(this)); // Backwards compatibility
if (typeof opts === 'string') {
_this._clientId = arguments[0];
if (arguments.length == 2) {
_this._endpoint = arguments[1];
}
} else if (_typeof(opts) === 'object') {
_this._clientId = opts.clientId;
_this._endpoint = opts.endpoint;
if (opts.storage === 'session') {
_this._storage = 'session';
} else {
_this._storage = 'local';
}
if (typeof window === 'undefined') {
_this._origin = opts.origin || undefined;
}
}
if (typeof _this._clientId !== 'string') {
throw new _exception.default("Invalid or lacking argument for LiveClient. You must provide a clientId. Check the dashboard", 'user');
}
_this._storage = _this._storage || 'local';
_this._endpoint = _this._endpoint || 'https://sdk.flow.ai';
_this._rest = new _rest.default(_this._endpoint);
_this._init();
(0, _debug.default)('Constructed a new LiveClient', _assertThisInitialized(_assertThisInitialized(_this)));
return _this;
}
/**
* Session Id of the connection
* @method
* @returns {?string} Null if no connection is active
**/
_createClass(LiveClient, [{
key: "start",
/**
* Start the client
* @method
* @param {string} threadId - Optional. When assigned, this is the default threadId for all messages that are send
* @param {string} sessionId - Optional. Must be unique for every connection
*
* @example
* // Start, will generate thread and sessionId
* client.start()
*
* @example
* // Start with your own custom threadId
* client.start('UNIQUE THREADID FOR USER')
**/
value: function start(threadId, sessionId) {
try {
if (sessionId && typeof sessionId !== 'string') {
throw new _exception.default("sessionId must be a string", 'user');
}
if (threadId && typeof threadId !== 'string') {
throw new _exception.default("threadId must be a string", 'user');
}
(0, _debug.default)("Starting the client with sessionId ".concat(sessionId, " and threadId '").concat(threadId, "'")); // Create a new backoff policy
this._backoff = new _backo.default({
min: 100,
max: 20000
}); // Create a new Thread
this.sessionId = sessionId; // Create a new Thread
this.threadId = threadId;
this._openConnection();
} catch (err) {
// Wrap the error
throw new _exception.default("Failed to start the client ".concat(err), 'connection', err);
}
}
/**
* Stop the client
* @desc Use this method to temp disconnect a client
*
* @example
* // Close the connection
* client.stop()
**/
}, {
key: "stop",
value: function stop() {
try {
(0, _debug.default)("Stopping the client");
this._closeConnection();
} catch (err) {
// Wrap the error
throw new _exception.default("Failed to stop the client", 'connection', err);
}
}
/**
* Close the connection and completely reset the client
*
* @example
* // Close the connection and reset the client
* client.destroy()
**/
}, {
key: "destroy",
value: function destroy() {
this._closeConnection();
this._init();
}
/**
* Send a Message
* @desc This method triggers a `LiveClient.MESSAGE_SEND` event
* @param {object} message - Message you want to send
*
* @example
* const originator = new Originator({
* name: "Jane"
* })
*
* const message = new Message({
* speech: "Hi!",
* originator
* })
*
* client.send(message)
* @returns {object} Message that was send
**/
}, {
key: "send",
value: function send(message) {
var _this2 = this;
(0, _debug.default)("Sending message", message);
if (!this.isConnected) {
throw new _exception.default("Could not send the message. The socket connection is disconnected.", 'user');
}
if (!(message instanceof _message.default)) {
throw new _exception.default("Could not send the message. You should send a valid Message object.", 'user');
} // Set the default
message.threadId = message.threadId || this.threadId; // Update threadId
this.threadId = message.threadId;
try {
this.emit(LiveClient.MESSAGE_SEND, message);
if (message.attachment && message.attachment instanceof _fileAttachment.default) {
var formData = message.attachment.payload.formData;
formData.append('payload', JSON.stringify(Object.assign({}, message, {
attachment: undefined,
clientId: this._clientId,
sessionId: this.sessionId
})));
(0, _debug.default)('Uploading formData', formData);
this._rest.upload(formData).then(function (result) {
if (result.status !== 'ok') {
_this2.emit(LiveClient.ERROR, new _exception.default(new Error('Failed to upload file.'), 'connection'));
} else {
_this2.emit(LiveClient.MESSAGE_DELIVERED, result.payload);
}
}).catch(function (err) {
(0, _debug.default)('Error while trying to upload a file', err);
_this2.emit(LiveClient.ERROR, new _exception.default(err, 'connection'));
});
return message;
}
var enveloppe = JSON.stringify({
type: 'message.send',
payload: message
});
(0, _debug.default)("Creating message enveloppe", enveloppe);
setTimeout(function () {
// We add a tiny delay because
// messages instantly send after 'connection'
// event get lost
_this2._socket.send(enveloppe);
}, 50);
} catch (err) {
this.emit(LiveClient.ERROR, new _exception.default(err));
}
return message;
}
/**
* Merge two threads from different channels.
* This methods is not yet publicy supported since we don't have a way yet to provide a mergerKey.
* @param {string} mergerKey - Unique token representing merge Request
* @param {string} threadId - Optional. The threadId to merge
* @param {string} sessionId - Optional. The sessionId to assign to the thread
**/
}, {
key: "merger",
value: function merger(mergerKey, threadId, sessionId) {
var _this3 = this;
(0, _debug.default)("Merging threads '".concat(mergerKey, "', threadId '").concat(threadId, "'"));
if (!this.isConnected) {
throw new _exception.default("Could merge anything, the connection is down.", 'user');
}
if (typeof mergerKey !== 'string' || mergerKey.length === 0) {
throw new _exception.default("Could not merge. You should privide a mergerKey.", 'user');
}
this._rest.post({
path: 'thread.merger',
payload: {
clientId: this._clientId,
threadId: threadId || this.threadId,
sessionId: sessionId || this.sessionId,
mergerKey: mergerKey
}
}).then(function (result) {
if (result.status !== 'ok') {
throw new Error("Unable to merge, received a status other then \"ok\". ".concat(result.payload.message));
}
}).catch(function (err) {
(0, _debug.default)('Error while trying to merge', err);
_this3.emit(LiveClient.ERROR, new _exception.default(err, 'connection'));
});
}
/**
* Request historic messages
* @param {string} threadId - Optional. Specify the threadId to retreive historic messages
*
* @example
* // Load any messages if there is a threadId
* // usefull when using with JS in the browser
* client.history()
*
* // Load messages using a custom threadId
* client.history('MY CUSTOM THREAD ID')
**/
}, {
key: "history",
value: function history(threadId) {
var _this4 = this;
if (!threadId && !_unique.default.exists({
clientId: this._clientId,
key: 'threadId',
engine: this._storage
})) {
return this.emit(LiveClient.NO_HISTORY);
}
this.threadId = threadId;
this.emit(LiveClient.REQUESTING_HISTORY);
this._rest.get({
path: 'thread.history',
queryParams: {
clientId: this._clientId,
threadId: this.threadId
}
}).then(function (result) {
if (result.status !== 'ok') {
throw new Error("Unable to fetch historic messages, received a status other then \"ok\". ".concat(result.payload.message));
}
_this4.emit(LiveClient.RECEIVED_HISTORY, result.payload);
}).catch(function (err) {
(0, _debug.default)('Error while trying to fetch the history', err);
_this4.emit(LiveClient.ERROR, new _exception.default(new Error('Error while trying to fetch the history'), 'history'));
});
}
/**
* Call to mark a thread as noticed.
* The library automatically throttles the number of calls
* @param {string} threadId - Optional. Specify the thread that is noticed
* @param {boolean} instantly - Optional. Instantly send notice. Default is false
*
* @example
* // Call that the client has seen all messages for the auto clientId
* client.noticed()
*
* // Mark messages based on a custom threadId
* client.noticed('MY CUSTOM THREAD ID')
**/
}, {
key: "noticed",
value: function noticed(threadId, instantly) {
var _this5 = this;
if (!threadId && !_unique.default.exists({
clientId: this._clientId,
key: 'threadId',
engine: this._storage
})) {
// Skip
throw new _exception.default("Could not send noticed. No threadId", 'user');
}
if (this._noticedTimeout) {
// Skip this call (already one running)
return;
}
this._noticedTimeout = setTimeout(function () {
_this5._noticedTimeout = null;
if (!_this5.isConnected) {
throw new _exception.default("Could not send noticed message. The socket connection is disconnected.", 'user');
}
var enveloppe = JSON.stringify({
type: 'thread.noticed',
payload: {
threadId: _this5.threadId
}
});
(0, _debug.default)("Creating notice enveloppe", enveloppe);
try {
_this5._socket.send(enveloppe);
} catch (err) {
_this5.emit(LiveClient.ERROR, new _exception.default(err));
}
}, instantly ? 1 : 5000);
}
/**
* Did we miss any messages?
* @param {string} threadId - Optional. Specify the thread to check unnoticed messags for
**/
}, {
key: "checkUnnoticed",
value: function checkUnnoticed(threadId) {
var _this6 = this;
if (!threadId && !_unique.default.exists({
clientId: this._clientId,
key: 'threadId',
engine: this._storage
})) {
return this.emit(LiveClient.CHECKED_UNNOTICED_MESSAGES, {
unnoticed: false
});
}
this.threadId = threadId;
this._rest.get({
path: 'thread.unnoticed',
queryParams: {
clientId: this._clientId,
threadId: this.threadId
}
}).then(function (result) {
if (result.status !== 'ok') {
throw new Error("Unable to check unnoticed messages, received a status other then \"ok\". ".concat(result.payload.message));
}
_this6.emit(LiveClient.CHECKED_UNNOTICED_MESSAGES, result.payload);
}).catch(function (err) {
(0, _debug.default)('Error while trying to find out unnoticed messages', err);
_this6.emit(LiveClient.ERROR, new _exception.default(new Error('Error while trying to find out unnoticed messages'), 'unnoticed'));
});
}
/**
* Setup the client
* @private
**/
}, {
key: "_init",
value: function _init() {
this._session = null;
this._thread = null;
this._socket = null;
this._keepAliveInterval = null;
this._reconnectTimeout = null;
this._noticedTimeout = null;
this._backoff = new _backo.default({
min: 100,
max: 20000
});
}
/**
* Try to reconnect
* @private
**/
}, {
key: "_reconnect",
value: function _reconnect() {
var _this7 = this;
if (!this._isAutoReconnect) {
(0, _debug.default)('Auto reconnect is disabled');
return;
}
var timeout = this._backoff.duration() + 1000;
(0, _debug.default)("Reconnecting with timeout in '".concat(timeout, "'ms"));
this._reconnectTimeout = setTimeout(function () {
_this7.emit(LiveClient.RECONNECTING);
_this7._openConnection();
}, timeout);
}
/**
* Try to open a connection
* @private
**/
}, {
key: "_openConnection",
value: function _openConnection() {
var _this8 = this;
this._isAutoReconnect = true; // Request endpoint to give a WS url
this._rest.get({
path: 'socket.info',
queryParams: {
clientId: this._clientId,
sessionId: this.sessionId,
threadId: this.threadId
}
}).then(function (result) {
if (result.status !== 'ok') {
throw new _exception.default("Unable to get a socket URL\": ".concat(result.payload.message), 'connection');
}
_this8._handleConnection(result.payload);
}).catch(function (err) {
console.error('LiveClient: Error while trying to connect', err);
if (!err.isFinal) {
_this8._reconnect();
}
_this8.emit(LiveClient.ERROR, new _exception.default(err, 'connection'));
});
}
/**
* Handle a received endpoint
* @private
**/
}, {
key: "_handleConnection",
value: function _handleConnection(payload) {
var _this9 = this;
if (!payload) {
throw new Error("Did not receive a valid response from the backend service");
}
var endpoint = payload.endpoint;
(0, _debug.default)("Opening the connection with endpoint '".concat(endpoint, "'"));
var socket = new _websocket.w3cwebsocket(endpoint, null, this._origin);
socket.onopen = function () {
(0, _debug.default)('Socket onopen');
_this9.emit(LiveClient.CONNECTED); // Clear any running interval
clearInterval(_this9._keepAliveInterval); // Create a new Interval
_this9._keepAliveInterval = _this9._keepAlive();
};
socket.onerror = function (evt) {
var msg = "Failed socket operation.";
switch (socket.readyState) {
case 0:
{
_this9.emit(LiveClient.ERROR, new _exception.default("".concat(msg, " Socket is busy connecting."), 'connection'));
break;
}
case 1:
{
_this9.emit(LiveClient.ERROR, new _exception.default("".concat(msg, " Socket is connected."), 'connection'));
break;
}
case 2:
{
_this9.emit(LiveClient.ERROR, new _exception.default("".concat(msg, " Connection is busy closing."), 'connection'));
break;
}
case 3:
{
_this9.emit(LiveClient.ERROR, new _exception.default("".concat(msg, " Connection is closed."), 'connection'));
break;
}
default:
{
_this9.emit(LiveClient.ERROR, new _exception.default(msg, 'connection'));
break;
}
}
};
socket.onclose = function (evt) {
(0, _debug.default)('Socket onclose', evt);
_this9._socket = null;
if (evt && evt.code === 1006) {
_this9.emit(LiveClient.ERROR, new _exception.default('The connection closed abnormally', 'connection', null, true));
_this9.emit(LiveClient.DISCONNECTED); // DIRTY FIX? Need to check this in the future
_this9._reconnect();
} else if (evt && evt.reason !== 'connection failed') {
_this9.emit(LiveClient.DISCONNECTED);
_this9._reconnect();
} else {
_this9.emit(LiveClient.DISCONNECTED);
}
};
socket.onmessage = function (evt) {
(0, _debug.default)('Socket onmessage');
if (typeof evt.data !== 'string' || evt.data.length == 0) {
_this9.emit(LiveClient.ERROR, new _exception.default('Failed to receive a valid websocet URL, please check the clientId', 'connection'));
return;
}
var _JSON$parse = JSON.parse(evt.data),
type = _JSON$parse.type,
payload = _JSON$parse.payload,
message = _JSON$parse.message;
(0, _debug.default)('Message received with type and payload', type, payload, message);
switch (type) {
case 'pong':
// Ignore pongs
break;
case 'message.received':
case 'activities.created':
_this9._handleReceived(payload);
break;
case 'error':
_this9.emit(LiveClient.ERROR, new _exception.default(message, 'connection'));
break;
case 'message.delivered':
case 'activities.delivered':
_this9._handleDelivered(payload);
break;
default:
{
(0, _debug.default)('Unknow message received with type and payload', type, payload);
_this9.emit(LiveClient.ERROR, new _exception.default("Unknown message received ".concat(evt.data), 'connection'));
}
}
};
this._socket = socket;
}
/**
* Handle a received message
* @private
* @param {object} payload
**/
}, {
key: "_handleReceived",
value: function _handleReceived(payload) {
(0, _debug.default)('Handling reply payload', payload);
this.emit(LiveClient.REPLY_RECEIVED, new _reply.default(payload));
}
/**
* Handle a received delivery confirmation
* @private
* @param {object} payload
**/
}, {
key: "_handleDelivered",
value: function _handleDelivered(payload) {
(0, _debug.default)('Handling delivered payload', payload);
this.emit(LiveClient.MESSAGE_DELIVERED, _message.default.build(payload));
}
/**
* Disconnnect
* @private
**/
}, {
key: "_closeConnection",
value: function _closeConnection() {
this._isAutoReconnect = false;
if (this._reconnectTimeout) {
// Whenever we close the connection manually,
// we kill any idle reconnect time outs
clearTimeout(this._reconnectTimeout);
}
if (this._noticedTimeout) {
// Whenever we close the connection manually,
// we kill any noticed timers
clearTimeout(this._noticedTimeout);
}
if (this._keepAliveInterval) {
// Stop any keep alive intervals
clearInterval(this._keepAliveInterval);
}
if (this.isConnected) {
(0, _debug.default)('Closing the socket');
this._socket.close();
} else {
(0, _debug.default)('No socket connection to close');
}
}
/**
* Send pings to keep the connection alive
* @private
**/
}, {
key: "_keepAlive",
value: function _keepAlive() {
var _this10 = this;
return setInterval(function () {
try {
if (_this10.isConnected) {
(0, _debug.default)('Sending keep alive packet');
_this10._socket.send(JSON.stringify({
type: 'ping'
}));
}
} catch (err) {
console.error('Error while sending a keepalive ping', err);
}
}, 1000 * 25);
}
}, {
key: "sessionId",
get: function get() {
var sessionId = this._session ? this._session.id() : null;
(0, _debug.default)("sessionId is '".concat(sessionId, "'"));
return sessionId;
}
/**
* Session Id of the connection
* @param {?string} value - Change the session ID
**/
,
set: function set(value) {
(0, _debug.default)("Creating a new sessionId with value '".concat(value, "'"));
this._session = new _unique.default({
clientId: this._clientId,
key: 'sessionId',
value: value,
engine: this._storage
});
}
/**
* Default Thread Id to be used for any messages being send
* @returns {?string} Null if no connection is active
**/
}, {
key: "threadId",
get: function get() {
var threadId = this._thread ? this._thread.id() : null;
(0, _debug.default)("threadId is '".concat(threadId, "'"));
return threadId;
}
/**
* Session Id of the connection
* @param {?string} value - Change the thread ID
**/
,
set: function set(value) {
(0, _debug.default)("Creating a new threadId with value '".concat(value, "'")); // Create a new Thread
this._thread = new _unique.default({
clientId: this._clientId,
key: 'threadId',
value: value,
engine: this._storage
});
}
/**
* Check if the connection is active
*
* @example
* if(client.isConnected) {
* // Do something awesome
* }
* @returns {boolean} True if the connection is active
**/
}, {
key: "isConnected",
get: function get() {
var isConnected = this._socket !== null;
(0, _debug.default)("isConnected is '".concat(isConnected, "'"));
return isConnected;
}
}]);
return LiveClient;
}(_events.default);
/**
* @constant
* @type {string}
* @desc Event that triggers when an error is received from the flow.ai platform
**/
LiveClient.ERROR = 'ERROR';
/**
* @constant
* @type {string}
* @desc Event that triggers when client is connected with platform
**/
LiveClient.CONNECTED = 'connected';
/**
* @constant
* @type {string}
* @desc Event that triggers when client tries to reconnect
**/
LiveClient.RECONNECTING = 'reconnecting';
/**
* @constant
* @type {string}
* @desc Event that triggers when the client gets disconnected
**/
LiveClient.DISCONNECTED = 'disconnected';
/**
* @constant
* @type {string}
* @desc Event that triggers when a new message is received from the platform
**/
LiveClient.REPLY_RECEIVED = 'message.received';
/**
* @constant
* @type {string}
* @desc Event that triggers when the client is sending a message to the platform
**/
LiveClient.MESSAGE_SEND = 'message.send';
/**
* @constant
* @type {string}
* @desc Event that triggers when the send message has been received by the platform
**/
LiveClient.MESSAGE_DELIVERED = 'message.delivered';
/**
* @constant
* @type {string}
* @desc Event that triggers when a request is made to load historic messages
**/
LiveClient.REQUESTING_HISTORY = 'requesting.history';
/**
* @constant
* @type {string}
* @desc Event that triggers when a request is made to load historic messages
**/
LiveClient.NO_HISTORY = 'no.history';
/**
* @constant
* @type {string}
* @desc Event that triggers when historic messages are received
**/
LiveClient.RECEIVED_HISTORY = 'received.history';
/**
* @constant
* @type {string}
* @desc Event that triggers when there are unnoticed messages
**/
LiveClient.CHECKED_UNNOTICED_MESSAGES = 'unnoticed.messages';
var _default = LiveClient;
exports.default = _default;