import { funnelManifest, type FunnelStepId } from '@/config/funnel.manifest'; import type { FunnelManifestExperiment } from '@funnelsgrove/runtime'; import { getChoiceTargetsFromEdges, isManifestStepId, resolveInitialStepId, resolveNextStepId, } from '@funnelsgrove/runtime'; export { FunnelStepId }; export const funnelStepSequence = funnelManifest.steps.map((step) => step.id) as FunnelStepId[]; export type FunnelExperimentConfig = FunnelManifestExperiment & { stepId: FunnelStepId; variants: readonly ( FunnelManifestExperiment['variants'][number] & { routeToStepId: FunnelStepId; } )[]; }; /** @deprecated Builder-only fallback. Published experiments come from runtime config. */ export const funnelExperiments: FunnelExperimentConfig[] = []; export const defaultFunnelStepId: FunnelStepId = funnelStepSequence[0]; const stepIdSet = new Set(funnelStepSequence); const stepIndexById = new Map( funnelStepSequence.map((stepId, index) => [stepId, index] as const), ); const stepPathById = new Map( funnelManifest.steps.map((step) => [step.id as FunnelStepId, step.path] as const), ); export const isFunnelStepId = (value: string): value is FunnelStepId => { return stepIdSet.has(value); }; export const getPathForStep = (stepId: FunnelStepId): string => { return stepPathById.get(stepId) || `/${stepId}`; }; export const getStepIdFromPath = (pathname: string): FunnelStepId | null => { const segment = pathname .split('?')[0] .split('#')[0] .split('/') .map((part) => part.trim()) .filter(Boolean)[0]; if (!segment || !isFunnelStepId(segment)) { return null; } return segment; }; export const getSequentialNextStepId = (stepId: FunnelStepId): FunnelStepId | null => { const stepIndex = stepIndexById.get(stepId); if (stepIndex === undefined) { return null; } return funnelStepSequence[stepIndex + 1] ?? null; }; export const getChoiceTargetsForStep = ( stepId: FunnelStepId, ): { yes?: FunnelStepId; no?: FunnelStepId } | null => { const choiceTargets = getChoiceTargetsFromEdges(funnelManifest, stepId); if (!choiceTargets) { return null; } return { yes: choiceTargets.yes && isFunnelStepId(choiceTargets.yes) ? choiceTargets.yes : undefined, no: choiceTargets.no && isFunnelStepId(choiceTargets.no) ? choiceTargets.no : undefined, }; }; export const resolveRuntimeInitialStepId = (input: { requestedStepId?: string | null; }): FunnelStepId => { const stepId = resolveInitialStepId({ manifest: funnelManifest, requestedStepId: input.requestedStepId, }); return isFunnelStepId(stepId) ? stepId : defaultFunnelStepId; }; export const resolveConfiguredNextStep = ( stepId: FunnelStepId, context: { attributes: Record; ignoreExperiment?: boolean; resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null; }, ): FunnelStepId | null => { const nextStepId = resolveNextStepId({ manifest: funnelManifest, currentStepId: stepId, attributes: context.attributes, ignoreExperiment: context.ignoreExperiment, resolveExperimentVariantKey: context.resolveExperimentVariantKey, }); return nextStepId && isManifestStepId(funnelManifest, nextStepId) && isFunnelStepId(nextStepId) ? nextStepId : null; };