Files
backend/src/tbank/instrument-metadata.service.ts
T

87 lines
2.5 KiB
TypeScript

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<string, Promise<TbankInstrument | null>>();
constructor(
private readonly tbank: TbankClient,
private readonly logger: StructuredLogger,
) {}
async resolve(
token: string,
instrumentUid: string | null | undefined,
figi: string | null | undefined,
): Promise<TbankInstrument | null> {
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<TbankInstrument | null>,
): Promise<TbankInstrument | null> {
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<TbankInstrument | null>,
): Promise<TbankInstrument | null> {
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;
}
}
}