wingbot
Version:
Enterprise Messaging Bot Conversation Engine
1,091 lines (942 loc) • 35.2 kB
JavaScript
/**
* @author David Menger
*/
'use strict';
const EventEmitter = require('events');
const crypto = require('crypto');
const { MemoryStateStorage } = require('./tools');
const Responder = require('./Responder');
const Request = require('./Request');
const Ai = require('./Ai');
const ReturnSender = require('./ReturnSender');
const { prepareState, mergeState, isUserInteraction } = require('./utils/stateVariables');
const LLM = require('./LLM');
const LLMMockProvider = require('./LLMMockProvider');
/** @typedef {import('./wingbot/CustomEntityDetectionModel').Intent} Intent */
/** @typedef {import('./ReducerWrapper')} ReducerWrapper */
/** @typedef {import('./Router')} Router */
/** @typedef {import('./BuildRouter')} BuildRouter */
/** @typedef {import('./analytics/consts').TrackingCategory} TrackingCategory */
/** @typedef {import('./analytics/consts').TrackingType} TrackingType */
/** @typedef {import('./analytics/consts').ResponseFlag} ResponseFlag */
/** @typedef {import('./LLM').LLMConfiguration} LLMConfiguration */
/**
* @typedef {object} AutoTypingConfig
* @prop {number} time - duration
* @prop {number} perCharacters - number of characters
* @prop {number} minTime - minimum writing time
* @prop {number} maxTime - maximum writing time
*/
/**
* @typedef {object} Plugin
* @prop {Function} [processMessage]
* @prop {Function} [beforeAiPreload]
* @prop {Function} [beforeProcessMessage]
* @prop {Function} [afterProcessMessage]
*/
/**
* @typedef {object} TrackingEvent
* @prop {TrackingType} type
* @prop {TrackingCategory} category
* @prop {string} action
* @prop {string} label
* @prop {number} value
*/
/**
* @typedef {object} TrackingObject
* @prop {TrackingEvent[]} events
*/
/**
* @typedef {object} InteractionEvent
* @prop {Request} req
* @prop {string[]} actions
* @prop {string|null} lastAction
* @prop {object} state
* @prop {object} data
* @prop {string|null} skill
* @prop {string|null} prevSkill
* @prop {string|null} pathname
* @prop {TrackingObject} tracking - deprecated
* @prop {TrackingEvent[]} events
* @prop {ResponseFlag|null} flag
* @prop {boolean} nonInteractive
* @prop {string[]} responseTexts
* @prop {boolean} doNotTrack
*/
/**
* @callback IInteractionHandler
* @param {InteractionEvent} params
* @returns {Promise|void}
*/
/**
* Interaction event fired after every interaction
*
* @event Processor#interaction
* @type {InteractionEvent}
*/
/**
* @typedef {object} ILogger
* @prop {Function} log
* @prop {Function} warn
* @prop {Function} error
*/
/**
*
* @template {ReducerWrapper|Router|BuildRouter} R
* @typedef {object} ProcessorOptions
* @prop {string} [appUrl] - url basepath for relative links
* @prop {IStateStorage} [stateStorage] - chatbot state storage
* @prop {object} [tokenStorage] - frontend token storage
* @prop {Function} [translator] - text translate function
* @prop {number} [timeout] - chat sesstion lock duration (30000)
* @prop {number} [justUpdateTimeout] - simple read and write lock (1000)
* @prop {number} [waitForLockedState] - wait when state is locked (12000)
* @prop {number} [retriesWhenWaiting] - number of attampts (6)
* @prop {Function} [nameFromState] - override the name translator
* @prop {boolean|AutoTypingConfig} [autoTyping] - enable or disable automatic typing
* @prop {ILogger} [log] - console like error logger
* @prop {object} [defaultState] - default chat state
* @prop {boolean} [autoSeen] - send seen automatically
* @prop {number} [redirectLimit] - maximum number of redirects at single request
* @prop {string} [secret] - Secret for calling orchestrator API
* @prop {string} [apiUrl] - Url for calling orchestrator API
* @prop {Function} [fetch] - Fetch function for calling orchestrator API
* @prop {number} [sessionDuration] - Session duration for analytic purposes
* @prop {LLMConfiguration} [llm] - LLM model configuration
* @prop {Preloader<R>} [preloader]
*/
/**
* @typedef {object} IntentAction
* @prop {string} action
* @prop {Intent} intent
* @prop {number} sort
* @prop {number} [score]
* @prop {boolean} local
* @prop {boolean} aboveConfidence
* @prop {boolean} [winner]
* @prop {object} meta
* @prop {string} title
* @prop {string} [meta.targetAppId]
* @prop {string|null} [meta.targetAction]
*/
/**
* @typedef {object} IStateStorage
* @prop {Function} saveState
* @prop {Function} getState
* @prop {Function} getOrCreateAndLock
*/
/**
* @template {ReducerWrapper|Router|BuildRouter} T
* @callback Preloader
* @param {T} router
* @param {Ai} ai
* @returns {Promise<void>}
*/
function NAME_FROM_STATE (state) {
if (state.user && state.user.firstName) {
return `${state.user.firstName} ${state.user.lastName}`;
}
if (state.user && state.user.name) {
return `${state.user.name}`;
}
return null;
}
const MAX_TS = 9999999999999;
const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
function toBase (number) {
let result = '';
let integer = number;
do {
result = CHARS[integer % 62] + result;
integer = Math.floor(integer / 62);
} while (integer > 0);
return result;
}
/**
* Messaging event processor
*
* @template {ReducerWrapper|Router|BuildRouter} T
* @class
* @fires Processor#interaction
*/
class Processor extends EventEmitter {
/**
* Creates an instance of Processor
*
* @param {T} reducer
* @param {ProcessorOptions<T>} [options] - processor options
*
* @memberOf Processor
*/
constructor (reducer, options = {}) {
super();
this.options = {
appUrl: '',
stateStorage: new MemoryStateStorage(),
tokenStorage: null,
translator: (w) => w,
timeout: 30000,
waitForLockedState: 12000,
retriesWhenWaiting: 6,
justUpdateTimeout: 1000,
log: console,
defaultState: {},
autoTyping: false,
autoSeen: false,
redirectLimit: 20,
nameFromState: NAME_FROM_STATE,
sessionDuration: 1800000 // 30 minutes
};
Object.assign(this.options, options);
this._preloaders = options.preloader ? [options.preloader] : [];
this.reducer = reducer;
/**
* @type {IStateStorage}
*/
this.stateStorage = this.options.stateStorage;
this.tokenStorage = this.options.tokenStorage;
/**
* @type {Plugin[]}
* @private
*/
this._plugins = [];
this._middlewares = [];
/** @type {IInteractionHandler[]} */
this._onInteractionHandlers = [];
}
/**
* Register asynchronous interaction handler function
*
* @param {IInteractionHandler} handler
* @returns {this}
*/
onInteraction (handler) {
this._onInteractionHandlers.push(handler);
return this;
}
/**
*
* @param {Plugin} plugin
*/
plugin (plugin) {
this._plugins.push(plugin);
// @ts-ignore
if (typeof plugin.middleware === 'function') {
this.options.log.warn('Middleware functions in Processor plugins are deprecated');
// @ts-ignore
this._middlewares.push(plugin.middleware());
}
}
_createPostBack (postbackAcumulator, req, res, features) {
const postBack = (action, inputData = {}, dispatchSync = false) => {
let data = inputData;
if (typeof data === 'function') {
// @ts-ignore
data = data();
}
if (dispatchSync) {
let previousAction;
return Promise.resolve(data)
.then((resolvedData) => {
let reduceResult;
Object.assign(resolvedData, { _localpostback: true });
previousAction = req.setAction(action, resolvedData);
if (typeof this.reducer === 'function') {
// @ts-ignore
reduceResult = this.reducer(req, res, postBack);
} else {
reduceResult = this.reducer.reduce(req, res, postBack);
}
return reduceResult;
})
.then((reduceResult) => {
req.setAction(previousAction);
return reduceResult;
});
}
res.finalMessageSent = true;
if (data instanceof Promise) {
postbackAcumulator.push(data
.then((result) => ({
action,
data: Object.assign(result || {}, { _localpostback: true }),
features
})));
} else {
Object.assign(data, { _localpostback: true });
postbackAcumulator.push({ action, data, features });
}
return Promise.resolve(undefined);
};
return postBack;
}
async _reportError (pageId, err, event, senderId = null) {
if (err.code === 204) {
this.options.log.log(`nothing sent: ${err.message}`, event);
return;
}
if (err.code !== 403) {
this.options.log.error(err, event);
}
if (!senderId) {
return;
}
try {
const state = await this._loadState(senderId, pageId, this.options.justUpdateTimeout);
Object.assign(state, {
lastSendError: new Date(),
lastErrorMessage: err.message,
lastErrorCode: err.code,
lastInteraction: new Date()
});
await this.stateStorage.saveState(state);
} catch (e) {
this.options.log.error(`failed to log error "${err.message}" because: ${e.message}`, e, err);
}
}
async _preload () {
return Promise.all([
Ai.ai.preloadDetectors(),
// @ts-ignore
this.reducer && typeof this.reducer.preload === 'function'
// @ts-ignore
? this.reducer.preload()
: Promise.resolve(),
...this._preloaders
.map((preloader) => preloader(this.reducer, Ai.ai))
]).catch((e) => this.options.log.error('preload error', e))
// mute log errors
.catch(() => {});
}
async processMessage (
message,
pageId = null,
messageSender = new ReturnSender(
{},
message && message.sender && message.sender.id,
message
),
responderData = {}
) {
const preloadPromise = this._preload();
try {
for (const plugin of this._plugins) {
if (typeof plugin.processMessage !== 'function') continue;
const res = await plugin.processMessage(message, pageId, messageSender);
if (typeof res !== 'object' || typeof res.status !== 'number') {
throw new Error('The plugin should always return the status code');
}
if (res.status === 200) {
await preloadPromise;
return res;
}
}
} catch (e) {
await preloadPromise;
const { code = 500 } = e;
await this._reportError(pageId, e, message);
return { status: code };
}
if (typeof message !== 'object' || message === null
|| !((message.sender && message.sender.id) || message.optin)
|| !(message.message || message.referral || message.optin
|| typeof message.intent === 'string'
|| (Array.isArray(message.entities) && message.entities.length !== 0)
|| message.pass_thread_control || message.postback
|| message.set_context
|| message.context
|| message.take_thread_control)) {
this.options.log.warn('message should be a valid messaging object', message);
await preloadPromise;
return { status: 400 };
}
const senderId = message.sender && message.sender.id;
// ignore messages from the page
if (pageId === senderId && senderId) {
await preloadPromise;
return { status: 304 };
}
/** @type {LLMConfiguration} */
const llmOptions = {
provider: new LLMMockProvider(),
...this.options.llm
};
if (typeof messageSender.logPrompt === 'function') {
Object.assign(llmOptions, {
logger: messageSender
});
}
const llm = new LLM(llmOptions, Ai.ai);
const result = await this
._dispatch(message, pageId, messageSender, responderData, preloadPromise, llm);
messageSender.defer(preloadPromise, this.options.log);
return result;
}
async _dispatch (message, pageId, messageSender, responderData, preloadPromise, llm) {
let req;
let res;
let state;
let data;
const errorHandler = (...e) => this._reportError(pageId, ...e);
try {
({
req, res, data, state
} = await this
._processMessage(
message,
pageId,
messageSender,
responderData,
llm,
preloadPromise
));
messageSender.defer(this._emitInteractionEvent(req, res, messageSender, state, data));
return messageSender.finished(req, res, null, errorHandler);
} catch (e) {
req = req || e.req;
res = res || e.res;
return messageSender.finished(req, res, e, errorHandler);
}
}
/**
*
* @param {Request} req
* @param {Responder} res
* @param {ReturnSender} messageSender
* @param {object} state
* @param {object} data
* @returns {Promise}
*/
_emitInteractionEvent (req, res, messageSender, state, data) {
const doNotTrack = data._initialEventShouldNotBeTracked === true;
const { _lastAction: lastAction = null, '§pathname': pathname = null } = req.state;
const actions = messageSender.visitedInteractions;
const skill = typeof res.newState._trackAsSkill === 'undefined'
? (req.state._trackAsSkill || null)
: res.newState._trackAsSkill;
const prevSkill = typeof res.newState._trackPrevSkill === 'undefined'
? (req.state._trackPrevSkill || null)
: res.newState._trackPrevSkill;
const { events = [] } = messageSender.tracking;
const event = {
doNotTrack,
responseTexts: messageSender.responseTexts,
req,
actions,
lastAction,
state,
data,
skill,
prevSkill,
pathname,
tracking: messageSender.tracking,
events,
flag: res.senderMeta.flag,
nonInteractive: !isUserInteraction(req) || doNotTrack
};
return Promise.allSettled([
...this._onInteractionHandlers
.map((handler) => Promise.resolve(handler(event))
.catch((e) => {
this.options.log.error('Executing Processor interaction event failed', e);
})),
new Promise((resolve) => {
process.nextTick(() => {
try {
this.emit('interaction', event);
} catch (e) {
this.options.log.error('Firing Processor interaction event failed', e);
}
resolve();
});
})
]);
}
/**
* Get matching NLP intents
*
* @param {string|object} text
* @param {string} [pageId]
* @param {string} [lang]
* @param {boolean} [allowEmptyAction]
* @returns {Promise<IntentAction[]>}
*/
async aiActionsForText (text, pageId = 'none', lang = null, allowEmptyAction = false) {
try {
// @ts-ignore
if (this.reducer && typeof this.reducer.preload === 'function') {
// @ts-ignore
await this.reducer.preload();
}
const request = typeof text === 'string'
? Request.text('none', text)
: text;
// @ts-ignore
const req = new Request(request, { lang }, pageId, this.reducer.globalIntents);
await Ai.ai.preloadAi(req);
const actions = req.aiActions();
if (actions.length === 0 && allowEmptyAction && req.intents.length > 0) {
const [intent] = req.intents;
return [
{
intent,
action: null,
sort: intent.score,
local: false,
aboveConfidence: intent.score >= Ai.ai.confidence,
meta: {},
title: null
}
];
}
return actions
.map((a) => ({
...a,
title: typeof a.title === 'function'
? a.title(req)
: a.title
}));
} catch (e) {
this.options.log.error('failed to fetch intent actions', e);
return [];
}
}
async _processMessage (
message,
pageId,
messageSender,
responderData,
llm,
preloadPromise = null,
senderMeta = null
) {
let senderId = message.sender && message.sender.id;
const fromEvent = !!preloadPromise;
// prevent infinite cycles
let { _actionCount: actionCount = 0 } = responderData;
actionCount++;
if (actionCount >= this.options.redirectLimit) {
return Promise.reject(new Error(`Reached ${actionCount} redirects on ${JSON.stringify(message)}. Check cyclic redirects.`));
}
Object.assign(responderData, { _actionCount: actionCount, _fromInitialEvent: fromEvent });
if (responderData._initialEventWasntTracked) {
Object.assign(responderData, {
_fromUntrackedInitialEvent: true, _initialEventWasntTracked: false
});
} else if (responderData._fromUntrackedInitialEvent) {
Object.assign(responderData, { _fromUntrackedInitialEvent: false });
}
const postbackAcumulator = [];
const [originalState, token] = await Promise.all([
this._loadState(senderId, pageId, this.options.timeout),
this._getOrCreateToken(senderId, pageId)
]);
let stateObject = originalState;
let req;
let res;
// let emitPromise = Promise.resolve();
try {
// ensure the request was not processed
const timestamp = message.timestamp || Date.now();
if (fromEvent
&& stateObject.lastTimestamps && message.timestamp
&& stateObject.lastTimestamps.indexOf(timestamp) !== -1) {
throw Object.assign(new Error('Message has been already processed'), { code: 204 });
}
// update state before run
const modState = await messageSender.modifyStateAfterLoad(stateObject, this);
if (modState) {
const modStateCopy = { ...modState };
if (modStateCopy.state) {
Object.assign(stateObject.state, modStateCopy.state);
delete modStateCopy.state;
}
Object.assign(stateObject, modStateCopy);
}
// prepare request and responder
let { state } = stateObject;
let configuration = {};
if ('getConfiguration' in this.reducer) {
configuration = this.reducer.getConfiguration();
if (configuration instanceof Promise) {
configuration = await configuration;
}
}
// @ts-ignore
req = new Request(
message,
state,
pageId,
// @ts-ignore
this.reducer.globalIntents,
{
apiUrl: this.options.apiUrl,
secret: this.options.secret,
fetch: this.options.fetch,
appId: responderData.appId
},
configuration
);
// process session
if (fromEvent) {
let {
_sct: sessionCount = 0,
_sid: sessionId = null,
_sst: sessionStart = 0,
_sts: sessionTs = (state._segStamp || 0),
_snew: sessionCreated
} = state;
const interactive = isUserInteraction(req);
const sessionExpired = interactive
&& (sessionTs + this.options.sessionDuration) < timestamp;
if (sessionExpired || !sessionId) {
sessionStart = timestamp;
sessionTs = timestamp;
sessionId = Processor._createSessionId(req.pageId, req.senderId, timestamp);
sessionCount++;
sessionCreated = true;
} else {
sessionCreated = false;
if (interactive) {
sessionTs = timestamp;
}
}
Object.assign(state, {
_sct: sessionCount,
_sid: sessionId,
_sst: sessionStart,
_sts: sessionTs,
_snew: sessionCreated
});
} /* else {
Object.assign(state, {
_snew: false
});
} */
prepareState(state, fromEvent, state._snew && fromEvent);
const features = [
...(this.options.features || []),
...req.features
];
const options = {
...this.options,
state: Object.freeze({ ...state }),
features,
pageId
};
res = new Responder(
senderId,
messageSender,
token,
options,
responderData,
configuration,
senderMeta,
llm
);
const postBack = this._createPostBack(postbackAcumulator, req, res, features);
let continueDispatching = true;
// run plugins
for (const plugin of this._plugins) {
if (typeof plugin.beforeAiPreload !== 'function') continue;
let out = plugin.beforeAiPreload(req, res);
if (out instanceof Promise) out = await out;
if (!out) { // end
continueDispatching = false;
break;
}
}
await Promise.all([
preloadPromise,
Ai.ai.preloadAi(req, res)
]);
// @deprecated backward compatibility
const aByAi = req.actionByAi();
if (aByAi && aByAi !== req.action()) {
res.setBookmark(aByAi);
}
// process setState
const setState = req.getSetState(req.AI_SETSTATE.EXCLUDE_WITH_SET_ENTITIES);
await Ai.ai.processSetStateEntities(req, setState);
const afterSetState = req
.getSetState(req.AI_SETSTATE.EXCLUDE_WITHOUT_SET_ENTITIES, setState);
const aiSetState = req.getSetState(req.AI_SETSTATE.ONLY);
Object.assign(req.state, setState, aiSetState, afterSetState);
res.setState({ ...setState, ...aiSetState, ...afterSetState });
// attach sender meta
const data = req.actionData();
if (typeof data._senderMeta === 'object') {
res._senderMeta = { ...data._senderMeta };
}
if (continueDispatching) {
// process plugin middlewares
for (const plugin of this._plugins) {
if (typeof plugin.beforeProcessMessage !== 'function') continue;
let out = plugin.beforeProcessMessage(req, res);
if (out instanceof Promise) out = await out;
if (!out) { // end
continueDispatching = false;
break;
}
}
}
if (continueDispatching) {
// process plugin middlewares
for (const middleware of this._middlewares) {
let out = middleware(req, res, postBack);
if (out instanceof Promise) out = await out;
if (out === null) { // end
continueDispatching = false;
break;
}
}
}
if (continueDispatching) {
if (this.options.autoSeen
&& res.isResponseType() // do not send seen, if it's a campaign
&& (!req.isReferral() || req.action())
&& fromEvent) {
res.seen();
}
// process the event
const reduceResult = this.reducer.reduce(req, res, postBack);
if (reduceResult instanceof Promise) { // note the result can be undefined
await reduceResult;
}
if (fromEvent) {
messageSender.defer(this._emitEvent(req, res), this.options.log);
}
}
if (continueDispatching) {
for (const plugin of this._plugins) {
if (typeof plugin.afterProcessMessage !== 'function') continue;
await Promise.resolve(plugin.afterProcessMessage(req, res));
}
}
// update state
const senderUpdate = await messageSender.modifyStateBeforeStore(req, res);
if (senderUpdate && senderUpdate.senderId) {
senderId = senderUpdate.senderId; // eslint-disable-line prefer-destructuring
stateObject = await this
._loadState(senderId, pageId, this.options.justUpdateTimeout);
state = stateObject.state; // eslint-disable-line prefer-destructuring
}
const lastInTurnover = postbackAcumulator.length === 0;
state = mergeState(state, req, res, senderUpdate, fromEvent, lastInTurnover);
let lastTimestamps = stateObject.lastTimestamps || [];
if (message.timestamp) {
lastTimestamps = lastTimestamps.slice(-9);
lastTimestamps.push(timestamp);
}
Object.assign(stateObject, {
state,
lastTimestamps,
lastInteraction: new Date(),
off: false,
name: this.options.nameFromState(state)
});
if (senderUpdate) {
delete senderUpdate.state;
Object.assign(stateObject, senderUpdate);
}
} catch (e) {
await this.stateStorage.saveState(originalState);
// await emitPromise;
Object.assign(e, { req, res });
throw e;
}
if (postbackAcumulator.length === 0) {
messageSender.defer(this.stateStorage.saveState(stateObject));
return {
req, res, data: res.data, state: stateObject.state
};
}
try {
await this.stateStorage.saveState(stateObject);
// process postbacks
const {
state = stateObject.state,
data = res.data
} = await this._processPostbacks(
postbackAcumulator,
senderId,
pageId,
messageSender,
responderData,
llm,
res.senderMeta
);
// await emitPromise; // probably has been resolved this time
return {
req, res, data, state
};
} catch (e) {
Object.assign(e, { req, res });
throw e;
}
}
static _shakeShort (str, outputLength) {
const senderHash = crypto.createHash('shake256', { outputLength })
.update(str)
.digest('hex');
return senderHash.match(/[a-f0-9]{1,13}/g)
.map((v) => toBase(parseInt(v, 16)))
.join('');
}
static _createSessionId (pageId, senderId, timestamp = Date.now()) {
const senderShort = Processor._shakeShort(`${senderId}|${pageId}}`, 12);
const rand = toBase(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
const randTS = toBase(Math.floor(Date.now() % 10000));
const ts = toBase(Math.floor(MAX_TS - timestamp));
// console.log({
// base: `${ts}.${senderShort}`.length,
// ts,
// senderShort,
// randTS,
// rand,
// randL: rand.length
// });
return `${ts}.${senderShort}`
.padEnd(28, randTS)
.padEnd(32, rand);
}
/**
*
* @private
* @param {Request} req
* @param {Responder} res
*/
_emitEvent (req, res) {
const { _lastAction: lastAction = null } = req.state;
let { _lastAction: act = null } = res.newState;
act = act || req.action();
const shouldNotTrack = res.data._initialEventShouldNotBeTracked === true;
if (shouldNotTrack) {
return Promise.resolve();
}
const trackingSkill = typeof res.newState._trackAsSkill === 'undefined'
? (req.state._trackAsSkill || null)
: res.newState._trackAsSkill;
const params = [
req.senderId,
act,
req.text(),
req,
lastAction,
false,
trackingSkill,
res
];
return new Promise((resolve) => {
process.nextTick(() => {
try {
this.emit('event', ...params);
} catch (e) {
this.options.log.error('Firing Processor event failed', e);
}
resolve();
});
});
}
_processPostbacks (
postbackAcumulator,
senderId,
pageId,
messageSender,
responderData,
llm,
senderMeta
) {
return postbackAcumulator.reduce((promise, postback) => promise
.then(() => postback)
.then(({ action, data = {}, features }) => {
let request;
if (typeof action === 'object') {
request = action;
} else {
request = Request.postBack(senderId, action, data);
}
Object.assign(request, { features });
return this._processMessage(
request,
pageId,
messageSender,
responderData,
llm,
null,
senderMeta
);
}), Promise.resolve({}));
}
_getOrCreateToken (senderId, pageId) {
if (!senderId || !this.tokenStorage) {
return null;
}
return this.tokenStorage.getOrCreateToken(senderId, pageId)
.then((token) => token.token);
}
_loadState (senderId, pageId, lock) {
if (!senderId) {
return Promise.resolve({
state: { ...this.options.defaultState }
});
}
return new Promise((resolve, reject) => {
let retries = this.options.retriesWhenWaiting;
const onLoad = (res) => {
if (!res) {
if (retries-- < 0) {
this.stateStorage
.getState(senderId, pageId)
.then((state) => {
this.options.log.warn(`Locked state: ${senderId}, lock: ${lock}, at ${Date.now()}`, state);
})
.catch(() => {});
reject(new Error(`Loading state timed out: another event is blocking it (${senderId}, lock: ${lock})`));
return;
}
this._model(senderId, pageId, lock, retries)
.then(onLoad)
.catch(reject);
} else {
resolve(res);
}
};
onLoad();
});
}
_wait () {
const wait = Math.round(this.options.waitForLockedState / this.options.retriesWhenWaiting);
return new Promise((r) => setTimeout(() => r(null), wait));
}
_model (senderId, pageId, timeout, retries) {
const { defaultState } = this.options;
const now = Date.now();
const warnIfItTookTooLong = (r = null) => {
const duration = Date.now() - now;
if (duration >= (this.options.waitForLockedState / 2)) {
try {
this.options.log.warn(`Loading state (${senderId}) for timeout ${timeout} took too long (${duration}ms).`);
} catch (e) {
// noop
}
}
return r;
};
return this.stateStorage
.getOrCreateAndLock(senderId, pageId, { ...defaultState }, timeout)
.then(warnIfItTookTooLong)
.catch((err) => {
warnIfItTookTooLong();
if (!err || err.code !== 11000) {
this.options.log.error('Bot processor load error', err);
}
if (retries === 0) {
return null;
}
return this._wait();
});
}
}
// console.log(Processor._createSessionId('p', 's'));
module.exports = Processor;