diff --git a/src/env.ts b/src/env.ts index 34ea364f..1a728a41 100644 --- a/src/env.ts +++ b/src/env.ts @@ -27,6 +27,8 @@ export const ngRokToken: string = process.env.NGROK_TOKEN || ""; export const authorTelegramAccount: string = process.env.AUTHOR_TELEGRAM_ACCOUNT || ""; +export const cacheSize: number = Number(process.env.CACHE_SIZE) || 50; + export const googleApi = { projectId: process.env.GOOGLE_PROJECT_ID || "", clientEmail: process.env.GOOGLE_CLIENT_EMAIL || "", diff --git a/src/scripts/dev.ts b/src/scripts/dev.ts index d2cd0e1d..aa06f7cd 100644 --- a/src/scripts/dev.ts +++ b/src/scripts/dev.ts @@ -9,6 +9,7 @@ import { ngRokToken, authorTelegramAccount, appVersion, + cacheSize, } from "../env"; import { Logger } from "../logger"; import { VoiceConverterOptions } from "../recognition/types"; @@ -39,7 +40,8 @@ export function run(): void { dbStat.statUrl, dbStat.appId, dbStat.appKey, - dbStat.masterKey + dbStat.masterKey, + cacheSize ); const bot = new TelegramBotModel(telegramBotApi, converter, stat).setAuthor( authorTelegramAccount diff --git a/src/scripts/start.ts b/src/scripts/start.ts index c1938cf1..dbe473da 100644 --- a/src/scripts/start.ts +++ b/src/scripts/start.ts @@ -11,6 +11,7 @@ import { authorTelegramAccount, appVersion, ngRokToken, + cacheSize, } from "../env"; import { getVoiceConverterInstance, @@ -42,7 +43,8 @@ export function run(): void { dbStat.statUrl, dbStat.appId, dbStat.appKey, - dbStat.masterKey + dbStat.masterKey, + cacheSize ); const bot = new TelegramBotModel(telegramBotApi, converter, stat).setAuthor( authorTelegramAccount diff --git a/src/statistic/cache.ts b/src/statistic/cache.ts new file mode 100644 index 00000000..3a35159a --- /dev/null +++ b/src/statistic/cache.ts @@ -0,0 +1,116 @@ +import { Logger } from "../logger"; +import { sSuffix } from "../text"; + +const logger = new Logger("cache"); + +export class CacheProvider { + private cache: Data[] = []; + + constructor( + private readonly cacheSize: number, + private readonly idKey: UniqId + ) { + if (!this.hasCacheEnabled()) { + logger.warn( + `Cache size is ${logger.y( + sSuffix("item", cacheSize) + )}, so the cache is turned off for ${logger.y(idKey)}` + ); + } else { + logger.info( + `Cache size is ${logger.y( + sSuffix("item", cacheSize) + )} initialized for ${logger.y(idKey)}` + ); + } + } + + public addItem(item: Data): void { + if (!this.hasCacheEnabled()) { + return; + } + + if (!item[this.idKey]) { + logger.error( + `The item with ${this.idKey}=${ + item[this.idKey] + } can not have empty index value. Caching skipped`, + new Error("Cache item can not have empty index value") + ); + return; + } + + const existingItem = this.cache.find( + (cItem) => cItem[this.idKey] === item[this.idKey] + ); + if (existingItem) { + logger.warn( + `The item with ${this.idKey}=${ + item[this.idKey] + } is already exists. Removing old data from the cache` + ); + + this.removeItem(item[this.idKey]); + } + + const newCacheData = [...this.cache, item]; + + if (newCacheData.length > this.cacheSize) { + logger.warn( + `Cache storage exceeds the limit of ${logger.y( + sSuffix("item", this.cacheSize) + )} and have a size of ${logger.y( + sSuffix("item", newCacheData.length) + )}. Old records will be removed to keep storage under the limit` + ); + } + + this.cache = newCacheData.slice( + Math.max(newCacheData.length - this.cacheSize, 0) + ); + + logger.info( + `Added cache item with ${this.idKey}=${item[this.idKey]}. Cache size=${ + this.cache.length + }` + ); + } + + public getItem(idValue: Data[UniqId]): Data | null { + if (!this.hasCacheEnabled()) { + return null; + } + + const cachedItem = this.cache.find( + (cItem) => cItem[this.idKey] === idValue + ); + + if (!cachedItem) { + logger.info( + `Did not find the item with ${this.idKey}=${idValue} in cache` + ); + return null; + } + + logger.info( + `Found the item with ${this.idKey}=${idValue} in cache. Skipping DB request` + ); + return cachedItem; + } + + public removeItem(idValue: Data[UniqId]): void { + if (!this.hasCacheEnabled()) { + return; + } + + this.cache = this.cache.filter((cItem) => cItem[this.idKey] !== idValue); + + logger.info( + `Removed cache item for ${this.idKey}=${idValue}. Cache size=${this.cache.length}` + ); + } + + private hasCacheEnabled(): boolean { + return this.cacheSize > 0; + } +} diff --git a/src/statistic/index.ts b/src/statistic/index.ts index 2b3d07a5..e87f3a8f 100644 --- a/src/statistic/index.ts +++ b/src/statistic/index.ts @@ -9,9 +9,16 @@ export class StatisticApi { statUrl: string, appId: string, appKey: string, - masterKey: string + masterKey: string, + cacheSize: number ) { this.node = new NodeStatisticApi(statUrl, appId, appKey, masterKey); - this.usage = new UsageStatisticApi(statUrl, appId, appKey, masterKey); + this.usage = new UsageStatisticApi( + statUrl, + appId, + appKey, + masterKey, + cacheSize + ); } } diff --git a/src/statistic/types.ts b/src/statistic/types.ts index 01314052..fa887303 100644 --- a/src/statistic/types.ts +++ b/src/statistic/types.ts @@ -1,3 +1,5 @@ +import { LanguageCode } from "../recognition/types"; + export enum NodeStatKey { Active = "active", SelfUrl = "selfUrl", @@ -11,3 +13,8 @@ export enum UsageStatKey { UserName = "user", CreatedAt = "createdAt", } + +export interface UsageStatCache { + [UsageStatKey.ChatId]: number; + [UsageStatKey.LangId]: LanguageCode; +} diff --git a/src/statistic/usage.ts b/src/statistic/usage.ts index 9779cf53..7717e3e5 100644 --- a/src/statistic/usage.ts +++ b/src/statistic/usage.ts @@ -1,21 +1,28 @@ import Parse from "parse/node"; import { LanguageCode } from "../recognition/types"; import { Logger } from "../logger"; -import { UsageStatKey } from "./types"; +import { UsageStatCache, UsageStatKey } from "./types"; +import { CacheProvider } from "./cache"; const logger = new Logger("db"); export class UsageStatisticApi { private readonly dbClass = "BotStat"; + private cache: CacheProvider; constructor( statUrl: string, appId: string, appKey: string, - masterKey: string + masterKey: string, + cacheSize: number ) { Parse.serverURL = statUrl; Parse.initialize(appId, appKey, masterKey); + this.cache = new CacheProvider( + cacheSize, + UsageStatKey.ChatId + ); } public updateUsageCount(chatId: number, username: string): Promise { @@ -25,15 +32,34 @@ export class UsageStatisticApi { } public updateLanguage(chatId: number, lang: LanguageCode): Promise { + this.cache.removeItem(chatId); + return this.getStatIfNotExists(chatId).then((stat) => this.updateStatLanguage(stat, lang) ); } public getLanguage(chatId: number, username: string): Promise { - return this.getStatIfNotExists(chatId, username).then((stat) => - stat.get(UsageStatKey.LangId) - ); + const cachedItem = this.cache.getItem(chatId); + if (cachedItem) { + return Promise.resolve(cachedItem[UsageStatKey.LangId]); + } + + return this.getStatIfNotExists(chatId, username).then((stat) => { + this.addCacheItem(stat); + return stat.get(UsageStatKey.LangId); + }); + } + + private addCacheItem(instance: Parse.Object): void { + const langId = instance.get(UsageStatKey.LangId); + const chatId = instance.get(UsageStatKey.ChatId); + const cachedItem: UsageStatCache = { + [UsageStatKey.LangId]: langId, + [UsageStatKey.ChatId]: chatId, + }; + + this.cache.addItem(cachedItem); } private getStatIfNotExists(