diff --git a/src/services/bet.client.ts b/src/services/bet.client.ts index 676222e..3d1226c 100644 --- a/src/services/bet.client.ts +++ b/src/services/bet.client.ts @@ -3,6 +3,7 @@ import {Account} from "../domain/account"; import axios from "axios"; import AsyncLock from "async-lock"; import {toDecimal} from "./utils"; +import {CacheRepository} from "./cache.repository"; export class BetDTO { constructor( @@ -22,11 +23,8 @@ export class BetDTO { } export class BetClient { - - private static CACHE_MS = 1000; - private lastFetch = new Map; - private caches = new Map(); private lock = new AsyncLock(); + private cacheRepository = new CacheRepository(1000); constructor(private readonly host: string) { } @@ -36,8 +34,9 @@ export class BetClient { const now = Date.now(); // use cache - if (now < (this.lastFetch.get(game.id) ?? 0) + BetClient.CACHE_MS) { - return this.caches.get(game.id)!; + const cached: BetDTO[] | null = this.cacheRepository.get(game.id); + if (cached !== null) { + return cached } // fetch @@ -47,8 +46,7 @@ export class BetClient { return[] }); - this.lastFetch.set(game.id, now); - this.caches.set(game.id, bets); + this.cacheRepository.set(game.id, bets); return bets; }); } @@ -56,7 +54,7 @@ export class BetClient { private async fetch(game: Game, account: Account): Promise { const res = await axios.get(`${this.host}/allround/${game.contract.id}/${account.address}`) .then(res => res.status == 200 ? res.data : []); - + return res.map((bet: any) => new BetDTO( bet.side, bet.sideMultipleChoice, diff --git a/src/services/blockchain.client.ts b/src/services/blockchain.client.ts index 0760784..62e2211 100644 --- a/src/services/blockchain.client.ts +++ b/src/services/blockchain.client.ts @@ -1,5 +1,5 @@ -import { Game, GameType } from "../domain/game"; -import { Round, RoundPrice, RoundStatus } from "../domain/round"; +import {Game, GameType} from "../domain/game"; +import {Round, RoundPrice, RoundStatus} from "../domain/round"; import { PredictChoice, PredictPrice, @@ -7,12 +7,13 @@ import { Round as RoundPriceContract, RoundChoice, } from "../artifacts/ts"; -import { addressFromContractId, subContractId, web3 } from "@alephium/web3"; -import { CoinGeckoClient } from "./coinGeckoClient"; +import {addressFromContractId, subContractId, web3} from "@alephium/web3"; +import {CoinGeckoClient} from "./coinGeckoClient"; import AsyncLock from "async-lock"; -import { contractExists, toDecimal } from "./utils"; -import { PredictMultipleChoice } from "../artifacts/ts/PredictMultipleChoice"; -import { RoundMultipleChoice } from "../artifacts/ts/RoundMultipleChoice"; +import {contractExists, toDecimal} from "./utils"; +import {PredictMultipleChoice} from "../artifacts/ts/PredictMultipleChoice"; +import {RoundMultipleChoice} from "../artifacts/ts/RoundMultipleChoice"; +import {CacheRepository} from "./cache.repository"; export function getRoundContractId( predictAlphContractId: string, @@ -27,12 +28,9 @@ function getEpochPath(epoch: bigint) { } export class BlockchainClient { - private static CACHE_EXP_ROUND_MS = 2000; - private static CACHE_EXP_CURRENT_EPOCH_MS = 2000; - private lastFetchCurrentRound: number = 0; - private lastFetchCurrentEpoch: number = 0; - private cachedRounds: Map = new Map(); - private cachedCurrentEpoch: Map = new Map(); + private cacheRound = new CacheRepository(2000); + private cacheEpoch = new CacheRepository(2000); + private cacheContractExist = new CacheRepository(2000); private lock = new AsyncLock(); constructor( @@ -50,16 +48,11 @@ export class BlockchainClient { } async getCurrentRound(game: Game): Promise { - const cached = this.cachedCurrentEpoch.get(game.id); - const now = Date.now(); + const cached: bigint | null = this.cacheEpoch.get(game.id); let epoch: bigint = BigInt("0"); // Used cached current epoch from main contract state - if ( - cached !== undefined && - now - this.lastFetchCurrentEpoch < - BlockchainClient.CACHE_EXP_CURRENT_EPOCH_MS - ) { + if (cached !== null ) { epoch = cached; return this.getRound(epoch, game); } @@ -86,38 +79,38 @@ export class BlockchainClient { epoch = gameState.fields.epoch-BigInt(1) } - this.cachedCurrentEpoch.set(game.id, epoch); - this.lastFetchCurrentEpoch = now; + this.cacheEpoch.set(game.id, epoch); return this.getRound(epoch, game); } + private async contractExists(address: string): Promise { + const cached: boolean | null = this.cacheContractExist.get(address); + if (cached !== null) { + return cached; + } + + const exist = await contractExists(address); + this.cacheContractExist.set(address, exist); + return exist; + } + private static key(epoch: bigint, game: Game): string { return `${epoch}${game.id}`; } async getRound(epoch: bigint, game: Game): Promise { return this.lock.acquire("GETROUND", async () => { - const cache = this.cachedRounds.get(BlockchainClient.key(epoch, game)); + const cached: Round | null = this.cacheRound.get(BlockchainClient.key(epoch, game)); const now = Date.now(); - // if round finished and cached return it - if (cache !== undefined && cache.status !== RoundStatus.RUNNING) { - return cache; - } - - // if running round but cache not expire return it - if ( - cache !== undefined && - now - this.lastFetchCurrentRound < BlockchainClient.CACHE_EXP_ROUND_MS - ) { - return cache; + // if round cached return it + if (cached !== null) { + return cached; } const round = await this.fetchRound(epoch, game); - if (round.status === RoundStatus.RUNNING) { - this.lastFetchCurrentRound = now; - } - this.cachedRounds.set(BlockchainClient.key(epoch, game), round); + const notExpire = round.status !== RoundStatus.RUNNING; + this.cacheRound.set(BlockchainClient.key(epoch, game), round, notExpire); return round; }); } diff --git a/src/services/cache.repository.ts b/src/services/cache.repository.ts new file mode 100644 index 0000000..1bb88cd --- /dev/null +++ b/src/services/cache.repository.ts @@ -0,0 +1,29 @@ +import {BetDTO} from "./bet.client"; + +export class CacheRepository { + private static FAR_AWAY = 9999999999999; + + private caches = new Map(); + + constructor(private expirationMs: number) {} + + get(id: string): T | null { + if (!this.caches.has(id)) { + return null; + } + + const { expireAt, data } = this.caches.get(id)!; + if (Date.now() > expireAt) { + return null; + } + + return data; + } + + set(id: string, data: T, notExpire = false){ + const expireAt = notExpire + ? CacheRepository.FAR_AWAY + : Date.now() + this.expirationMs; + this.caches.set(id, {expireAt, data}); + } +} diff --git a/src/services/coinGeckoClient.ts b/src/services/coinGeckoClient.ts index ad56654..d633e6e 100644 --- a/src/services/coinGeckoClient.ts +++ b/src/services/coinGeckoClient.ts @@ -1,28 +1,32 @@ import axios from "axios"; +import {CacheRepository} from "./cache.repository"; export class CoinGeckoClient { - private static readonly ONE_MINUTE = 60*1000; - private lastValues: Map = new Map(); - private lastDates: Map = new Map(); + private cacheRepository = new CacheRepository(60 * 1000); constructor() { } async getPriceAlph(symbol: string): Promise { - if ((Date.now() - (this.lastDates.get(symbol) ?? 0)) > CoinGeckoClient.ONE_MINUTE) { - try { - const res = await axios.get( - `https://api.coingecko.com/api/v3/simple/price?ids=${symbol}&vs_currencies=usd`, - {validateStatus: (status) => true}, - ); - if (res.status === 200) { - this.lastValues.set(symbol, res.data[symbol].usd); - } - } catch (e) {} finally { - this.lastDates.set(symbol, Date.now()); + const cached: number | null = this.cacheRepository.get(symbol); + if (cached !== null) { + return cached; + } + + try { + const res = await axios.get( + `https://api.coingecko.com/api/v3/simple/price?ids=${symbol}&vs_currencies=usd`, + {validateStatus: (status) => true}, + ); + if (res.status === 200) { + this.cacheRepository.set(symbol, res.data[symbol].usd); + } else { + this.cacheRepository.set(symbol, 0); } + } catch (e) { + this.cacheRepository.set(symbol, 0); } - return this.lastValues.get(symbol) ?? 0; + return this.cacheRepository.get(symbol) ?? 0; } }