Fix loading of recommended methods
This commit is contained in:
@@ -12,6 +12,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|||||||
import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
|
import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
|
||||||
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
||||||
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
||||||
|
import { usePrefetchMethodFacetRecommendations } from "./hooks/usePrefetchMethodFacetRecommendations";
|
||||||
import { useCreateFlowFinalize } from "./hooks/useCreateFlowFinalize";
|
import { useCreateFlowFinalize } from "./hooks/useCreateFlowFinalize";
|
||||||
import { useTemplateReviewActions } from "./hooks/useTemplateReviewActions";
|
import { useTemplateReviewActions } from "./hooks/useTemplateReviewActions";
|
||||||
import { useCompletedRuleShareExport } from "./hooks/useCompletedRuleShareExport";
|
import { useCompletedRuleShareExport } from "./hooks/useCompletedRuleShareExport";
|
||||||
@@ -167,6 +168,7 @@ function CreateFlowLayoutContent({
|
|||||||
replaceState,
|
replaceState,
|
||||||
markCreateFlowInteraction,
|
markCreateFlowInteraction,
|
||||||
} = useCreateFlow();
|
} = useCreateFlow();
|
||||||
|
usePrefetchMethodFacetRecommendations();
|
||||||
const manageStakeholdersIntent =
|
const manageStakeholdersIntent =
|
||||||
searchParams?.get(CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY) ===
|
searchParams?.get(CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY) ===
|
||||||
CREATE_FLOW_MANAGE_STAKEHOLDERS_VALUE;
|
CREATE_FLOW_MANAGE_STAKEHOLDERS_VALUE;
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { buildFacetQueryString } from "../../../../lib/create/buildFacetQueryString";
|
import { buildFacetQueryString } from "../../../../lib/create/buildFacetQueryString";
|
||||||
import type { MethodFacetApiSectionId } from "../../../../lib/create/customRuleFacets";
|
import type { MethodFacetApiSectionId } from "../../../../lib/create/customRuleFacets";
|
||||||
|
import {
|
||||||
|
buildFacetRecommendationRequestKey,
|
||||||
|
getCachedFacetScores,
|
||||||
|
loadFacetScores,
|
||||||
|
} from "../../../../lib/create/facetRecommendationsLoad";
|
||||||
import { useCreateFlow } from "../context/CreateFlowContext";
|
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +30,34 @@ export type FacetRecommendationsResult = {
|
|||||||
|
|
||||||
const EMPTY_SCORES: Record<string, number> = {};
|
const EMPTY_SCORES: Record<string, number> = {};
|
||||||
|
|
||||||
|
function initialFacetRecommendationsResult(
|
||||||
|
section: RecommendationSection,
|
||||||
|
queryString: string,
|
||||||
|
): FacetRecommendationsResult {
|
||||||
|
const hasAnyFacets = queryString.length > 0;
|
||||||
|
if (!hasAnyFacets) {
|
||||||
|
return {
|
||||||
|
isReady: true,
|
||||||
|
scoresBySlug: EMPTY_SCORES,
|
||||||
|
hasAnyFacets: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const requestKey = buildFacetRecommendationRequestKey(section, queryString);
|
||||||
|
const cached = getCachedFacetScores(requestKey);
|
||||||
|
if (cached) {
|
||||||
|
return {
|
||||||
|
isReady: true,
|
||||||
|
scoresBySlug: cached,
|
||||||
|
hasAnyFacets: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isReady: false,
|
||||||
|
scoresBySlug: EMPTY_SCORES,
|
||||||
|
hasAnyFacets: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `GET /api/create-flow/methods?section=<section>&facet.*=...` for the
|
* Calls `GET /api/create-flow/methods?section=<section>&facet.*=...` for the
|
||||||
* card-deck step `section` and returns a `slug → score` map for re-ranking
|
* card-deck step `section` and returns a `slug → score` map for re-ranking
|
||||||
@@ -46,14 +79,9 @@ export function useFacetRecommendations(
|
|||||||
);
|
);
|
||||||
const hasAnyFacets = queryString.length > 0;
|
const hasAnyFacets = queryString.length > 0;
|
||||||
|
|
||||||
const [result, setResult] = useState<FacetRecommendationsResult>({
|
const [result, setResult] = useState<FacetRecommendationsResult>(() =>
|
||||||
isReady: !hasAnyFacets,
|
initialFacetRecommendationsResult(section, queryString),
|
||||||
scoresBySlug: EMPTY_SCORES,
|
);
|
||||||
hasAnyFacets,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track the last successful request input so we don't re-fetch on every state poke.
|
|
||||||
const lastQueryRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasAnyFacets) {
|
if (!hasAnyFacets) {
|
||||||
@@ -62,51 +90,34 @@ export function useFacetRecommendations(
|
|||||||
scoresBySlug: EMPTY_SCORES,
|
scoresBySlug: EMPTY_SCORES,
|
||||||
hasAnyFacets: false,
|
hasAnyFacets: false,
|
||||||
});
|
});
|
||||||
lastQueryRef.current = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const requestKey = `${section}?${queryString}`;
|
|
||||||
if (lastQueryRef.current === requestKey) return;
|
|
||||||
lastQueryRef.current = requestKey;
|
|
||||||
|
|
||||||
const ctrl = new AbortController();
|
const requestKey = buildFacetRecommendationRequestKey(section, queryString);
|
||||||
setResult((prev) => ({ ...prev, isReady: false, hasAnyFacets: true }));
|
const cached = getCachedFacetScores(requestKey);
|
||||||
fetch(`/api/create-flow/methods?section=${section}&${queryString}`, {
|
if (cached) {
|
||||||
credentials: "include",
|
|
||||||
signal: ctrl.signal,
|
|
||||||
})
|
|
||||||
.then(async (res) => {
|
|
||||||
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
||||||
return (await res.json()) as {
|
|
||||||
methods?: { slug: string; matches?: { score?: number } }[];
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
const scoresBySlug: Record<string, number> = {};
|
|
||||||
for (const m of json.methods ?? []) {
|
|
||||||
if (typeof m.slug === "string") {
|
|
||||||
scoresBySlug[m.slug] = m.matches?.score ?? 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setResult({ isReady: true, scoresBySlug, hasAnyFacets: true });
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if ((e as { name?: string }).name === "AbortError") return;
|
|
||||||
setResult({
|
setResult({
|
||||||
isReady: true,
|
isReady: true,
|
||||||
scoresBySlug: EMPTY_SCORES,
|
scoresBySlug: cached,
|
||||||
hasAnyFacets: true,
|
hasAnyFacets: true,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setResult((prev) =>
|
||||||
|
prev.isReady && prev.hasAnyFacets
|
||||||
|
? { ...prev, isReady: false }
|
||||||
|
: { isReady: false, scoresBySlug: EMPTY_SCORES, hasAnyFacets: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
void loadFacetScores(section, queryString).then((scoresBySlug) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setResult({ isReady: true, scoresBySlug, hasAnyFacets: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ctrl.abort();
|
cancelled = true;
|
||||||
// Clear the dedup key so React 19 Strict Mode's mount → unmount → mount
|
|
||||||
// cycle (and any future remount) re-issues the request instead of
|
|
||||||
// returning early on the same key.
|
|
||||||
if (lastQueryRef.current === requestKey) {
|
|
||||||
lastQueryRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [section, queryString, hasAnyFacets]);
|
}, [section, queryString, hasAnyFacets]);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function useMethodCardDeckOrdering(
|
|||||||
methods: readonly MethodEntry[],
|
methods: readonly MethodEntry[],
|
||||||
selectedIds: readonly string[],
|
selectedIds: readonly string[],
|
||||||
) {
|
) {
|
||||||
const { scoresBySlug, hasAnyFacets } = useFacetRecommendations(section);
|
const { scoresBySlug, hasAnyFacets, isReady } =
|
||||||
|
useFacetRecommendations(section);
|
||||||
|
const recommendationsReady = !hasAnyFacets || isReady;
|
||||||
|
|
||||||
const rankedMethods = useMemo(
|
const rankedMethods = useMemo(
|
||||||
() => rankMethodsByScore(methods, scoresBySlug),
|
() => rankMethodsByScore(methods, scoresBySlug),
|
||||||
@@ -90,5 +92,6 @@ export function useMethodCardDeckOrdering(
|
|||||||
recommendedIds,
|
recommendedIds,
|
||||||
sampleCards,
|
sampleCards,
|
||||||
methodById,
|
methodById,
|
||||||
|
recommendationsReady,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { buildFacetQueryString } from "../../../../lib/create/buildFacetQueryString";
|
||||||
|
import { METHOD_FACET_API_SECTION_IDS } from "../../../../lib/create/customRuleFacets";
|
||||||
|
import { loadFacetScores } from "../../../../lib/create/facetRecommendationsLoad";
|
||||||
|
import { useCreateFlow } from "../context/CreateFlowContext";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warms the facet recommendation cache for all method-deck sections once the
|
||||||
|
* user has community facet selections, so method screens can render ranked
|
||||||
|
* cards on first paint instead of flashing authoring order.
|
||||||
|
*/
|
||||||
|
export function usePrefetchMethodFacetRecommendations(): void {
|
||||||
|
const { state } = useCreateFlow();
|
||||||
|
const queryString = useMemo(
|
||||||
|
() => buildFacetQueryString(state),
|
||||||
|
[state],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (queryString.length === 0) return;
|
||||||
|
for (const section of METHOD_FACET_API_SECTION_IDS) {
|
||||||
|
void loadFacetScores(section, queryString);
|
||||||
|
}
|
||||||
|
}, [queryString]);
|
||||||
|
}
|
||||||
@@ -97,7 +97,8 @@ export function CommunicationMethodsScreen() {
|
|||||||
[comm.methods, selectedIds, state.customMethodCardMetaById],
|
[comm.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
||||||
|
useMethodCardDeckOrdering(
|
||||||
"communication",
|
"communication",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -735,7 +736,11 @@ export function CommunicationMethodsScreen() {
|
|||||||
justification="center"
|
justification="center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}>
|
<div
|
||||||
|
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||||
|
aria-busy={!recommendationsReady}
|
||||||
|
>
|
||||||
|
{recommendationsReady ? (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -752,6 +757,7 @@ export function CommunicationMethodsScreen() {
|
|||||||
compactDesktopLayout="flexWrap"
|
compactDesktopLayout="flexWrap"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ export function ConflictManagementScreen() {
|
|||||||
[cm.methods, selectedIds, state.customMethodCardMetaById],
|
[cm.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
||||||
|
useMethodCardDeckOrdering(
|
||||||
"conflictManagement",
|
"conflictManagement",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -734,7 +735,11 @@ export function ConflictManagementScreen() {
|
|||||||
justification="center"
|
justification="center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}>
|
<div
|
||||||
|
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||||
|
aria-busy={!recommendationsReady}
|
||||||
|
>
|
||||||
|
{recommendationsReady ? (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -751,6 +756,7 @@ export function ConflictManagementScreen() {
|
|||||||
compactDesktopLayout="pyramidFive"
|
compactDesktopLayout="pyramidFive"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ export function MembershipMethodsScreen() {
|
|||||||
[mem.methods, selectedIds, state.customMethodCardMetaById],
|
[mem.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
||||||
|
useMethodCardDeckOrdering(
|
||||||
"membership",
|
"membership",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -727,7 +728,11 @@ export function MembershipMethodsScreen() {
|
|||||||
justification="center"
|
justification="center"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}>
|
<div
|
||||||
|
className={CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS}
|
||||||
|
aria-busy={!recommendationsReady}
|
||||||
|
>
|
||||||
|
{recommendationsReady ? (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -744,6 +749,7 @@ export function MembershipMethodsScreen() {
|
|||||||
compactDesktopLayout="pyramidFive"
|
compactDesktopLayout="pyramidFive"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export function DecisionApproachesScreen() {
|
|||||||
[da.methods, selectedIds, state.customMethodCardMetaById],
|
[da.methods, selectedIds, state.customMethodCardMetaById],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { sampleCards, compactCardIds, methodById } = useMethodCardDeckOrdering(
|
const { sampleCards, compactCardIds, methodById, recommendationsReady } =
|
||||||
|
useMethodCardDeckOrdering(
|
||||||
"decisionApproaches",
|
"decisionApproaches",
|
||||||
mergedMethods,
|
mergedMethods,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
@@ -761,7 +762,11 @@ export function DecisionApproachesScreen() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0">
|
<div
|
||||||
|
className="flex w-full min-w-0 flex-col items-stretch gap-6 py-0"
|
||||||
|
aria-busy={!recommendationsReady}
|
||||||
|
>
|
||||||
|
{recommendationsReady ? (
|
||||||
<CardStack
|
<CardStack
|
||||||
cards={sampleCards}
|
cards={sampleCards}
|
||||||
selectedIds={selectedIds}
|
selectedIds={selectedIds}
|
||||||
@@ -791,6 +796,7 @@ export function DecisionApproachesScreen() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
headerLockupSize={mdUp ? "L" : "M"}
|
headerLockupSize={mdUp ? "L" : "M"}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Create
|
<Create
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { MethodFacetApiSectionId } from "./customRuleFacets";
|
||||||
|
|
||||||
|
export type FacetScoresBySlug = Record<string, number>;
|
||||||
|
|
||||||
|
const EMPTY_SCORES: FacetScoresBySlug = {};
|
||||||
|
|
||||||
|
const cache = new Map<string, FacetScoresBySlug>();
|
||||||
|
const inFlight = new Map<string, Promise<FacetScoresBySlug>>();
|
||||||
|
|
||||||
|
export function buildFacetRecommendationRequestKey(
|
||||||
|
section: MethodFacetApiSectionId,
|
||||||
|
queryString: string,
|
||||||
|
): string {
|
||||||
|
return `${section}?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCachedFacetScores(
|
||||||
|
requestKey: string,
|
||||||
|
): FacetScoresBySlug | undefined {
|
||||||
|
return cache.get(requestKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScoresFromMethodsJson(json: {
|
||||||
|
methods?: { slug: string; matches?: { score?: number } }[];
|
||||||
|
}): FacetScoresBySlug {
|
||||||
|
const scoresBySlug: FacetScoresBySlug = {};
|
||||||
|
for (const m of json.methods ?? []) {
|
||||||
|
if (typeof m.slug === "string") {
|
||||||
|
scoresBySlug[m.slug] = m.matches?.score ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scoresBySlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchFacetScoresFromApi(
|
||||||
|
section: MethodFacetApiSectionId,
|
||||||
|
queryString: string,
|
||||||
|
): Promise<FacetScoresBySlug> {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/create-flow/methods?section=${section}&${queryString}`,
|
||||||
|
{ credentials: "include" },
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(`status ${res.status}`);
|
||||||
|
const json = (await res.json()) as {
|
||||||
|
methods?: { slug: string; matches?: { score?: number } }[];
|
||||||
|
};
|
||||||
|
return parseScoresFromMethodsJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads facet recommendation scores for one method deck. Results are cached
|
||||||
|
* and in-flight requests are deduped so prefetch + screen hooks share work.
|
||||||
|
*/
|
||||||
|
export function loadFacetScores(
|
||||||
|
section: MethodFacetApiSectionId,
|
||||||
|
queryString: string,
|
||||||
|
): Promise<FacetScoresBySlug> {
|
||||||
|
const requestKey = buildFacetRecommendationRequestKey(section, queryString);
|
||||||
|
const cached = cache.get(requestKey);
|
||||||
|
if (cached) return Promise.resolve(cached);
|
||||||
|
|
||||||
|
let pending = inFlight.get(requestKey);
|
||||||
|
if (!pending) {
|
||||||
|
pending = fetchFacetScoresFromApi(section, queryString)
|
||||||
|
.then((scores) => {
|
||||||
|
cache.set(requestKey, scores);
|
||||||
|
return scores;
|
||||||
|
})
|
||||||
|
.catch(() => EMPTY_SCORES)
|
||||||
|
.finally(() => {
|
||||||
|
inFlight.delete(requestKey);
|
||||||
|
});
|
||||||
|
inFlight.set(requestKey, pending);
|
||||||
|
}
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
+7
-1
@@ -2,7 +2,7 @@ import createMDX from "@next/mdx";
|
|||||||
|
|
||||||
/* eslint-env node */
|
/* eslint-env node */
|
||||||
|
|
||||||
/** Keep viewBox so inline SVGR art can scale/center like `object-contain`. */
|
/** Keep viewBox and unique clip/mask IDs when multiple SVGR icons share a page. */
|
||||||
const svgrLoaderOptions = {
|
const svgrLoaderOptions = {
|
||||||
svgoConfig: {
|
svgoConfig: {
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -14,6 +14,12 @@ const svgrLoaderOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "prefixIds",
|
||||||
|
params: {
|
||||||
|
prefixClassNames: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
buildFacetRecommendationRequestKey,
|
||||||
|
getCachedFacetScores,
|
||||||
|
loadFacetScores,
|
||||||
|
} from "../../lib/create/facetRecommendationsLoad";
|
||||||
|
|
||||||
|
describe("loadFacetScores", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
methods: [
|
||||||
|
{ slug: "slack", matches: { score: 3 } },
|
||||||
|
{ slug: "email", matches: { score: 1 } },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caches scores by section and facet query", async () => {
|
||||||
|
const queryString = "facet.size=small";
|
||||||
|
const scores = await loadFacetScores("communication", queryString);
|
||||||
|
|
||||||
|
expect(scores).toEqual({ slack: 3, email: 1 });
|
||||||
|
expect(
|
||||||
|
getCachedFacetScores(
|
||||||
|
buildFacetRecommendationRequestKey("communication", queryString),
|
||||||
|
),
|
||||||
|
).toEqual(scores);
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await loadFacetScores("communication", queryString);
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dedupes in-flight requests for the same key", async () => {
|
||||||
|
const queryString = "facet.size=small";
|
||||||
|
const [a, b] = await Promise.all([
|
||||||
|
loadFacetScores("membership", queryString),
|
||||||
|
loadFacetScores("membership", queryString),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(a).toEqual(b);
|
||||||
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user