import { Injectable } from '@nestjs/common'; import { LRUCache } from 'lru-cache'; import { StructuredLogger } from '../common/logging/structured-logger.service'; import { TbankClient, type TbankInstrument } from './tbank.client'; const metadataForMs = 86_400_000; const maxCachedInstruments = 500; const missingInstrument = Symbol('missingInstrument'); @Injectable() export class InstrumentMetadataService { private readonly metadata = new LRUCache< string, TbankInstrument | typeof missingInstrument >({ max: maxCachedInstruments, ttl: metadataForMs, }); private readonly pending = new Map>(); constructor( private readonly tbank: TbankClient, private readonly logger: StructuredLogger, ) {} async resolve( token: string, instrumentUid: string | null | undefined, figi: string | null | undefined, ): Promise { const uid = instrumentUid?.trim(); if (uid) { const byUid = await this.lookup(`uid:${uid}`, () => this.tbank.getInstrumentByUid(token, uid), ); if (byUid) return byUid; } const instrumentFigi = figi?.trim(); return instrumentFigi ? this.lookup(`figi:${instrumentFigi}`, () => this.tbank.getInstrumentByFigi(token, instrumentFigi), ) : null; } private async lookup( key: string, fetch: () => Promise, ): Promise { const cached = this.metadata.get(key); if (cached !== undefined) return cached === missingInstrument ? null : cached; const pending = this.pending.get(key); if (pending) return pending; const request = this.fetch(key, fetch); this.pending.set(key, request); try { return await request; } finally { this.pending.delete(key); } } private async fetch( key: string, request: () => Promise, ): Promise { try { const value = await request(); this.metadata.set(key, value ?? missingInstrument); this.logger.event('debug', 'instrument-metadata.resolved', { found: Boolean(value?.name?.trim() || value?.ticker?.trim()), }); return value; } catch (error) { this.logger.event('warn', 'instrument-metadata.failed', { code: error instanceof Error && 'response' in error ? ((error as { response?: { code?: string } }).response?.code ?? 'UNKNOWN') : 'UNKNOWN', }); return null; } } }