refactor!: simplify runtime coordination #7
Keep financial ownership, atomic publication and refresh CAS. Failed refresh leaves cookies untouched; logout still clears them. Remove full-history candle prefetch and special metadata retries. Use native Pino request context instead of parallel middleware. BREAKING CHANGE: remove investment stateVersion, queued sync state, and tbank-sync status progress/resources. Consumers must observe resources through sync=observe with the updated frontend contract.
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ServiceUnavailableException,
|
||||
} from '@nestjs/common';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { RequestContextService } from '../../../../common/logging/request-context.service';
|
||||
import { StructuredLogger } from '../../../../common/logging/structured-logger.service';
|
||||
import { isTbankErrorCode, TbankErrorCode } from '../../../../integrations/tbank/tbank-error-code';
|
||||
import {
|
||||
@@ -19,7 +17,6 @@ import {
|
||||
type TbankPortfolio,
|
||||
tbankSourceTtlMs,
|
||||
} from '../../../../integrations/tbank/tbank.api';
|
||||
import { CandleService } from '../../instruments/instruments.api';
|
||||
import { investmentResourceMeta } from '../../investment-resource.mapper';
|
||||
import { portfolioStructure } from '../domain/portfolio-structure';
|
||||
import { sumMoneyByCurrency } from '../domain/portfolio-totals';
|
||||
@@ -38,23 +35,19 @@ const membershipFreshForMs = tbankSourceTtlMs.membership;
|
||||
@Injectable()
|
||||
export class PortfolioService {
|
||||
private readonly processRunId = randomUUID();
|
||||
private readonly active = new Set<string>();
|
||||
private readonly pending = new Map<string, Promise<void>>();
|
||||
private reservations: Promise<unknown> = Promise.resolve();
|
||||
|
||||
constructor(
|
||||
private readonly access: TbankAccessService,
|
||||
private readonly tbank: TbankClient,
|
||||
private readonly requestContext: RequestContextService,
|
||||
private readonly metadata: InstrumentMetadataService,
|
||||
private readonly repository: PortfolioRepository,
|
||||
private readonly membership: TbankMembershipService,
|
||||
private readonly logger: StructuredLogger,
|
||||
private readonly candles: CandleService,
|
||||
) {}
|
||||
|
||||
async saved(userId: string, accountId: string, observe = false) {
|
||||
this.requestContext.set({ userId, brokerageAccountId: accountId });
|
||||
const connection = await this.access.local(userId, accountId);
|
||||
if (!observe) {
|
||||
const discovery = await this.membership.request(
|
||||
@@ -123,28 +116,6 @@ export class PortfolioService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!observe) {
|
||||
void this.candles
|
||||
.prefetch(
|
||||
userId,
|
||||
connection.revision,
|
||||
(saved?.accounts ?? [])
|
||||
.filter(
|
||||
(account) =>
|
||||
accountId === 'all' || account.accountId === accountId,
|
||||
)
|
||||
.flatMap(
|
||||
(account) =>
|
||||
(
|
||||
account.portfolio?.payload as unknown as
|
||||
PortfolioDto | undefined
|
||||
)?.positions ?? [],
|
||||
),
|
||||
)
|
||||
.catch(() =>
|
||||
this.logger.event('warn', 'portfolio.candle-prefetch-unavailable'),
|
||||
);
|
||||
}
|
||||
const membershipMeta = investmentResourceMeta({
|
||||
resourceKey: 'accounts/membership',
|
||||
dataRevision: String(saved?.membershipRevision ?? MISSING_DATA_REVISION),
|
||||
@@ -319,7 +290,7 @@ export class PortfolioService {
|
||||
revision,
|
||||
accountId,
|
||||
this.processRunId,
|
||||
this.active,
|
||||
this.pending,
|
||||
force,
|
||||
);
|
||||
if (!requested) return null;
|
||||
@@ -330,7 +301,6 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
const { id, attemptId } = requested.work;
|
||||
this.active.add(attemptId);
|
||||
const done = new Promise<void>((resolve) => setImmediate(resolve))
|
||||
.then(async () => {
|
||||
const attempt = { id, attemptId, userId, revision };
|
||||
@@ -354,13 +324,6 @@ export class PortfolioService {
|
||||
for (const position of snapshot.positions) {
|
||||
await this.metadata.request(userId, revision, position);
|
||||
}
|
||||
if (!force) {
|
||||
await this.candles.prefetch(
|
||||
userId,
|
||||
revision,
|
||||
snapshot.positions,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -377,7 +340,6 @@ export class PortfolioService {
|
||||
}),
|
||||
)
|
||||
.finally(() => {
|
||||
this.active.delete(attemptId);
|
||||
this.pending.delete(attemptId);
|
||||
});
|
||||
this.pending.set(attemptId, done);
|
||||
@@ -387,18 +349,6 @@ export class PortfolioService {
|
||||
return reserved;
|
||||
}
|
||||
|
||||
async get(userId: string, accountId: string): Promise<PortfolioDto> {
|
||||
const result = await this.read(userId, accountId);
|
||||
if (!result.data) {
|
||||
throw new ServiceUnavailableException({ code: TbankErrorCode.Unavailable });
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
getAll(userId: string): Promise<PortfolioDto> {
|
||||
return this.get(userId, 'all');
|
||||
}
|
||||
|
||||
private async normalize(
|
||||
upstream: NonNullable<TbankPortfolio['positions']>,
|
||||
money: TbankMoney[],
|
||||
|
||||
Reference in New Issue
Block a user