refactor: replace ignored numeric literals with constants
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 2m17s
Conventional commits / commits (pull_request) Successful in 31s
Backend security / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 1m56s

This commit is contained in:
2026-09-28 11:24:59 +03:00
parent 9b3558f96c
commit 9cc9fbdcff
60 changed files with 510 additions and 283 deletions
@@ -11,6 +11,7 @@ import { accountHistory } from '../../investment-sync-plan';
import {
type OperationInterval,
operationIntervals,
operationsSyncPriority,
planOperationRead,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
@@ -19,8 +20,9 @@ import {
type OperationAttempt,
} from '../infrastructure/brokerage-operations.repository';
const UPSTREAM_RANGE_MARGIN_SECONDS = 1;
const operationsPageSize = 1000;
const priorities = { screen: 1, history: 2 } as const;
@Injectable()
export class BrokerageOperationsSyncService {
@@ -129,7 +131,7 @@ export class BrokerageOperationsSyncService {
userId: string,
intervals: OperationInterval[],
atomic: boolean,
priority: keyof typeof priorities = 'screen',
priority: keyof typeof operationsSyncPriority = 'screen',
revision?: string,
): Promise<null | string> {
const connection = await this.access.local(userId);
@@ -162,7 +164,7 @@ export class BrokerageOperationsSyncService {
atomic,
this.runningAttempts,
// eslint-disable-next-line security/detect-object-injection -- The priority belongs to the internal priority union.
priorities[priority],
operationsSyncPriority[priority],
);
if (!requested) return null;
const { id, attemptId, revision } = requested.work;
@@ -257,12 +259,12 @@ export class BrokerageOperationsSyncService {
// Upstream inclusivity is not assumed; storage selects exact [from,to).
from: {
seconds: String(
Math.floor(interval.from.valueOf() / MILLISECONDS_PER_SECOND) - 1,
Math.floor(interval.from.valueOf() / MILLISECONDS_PER_SECOND) - UPSTREAM_RANGE_MARGIN_SECONDS,
),
},
to: {
seconds: String(
Math.ceil(interval.to.valueOf() / MILLISECONDS_PER_SECOND) + 1,
Math.ceil(interval.to.valueOf() / MILLISECONDS_PER_SECOND) + UPSTREAM_RANGE_MARGIN_SECONDS,
),
},
...(interval.instrumentUid ?
@@ -19,6 +19,7 @@ import {
} from '../../investment-sync-plan';
import {
operationIntervalsOverlap,
operationsSyncPriority,
planOperationRead,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
@@ -30,6 +31,14 @@ import {
} from '../presentation/brokerage-operation.dto';
import { BrokerageOperationsSyncService } from './brokerage-operations-sync.service';
const EXCLUSIVE_END_OFFSET_MS = 1;
const FIRST_RESULT_OFFSET = 0;
const MISSING_MEMBERSHIP_REVISION = 0;
const STALE_FRESHNESS_MS = 0;
const UNKNOWN_AS_OF_MS = 0;
const LAST_ROW_INDEX = -1;
const UNIX_EPOCH_MS = 0;
type LocalOperationsCursor = {
accountId: string;
at: string;
@@ -106,7 +115,7 @@ export class BrokerageOperationsService {
screen: 'operations',
accountId,
from: from.toISOString(),
to: new Date(to.valueOf() - 1).toISOString(),
to: new Date(to.valueOf() - EXCLUSIVE_END_OFFSET_MS).toISOString(),
},
{
accounts: selected,
@@ -244,7 +253,7 @@ export class BrokerageOperationsService {
const knownBoundary = accounts.every(
(account) => account.openedOn !== null,
);
const rows = saved?.rows.slice(0, limit) ?? [];
const rows = saved?.rows.slice(FIRST_RESULT_OFFSET, limit) ?? [];
const instruments = await this.metadata.savedMany(
rows.map((row) => row.payload as TbankOperationItem),
);
@@ -278,7 +287,7 @@ export class BrokerageOperationsService {
}
const membershipMeta = investmentResourceMeta({
resourceKey: 'accounts/membership',
dataRevision: String(saved?.connection.membershipRevision ?? 0),
dataRevision: String(saved?.connection.membershipRevision ?? MISSING_MEMBERSHIP_REVISION),
availability: available ? 'complete' : 'missing',
fetchedAt: saved?.connection.membershipAt ?? null,
freshForMs: tbankSourceTtlMs.membership,
@@ -296,7 +305,7 @@ export class BrokerageOperationsService {
'missing',
fetchedAt: lastSuccessfulAt,
freshForMs: plan.intervals.some((interval) => interval.stale) ?
0 :
STALE_FRESHNESS_MS :
Number.POSITIVE_INFINITY,
work: work ?? saved?.membershipWork ?? null,
dependencies: [membershipMeta, ...metadataDependencies],
@@ -321,7 +330,7 @@ export class BrokerageOperationsService {
(interval) =>
interval.accountId === account.accountId,
)
.map((interval) => interval.asOf?.valueOf() ?? 0),
.map((interval) => interval.asOf?.valueOf() ?? UNKNOWN_AS_OF_MS),
),
),
),
@@ -329,11 +338,11 @@ export class BrokerageOperationsService {
null,
historyBoundary: knownBoundary ? 'known' : 'unknown',
historyState: saved?.work.some(
(work) => work.state === 'running' && work.priority > 1,
(work) => work.state === 'running' && work.priority > operationsSyncPriority.screen,
) ?
'loading' :
saved?.work.some(
(work) => work.state === 'failed' && work.priority > 1,
(work) => work.state === 'failed' && work.priority > operationsSyncPriority.screen,
) ?
'failed' :
knownBoundary &&
@@ -346,7 +355,7 @@ export class BrokerageOperationsService {
'missing',
},
});
const last = rows.at(-1);
const last = rows.at(LAST_ROW_INDEX);
const hasNext = Boolean(saved && saved.rows.length > limit);
return {
data:
@@ -448,7 +457,7 @@ export class BrokerageOperationsService {
userId,
revision: connection.revision,
accountId,
from: new Date(0),
from: new Date(UNIX_EPOCH_MS),
to: new Date(),
categories: ['income'],
filterKey: `income/${accountId}`,
@@ -486,7 +495,7 @@ export class BrokerageOperationsService {
const now = new Date();
const membership = investmentResourceMeta({
resourceKey: 'accounts/membership',
dataRevision: String(saved?.connection.membershipRevision ?? 0),
dataRevision: String(saved?.connection.membershipRevision ?? MISSING_MEMBERSHIP_REVISION),
availability: saved?.connection.membershipAt ? 'complete' : 'missing',
fetchedAt: saved?.connection.membershipAt ?? null,
freshForMs: tbankSourceTtlMs.membership,
@@ -559,7 +568,7 @@ export class BrokerageOperationsService {
'partial' :
'missing',
fetchedAt,
freshForMs: plan.intervals.some((range) => range.stale) ? 0 : Infinity,
freshForMs: plan.intervals.some((range) => range.stale) ? STALE_FRESHNESS_MS : Infinity,
work,
dependencies: [membership],
coverage: {
@@ -1,3 +1,11 @@
export const operationsSyncPriority = { screen: 1, history: 2 } as const;
const LAST_INTERVAL_INDEX = -1;
const NEXT_MONTH_OFFSET = 1;
const FIRST_DAY_OF_MONTH = 1;
const EMPTY_INTERVAL_COUNT = 0;
const EMPTY_SOURCE_COUNT = 0;
export type OperationInterval = {
accountId: string;
from: Date;
@@ -31,7 +39,7 @@ export function operationIntervals(
for (const interval of (groups.get(key) ?? []).sort(
(left, right) => left.from.valueOf() - right.from.valueOf(),
)) {
const previous = merged.at(-1);
const previous = merged.at(LAST_INTERVAL_INDEX);
if (previous && interval.from <= previous.to) {
previous.to = new Date(
Math.max(previous.to.valueOf(), interval.to.valueOf()),
@@ -42,7 +50,7 @@ export function operationIntervals(
let from = interval.from;
while (from < interval.to) {
const nextMonth = new Date(
Date.UTC(from.getUTCFullYear(), from.getUTCMonth() + 1, 1),
Date.UTC(from.getUTCFullYear(), from.getUTCMonth() + NEXT_MONTH_OFFSET, FIRST_DAY_OF_MONTH),
);
const to = new Date(
Math.min(nextMonth.valueOf(), interval.to.valueOf()),
@@ -111,7 +119,7 @@ export function planOperationRead(
options: { force?: boolean; movingEnd?: boolean } = {},
) {
const monthStart = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), FIRST_DAY_OF_MONTH),
);
const observedEnd = Math.max(
...requested.map((interval) => interval.to.valueOf()),
@@ -146,10 +154,10 @@ export function planOperationRead(
}
}
const missing = uncoveredOperationIntervals([effective], ranges);
const complete = missing.length === 0;
const complete = missing.length === EMPTY_INTERVAL_COUNT;
const stale =
current &&
sources.length > 0 &&
sources.length > EMPTY_SOURCE_COUNT &&
uncoveredOperationIntervals(
ranges
.map((row) => ({
@@ -161,7 +169,7 @@ export function planOperationRead(
}))
.filter((row) => row.from < row.to),
fresh.map((row) => ({ ...row, instrumentUid: target.instrumentUid })),
).length > 0;
).length > EMPTY_INTERVAL_COUNT;
if (options.force) load.push(target);
else if (stale) load.push({ ...target, from: monthStart, to: now });
else load.push(...missing);
@@ -10,6 +10,8 @@ import {
OperationCategory,
} from '../presentation/brokerage-operation.dto';
const MISSING_TIMESTAMP_COMPONENT = 0;
const NANOSECONDS_PER_MILLISECOND = 1_000_000;
const types: Record<string, OperationCategory> = {
@@ -89,7 +91,7 @@ export function operationTimestamp(
value: undefined | { nanos?: number; seconds?: number | string },
) {
return new Date(
Number(value?.seconds ?? 0) * MILLISECONDS_PER_SECOND +
Math.floor((value?.nanos ?? 0) / NANOSECONDS_PER_MILLISECOND),
Number(value?.seconds ?? MISSING_TIMESTAMP_COMPONENT) * MILLISECONDS_PER_SECOND +
Math.floor((value?.nanos ?? MISSING_TIMESTAMP_COMPONENT) / NANOSECONDS_PER_MILLISECOND),
);
}
@@ -22,12 +22,21 @@ import {
type OperationInterval,
operationIntervals,
operationIntervalsOverlap,
operationsSyncPriority,
uncoveredOperationIntervals,
} from '../domain/operations-sync-plan';
import {
operationCategory,
operationTimestamp,
} from './brokerage-operation.mapper';
const CURSOR_LOOKAHEAD_ROWS = 1;
const EMPTY_INTERVAL_COUNT = 0;
const SINGLE_WORK_GROUP = 1;
const INITIAL_PAGE_COUNT = 0;
const PAGE_INCREMENT = 1;
const EXPECTED_UPDATED_WORK_ITEMS = 1;
export type { OperationInterval } from '../domain/operations-sync-plan';
export type OperationAttempt = {
@@ -202,7 +211,7 @@ export class BrokerageOperationsRepository {
{ accountId: 'asc' },
{ brokerOperationId: 'asc' },
],
...(input.limit === undefined ? {} : { take: input.limit + 1 }),
...(input.limit === undefined ? {} : { take: input.limit + CURSOR_LOOKAHEAD_ROWS }),
});
return {
connection,
@@ -280,7 +289,7 @@ export class BrokerageOperationsRepository {
) {
throw new NotFoundException();
}
if (priority > 1) {
if (priority > operationsSyncPriority.screen) {
for (const account of accounts.filter((account) =>
intervals.some(
(interval) => interval.accountId === account.accountId,
@@ -331,7 +340,7 @@ export class BrokerageOperationsRepository {
to: row.to,
instrumentUid: interval.instrumentUid,
})),
).length === 0,
).length === EMPTY_INTERVAL_COUNT,
);
const active = await tx.tbankSyncWork.findMany({
where: {
@@ -366,7 +375,7 @@ export class BrokerageOperationsRepository {
const upgradeGroup =
publicationAtomic && previousIntent && !previousIntent.atomic;
if (
touching.length <= 1 &&
touching.length <= SINGLE_WORK_GROUP &&
previous?.state === 'running' &&
previous.processRunId === processRunId &&
runningAttempts.has(previous.attemptId) &&
@@ -394,10 +403,10 @@ export class BrokerageOperationsRepository {
screenIntervals: [
...new Set([
...(previousIntent?.screenIntervals ??
(previous.priority === 1 ?
(previous.priority === operationsSyncPriority.screen ?
(previousIntent?.intervals ?? []) :
[])),
...(priority === 1 ?
...(priority === operationsSyncPriority.screen ?
[...pending, ...additions]
.filter((item) =>
intervals.some((target) =>
@@ -460,7 +469,7 @@ export class BrokerageOperationsRepository {
await tx.tbankSyncStage.deleteMany({ where: { workId: previous.id } });
}
const targets =
recovering && !upgradeGroup && touching.length <= 1 ?
recovering && !upgradeGroup && touching.length <= SINGLE_WORK_GROUP ?
previous?.scopeKey === scopeKey ?
retained :
[...retained, ...uncoveredOperationIntervals(intervals, retained)] :
@@ -491,13 +500,13 @@ export class BrokerageOperationsRepository {
screenIntervals: targets
.filter(
(target) =>
(priority === 1 &&
(priority === operationsSyncPriority.screen &&
intervals.some((incoming) =>
operationIntervalsOverlap(target, incoming),
)) ||
(
previousIntent?.screenIntervals ??
(previous?.priority === 1 ?
(previous?.priority === operationsSyncPriority.screen ?
(previousIntent?.intervals ?? []) :
[])
).some((key) =>
@@ -505,7 +514,7 @@ export class BrokerageOperationsRepository {
),
)
.map(intervalKey),
atomic: atomicTargets.length > 0,
atomic: atomicTargets.length > EMPTY_INTERVAL_COUNT,
};
const data = {
intent,
@@ -703,7 +712,7 @@ export class BrokerageOperationsRepository {
...(progress?.seen ?? []),
...(response.hasNext ? [nextCursor] : []),
],
pages: (progress?.pages ?? 0) + 1,
pages: (progress?.pages ?? INITIAL_PAGE_COUNT) + PAGE_INCREMENT,
};
await tx.tbankSyncStage.upsert({
where: {
@@ -891,11 +900,11 @@ export class BrokerageOperationsRepository {
priority: remaining.some((key) =>
(
intent.screenIntervals ??
(work.priority === 1 ? intent.intervals : [])
(work.priority === operationsSyncPriority.screen ? intent.intervals : [])
).includes(key),
) ?
1 :
2,
operationsSyncPriority.screen :
operationsSyncPriority.history,
progress: { remainingIntervals: remaining.length },
},
});
@@ -932,7 +941,7 @@ async function claim(
},
data: { updatedAt: new Date() },
});
return result.count === 1;
return result.count === EXPECTED_UPDATED_WORK_ITEMS;
}
function decodeInterval(key: string): OperationInterval {
@@ -20,6 +20,8 @@ import {
InvestmentResourceMetaDto,
} from '../../investment-resource.dto';
const MIN_OPERATIONS_PAGE_SIZE = 1;
const MAX_CURSOR_LENGTH = 4096;
const MAX_OPERATIONS_PAGE_SIZE = 1000;
export const DEFAULT_OPERATIONS_PAGE_SIZE = 100;
@@ -70,14 +72,14 @@ export class BrokerageOperationsQueryDto extends InvestmentReadQueryDto {
cursor?: string;
@ApiPropertyOptional({
minimum: 1,
minimum: MIN_OPERATIONS_PAGE_SIZE,
maximum: MAX_OPERATIONS_PAGE_SIZE,
default: DEFAULT_OPERATIONS_PAGE_SIZE,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Min(MIN_OPERATIONS_PAGE_SIZE)
@Max(MAX_OPERATIONS_PAGE_SIZE)
limit?: number;
}