93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
const ONBOARDING_VERSION = "ayanam-onboarding-v3";
|
|
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<Payload> = {
|
|
readonly identity: OnboardingCacheIdentity;
|
|
readonly observedVersion: string | null;
|
|
readonly generatedAtMs: number;
|
|
readonly nowMs: number;
|
|
readonly cachedPayload: Payload | null;
|
|
};
|
|
|
|
type OnboardingCacheDecision<Payload> =
|
|
| { 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<Payload>(
|
|
observation: OnboardingCacheObservation<Payload>,
|
|
): OnboardingCacheDecision<Payload> {
|
|
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,
|
|
};
|
|
}
|