summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js329
1 files changed, 329 insertions, 0 deletions
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<Opt<TodayRecommendationsResult>>: 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<Opt<TodayRecommendationsResult>>: 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<TodayRecommendation | undefined>: 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<string[]>: 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<JSONData>: 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<TodayRecommendation>: 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