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
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:
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
+24
-15
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user