From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../on-device-recommendations-today.js | 329 +++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js') diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js new file mode 100644 index 0000000..df2fa73 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js @@ -0,0 +1,329 @@ +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 \ No newline at end of file -- cgit v1.2.3