feat: reconcile hourly investment resource states

This commit is contained in:
2026-10-09 19:56:14 +03:00
parent 2facd8675c
commit aee330c6c2
25 changed files with 445 additions and 86 deletions
@@ -1,41 +1,34 @@
import { InvestmentResourceMetaDto } from './investment-resource.dto';
type LegacyAvailability = 'available' | 'complete' | 'connection-unavailable' | 'initial-loading' | 'missing' | 'partial' | 'temporarily-unavailable';
type Work = null | { errorCode: null | string; id: string; state: string };
type Attempt = NonNullable<InvestmentResourceMetaDto['lastAttempt']>;
export function investmentResourceMeta(input: {
availability: InvestmentResourceMetaDto['availability'];
availability: LegacyAvailability;
coverage?: NonNullable<InvestmentResourceMetaDto['coverage']>;
dataRevision: string;
dependencies?: InvestmentResourceMetaDto[];
fetchedAt: Date | null;
freshForMs: number;
lastAttempt?: Attempt;
resourceKey: string;
unavailableReason?: string;
work: null | { errorCode: null | string; id: string; state: string };
work: Work;
}): InvestmentResourceMetaDto {
const ownSync = {
id: input.work?.id ?? null,
state:
input.work?.state === 'running'
? ('running' as const)
: input.work?.state === 'failed'
? ('failed' as const)
: ('idle' as const),
errorCode: input.work?.errorCode ?? null,
};
const states = [
const ownSync = sourceSync(input.work);
const syncStates = [
ownSync,
...(input.dependencies ?? [])
.filter((dependency) => !dependency.unavailableReason)
.map((dependency) => dependency.sync),
.map((dependency) => dependency.sync)
.filter((state): state is typeof ownSync => Boolean(state)),
];
const sync =
states.find((state) => state.state === 'running') ??
states.find((state) => state.state === 'failed') ??
const sourceWork =
syncStates.find((state) => state.state === 'running') ??
syncStates.find((state) => state.state === 'failed') ??
ownSync;
const sourceFreshness = input.fetchedAt
? Date.now() - input.fetchedAt.valueOf() < input.freshForMs
? ('fresh' as const)
: ('stale' as const)
: ('unknown' as const);
const sourceFreshness = freshnessOf(input.fetchedAt, input.freshForMs);
const freshness = [
sourceFreshness,
...(input.dependencies ?? [])
@@ -57,16 +50,70 @@ export function investmentResourceMeta(input: {
? { unavailableReason: input.unavailableReason }
: {}),
dataRevision: input.dataRevision,
availability: input.availability,
freshness: freshness.includes('unknown')
? ('unknown' as const)
: freshness.includes('stale')
? ('stale' as const)
: ('fresh' as const),
availability: availability(input.availability, input.work),
completeness:
availability(input.availability, input.work) === 'available'
? ('complete' as const)
: ('incomplete' as const),
freshness: combinedFreshness(freshness),
lastSuccessfulAt: timestamps[0] ?? null,
sync,
lastAttempt: input.lastAttempt ??
attempt(input.work),
...(input.coverage ? { coverage: input.coverage } : {}),
...(input.dependencies ? { dependencies: input.dependencies } : {}),
};
return meta;
return Object.defineProperty(meta, 'sync', {
enumerable: false,
value: sourceWork,
}) as InvestmentResourceMetaDto;
}
function sourceSync(work: Work) {
let state: 'failed' | 'idle' | 'running' = 'idle';
if (work?.state === 'running') state = 'running';
else if (work?.state === 'failed') state = 'failed';
return { id: work?.id ?? null, state, errorCode: work?.errorCode ?? null };
}
function freshnessOf(fetchedAt: Date | null, freshForMs: number) {
if (!fetchedAt) return 'unknown' as const;
return Date.now() - fetchedAt.valueOf() < freshForMs
? ('fresh' as const)
: ('stale' as const);
}
function combinedFreshness(values: InvestmentResourceMetaDto['freshness'][]) {
if (values.includes('unknown')) return 'unknown' as const;
if (values.includes('stale')) return 'stale' as const;
return 'fresh' as const;
}
function attempt(work: Work): Attempt {
const base = { startedAt: null, finishedAt: null };
if (work?.state === 'running') return { ...base, state: 'running', problems: [] };
if (work?.state === 'failed') {
return {
...base,
state: 'failed',
problems: work.errorCode
? [{ section: 'membership', accountId: null, errorCode: work.errorCode }]
: [],
};
}
return { ...base, state: 'none', problems: [] };
}
function availability(value: LegacyAvailability, work: Work) {
if (
value === 'available' ||
value === 'connection-unavailable' ||
value === 'initial-loading' ||
value === 'temporarily-unavailable'
) {
return value;
}
if (value === 'complete') return 'available' as const;
return work?.state === 'running'
? ('initial-loading' as const)
: ('temporarily-unavailable' as const);
}