87 lines
2.5 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|