UNPKG

@sprucelabs/mercury-client

Version:

The simple way to interact with the Spruce Experience Platform

606 lines (605 loc) • 26.3 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import AbstractSpruceError from '@sprucelabs/error'; import { SchemaError, validateSchemaValues, } from '@sprucelabs/schema'; import { eventContractUtil, eventResponseUtil, eventNameUtil, } from '@sprucelabs/spruce-event-utils'; import { buildLog } from '@sprucelabs/spruce-skill-utils'; import { io } from 'socket.io-client'; import SpruceError from '../errors/SpruceError.js'; import socketIoEventUtil from '../utilities/socketIoEventUtil.utility.js'; class MercurySocketIoClient { get eventContract() { return this._eventContract; } set eventContract(contract) { this._eventContract = contract; } constructor(options) { this.log = buildLog('MercurySocketIoClient'); this.proxyToken = null; this.listenerMap = new WeakMap(); this.isReAuthing = false; this.reconnectPromise = null; this.connectionRetriesRemaining = 5; this.registeredListeners = []; this.allowNextEventToBeAuthenticate = false; this.shouldAutoRegisterListeners = true; this.isManuallyDisconnected = false; this.isReconnecting = false; this.skipWaitIfReconnecting = false; this.shouldRegisterProxyOnReconnect = false; const { host, eventContract, emitTimeoutMs, reconnectDelayMs, shouldReconnect, maxEmitRetries = 5, connectionRetries } = options, ioOptions = __rest(options, ["host", "eventContract", "emitTimeoutMs", "reconnectDelayMs", "shouldReconnect", "maxEmitRetries", "connectionRetries"]); this.host = host; this.ioOptions = Object.assign(Object.assign({}, ioOptions), { withCredentials: false }); this.eventContract = eventContract; this.emitTimeoutMs = emitTimeoutMs !== null && emitTimeoutMs !== void 0 ? emitTimeoutMs : 30000; this.reconnectDelayMs = reconnectDelayMs !== null && reconnectDelayMs !== void 0 ? reconnectDelayMs : 5000; this.shouldReconnect = shouldReconnect !== null && shouldReconnect !== void 0 ? shouldReconnect : true; this.id = new Date().getTime().toString(); this.maxEmitRetries = maxEmitRetries; this.connectionRetriesRemaining = connectionRetries !== null && connectionRetries !== void 0 ? connectionRetries : 5; this.connectionRetries = connectionRetries !== null && connectionRetries !== void 0 ? connectionRetries : 5; } connect() { return __awaiter(this, void 0, void 0, function* () { this.socket = MercurySocketIoClient.io(this.host, this.ioOptions); this.emitStatusChange('connecting'); yield new Promise((resolve, reject) => { var _a, _b; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on('connect', () => { var _a, _b; this.connectionRetriesRemaining = this.connectionRetries; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners(); if (!this.isReconnecting) { this.emitStatusChange('connected'); } if (this.shouldReconnect) { (_b = this.socket) === null || _b === void 0 ? void 0 : _b.once('disconnect', (opts) => __awaiter(this, void 0, void 0, function* () { this.log.error('Mercury disconnected, reason:', opts); yield this.attemptReconnectAfterDelay(); })); } this.attachConnectError(); resolve(undefined); }); (_b = this.socket) === null || _b === void 0 ? void 0 : _b.on('timeout', () => { reject(new SpruceError({ code: 'TIMEOUT', eventName: 'connect', timeoutMs: 20000, friendlyMessage: `Uh Oh! I'm having trouble reaching HQ! Double check you have good internet and try again. In the meantime I'll try some things on my side and see what I can do. 🤞`, })); }); this.attachConnectError(reject, resolve); }); }); } emitStatusChange(status) { //@ts-ignore void this.emit('connection-status-change', { payload: { status, }, }); } attachConnectError(reject, resolve) { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on('connect_error', (err) => __awaiter(this, void 0, void 0, function* () { var _a; const error = this.mapSocketErrorToSpruceError(err); //@ts-ignore (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners(); this.log.error('Failed to connect to Mercury', error.message); this.log.error('Connection retries left', `${this.connectionRetriesRemaining}`); if (this.connectionRetriesRemaining === 0) { reject === null || reject === void 0 ? void 0 : reject(error); return; } try { this.isReconnecting = false; yield this.attemptReconnectAfterDelay(); resolve === null || resolve === void 0 ? void 0 : resolve(); } catch (err) { //@ts-ignore reject === null || reject === void 0 ? void 0 : reject(err); } })); } attemptReconnectAfterDelay() { return __awaiter(this, arguments, void 0, function* (retriesLeft = this.maxEmitRetries) { if (this.isManuallyDisconnected) { this.isReconnecting = false; return; } if (this.isReconnecting) { return; } this.emitStatusChange('disconnected'); delete this.authPromise; this.isReconnecting = true; this.proxyToken = null; this.reconnectPromise = new Promise((resolve, reject) => { if (this.lastAuthOptions) { this.isReAuthing = true; } setTimeout(() => __awaiter(this, void 0, void 0, function* () { yield this.reconnect(resolve, reject, retriesLeft); }), this.reconnectDelayMs); }); return this.reconnectPromise; }); } reconnect(resolve, reject, retriesLeft) { return __awaiter(this, void 0, void 0, function* () { var _a; try { this.connectionRetriesRemaining--; const key = new Date().getTime(); this.reconnectKey = key; yield this.connect(); if (this.reconnectKey !== key) { return; } if (this.isManuallyDisconnected) { this.isReconnecting = false; return; } this.skipWaitIfReconnecting = true; if (this.lastAuthOptions) { yield this.authenticate(this.lastAuthOptions); } if (this.isManuallyDisconnected) { this.isReconnecting = false; return; } if (this.shouldRegisterProxyOnReconnect) { yield this.registerProxyToken(); } if (this.isManuallyDisconnected) { this.isReconnecting = false; return; } yield this.reRegisterAllListeners(); this.emitStatusChange('connected'); this.isReAuthing = false; this.isReconnecting = false; this.skipWaitIfReconnecting = false; resolve(); } catch (err) { ; ((_a = console.error) !== null && _a !== void 0 ? _a : console.log)(err.message); this.isReconnecting = false; this.skipWaitIfReconnecting = false; retriesLeft = retriesLeft - 1; if ((err.options.code === 'TIMEOUT' || err.options.code === 'CONNECTION_FAILED') && retriesLeft > 0) { yield this.attemptReconnectAfterDelay(retriesLeft) .then(resolve) .catch(reject); } else { this.lastAuthOptions = undefined; reject(err); } } }); } waitIfReconnecting() { return __awaiter(this, void 0, void 0, function* () { yield this.reconnectPromise; }); } reRegisterAllListeners() { return __awaiter(this, void 0, void 0, function* () { const listeners = this.registeredListeners; this.registeredListeners = []; const all = Promise.all(listeners.map((listener) => this.on(listener[0], listener[1]))); yield all; }); } mapSocketErrorToSpruceError(err) { var _a; const originalError = new Error((_a = err.message) !== null && _a !== void 0 ? _a : err); if (err.stack) { originalError.stack = err.stack; } //@ts-ignore originalError.socketError = err; switch (err.message) { case 'timeout': return new SpruceError({ code: 'TIMEOUT', eventName: 'connect', timeoutMs: 10000, }); case 'xhr poll error': return new SpruceError({ code: 'CONNECTION_FAILED', host: this.host, statusCode: +err.description || 503, originalError, }); default: return new SpruceError({ code: 'UNKNOWN_ERROR', originalError, friendlyMessage: `Something went wrong when working with socketio`, }); } } emit(eventName, targetAndPayload, cb) { return __awaiter(this, void 0, void 0, function* () { const isLocalEvent = this.isEventLocal(eventName); if (isLocalEvent) { return this.handleLocalEmit(eventName, targetAndPayload); } return this._emit(this.maxEmitRetries, eventName, targetAndPayload, cb); }); } handleLocalEmit(eventName, targetAndPayload) { const listeners = this.registeredListeners.filter((r) => r[0] === eventName); for (const listener of listeners) { const cb = listener === null || listener === void 0 ? void 0 : listener[1]; cb === null || cb === void 0 ? void 0 : cb({ //@ts-ignore payload: targetAndPayload === null || targetAndPayload === void 0 ? void 0 : targetAndPayload.payload, }); } return { responses: [], totalContracts: 0, totalErrors: 0, totalResponses: 0, }; } emitAndFlattenResponses(eventName, payload, cb) { return __awaiter(this, void 0, void 0, function* () { const results = yield this.emit(eventName, payload, cb); const { payloads, errors } = eventResponseUtil.getAllResponsePayloadsAndErrors(results, SpruceError); if (errors === null || errors === void 0 ? void 0 : errors[0]) { throw errors[0]; } return payloads; }); } _emit(retriesRemaining, eventName, payload, cb) { return __awaiter(this, void 0, void 0, function* () { var _a; if (!this.skipWaitIfReconnecting) { yield this.waitIfReconnecting(); } if (!this.allowNextEventToBeAuthenticate && eventName === authenticateFqen) { throw new SchemaError({ code: 'INVALID_PARAMETERS', parameters: ['eventName'], friendlyMessage: `You can't emit '${authenticateFqen}' event directly. Use client.authenticate() so all your auth is preserved.`, }); } else if (eventName === authenticateFqen) { this.allowNextEventToBeAuthenticate = false; } if (this.isManuallyDisconnected) { throw new SpruceError({ code: 'NOT_CONNECTED', action: 'emit', fqen: eventName, }); } this.assertValidEmitTargetAndPayload(eventName, payload); const responseEventName = eventNameUtil.generateResponseEventName(eventName); const singleResponsePromises = []; const singleResponseHandler = (response) => __awaiter(this, void 0, void 0, function* () { if (cb) { let resolve; singleResponsePromises.push(new Promise((r) => { resolve = r; })); yield cb(eventResponseUtil.mutatingMapSingleResonseErrorsToSpruceErrors(response, SpruceError)); //@ts-ignore resolve(); } }); if (cb) { (_a = this.socket) === null || _a === void 0 ? void 0 : _a.on(responseEventName, singleResponseHandler); } const args = []; if (payload || this.proxyToken) { const p = Object.assign({}, payload); if (eventName !== authenticateFqen && this.proxyToken && !p.source) { p.source = { proxyToken: this.proxyToken, }; } args.push(p); } const results = yield new Promise((resolve, reject) => { var _a; try { const emitTimeout = setTimeout(() => __awaiter(this, void 0, void 0, function* () { var _a; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.off(responseEventName, singleResponseHandler); if (retriesRemaining == 0) { const err = new SpruceError({ code: 'TIMEOUT', eventName, timeoutMs: this.emitTimeoutMs, isConnected: this.isSocketConnected(), totalRetries: this.maxEmitRetries, }); reject(err); return; } retriesRemaining--; try { if (eventName === authenticateFqen && this.authRawResults) { resolve(this.authRawResults); return; } this.allowNextEventToBeAuthenticate = true; //@ts-ignore const results = yield this._emit(retriesRemaining, eventName, payload, cb); //@ts-ignore resolve(results); } catch (err) { reject(err); } }), this.emitTimeoutMs); args.push((results) => { var _a; clearTimeout(emitTimeout); this.handleConfirmPinResponse(eventName, results); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.off(responseEventName, singleResponseHandler); resolve(results); }); const ioName = socketIoEventUtil.toSocketName(eventName); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit(ioName, ...args); } catch (err) { reject(err); } }); yield Promise.all(singleResponsePromises); return eventResponseUtil.mutatingMapAggregateResponseErrorsToSpruceErrors(results, SpruceError); }); } assertValidEmitTargetAndPayload(eventName, payload) { const signature = this.getEventSignatureByName(eventName); if (signature.emitPayloadSchema) { try { validateSchemaValues(signature.emitPayloadSchema, payload !== null && payload !== void 0 ? payload : {}); } catch (err) { throw new SpruceError({ code: 'INVALID_PAYLOAD', originalError: err, eventName, }); } } else if (payload && this.eventContract) { throw new SpruceError({ code: 'UNEXPECTED_PAYLOAD', eventName, }); } } handleConfirmPinResponse(eventName, results) { var _a, _b; const payload = (_b = (_a = results === null || results === void 0 ? void 0 : results.responses) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.payload; if (eventName.search('confirm-pin') === 0 && (payload === null || payload === void 0 ? void 0 : payload.person)) { this.lastAuthOptions = { token: payload.token }; this.auth = { person: payload.person, }; } } getEventSignatureByName(eventName) { if (!this.eventContract) { return {}; } return eventContractUtil.getSignatureByName(this.eventContract, eventName); } setShouldAutoRegisterListeners(should) { this.shouldAutoRegisterListeners = should; } on(eventName, cb) { return __awaiter(this, void 0, void 0, function* () { var _a, _b, _c; this.registeredListeners.push([eventName, cb]); const isLocalEvent = this.isEventLocal(eventName); if (isLocalEvent) { return; } if (!isLocalEvent && this.shouldAutoRegisterListeners) { //@ts-ignore const results = yield this.emit('register-listeners::v2020_12_25', { payload: { events: [{ eventName }] }, }); if (results.totalErrors > 0) { const options = (_b = (_a = results.responses[0].errors) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 'UNKNOWN_ERROR'; throw AbstractSpruceError.parse(options, SpruceError); } } const listener = (targetAndPayload, ioCallback) => __awaiter(this, void 0, void 0, function* () { if (cb) { try { const results = yield cb(targetAndPayload); if (ioCallback) { ioCallback(results); } } catch (err) { let thisErr = err; if (ioCallback) { if (!(err instanceof AbstractSpruceError)) { thisErr = new SpruceError({ //@ts-ignore code: 'LISTENER_ERROR', fqen: eventName, friendlyMessage: err.message, originalError: err, }); } ioCallback({ errors: [thisErr.toObject()] }); } } } }); this.listenerMap.set(cb, listener); (_c = this.socket) === null || _c === void 0 ? void 0 : _c.on(eventName, //@ts-ignore listener); }); } isEventLocal(eventName) { return eventName === 'connection-status-change'; } off(eventName, cb) { return __awaiter(this, void 0, void 0, function* () { this.removeLocalListener(cb, eventName); return new Promise((resolve, reject) => { var _a; if (!this.socket || !this.auth || this.isEventLocal(eventName)) { resolve(0); return; } (_a = this.socket) === null || _a === void 0 ? void 0 : _a.emit('unregister-listeners::v2020_12_25', { payload: { fullyQualifiedEventNames: [eventName], }, }, (results) => { if (results.totalErrors > 0) { const err = AbstractSpruceError.parse(results.responses[0].errors[0], SpruceError); reject(err); } else { resolve(results.responses[0].payload.unregisterCount); } }); }); }); } removeLocalListener(cb, eventName) { var _a, _b; const listener = this.listenerMap.get(cb); if (listener) { this.listenerMap.delete(cb); (_a = this.socket) === null || _a === void 0 ? void 0 : _a.off(eventName, listener); } else { (_b = this.socket) === null || _b === void 0 ? void 0 : _b.removeAllListeners(eventName); } } getId() { return this.id; } disconnect() { return __awaiter(this, void 0, void 0, function* () { var _a; this.isManuallyDisconnected = true; if (this.isSocketConnected()) { //@ts-ignore (_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners(); yield new Promise((resolve) => { var _a, _b; (_a = this.socket) === null || _a === void 0 ? void 0 : _a.once('disconnect', () => { this.socket = undefined; resolve(undefined); }); (_b = this.socket) === null || _b === void 0 ? void 0 : _b.disconnect(); }); } return; }); } authenticate(options) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const { skillId, apiKey, token } = options; if (this.authPromise) { yield this.authPromise; return { skill: (_a = this.auth) === null || _a === void 0 ? void 0 : _a.skill, person: (_b = this.auth) === null || _b === void 0 ? void 0 : _b.person, }; } this.lastAuthOptions = options; this.allowNextEventToBeAuthenticate = true; //@ts-ignore this.authPromise = this.emit('authenticate::v2020_12_25', { payload: { skillId, apiKey, token, }, }); const results = yield this.authPromise; //@ts-ignore const { auth } = eventResponseUtil.getFirstResponseOrThrow(results); this.authRawResults = results; this.auth = auth; return { skill: auth.skill, person: auth.person, }; }); } isAuthenticated() { return !!this.auth; } isConnected() { return !this.isReAuthing && this.isSocketConnected(); } isSocketConnected() { var _a, _b; return (_b = (_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected) !== null && _b !== void 0 ? _b : false; } getProxyToken() { return this.proxyToken; } setProxyToken(token) { this.proxyToken = token; } registerProxyToken() { return __awaiter(this, void 0, void 0, function* () { const results = yield this.emit('register-proxy-token::v2020_12_25'); //@ts-ignore const { token } = eventResponseUtil.getFirstResponseOrThrow(results); this.setProxyToken(token); this.shouldRegisterProxyOnReconnect = true; return token; }); } getIsTestClient() { return false; } } MercurySocketIoClient.io = io; export default MercurySocketIoClient; export const authenticateFqen = 'authenticate::v2020_12_25';