@softvisio/core
Version:
Softisio core
208 lines (151 loc) • 5.22 kB
JavaScript
import Events from "#lib/events";
import sql from "#lib/sql";
import Counter from "#lib/threads/counter";
import Mutex from "#lib/threads/mutex";
import Signal from "#lib/threads/signal";
import TelegramBotRequest from "./request.js";
const DEFAULT_LINIT = 100;
const SQL = {
"unlockUpdates": sql`UPDATE telegram_bot_update SET locked = FALSE WHERE telegram_bot_id = ?`,
"getUpdates": sql`
WITH cte AS (
SELECT id FROM telegram_bot_update WHERE telegram_bot_id = ? AND locked = FALSE ORDER BY update_id LIMIT ?
)
UPDATE
telegram_bot_update
SET
locked = TRUE
FROM
cte
WHERE
telegram_bot_update.id = cte.id
RETURNING
telegram_bot_update.id,
type,
data
`.prepare(),
"deleteUpdate": sql`DELETE FROM telegram_bot_update WHERE id = ?`.prepare(),
};
export default class TelegramBotProcessor {
#bot;
#start = false;
#started = false;
#abortController = new AbortController();
#activityCounter = new Counter();
#mutex;
#chatMutexes = new Mutex.Set();
#onTelegramUpdate;
constructor ( bot, onTelegramUpdate ) {
this.#bot = bot;
this.#onTelegramUpdate = onTelegramUpdate;
if ( this.bot.app.cluster ) {
this.#mutex = this.bot.app.cluster.mutexes.get( "telegram/bot/process-updates/" + this.bot.id );
}
}
// properties
get bot () {
return this.#bot;
}
get dbh () {
return this.#bot.dbh;
}
get isStarted () {
return this.#started;
}
// public
start () {
this.#start = true;
this.#run();
return result( 200 );
}
async stop () {
this.#start = false;
if ( !this.#started ) return;
this.#abortController.abort();
return this.#activityCounter.wait();
}
// private
async #run () {
if ( !this.#start ) return;
if ( this.#started ) return;
this.#started = true;
this.#activityCounter.value++;
const hasUpdatesSignal = new Signal(),
events = new Events().link( this.dbh ).on( `telegram/telegram-bot-update/${ this.bot.id }/create`, data => {
hasUpdatesSignal.broadcast( data.chat_id );
} );
START: while ( true ) {
let signal = this.#abortController.signal;
// aborted
if ( signal.aborted ) break START;
await this.dbh.waitConnect( signal );
if ( signal.aborted ) break START;
signal = AbortSignal.any( [ signal, this.dbh.abortSignal ] );
// mutex
if ( this.#mutex ) {
// unlock mutex in case or repeat
await this.#mutex.unlock();
// lock mutex
await this.#mutex.lock( { signal } );
// aborted
if ( signal.aborted ) break START;
signal = AbortSignal.any( [ signal, this.#mutex.abortSignal ] );
}
// unlock updates
const res = await this.dbh.do( SQL.unlockUpdates, [ this.id ] );
if ( !res.ok ) continue START;
// clear users cache, because user state is not symc
this.bot.users.clear();
// start updates cycle
GET_UPDATES: while ( true ) {
// aborted
if ( signal.aborted ) continue START;
const updates = await this.dbh.select( SQL.getUpdates, [ this.bot.id, DEFAULT_LINIT ] );
if ( !updates.ok ) continue START;
// if no updates awailable - wait for event
if ( !updates.data ) {
const chatId = await hasUpdatesSignal.wait( { signal } );
if ( chatId ) {
// XXX check, if can start new chat thread
}
continue GET_UPDATES;
}
for ( const update of updates.data ) {
this.#processUpdate( update, signal );
}
}
}
events.clear();
// unlock mutex
if ( this.#mutex ) await this.#mutex.unlock();
// stop
this.#started = false;
this.#abortController = new AbortController();
this.#activityCounter.value--;
if ( this.#start ) this.#run();
}
async #processUpdate ( update, signal ) {
const req = new TelegramBotRequest( this.bot, signal, update );
const mutex = this.#chatMutexes.get( req.chat?.id ?? -1 );
this.#activityCounter.value++;
ABORT: {
await mutex.lock( signal );
// aborted
if ( signal.aborted ) break ABORT;
try {
await this.#onTelegramUpdate( req );
}
catch ( e ) {
console.log( "Telegram error:", e );
}
// delete update
while ( true ) {
if ( signal.aborted ) break;
const res = await this.dbh.do( SQL.deleteUpdate, [ req.id ] );
if ( res.ok ) break;
}
mutex.unlock();
}
this.#activityCounter.value--;
}
}