feat: reconcile hourly investment resource states
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user