feat: enrich ledger events with logo metadata
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user