import { createHash } from 'node:crypto'; import { InvestmentResourceMetaDto } from './investment-resource.dto'; export function investmentResourceMeta(input: { resourceKey: string; dataRevision: string; availability: InvestmentResourceMetaDto['availability']; fetchedAt: Date | null; freshForMs: number; work: { id: string; state: string; errorCode: string | null } | null; dependencies?: InvestmentResourceMetaDto[]; coverage?: InvestmentResourceMetaDto['coverage']; unavailableReason?: string; }): 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 = [ ownSync, ...(input.dependencies ?? []) .filter((dependency) => !dependency.unavailableReason) .map((dependency) => dependency.sync), ]; const sync = states.find((state) => state.state === 'running') ?? states.find((state) => state.state === 'queued') ?? states.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 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)) .sort(); const meta = { resourceKey: input.resourceKey, ...(input.unavailableReason ? { 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), lastSuccessfulAt: timestamps[0] ?? null, sync, ...(input.coverage ? { coverage: input.coverage } : {}), ...(input.dependencies ? { dependencies: input.dependencies } : {}), }; return { ...meta, stateVersion: createHash('sha256') .update( JSON.stringify({ ...meta, // The requested window is identified by resourceKey. Its moving "now" // boundary is not a change to saved coverage between status and GET. coverage: meta.coverage ? { ...meta.coverage, requested: undefined } : undefined, }), ) .digest('base64url'), }; }