import type { components } from "../../contracts/studio/generated.ts"; import type { PublicationAggregate, PublicPreview, ValidationReport, WorkingCopy, WorkingCopyDetail, } from "../../contracts/studio/contract.ts"; export type ValidationResultState = "NOT_RUN" | "INVALID" | "WARNINGS" | "VALID"; export type ValidationFreshness = "NONE" | "CURRENT" | "STALE"; export type PreviewState = "NONE" | "CURRENT" | "STALE" | "EXPIRED"; export type NextAction = components["schemas"]["NextAction"]; export type ValidationState = { result: ValidationResultState; freshness: ValidationFreshness; }; export type StudioDocumentState = { validation: ValidationState; preview: PreviewState; nextAction: NextAction; }; export type DocumentStateInput = { // eslint-disable-next-line no-restricted-globals -- Exact Studio contract field. document: WorkingCopy; validation: ValidationReport | null; preview: PublicPreview | null; publication: PublicationAggregate | null; dependencyRevision: string; now: Date | string; }; export type WorkingCopyDetailStateInput = WorkingCopyDetail & { now: Date | string; }; type StateInput = DocumentStateInput | WorkingCopyDetailStateInput; function normalize(input: StateInput): DocumentStateInput { if ("validation" in input) return input; return { document: input.document, validation: input.currentValidation, preview: input.latestPreview, publication: input.currentPublication, dependencyRevision: input.dependencyRevision, now: input.now, }; } function timestamp(value: Date | string): number { return value instanceof Date ? value.getTime() : Date.parse(value); } function isStrictlyFuture(value: string, now: Date | string): boolean { const expiry = timestamp(value); const current = timestamp(now); return Number.isFinite(expiry) && Number.isFinite(current) && current < expiry; } function isExpired(value: string, now: Date | string): boolean { const expiry = timestamp(value); const current = timestamp(now); return Number.isFinite(expiry) && Number.isFinite(current) && current >= expiry; } export function deriveValidationState(input: StateInput): ValidationState { const { document, validation, dependencyRevision, now } = normalize(input); if (validation === null) { return { result: "NOT_RUN", freshness: "NONE" }; } const freshness: ValidationFreshness = validation.validatedVersion === document.version && isStrictlyFuture(validation.validUntil, now) && validation.dependencyRevision === dependencyRevision ? "CURRENT" : "STALE"; return { result: validation.status, freshness }; } export function derivePreviewState(input: StateInput): PreviewState { const normalized = normalize(input); const { document, validation, preview, now } = normalized; if (preview === null) return "NONE"; if (isExpired(preview.expiresAt, now)) return "EXPIRED"; return preview.previewVersion === document.version && validation !== null && preview.validationId === validation.validationId && deriveValidationState(normalized).freshness === "CURRENT" ? "CURRENT" : "STALE"; } function isCompletelyEmpty(document: WorkingCopy): boolean { return [document.title, document.slug, document.summary].every( (value) => value.trim().length === 0, ); } export function deriveNextAction(input: StateInput): NextAction { const normalized = normalize(input); const { document, publication } = normalized; if (publication?.status === "PUBLISHED" && publication.publishedVersion === document.version) { return "NONE"; } if (isCompletelyEmpty(document)) return "CONTINUE_EDITING"; const validation = deriveValidationState(normalized); if (validation.freshness !== "CURRENT") return "VALIDATE"; if (validation.result === "INVALID") return "FIX_VALIDATION"; const preview = derivePreviewState(normalized); if (preview !== "CURRENT") return "CREATE_PREVIEW"; return "PUBLISH"; } export function deriveDocumentState(input: StateInput): StudioDocumentState { const normalized = normalize(input); return { validation: deriveValidationState(normalized), preview: derivePreviewState(normalized), nextAction: deriveNextAction(normalized), }; }