import { createHash } from "node:crypto"; // v5 drops the unrendered greeting and widens suggestions to every consultation // domain, so every v4 payload cached in profiles must be regenerated once. const ONBOARDING_VERSION = "ayanam-onboarding-v5"; export const ONBOARDING_CLAIM_TTL_MS = 2 * 60 * 1000; type OnboardingProfileInput = { readonly name: string | null; readonly birthDate: string | null; readonly birthTime: string | null; readonly reportedBirthTime?: string | null; readonly activeBirthTime: string | null; readonly birthTimeSource?: string | null; readonly birthTimePeriod?: string | null; readonly birthTimeClue?: string | null; readonly uncertaintyBeforeMinutes?: number | null; readonly uncertaintyAfterMinutes?: number | null; readonly birthTimeStatus: string | null; readonly countryCode: string | null; readonly provinceCode: string | null; readonly cityCode: string | null; }; export type OnboardingCacheIdentity = { readonly readyVersion: string; readonly pendingVersion: string; }; type OnboardingCacheObservation = { readonly identity: OnboardingCacheIdentity; readonly observedVersion: string | null; readonly generatedAtMs: number; readonly nowMs: number; readonly cachedPayload: Payload | null; }; type OnboardingCacheDecision = | { readonly kind: "ready"; readonly payload: Payload } | { readonly kind: "pending" } | { readonly kind: "claim"; readonly expectedVersion: string | null; readonly pendingVersion: string; }; export function createOnboardingCacheIdentity( profile: OnboardingProfileInput, ): OnboardingCacheIdentity { const fingerprint = createHash("sha256") .update(JSON.stringify([ profile.name, profile.birthDate, profile.birthTime, profile.reportedBirthTime, profile.activeBirthTime, profile.birthTimeSource, profile.birthTimePeriod, profile.birthTimeClue, profile.uncertaintyBeforeMinutes, profile.uncertaintyAfterMinutes, profile.birthTimeStatus, profile.countryCode, profile.provinceCode, profile.cityCode, ])) .digest("hex"); return { readyVersion: `${ONBOARDING_VERSION}:${fingerprint}`, pendingVersion: `${ONBOARDING_VERSION}:pending:${fingerprint}`, }; } export function decideOnboardingCache( observation: OnboardingCacheObservation, ): OnboardingCacheDecision { if (observation.observedVersion === observation.identity.readyVersion && observation.cachedPayload !== null) { return { kind: "ready", payload: observation.cachedPayload }; } if (observation.observedVersion === observation.identity.pendingVersion && Number.isFinite(observation.generatedAtMs) && observation.nowMs - observation.generatedAtMs < ONBOARDING_CLAIM_TTL_MS) { return { kind: "pending" }; } return { kind: "claim", expectedVersion: observation.observedVersion, pendingVersion: observation.identity.pendingVersion, }; }