refactor: replace ignored numeric literals with constants

This commit is contained in:
2026-09-28 16:37:01 +03:00
committed by ksv741
parent a970eaaf7a
commit b78c215ff1
60 changed files with 510 additions and 283 deletions
+11 -5
View File
@@ -1,6 +1,12 @@
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { constants as httpStatus } from 'node:http2';
const MIN_PRICE_PERCENT_EXCLUSIVE = 0;
const FIRST_PAGE_OFFSET = 0;
const EXPECTED_CURSOR_ROWS = 1;
const EMPTY_PAGE_SIZE = 0;
const MIN_CURSOR_VALUE = 0;
const FALLBACK_PAGE_SIZE = 100;
const baseUrl = 'https://iss.moex.com/iss';
@@ -52,7 +58,7 @@ export class MoexClient {
if (
value === null ||
pricePercent === null ||
Number(pricePercent) <= 0 ||
Number(pricePercent) <= MIN_PRICE_PERCENT_EXCLUSIVE ||
!date ||
!/^\d{4}-\d{2}-\d{2}$/.test(date) ||
!Number.isFinite(Date.parse(date)) ||
@@ -114,7 +120,7 @@ export class MoexClient {
paginated = true,
): Promise<Record<string, unknown>[]> {
const rows: Record<string, unknown>[] = [];
let start = 0;
let start = FIRST_PAGE_OFFSET;
for (;;) {
const query = new URLSearchParams({
'iss.meta': 'off',
@@ -139,11 +145,11 @@ export class MoexClient {
const total = integer(cursor?.total);
const pageSize = integer(cursor?.pagesize);
if (
cursors.length !== 1 ||
cursors.length !== EXPECTED_CURSOR_ROWS ||
index !== start ||
total === null ||
pageSize === null ||
pageSize === 0 ||
pageSize === EMPTY_PAGE_SIZE ||
current.length !== Math.min(pageSize, total - start)
) {
throw new ServiceUnavailableException({ code: 'MOEX_UNAVAILABLE' });
@@ -207,5 +213,5 @@ function currencyText(value: unknown): null | string {
function integer(value: unknown): null | number {
const result = Number(numberText(value) ?? NaN);
return Number.isSafeInteger(result) && result >= 0 ? result : null;
return Number.isSafeInteger(result) && result >= MIN_CURSOR_VALUE ? result : null;
}
+8 -3
View File
@@ -2,6 +2,11 @@ import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { load } from 'cheerio';
import { LRUCache } from 'lru-cache';
import { ISO_DATE_LENGTH, ISO_DATE_START } from '../../common/time';
const EXPECTED_REGISTRY_TABLES = 1;
const RATING_COLUMN = 2;
const REGISTRY_CACHE_TTL_MS = 21_600_000;
const REQUEST_TIMEOUT_MS = 10_000;
const COLUMN_COUNT = 6;
@@ -55,7 +60,7 @@ export class NcrRatingsClient {
.map((_, cell) => $(cell).text().trim())
.get();
if (
table.length !== 1 ||
table.length !== EXPECTED_REGISTRY_TABLES ||
headings.length !== COLUMN_COUNT ||
headings[2] !== 'Рейтинг' ||
// eslint-disable-next-line security/detect-object-injection -- Column indexes are local numeric constants.
@@ -73,7 +78,7 @@ export class NcrRatingsClient {
const isin = cells.eq(ISIN_COLUMN).text().trim().toUpperCase();
// Unassigned issues cannot safely be matched to a held instrument.
if (!/^[A-Z]{2}[A-Z0-9]{9}\d$/.test(isin)) return;
const raw = cells.eq(2).text().replace(/\s+/g, ' ').trim();
const raw = cells.eq(RATING_COLUMN).text().replace(/\s+/g, ' ').trim();
const withdrawn = raw.toLowerCase() === 'рейтинг отозван';
if (
!withdrawn &&
@@ -93,7 +98,7 @@ export class NcrRatingsClient {
const date = `${dateParts[3]}-${dateParts[2]}-${dateParts[1]}`;
if (
!Number.isFinite(Date.parse(date)) ||
new Date(date).toISOString().slice(0, 10) !== date
new Date(date).toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH) !== date
) {
throw new Error('NCR date invalid');
}
+3 -1
View File
@@ -1,12 +1,14 @@
import { Decimal } from '../../common/decimal';
const MISSING_PROTO_COMPONENT = 0;
const NANO = 1_000_000_000n;
export type ProtoDecimal = { nano?: number | string; units?: number | string };
export function decimal(value: ProtoDecimal): Decimal {
return {
value: BigInt(value.units ?? 0) * NANO + BigInt(value.nano ?? 0),
value: BigInt(value.units ?? MISSING_PROTO_COMPONENT) * NANO + BigInt(value.nano ?? MISSING_PROTO_COMPONENT),
scale: 9,
};
}
@@ -13,6 +13,11 @@ import type { TbankInstrument } from './tbank.client';
import { PrismaService } from '../../prisma/prisma.service';
import { tbankSourceTtlMs } from './tbank-source.policy';
const FIRST_BATCH_OFFSET = 0;
const UNCHANGED_REVISION_INCREMENT = 0;
const REVISION_INCREMENT = 1;
const MISSING_DATA_REVISION = 0;
export type MetadataIdentity = {
figi?: null | string;
instrumentUid?: null | string;
@@ -59,7 +64,7 @@ export class InstrumentMetadataRepository {
),
];
for (
let offset = 0;
let offset = FIRST_BATCH_OFFSET;
offset < values.length;
offset += metadataReadBatchSize
) {
@@ -81,7 +86,7 @@ export class InstrumentMetadataRepository {
];
const instruments: MetadataRow[] = [];
for (
let offset = 0;
let offset = FIRST_BATCH_OFFSET;
offset < uids.length;
offset += metadataReadBatchSize
) {
@@ -229,8 +234,8 @@ export class InstrumentMetadataRepository {
...data,
dataRevision: {
increment: isDeepStrictEqual(previous?.metadata, payload) ?
0 :
1,
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
},
},
});
@@ -252,8 +257,8 @@ export class InstrumentMetadataRepository {
fetchedAt: now,
dataRevision: {
increment: isDeepStrictEqual(previous?.matchedUids, matchedUids) ?
0 :
1,
UNCHANGED_REVISION_INCREMENT :
REVISION_INCREMENT,
},
},
});
@@ -323,7 +328,7 @@ function selectMetadata(
dataRevision: used
.map(
(row) =>
`${row.lookup?.dataRevision ?? 0}:${row.instrument?.dataRevision ?? 0}`,
`${row.lookup?.dataRevision ?? MISSING_DATA_REVISION}:${row.instrument?.dataRevision ?? MISSING_DATA_REVISION}`,
)
.join('/'),
};
@@ -13,6 +13,10 @@ import { supportedTbankAccounts } from './tbank-account.policy';
import { tbankSourceNeedsRefresh } from './tbank-source.policy';
import { TbankTokenService } from './tbank-token.service';
const EXPECTED_UPDATED_CONNECTIONS = 1;
const MEMBERSHIP_REVISION_INCREMENT = 1;
const UNCHANGED_MEMBERSHIP_REVISION_INCREMENT = 0;
export type TbankSyncAttempt = {
attemptId: string;
id: string;
@@ -101,7 +105,7 @@ export class TbankAccessService {
},
data: { state: 'failed', errorCode: 'TBANK_UNAVAILABLE' },
});
return result.count === 1;
return result.count === EXPECTED_UPDATED_CONNECTIONS;
}
async tokenForAttempt(attempt: TbankSyncAttempt): Promise<null | string> {
@@ -184,7 +188,7 @@ export class TbankAccessService {
where: { userId_revision: { userId, revision } },
data: {
membershipAt: fetchedAt,
membershipRevision: { increment: changed ? 1 : 0 },
membershipRevision: { increment: changed ? MEMBERSHIP_REVISION_INCREMENT : UNCHANGED_MEMBERSHIP_REVISION_INCREMENT },
},
});
if (completeWork) {
@@ -1,4 +1,4 @@
import { MILLISECONDS_PER_SECOND } from '../../common/time';
import { ISO_DATE_LENGTH, ISO_DATE_START, MILLISECONDS_PER_SECOND } from '../../common/time';
import { TbankAccount } from './tbank.client';
const accountTypes = {
@@ -43,5 +43,5 @@ function openedOn(seconds: number | string | undefined): null | string {
if (seconds === undefined) return null;
const date = new Date(Number(seconds) * MILLISECONDS_PER_SECOND);
if (Number.isNaN(date.valueOf())) return null;
return date.toISOString().slice(0, 10);
return date.toISOString().slice(ISO_DATE_START, ISO_DATE_LENGTH);
}
+14 -7
View File
@@ -30,6 +30,13 @@ import { StructuredLogger } from '../../common/logging/structured-logger.service
import { MILLISECONDS_PER_SECOND } from '../../common/time';
import { Environment } from '../../config/env.validation';
const INITIAL_REQUEST_COUNT = 0;
const MIN_RATE_DELAY_MS = 1;
const FIRST_ATTEMPT = 1;
const ATTEMPT_INCREMENT = 1;
const NO_RPC_ATTEMPTS = 0;
const NEXT_DAY_OFFSET = 1;
const RATE_WINDOW_MS = 1_000;
const METADATA_MAX_ATTEMPTS = 3;
const RPC_MAX_ATTEMPTS = 2;
@@ -124,7 +131,7 @@ type TbankNamespace = {
class RateLimiter {
private readonly waiting: (() => void)[] = [];
private starts = 0;
private starts = INITIAL_REQUEST_COUNT;
private windowStartedAt = Date.now();
constructor(private readonly perSecond: number) {}
@@ -149,16 +156,16 @@ class RateLimiter {
const now = Date.now();
if (now - this.windowStartedAt >= RATE_WINDOW_MS) {
this.windowStartedAt = now;
this.starts = 0;
this.starts = INITIAL_REQUEST_COUNT;
}
while (this.starts < this.perSecond && this.waiting.length) {
this.starts += 1;
this.starts++;
this.waiting.shift()?.();
}
if (this.waiting.length && this.starts === this.perSecond) {
setTimeout(
() => this.drain(),
Math.max(1, RATE_WINDOW_MS - (now - this.windowStartedAt)),
Math.max(MIN_RATE_DELAY_MS, RATE_WINDOW_MS - (now - this.windowStartedAt)),
);
}
}
@@ -472,7 +479,7 @@ export class TbankClient {
): Promise<Response> {
const metadataCall = method === 'getInstrumentBy';
const maxAttempts = metadataCall ? METADATA_MAX_ATTEMPTS : RPC_MAX_ATTEMPTS;
for (let attempts = 1; attempts <= maxAttempts; attempts += 1) {
for (let attempts = FIRST_ATTEMPT; attempts <= maxAttempts; attempts += ATTEMPT_INCREMENT) {
const startedAt = performance.now();
const queueFull = this.queue.size >= MAX_QUEUE_SIZE;
try {
@@ -520,7 +527,7 @@ export class TbankClient {
this.logger.event('error', 'tbank.rpc.failed', {
operation: method,
...failureContext,
attempts: queueFull && !metadataCall ? 0 : attempts,
attempts: queueFull && !metadataCall ? NO_RPC_ATTEMPTS : attempts,
durationMs: Math.round(performance.now() - startedAt),
});
if (
@@ -564,6 +571,6 @@ export class TbankClient {
// Calendar owners supply inclusive UTC days; RPC timestamps use [from, next(to)).
function utcSeconds(date: string, nextDay = false): number {
const instant = new Date(`${date}T00:00:00Z`);
if (nextDay) instant.setUTCDate(instant.getUTCDate() + 1);
if (nextDay) instant.setUTCDate(instant.getUTCDate() + NEXT_DAY_OFFSET);
return Math.floor(instant.valueOf() / MILLISECONDS_PER_SECOND);
}
+5 -3
View File
@@ -1,5 +1,7 @@
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
const VERSION_SIZE_BYTES = 1;
const KEY_SIZE_BYTES = 32;
const VERSION = 1;
@@ -34,15 +36,15 @@ export class TbankCrypto {
decrypt(payload: string): string {
const bytes = Buffer.from(payload, 'base64');
if (bytes[0] !== VERSION || bytes.length < 1 + NONCE_SIZE + TAG_SIZE) {
if (bytes[0] !== VERSION || bytes.length < VERSION_SIZE_BYTES + NONCE_SIZE + TAG_SIZE) {
throw new Error('Invalid encrypted token');
}
const nonce = bytes.subarray(1, 1 + NONCE_SIZE);
const nonce = bytes.subarray(VERSION_SIZE_BYTES, VERSION_SIZE_BYTES + NONCE_SIZE);
const tag = bytes.subarray(-TAG_SIZE);
const decipher = createDecipheriv('aes-256-gcm', this.key, nonce);
decipher.setAuthTag(tag);
return Buffer.concat([
decipher.update(bytes.subarray(1 + NONCE_SIZE, -TAG_SIZE)),
decipher.update(bytes.subarray(VERSION_SIZE_BYTES + NONCE_SIZE, -TAG_SIZE)),
decipher.final(),
]).toString('utf8');
}