125 lines
4.4 KiB
TypeScript
125 lines
4.4 KiB
TypeScript
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: LegacyAvailability;
|
|
coverage?: NonNullable<InvestmentResourceMetaDto['coverage']>;
|
|
dataRevision: string;
|
|
dependencies?: InvestmentResourceMetaDto[];
|
|
fetchedAt: Date | null;
|
|
freshForMs: number;
|
|
lastAttempt?: Attempt;
|
|
resourceKey: string;
|
|
unavailableReason?: string;
|
|
work: Work;
|
|
}): InvestmentResourceMetaDto {
|
|
const ownSync = sourceSync(input.work);
|
|
const syncStates = [
|
|
ownSync,
|
|
...(input.dependencies ?? [])
|
|
.filter((dependency) => !dependency.unavailableReason)
|
|
.map((dependency) => dependency.sync)
|
|
.filter((state): state is typeof ownSync => Boolean(state)),
|
|
];
|
|
const sourceWork =
|
|
syncStates.find((state) => state.state === 'running') ??
|
|
syncStates.find((state) => state.state === 'failed') ??
|
|
ownSync;
|
|
const sourceFreshness = freshnessOf(input.fetchedAt, input.freshForMs);
|
|
const freshness = [
|
|
sourceFreshness,
|
|
...(input.dependencies ?? [])
|
|
.filter((dependency) => !dependency.unavailableReason)
|
|
.map((dependency) => dependency.freshness),
|
|
];
|
|
const timestamps = [
|
|
input.fetchedAt?.toISOString(),
|
|
...(input.dependencies ?? []).map(
|
|
(dependency) => dependency.lastSuccessfulAt,
|
|
),
|
|
]
|
|
.filter((timestamp): timestamp is string => Boolean(timestamp))
|
|
// eslint-disable-next-line sonarjs/no-alphabetical-sort -- Code-unit ordering is intentional for canonical keys and ISO dates; locale collation would change persisted bindings.
|
|
.sort();
|
|
const meta = {
|
|
resourceKey: input.resourceKey,
|
|
...(input.unavailableReason
|
|
? { unavailableReason: input.unavailableReason }
|
|
: {}),
|
|
dataRevision: input.dataRevision,
|
|
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,
|
|
lastAttempt: input.lastAttempt ??
|
|
attempt(input.work),
|
|
...(input.coverage ? { coverage: input.coverage } : {}),
|
|
...(input.dependencies ? { dependencies: input.dependencies } : {}),
|
|
};
|
|
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;
|
|
if (value === 'partial') {
|
|
return work?.state === 'running'
|
|
? ('initial-loading' as const)
|
|
: ('temporarily-unavailable' as const);
|
|
}
|
|
return work?.state === 'running'
|
|
? ('initial-loading' as const)
|
|
: ('temporarily-unavailable' as const);
|
|
}
|