import { isNothing, isSome } from "@jet/environment"; import * as validation from "@jet/environment/json/validation"; import * as serverData from "../../foundation/json-parsing/server-data"; import * as mediaDataFetching from "../../foundation/media/data-fetching"; import * as mediaNetwork from "../../foundation/media/network"; import { Parameters } from "../../foundation/network/url-constants"; import { allOptional } from "../../foundation/util/promise-util"; import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common"; import * as lottery from "../util/lottery"; import { startPromiseWithAdditionalTimeout } from "../util/timeout-manager-util"; import { isPersonalizationAvailable } from "./on-device-personalization"; export const todayTabODPTimeoutUseCase = "todayTabPersonalization"; const displayContextLogString = "OnDeviceRecommendationsTodayShelfController"; /** * Convenience function for determining if Today tab Arcade personalization is available. */ export function isTodayTabArcadePersonalizationAvailable(objectGraph) { const isDataPersonalizationAvailable = isPersonalizationAvailable(objectGraph); const isiOS = objectGraph.client.isiOS; const isFeatureEnabledForCurrentUser = lottery.isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.todayTabArcadePersonalizationRate); // Personalization is enabled only IF // - Data personalization is available AND // - Client is iOS AND // - Feature is enabled for current user return isDataPersonalizationAvailable && isiOS && isFeatureEnabledForCurrentUser; } /** * Fetches and returns Today recommendations result using on-device recommendations manager with timeout. * @param timeout: Optional timeout in seconds. * @returns Promise>: Today recommendations result. */ export async function fetchTodayRecommendationsWithTimeout(objectGraph, timeout) { return await startPromiseWithAdditionalTimeout(objectGraph, fetchTodayRecommendations(objectGraph), timeout, todayTabODPTimeoutUseCase); } /** * Fetches and returns Today recommendations result using on-device recommendations manager. * @returns Promise>: Today recommendations result. */ async function fetchTodayRecommendations(objectGraph) { try { const useCases = await fetchUseCasesForTab("today", objectGraph); const recommendationPromises = useCases.map(async (useCase) => await fetchTodayRecommendationForUseCase(objectGraph, useCase)); const recommendationPromiseResults = await allOptional(recommendationPromises); const recommendations = recommendationPromiseResults .map((promiseResult) => { if (promiseResult.success) { return promiseResult.value; } else { return undefined; } }) .filter(isSome); return new TodayRecommendationsResult(recommendations); } catch (error) { objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP for Today recommendations: ${error}`); return undefined; } } /** * Fetches Today Recommendation for a use case using on-device recommendations manager. * - useCase: On-device personalization use case. * @returns Promise: Today recommendation. */ async function fetchTodayRecommendationForUseCase(objectGraph, useCase) { try { const odrResponse = await fetchTodayRecommendation(useCase, objectGraph); const recommendedCandidatesAndMetrics = await makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse); const recoMetrics = recommendedCandidatesAndMetrics.metrics; const recommendedCandidates = recommendedCandidatesAndMetrics.candidates; if (recommendedCandidates.length === 0) { return undefined; } // Select the first candidate as we support only one candidate for each use case. const recommendedCandidate = recommendedCandidates[0]; const todayRecommendationPromise = await makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph); return todayRecommendationPromise; } catch (error) { objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`); return undefined; } } /** * Fetches and returns use cases for given tab using on-device recommendations manager. * - tab: Navigation tab. * @returns Promise: Use cases for given tab. */ export async function fetchUseCasesForTab(tab, objectGraph) { if (serverData.isNullOrEmpty(objectGraph.user.dsid)) { const errorMessage = `${displayContextLogString}: User is currently not signed in.`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } try { const odrResponse = await objectGraph.onDeviceRecommendationsManager.performRequest({ type: "fetchUseCases", tab: tab, dsId: objectGraph.user.dsid, }); const useCases = serverData.asArrayOrEmpty(odrResponse["useCases"]); if (serverData.isNullOrEmpty(useCases)) { const errorMessage = `${displayContextLogString}: ODP returned no use cases for tab: ${tab}`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } return useCases; } catch (error) { const errorMessage = `${displayContextLogString}: Failed to fetch ODP use cases for tab: ${tab}, with error: ${error}`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } } /** * Fetches and returns Today recommendation response for given use case using on-device recommendations manager. * - useCase: Use case. * @returns Promise: Today recommendation response. */ export async function fetchTodayRecommendation(useCase, objectGraph) { if (serverData.isNullOrEmpty(objectGraph.user.dsid)) { const errorMessage = `${displayContextLogString}: User is currently not signed in.`; throw new Error(errorMessage); } try { return await objectGraph.onDeviceRecommendationsManager.performRequest({ type: "fetchRecommendations", dsId: objectGraph.user.dsid, useCase: useCase, }); } catch (error) { const errorMessage = `${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } } /** * Makes and returns Today recommended candidates and metrics from given useCase and on-device recommendation response. * - useCase: Use case. * - odrResponse: On-device recommendation response. * @returns { candidates: TodayRecommendedCandidate[]; metrics: JSONData }: Today recommended candidates and reco metrics. */ export async function makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse) { const recoCandidates = serverData.asArrayOrEmpty(odrResponse["candidates"]); if (serverData.isNullOrEmpty(recoCandidates)) { const errorMessage = `${displayContextLogString}: ODP returned no candidates for useCase: ${useCase}`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } const recoMetrics = serverData.asJSONData(odrResponse["metrics"]); const recommendedCandidates = recoCandidates.map((candidate) => makeRecommendedCandidate(candidate)).filter(isSome); if (serverData.isNull(recoMetrics) || serverData.isNullOrEmpty(recommendedCandidates)) { const errorMessage = `${displayContextLogString}: ODP candidates could not be parsed for useCase: ${useCase}`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } return { candidates: recommendedCandidates, metrics: recoMetrics }; } /** * Makes and returns Today recommendation for given use case and candidates using MAPI response. * - useCase: Use case. * - recommendedCandidate: Recommended candidate. * - recoMetrics: Reco metrics. * @returns Promise: Today recommendation. */ export async function makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph) { const mediaApiRequest = new mediaDataFetching.Request(objectGraph, recommendedCandidate.data, true) .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase) .addingQuery(Parameters.filterRecommendable, "true"); groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest); try { const mapiResponse = await mediaNetwork.fetchData(objectGraph, mediaApiRequest); return new TodayRecommendation(useCase, [recommendedCandidate], recoMetrics, mapiResponse); } catch (error) { const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for: ${recommendedCandidate.data}, with error: ${error}`; validation.unexpectedType("defaultValue", errorMessage, null); throw new Error(errorMessage); } } /** * Makes and returns Today recommended candidate using given candidate data. * - candidate: Candidate data. * @returns TodayRecommendedCandidate: Today recommended candidate. */ export function makeRecommendedCandidate(candidate) { // Extract candidate ID and type from candidate data. const candidateID = serverData.asString(candidate.id); const candidateType = serverData.asString(candidate.type); if (serverData.isNull(candidateID)) { return undefined; } let storyIDs = []; let mediaType; switch (candidateType) { case "editorialItemGroup": // Exract story candidates to use within the story group. const storyCandidates = serverData.asArrayOrEmpty(candidate.candidates); // Create story IDs from story candidates. storyIDs = storyCandidates .map((storyCandidate) => serverData.asString(storyCandidate.id)) .filter((storyID) => isSome(storyID)); mediaType = "editorial-item-groups"; break; case "editorialItem": mediaType = "editorial-items"; break; default: return undefined; } let candidatesData = []; candidatesData.push({ id: candidateID, type: mediaType, }); // If there are any story IDs, add story candidates to candidates data. if (serverData.isDefinedNonNullNonEmpty(storyIDs)) { const storyCandidates = storyIDs.map((storyID) => ({ id: storyID, type: "editorial-items", })); candidatesData = candidatesData.concat(storyCandidates); } return new TodayRecommendedCandidate(candidateID, mediaType, storyIDs, candidatesData); } /** * A container for Today recommendations that also makes story data and story group data. */ export class TodayRecommendationsResult { /** * Initializes today recommendations results. * @param recommendations - Today recommendations. */ constructor(recommendations) { this.recommendations = recommendations; } /** * Returns story data from recommendations that has the given use case. * @param useCase - Use case that is used to match a story. * @returns Story data for given use case and type, or null if not found. */ storyData(useCase) { var _a; const recommendation = this.recommendationForUseCase(useCase); const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-items"); if (isNothing(recommendation) || isNothing(candidate)) { return undefined; } return (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === candidate.id); } /** * Returns story group data from recommendations that has the given use case. * @param useCase - Use case that is used to match a story group. * @returns Story group data for given use case and type, or null if not found. */ storyGroupData(useCase) { var _a, _b; const recommendation = this.recommendationForUseCase(useCase); const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-item-groups"); if (isNothing(recommendation) || isNothing(candidate)) { return undefined; } const storyGroupData = (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === (candidate === null || candidate === void 0 ? void 0 : candidate.id)); const storiesData = (_b = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _b === void 0 ? void 0 : _b.data.filter((item) => candidate.candidateIDs.includes(item.id)); if (isNothing(storyGroupData) || isNothing(storiesData)) { return undefined; } storyGroupData["meta"] = { associations: { recommendations: { data: storiesData, }, }, }; return storyGroupData; } /** * Returns the first recommendation that has the given use case. * @param useCase - Use case that is used to match a recommendation. * @returns Recommendation for given use case or null if not found. */ recommendationForUseCase(useCase) { return this.recommendations.find((recommendation) => recommendation.useCase === useCase); } } /** * Today recommendation with a use case, candidates, metrics and a data container. */ export class TodayRecommendation { /** * Initializes today recommendation. * @param useCase - Use case that is used to match recommendations. * @param candidates - Recommended candidates to use as replacements. * @param recoMetrics - Metrics for given candidates. * @param dataContainer - Data returned from MAPI for recommended candidates. */ constructor(useCase, candidates, recoMetrics, dataContainer) { this.useCase = useCase; this.candidates = candidates; this.recoMetrics = recoMetrics; this.dataContainer = dataContainer; } /// Returns the first candidate that has the given type. candidate(type) { return this.candidates.find((candidate) => candidate.type === type); } } /** * Today recommended candidate with its id, type, data * and candidate IDs (only if it is a candidate for a story group). */ export class TodayRecommendedCandidate { /** * Initializes today recommended candidate. * @param id - Candidate id i.e. story ID or story group ID. * @param type - Candidate type i.e. "editorial-items" or "editorial-item-groups". * @param candidateIDs - Candidate IDs. * @param data - Data to use while fetching from MAPI. */ constructor(id, type, candidateIDs, data) { this.id = id; this.type = type; this.candidateIDs = candidateIDs; this.data = data; } } //# sourceMappingURL=on-device-recommendations-today.js.map