feat(state): replace in-memory Map with SQLite-backed stateService (#59)
- Create src/services/stateService.js with get/set/delete/has API - Create migration 004_user_states.js (chat_id PK, state_data JSON, updated_at) - TTL of 24 hours — expired states auto-deleted - Cleanup job runs every hour (setInterval) - Replace src/context/userStates.js Map with async stateService proxy - Add await to all 45 userStates.get/set/delete/has calls across 13 files - Add initStates() call in index.js startup sequence - All state survives bot restarts now 18 files changed, 172 insertions, 46 deletions
This commit is contained in:
@@ -1,2 +1,11 @@
|
||||
const userStates = new Map();
|
||||
import { get, set, del, has, initStates } from '../services/stateService.js';
|
||||
|
||||
const userStates = {
|
||||
get: (chatId) => get(chatId),
|
||||
set: (chatId, value) => set(chatId, value),
|
||||
delete: (chatId) => del(chatId),
|
||||
has: (chatId) => has(chatId),
|
||||
initStates,
|
||||
};
|
||||
|
||||
export default userStates;
|
||||
@@ -84,7 +84,7 @@ export default class AdminDumpHandler {
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
userStates.set(chatId, { action: 'upload_database_dump' });
|
||||
await userStates.set(chatId, { action: 'upload_database_dump' });
|
||||
|
||||
await bot.editMessageText(
|
||||
'Please upload database dump',
|
||||
@@ -121,7 +121,7 @@ export default class AdminDumpHandler {
|
||||
static async handleDumpImport(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'upload_database_dump') {
|
||||
return false;
|
||||
@@ -145,7 +145,7 @@ export default class AdminDumpHandler {
|
||||
|
||||
const statistics = await this.getDumpStatistic();
|
||||
await bot.sendMessage(chatId, JSON.stringify(statistics, null, 2));
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} else {
|
||||
await bot.sendMessage(chatId, 'Please upload a valid .zip file.');
|
||||
return true;
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class AdminLocationHandler {
|
||||
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
|
||||
userStates.set(chatId, { action: 'add_location' });
|
||||
await userStates.set(chatId, { action: 'add_location' });
|
||||
|
||||
await bot.editMessageText(
|
||||
'Please enter the location in the following format:\nCountry|City|District',
|
||||
@@ -30,7 +30,7 @@ export default class AdminLocationHandler {
|
||||
|
||||
static async handleLocationInput(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'add_location') {
|
||||
return false;
|
||||
@@ -87,7 +87,7 @@ export default class AdminLocationHandler {
|
||||
throw new Error('Failed to insert location');
|
||||
}
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
await db.runAsync('ROLLBACK');
|
||||
|
||||
@@ -166,7 +166,7 @@ export default class AdminLocationHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
|
||||
try {
|
||||
const locations = await db.allAsync(`
|
||||
@@ -362,6 +362,6 @@ export default class AdminLocationHandler {
|
||||
}
|
||||
);
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ export default class AdminUserHandler {
|
||||
}
|
||||
);
|
||||
|
||||
userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
||||
await userStates.set(chatId, { action: "edit_bonus_balance", telegram_id: telegramId });
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Error in handleEditUserBalance');
|
||||
await bot.sendMessage(chatId, 'Error loading user wallets. Please try again.');
|
||||
@@ -462,7 +462,7 @@ export default class AdminUserHandler {
|
||||
}
|
||||
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'edit_bonus_balance') {
|
||||
return false;
|
||||
@@ -482,6 +482,6 @@ export default class AdminUserHandler {
|
||||
await bot.sendMessage(chatId, 'Something went wrong');
|
||||
}
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default class CategoryAddHandler {
|
||||
|
||||
static async handleCategoryInput(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!state || !state.action?.startsWith('add_category_')) {
|
||||
return false;
|
||||
@@ -51,7 +51,7 @@ export default class CategoryAddHandler {
|
||||
}
|
||||
);
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
if (error.code === 'SQLITE_CONSTRAINT') {
|
||||
await bot.sendMessage(chatId, 'This category already exists in this location.');
|
||||
@@ -72,7 +72,7 @@ export default class CategoryAddHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const locationId = callbackQuery.data.replace('add_category_', '');
|
||||
|
||||
userStates.set(chatId, {action: `add_category_${locationId}`});
|
||||
await userStates.set(chatId, {action: `add_category_${locationId}`});
|
||||
|
||||
const location = await LocationService.getLocationById(locationId);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export default class CategoryEditHandler {
|
||||
|
||||
static async handleCategoryUpdate(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!state || !state.action?.startsWith('edit_category_')) {
|
||||
return false;
|
||||
@@ -48,7 +48,7 @@ export default class CategoryEditHandler {
|
||||
}
|
||||
);
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Error updating category');
|
||||
await bot.sendMessage(chatId, 'Ошибка обновления категории. Пожалуйста, попробуйте снова.');
|
||||
@@ -65,7 +65,7 @@ export default class CategoryEditHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const [locationId, categoryId] = callbackQuery.data.replace('edit_category_', '').split('_');
|
||||
|
||||
userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` });
|
||||
await userStates.set(chatId, { action: `edit_category_${locationId}_${categoryId}` });
|
||||
|
||||
await bot.editMessageText(
|
||||
'Пожалуйста, введите новое название категории:',
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class CreateHandler {
|
||||
const jsonExample = JSON.stringify(sampleProducts, null, 2);
|
||||
const message = `To add product, send a JSON file with product in the following format:\n\n<pre>${jsonExample}</pre>\n\nProduct must have all the fields shown above.\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
||||
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
action: 'import_products',
|
||||
locationId,
|
||||
categoryId
|
||||
|
||||
@@ -52,7 +52,7 @@ export default class DistrictHandler {
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const [country, city, district] = callbackQuery.data.replace('prod_district_', '').split('_');
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
|
||||
try {
|
||||
const location = await LocationService.getLocation(country, city, district);
|
||||
|
||||
@@ -9,7 +9,7 @@ import logger from '../../../utils/logger.js';
|
||||
export default class EditImportHandler {
|
||||
static async handleProductEditImport(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!state || state.action !== 'edit_product') {
|
||||
return false;
|
||||
@@ -84,7 +84,7 @@ export default class EditImportHandler {
|
||||
}
|
||||
});
|
||||
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Error importing products');
|
||||
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class EditStartHandler {
|
||||
const jsonExample = JSON.stringify(sampleProduct, null, 2);
|
||||
const message = `To edit product, send a JSON file with product data:\n\n<pre>${jsonExample}</pre>\n\nYou can either:\n1. Send the JSON as text\n2. Upload a .json file`;
|
||||
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
action: 'edit_product',
|
||||
locationId,
|
||||
categoryId,
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class ImportHandler {
|
||||
|
||||
static async handleProductImport(msg) {
|
||||
const chatId = msg.chat.id;
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
if (!state || state.action !== 'import_products') return false;
|
||||
if (!isAdmin(msg.from.id)) {
|
||||
await bot.sendMessage(chatId, 'Unauthorized access.');
|
||||
@@ -46,7 +46,7 @@ export default class ImportHandler {
|
||||
inline_keyboard: [[{ text: '« Back to Products', callback_data: `prod_category_${state.locationId}_${state.categoryId}` }]]
|
||||
}
|
||||
});
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'Error importing products');
|
||||
await bot.sendMessage(chatId, 'Error importing products. Please check the data and try again.');
|
||||
|
||||
@@ -76,7 +76,7 @@ export default class ViewHandler {
|
||||
}
|
||||
}
|
||||
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
msgToDelete: [photoMessage.message_id, hiddenPhotoMessage.message_id]
|
||||
})
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export default class UserProductHandler {
|
||||
}
|
||||
|
||||
// Сохраняем текстовое представление локации в состоянии пользователя
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
location: `${country}_${city}_${district}`
|
||||
});
|
||||
|
||||
@@ -189,7 +189,7 @@ export default class UserProductHandler {
|
||||
await bot.deleteMessage(chatId, messageId);
|
||||
|
||||
// Получаем состояние пользователя
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
// Удаляем сообщение с фотографией, если оно существует
|
||||
if (state && state.photoMessageId) {
|
||||
@@ -243,7 +243,7 @@ export default class UserProductHandler {
|
||||
);
|
||||
|
||||
// Сохраняем состояние пользователя
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
...state,
|
||||
action: 'viewing_category',
|
||||
categoryId,
|
||||
@@ -332,7 +332,7 @@ export default class UserProductHandler {
|
||||
await bot.deleteMessage(chatId, messageId);
|
||||
|
||||
// Получаем состояние пользователя
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
// Удаляем сообщение с фотографией, если оно существует
|
||||
if (state?.photoMessageId) {
|
||||
@@ -390,7 +390,7 @@ export default class UserProductHandler {
|
||||
});
|
||||
|
||||
// Сохраняем ID сообщения с фотографией и ID сообщения с товаром в состояние пользователя
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
action: 'buying_product',
|
||||
productId,
|
||||
quantity: 1,
|
||||
@@ -408,7 +408,7 @@ export default class UserProductHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const productId = callbackQuery.data.replace('increase_quantity_', '');
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
try {
|
||||
const product = await ProductService.getProductById(productId);
|
||||
@@ -428,7 +428,7 @@ export default class UserProductHandler {
|
||||
const newQuantity = Math.min(currentQuantity + 1, product.quantity_in_stock);
|
||||
|
||||
// Update state
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
...state,
|
||||
quantity: newQuantity
|
||||
});
|
||||
@@ -468,7 +468,7 @@ export default class UserProductHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const messageId = callbackQuery.message.message_id;
|
||||
const productId = callbackQuery.data.replace('decrease_quantity_', '');
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
try {
|
||||
const product = await ProductService.getProductById(productId)
|
||||
@@ -488,7 +488,7 @@ export default class UserProductHandler {
|
||||
const newQuantity = Math.max(currentQuantity - 1, 1);
|
||||
|
||||
// Update state
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
...state,
|
||||
quantity: newQuantity
|
||||
});
|
||||
@@ -528,7 +528,7 @@ export default class UserProductHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const productId = callbackQuery.data.replace('buy_product_', '');
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
try {
|
||||
const user = await UserService.getUserByTelegramId(telegramId);
|
||||
@@ -622,7 +622,7 @@ export default class UserProductHandler {
|
||||
);
|
||||
|
||||
// Сохранение ID сообщения с фотографией в состояние пользователя
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
...state,
|
||||
photoMessageId: state?.photoMessageId || null,
|
||||
purchaseMessageId: purchaseMessage.message_id
|
||||
@@ -637,7 +637,7 @@ export default class UserProductHandler {
|
||||
const chatId = callbackQuery.message.chat.id;
|
||||
const telegramId = callbackQuery.from.id;
|
||||
const [walletType, productId, quantity] = callbackQuery.data.replace('pay_with_', '').split('_');
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
|
||||
if (!Validators.isValidWalletType(walletType)) {
|
||||
await bot.sendMessage(chatId, 'Invalid wallet type.');
|
||||
@@ -670,7 +670,7 @@ export default class UserProductHandler {
|
||||
const balance = user.total_balance + user.bonus_balance;
|
||||
|
||||
if (totalPrice > balance) {
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
await bot.editMessageText(`Not enough money`, {
|
||||
chat_id: chatId,
|
||||
message_id: callbackQuery.message.message_id,
|
||||
@@ -738,7 +738,7 @@ export default class UserProductHandler {
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
|
||||
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
action: 'viewing_purchase',
|
||||
purchaseId,
|
||||
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
||||
|
||||
@@ -95,7 +95,7 @@ export default class UserPurchaseHandler {
|
||||
}
|
||||
|
||||
// Удаляем сообщение с Hidden Photo, если оно существует
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
if (state?.hiddenPhotoMessageId) {
|
||||
try {
|
||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
||||
@@ -114,7 +114,7 @@ export default class UserPurchaseHandler {
|
||||
});
|
||||
|
||||
// Удаляем состояние пользователя
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
} catch (e) {
|
||||
logger.error({ err: e }, 'Error in handlePurchaseListPage');
|
||||
await bot.sendMessage(chatId, 'Error loading purchase history. Please try again.');
|
||||
@@ -168,7 +168,7 @@ export default class UserPurchaseHandler {
|
||||
const category = await CategoryService.getCategoryById(product.category_id);
|
||||
|
||||
// Удаляем старое сообщение с Hidden Photo, если оно существует
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
if (state?.hiddenPhotoMessageId) {
|
||||
try {
|
||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
||||
@@ -219,7 +219,7 @@ export default class UserPurchaseHandler {
|
||||
await bot.deleteMessage(chatId, callbackQuery.message.message_id);
|
||||
|
||||
// Сохраняем ID сообщения с Hidden Photo в состояние пользователя
|
||||
userStates.set(chatId, {
|
||||
await userStates.set(chatId, {
|
||||
action: 'viewing_purchase',
|
||||
purchaseId,
|
||||
hiddenPhotoMessageId: hiddenPhotoMessage ? hiddenPhotoMessage.message_id : null
|
||||
@@ -279,7 +279,7 @@ export default class UserPurchaseHandler {
|
||||
await bot.deleteMessage(chatId, messageId);
|
||||
|
||||
// Удаляем Hidden Photo, если оно существует
|
||||
const state = userStates.get(chatId);
|
||||
const state = await userStates.get(chatId);
|
||||
if (state?.hiddenPhotoMessageId) {
|
||||
try {
|
||||
await bot.deleteMessage(chatId, state.hiddenPhotoMessageId);
|
||||
@@ -289,7 +289,7 @@ export default class UserPurchaseHandler {
|
||||
}
|
||||
|
||||
// Удаляем состояние пользователя
|
||||
userStates.delete(chatId);
|
||||
await userStates.delete(chatId);
|
||||
|
||||
// Открываем список покупок для пользователя
|
||||
await this.showPurchases({ chat: { id: chatId }, from: { id: callbackQuery.from.id } });
|
||||
|
||||
@@ -8,8 +8,11 @@ import adminHandler from './handlers/adminHandlers/adminHandler.js';
|
||||
import callbackRouter from './router/callbackRouter.js';
|
||||
import messageRouter from './router/messageRouter.js';
|
||||
|
||||
import { initStates } from './services/stateService.js';
|
||||
|
||||
await runMigrations();
|
||||
await cleanUpInvalidForeignKeys();
|
||||
await initStates();
|
||||
|
||||
const logDebug = (action, functionName) => {
|
||||
logger.debug({ action, functionName }, 'Button Press');
|
||||
|
||||
10
src/migrations/004_user_states.js
Normal file
10
src/migrations/004_user_states.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export default async function migration004(db) {
|
||||
await db.runAsync(`
|
||||
CREATE TABLE IF NOT EXISTS user_states (
|
||||
chat_id TEXT PRIMARY KEY,
|
||||
state_data TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
console.log('Migration 004: user_states table created');
|
||||
}
|
||||
@@ -38,6 +38,7 @@ export async function runMigrations() {
|
||||
(await import('./001_initial_schema.js')).default,
|
||||
(await import('./002_add_columns.js')).default,
|
||||
(await import('./003_add_indexes.js')).default,
|
||||
(await import('./004_user_states.js')).default,
|
||||
];
|
||||
|
||||
for (let i = currentVersion; i < migrations.length; i++) {
|
||||
|
||||
103
src/services/stateService.js
Normal file
103
src/services/stateService.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import db from '../config/database.js';
|
||||
import logger from '../utils/logger.js';
|
||||
|
||||
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export async function initStates() {
|
||||
if (initialized) return;
|
||||
|
||||
await db.runAsync(`
|
||||
CREATE TABLE IF NOT EXISTS user_states (
|
||||
chat_id TEXT PRIMARY KEY,
|
||||
state_data TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
initialized = true;
|
||||
logger.info('user_states table initialized');
|
||||
|
||||
setInterval(cleanExpired, CLEANUP_INTERVAL_MS);
|
||||
cleanExpired();
|
||||
}
|
||||
|
||||
function serialize(value) {
|
||||
if (value === undefined) return null;
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function deserialize(json) {
|
||||
if (!json) return undefined;
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(chatId) {
|
||||
const row = await db.getAsync(
|
||||
'SELECT state_data, updated_at FROM user_states WHERE chat_id = ?',
|
||||
[String(chatId)]
|
||||
);
|
||||
|
||||
if (!row) return undefined;
|
||||
|
||||
if (Date.now() - row.updated_at > TTL_MS) {
|
||||
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return deserialize(row.state_data);
|
||||
}
|
||||
|
||||
export async function set(chatId, value) {
|
||||
const data = serialize(value);
|
||||
const now = Date.now();
|
||||
|
||||
await db.runAsync(
|
||||
`INSERT INTO user_states (chat_id, state_data, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET state_data = ?, updated_at = ?`,
|
||||
[String(chatId), data, now, data, now]
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function del(chatId) {
|
||||
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
||||
}
|
||||
|
||||
export async function has(chatId) {
|
||||
const row = await db.getAsync(
|
||||
'SELECT updated_at FROM user_states WHERE chat_id = ?',
|
||||
[String(chatId)]
|
||||
);
|
||||
|
||||
if (!row) return false;
|
||||
|
||||
if (Date.now() - row.updated_at > TTL_MS) {
|
||||
await db.runAsync('DELETE FROM user_states WHERE chat_id = ?', [String(chatId)]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function cleanExpired() {
|
||||
const cutoff = Date.now() - TTL_MS;
|
||||
const result = await db.runAsync(
|
||||
'DELETE FROM user_states WHERE updated_at < ?',
|
||||
[cutoff]
|
||||
);
|
||||
if (result.changes > 0) {
|
||||
logger.info({ expiredCount: result.changes }, 'Cleaned expired user states');
|
||||
}
|
||||
}
|
||||
|
||||
const userStates = { get, set, delete: del, has, initStates, cleanExpired };
|
||||
export default userStates;
|
||||
Reference in New Issue
Block a user