summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/personalization
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/personalization')
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js73
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js370
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js134
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js190
-rw-r--r--node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js329
5 files changed, 1096 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js
new file mode 100644
index 0000000..f2430e9
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js
@@ -0,0 +1,73 @@
+import * as serverData from "../../foundation/json-parsing/server-data";
+import { demoteByEngagements } from "@amp/amd-apps";
+// The AMD key used to retrieve the engagement events from the app store.
+export const AMSEngagementAppStoreEventKey = "appStore.getEngagementEvents";
+/**
+ * personalize the dataItems based on the impression data for that shelf.
+ *
+ * @param objectGraph The object graph.
+ * @param dataItems The data items to personalize.
+ * @param impressionData The impression data to use for personalization.
+ * @returns The personalized data items.
+ */
+export function personalizeDataItems(dataItems, impressionData, shelfRecoMetricsData) {
+ const shelfAlgoId = shelfRecoMetricsData["reco_algoId"];
+ if (serverData.isNullOrEmpty(shelfAlgoId) ||
+ serverData.isNullOrEmpty(dataItems) ||
+ serverData.isNullOrEmpty(impressionData[shelfAlgoId])) {
+ return dataItems;
+ }
+ // First create Candidate objects from the data items.
+ const candidates = dataItems.map((dataItem) => {
+ var _a;
+ const adamId = serverData.asNumber(dataItem.id);
+ const score = (_a = serverData.asNumber(dataItem, "meta.personalizationData.score")) !== null && _a !== void 0 ? _a : 0;
+ const candidate = { identifier: adamId, score: score };
+ return candidate;
+ });
+ // Demote the candidates based on the impression data.
+ const shelfRecoData = impressionData[shelfAlgoId];
+ const rerankedCandidates = demoteByEngagements(candidates, shelfRecoData);
+ // Create a lookup map from dataItems to allow faster rearranging
+ const dictionary = dataItems.reduce((acc, item) => ({ ...acc, [item.id]: item }), {});
+ // create a new array based on the rearranged Candidate array
+ const rearranged = rerankedCandidates.map((candidate) => dictionary[candidate.identifier.toString()]);
+ return rearranged;
+}
+/**
+ * Takes the AMD response and creates a map of EngagementEvent per shelf.
+ *
+ * @param objectGraph The object graph.
+ * @param data The data to convert.
+ * @returns The map of EngagementEvent per shelf.
+ */
+export function impressionEventsFromData(objectGraph, data) {
+ // go through all the impressionData and return the EgagementData
+ if (!serverData.isDefinedNonNullNonEmpty(data)) {
+ return {};
+ }
+ // Impression data is a map of shelf ids to impression arrays.
+ // We want to keep this relationship so only impressions for a given shelf are returned.
+ const impressionData = serverData.asDictionary(data, "data.engagementEvents.impression");
+ const convertedMap = {};
+ // Iterate over each key in the original map and convert the data into EngagementEvents which can be passed to the demoteByEngagements function.
+ for (const key in impressionData) {
+ if (serverData.isDefinedNonNullNonEmpty(key)) {
+ const impressions = serverData.asArrayOrEmpty(impressionData, key);
+ convertedMap[key] = impressions.map((impression) => {
+ const adamId = serverData.asNumber(impression["adamId"]);
+ const timestamp = serverData.asNumber(impression["eventTimeMillis"]);
+ const event = { adamIdentifier: adamId, timestamp: timestamp };
+ return event;
+ });
+ }
+ }
+ return convertedMap;
+}
+/**
+ * Convenience function for determining if data personalization is available.
+ */
+export function isImpressionDemotionAvailable(objectGraph) {
+ return objectGraph.client.isiOS && objectGraph.bag.enableRecoOnDeviceReordering;
+}
+//# sourceMappingURL=on-device-impression-demotion.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js
new file mode 100644
index 0000000..538e708
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js
@@ -0,0 +1,370 @@
+import { isNothing } from "@jet/environment";
+import * as serverData from "../../foundation/json-parsing/server-data";
+import { diversifyDataItems, getOrderedAppIds, getUpdatedScoreAfterBoosting, PersonalizedData, } from "./on-device-recommendations-common";
+/**
+ * This utility class simplifies processing the raw data, by decorating with some key properties.
+ * */
+class PersonalizedDataDefault extends PersonalizedData {
+ constructor(rawData) {
+ super();
+ this.rawData = rawData;
+ this.isExactMatch = false;
+ this.isWildcardMatch = false;
+ this.isUnpersonalizedMatch = false;
+ this.isFallbackMatch = false;
+ this.appId = null;
+ this.groupId = null;
+ this.score = 0;
+ this.modifiedScore = 0;
+ this.onDeviceScore = 0;
+ }
+}
+// Represents a "match all" wildcard segment. Any data items that have this segment are always considered a match.
+const alwaysMatchUserSegment = "-1";
+/**
+ * Converts a list of raw data blobs into a list that has been personalized for the user, based upon on device personalization data.
+ *
+ * If use_segment_scores is true, the rules we follow here are:
+ * 1. Choose the data items that have personalization segments which match the user
+ * 2. Remove some data items so that there is only one per group
+ * 3. Bring any data items where the user exactly matches the personalization segment to the front of the list
+ *
+ * If needed, we may also include fallback results to reach a preferred number of results. For any group where no matches are found, the last
+ * item in that group can be used as a fallback. We can only ever have one item per group, so it may not always be possible to reach the
+ * preferred number of results.
+ *
+ * If use_signals is true, we rerank content using the on-device scores
+ *
+ * @param dataItems The raw data blobs.
+ * @param onDevicePersonalizationDataContainer The on device personalization data container for the user, used for matching segments against the dataItems.
+ * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should always be included in the results.
+ * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount.
+ * @param preferredResultCount The preferred number of items to be included in the results.
+ * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
+ * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
+ * @returns The personalized set of data. This will be a subset (or all) of the original dataItems, and metrics data.
+ */
+export function personalizeDataItems(objectGraph, dataItems, onDevicePersonalizationDataContainer, includeItemsWithNoPersonalizationData, allowUnmatchedFallbackResults, preferredResultCount, parentAppId, diversify) {
+ var _a;
+ let sortResult = { sortedDataItems: [], processingType: 0 };
+ const useSignals = (_a = onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.metricsData["use_signals"]) !== null && _a !== void 0 ? _a : false;
+ if (!useSignals) {
+ // First decorate our raw dataItems with segment and group information
+ const personalizedDataItems = personalizedDataItemsFromDataItems(objectGraph, dataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.personalizationData, includeItemsWithNoPersonalizationData, parentAppId);
+ // Get server side ordering of app Ids to be used for diversification
+ const serverSideAppIdsOrdering = getOrderedAppIds(personalizedDataItems);
+ // Now iterate through the list of personalizedDataItems, and choose one per group
+ const matchedDataItemsIncludingFallback = filterDataItemsIntoOnePerGroup(objectGraph, personalizedDataItems);
+ // Now sort the data items, respecting our preferredResultCount if needed
+ sortResult = sortDataItems(objectGraph, matchedDataItemsIncludingFallback, allowUnmatchedFallbackResults, serverSideAppIdsOrdering, preferredResultCount, diversify);
+ }
+ else {
+ // First decorate our raw dataItems with frequency, recency, usage information
+ const personalizedDataItems = personalizedDataItemsFromDataItemsOnDeviceSignals(objectGraph, dataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.personalizationData, includeItemsWithNoPersonalizationData, parentAppId);
+ // Now sort the data items
+ const sortedDataItems = getUpdatedScoreAfterBoosting(personalizedDataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.metricsData);
+ const orderWasNotChanged = personalizedDataItems.every((dataItem, index) => {
+ return dataItem === sortedDataItems[index];
+ });
+ sortResult = {
+ sortedDataItems: sortedDataItems,
+ processingType: orderWasNotChanged
+ ? 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */
+ : 2 /* onDevicePersonalization.ProcessingType.contentsSorted */,
+ };
+ if (serverData.isDefinedNonNull(preferredResultCount) &&
+ sortResult.sortedDataItems.length >= preferredResultCount) {
+ sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount);
+ }
+ }
+ // We only need to return the raw data blobs, so remove the personalization decoration
+ const finalDataItems = sortResult.sortedDataItems.map((personalizedDataItem) => personalizedDataItem.rawData);
+ // Generate the processing type value
+ const filterType = dataItems.length !== finalDataItems.length
+ ? 1 /* onDevicePersonalization.ProcessingType.contentsFiltered */
+ : 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */;
+ const processingType = filterType + sortResult.processingType;
+ return {
+ personalizedData: finalDataItems,
+ processingType: processingType,
+ };
+}
+/**
+ * Creates a list of `PersonalizedData` objects, based on the input raw data items.
+ *
+ * @param dataItems The raw data blobs.
+ * @param onDevicePersonalizationData The on device personalization data, used for matching personalization segments against the dataItems.
+ * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should be included in the results.
+ * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
+ * @returns A list of PersonalizedData objects.
+ */
+function personalizedDataItemsFromDataItemsOnDeviceSignals(objectGraph, dataItems, onDevicePersonalizationData, includeItemsWithNoPersonalizationData, parentAppId) {
+ const personalizedDataItems = [];
+ for (const data of dataItems) {
+ const personalizedData = new PersonalizedDataDefault(data);
+ // Filter out invalid data
+ const score = serverData.asNumber(data, "meta.personalizationData.score");
+ let appId = serverData.asString(data, "meta.personalizationData.appId");
+ if ((isNothing(appId) || appId.length === 0) && (parentAppId === null || parentAppId === void 0 ? void 0 : parentAppId.length) > 0) {
+ // If we have a parentAppId this means we are coming from search, where `appId` is not provided.
+ appId = parentAppId;
+ }
+ if (isNothing(appId) || appId.length === 0) {
+ // Personalization data is missing or invalid. This may sometimes be valid, eg. evergreen today stories for when reco times out.
+ if (includeItemsWithNoPersonalizationData) {
+ personalizedData.isUnpersonalizedMatch = true;
+ personalizedDataItems.push(personalizedData);
+ }
+ continue;
+ }
+ if (serverData.isDefinedNonNull(onDevicePersonalizationData)) {
+ const onDevicePersonalizationDataForApp = onDevicePersonalizationData[appId];
+ if (serverData.isDefinedNonNull(onDevicePersonalizationDataForApp) &&
+ serverData.isDefinedNonNull(onDevicePersonalizationDataForApp.onDeviceSignals)) {
+ personalizedData.onDeviceScore = +onDevicePersonalizationDataForApp.onDeviceSignals;
+ }
+ }
+ personalizedData.appId = appId;
+ personalizedData.score = score !== null && score !== void 0 ? score : 0;
+ personalizedDataItems.push(personalizedData);
+ }
+ return personalizedDataItems;
+}
+/**
+ * Creates a list of `PersonalizedData` objects, based on the input raw data items.
+ *
+ * @param dataItems The raw data blobs.
+ * @param onDevicePersonalizationData The on device personalization data, used for matching personalization segments against the dataItems.
+ * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should be included in the results.
+ * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
+ * @returns A list of PersonalizedData objects.
+ */
+function personalizedDataItemsFromDataItems(objectGraph, dataItems, onDevicePersonalizationData, includeItemsWithNoPersonalizationData, parentAppId) {
+ const personalizedDataItems = [];
+ for (const data of dataItems) {
+ const personalizedData = new PersonalizedDataDefault(data);
+ // Filter out invalid data
+ const rawDataUserSegments = serverData.asString(data, "meta.personalizationData.segId");
+ let appId = serverData.asString(data, "meta.personalizationData.appId");
+ let groupId = serverData.asString(data, "meta.personalizationData.grpId");
+ if ((isNothing(appId) || appId.length === 0) && (parentAppId === null || parentAppId === void 0 ? void 0 : parentAppId.length) > 0) {
+ // If we have a parentAppId this means we are coming from search, where `appId` and `grpId` are not provided.
+ // Normally we filter our data items to only allow one item per group, so in this case we allocate a random
+ // group ID, so that none of the data items get filtered out for that reason. Later on as part of search
+ // results processing we will pick the first (valid) result, but only after ODP has finished.
+ appId = parentAppId;
+ groupId = objectGraph.random.nextUUID();
+ }
+ if (serverData.isNullOrEmpty(rawDataUserSegments) ||
+ serverData.isNullOrEmpty(appId) ||
+ serverData.isNullOrEmpty(groupId)) {
+ // Personalization data is missing or invalid. This may sometimes be valid, eg. evergreen today stories for when reco times out.
+ if (includeItemsWithNoPersonalizationData) {
+ personalizedData.isUnpersonalizedMatch = true;
+ personalizedDataItems.push(personalizedData);
+ }
+ continue;
+ }
+ // Check if the data has the match all user segment
+ const dataUserSegments = rawDataUserSegments.split(",");
+ if (dataUserSegments.includes(alwaysMatchUserSegment)) {
+ personalizedData.isWildcardMatch = true;
+ }
+ // Check if any of the data segments match with the on device personalization data
+ if (serverData.isDefinedNonNull(onDevicePersonalizationData)) {
+ const onDevicePersonalizationDataForApp = onDevicePersonalizationData[appId];
+ if (serverData.isDefinedNonNull(onDevicePersonalizationDataForApp)) {
+ for (const dataUserSegment of dataUserSegments) {
+ if (onDevicePersonalizationDataForApp.userSegments.includes(dataUserSegment)) {
+ personalizedData.isExactMatch = true;
+ break;
+ }
+ }
+ }
+ }
+ personalizedData.appId = appId;
+ personalizedData.groupId = groupId;
+ personalizedDataItems.push(personalizedData);
+ }
+ return personalizedDataItems;
+}
+/**
+ * Iterates through the list of given data items, and ensures we only have one per group.
+ *
+ * @param dataItems The data items to processed.
+ * @returns A subset of dataItems, with only one dataItem per group.
+ */
+function filterDataItemsIntoOnePerGroup(objectGraph, dataItems) {
+ var _a;
+ const filledGroupIds = new Set();
+ const matchedDataItemsIncludingMultipleFallbacksPerGroup = [];
+ // Determine which groups have any exact matches
+ const groupIdsWithExactMatchesArray = dataItems
+ .filter((dataItem) => {
+ return dataItem.isExactMatch;
+ })
+ .map((dataItem) => {
+ return dataItem.groupId;
+ });
+ const groupIdsWithExactMatches = new Set(groupIdsWithExactMatchesArray);
+ // Now iterate through our data items, and filter out any we don't need
+ dataItems.forEach((dataItem, index) => {
+ // If an item has no group, we always include it. This would only happen for
+ // data which is missing valid personalization metadata, and we have specifically
+ // opted in to including these items in the results.
+ if (serverData.isNullOrEmpty(dataItem.groupId)) {
+ matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
+ return;
+ }
+ // We already have a match for this group, so move onto the next item
+ if (filledGroupIds.has(dataItem.groupId)) {
+ return;
+ }
+ // This item is an unpersonalized match, which will only occur if we permit this.
+ // These are always added to the result set.
+ if (dataItem.isUnpersonalizedMatch) {
+ matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
+ return;
+ }
+ // This item is the first exact match for this group, so add it into our result set
+ if (dataItem.isExactMatch) {
+ filledGroupIds.add(dataItem.groupId);
+ matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
+ return;
+ }
+ // If we know we have an exact match somewhere else for this group, we can just
+ // continue on to the next item, as the exact match will be picked later.
+ if (groupIdsWithExactMatches.has(dataItem.groupId)) {
+ return;
+ }
+ // We have no exact matches for this group, so we can now take wildcard matches.
+ if (dataItem.isWildcardMatch) {
+ filledGroupIds.add(dataItem.groupId);
+ matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
+ return;
+ }
+ // This item is not a match. As we don't have any matches for this group yet,
+ // we can mark it as a fallback. This does not necessarily mean it will be used,
+ // but it does mean it becomes available for use. groupIDs are not necessarily in
+ // sequential order, so we mark all of these as fallbacks, and filter them further below.
+ dataItem.isFallbackMatch = true;
+ matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem);
+ });
+ // We now need to remove all the fallback items except for the last one in each group, so iterate
+ // through in reverse order and filter out any duplicates
+ const matchedDataItemsWithOneFallbackPerGroup = [];
+ const reversedMatchedDataItems = matchedDataItemsIncludingMultipleFallbacksPerGroup.slice().reverse();
+ for (const dataItem of reversedMatchedDataItems) {
+ if (dataItem.isFallbackMatch) {
+ if (filledGroupIds.has(dataItem.groupId)) {
+ continue;
+ }
+ }
+ matchedDataItemsWithOneFallbackPerGroup.push(dataItem);
+ if (((_a = dataItem.groupId) === null || _a === void 0 ? void 0 : _a.length) > 0) {
+ filledGroupIds.add(dataItem.groupId);
+ }
+ }
+ // Return to our original order
+ matchedDataItemsWithOneFallbackPerGroup.reverse();
+ return matchedDataItemsWithOneFallbackPerGroup;
+}
+/**
+ * Sorts the given list of data items, and optionally restricts the list to a specified number of results.
+ *
+ * @param dataItems The data items to process.
+ * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount.
+ * @param preferredResultCount? The preferrd number of results.
+ * @param serverSideAppIdsOrdering List of ordered app ids from server side
+ * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
+ * @returns The sorted list of dataItems, optionally restricted in length,
+ */
+function sortDataItems(objectGraph, dataItems, allowUnmatchedFallbackResults, serverSideAppIdsOrdering, preferredResultCount, diversify) {
+ let sortResult;
+ // Excluding fallback results is the preferred route, but if the number of results is less than our preferredResultCount, we will need to use the fallback results.
+ const dataItemsWithoutFallback = dataItems.filter((data) => data.isExactMatch || data.isWildcardMatch || data.isUnpersonalizedMatch || serverData.isNull(data.groupId));
+ if (serverData.isNull(preferredResultCount)) {
+ // There is no preferred number of results, so simply perform our final sort and then return
+ sortResult = sortAndDiversify(dataItemsWithoutFallback, serverSideAppIdsOrdering, diversify);
+ }
+ else if (dataItemsWithoutFallback.length >= preferredResultCount || !allowUnmatchedFallbackResults) {
+ // There is a preferred number of results, but we either have enough items without needing to utilise
+ // any fallback matches, or we don't allow fallback results.
+ sortResult = sortAndDiversify(dataItemsWithoutFallback, serverSideAppIdsOrdering, diversify);
+ sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount);
+ }
+ else {
+ // There is a preferred number of results, and we need to use fallback matches in order to
+ // meet this number. We may still fall short, but this gets us as close as possible.
+ sortResult = sortAndDiversify(dataItems, serverSideAppIdsOrdering, diversify);
+ sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount);
+ }
+ return sortResult;
+}
+/**
+ * Rearranges a list of dataItems, so that any where there is an exact segment match are moved to the front of the list.
+ *
+ * @param dataItems The data items to process.
+ * @param serverSideAppIdsOrdering List of ordered app ids from server side
+ * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
+ * @returns The sorted list of data items.
+ */
+function sortAndDiversify(dataItems, serverSideAppIdsOrdering, diversify) {
+ const exactMatchDataItems = dataItems.filter((value) => value.isExactMatch);
+ let otherDataItems = dataItems.filter((value) => !value.isExactMatch);
+ if (serverData.isDefinedNonNull(diversify) && diversify) {
+ otherDataItems = diversifyDataItems(otherDataItems, serverSideAppIdsOrdering);
+ }
+ const sortedDataItems = exactMatchDataItems.concat(otherDataItems);
+ const orderWasNotChanged = dataItems.every((dataItem, index) => {
+ return dataItem === sortedDataItems[index];
+ });
+ return {
+ sortedDataItems: sortedDataItems,
+ processingType: orderWasNotChanged
+ ? 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */
+ : 2 /* onDevicePersonalization.ProcessingType.contentsSorted */,
+ };
+}
+/**
+ * Filters a list of raw data blobs into a list which only includes non-personalized data, or data that is set to "match all".
+ *
+ * @param dataItems The raw data blobs.
+ * @param preferredResultCount The preferred number of items to be included in the results.
+ * @returns The filtered set of data blobs. This will be a subset (or all) of the original dataItems.
+ */
+export function removePersonalizedDataItems(objectGraph, dataItems, preferredResultCount) {
+ let filteredDataItems = [];
+ const filledGroupIds = new Set();
+ for (const data of dataItems) {
+ // If the personalization data is invalid or empty, we keep this in our result set.
+ const rawDataUserSegments = serverData.asString(data, "meta.personalizationData.segId");
+ const appId = serverData.asString(data, "meta.personalizationData.appId");
+ const groupId = serverData.asString(data, "meta.personalizationData.grpId");
+ if (serverData.isNullOrEmpty(rawDataUserSegments) ||
+ serverData.isNullOrEmpty(appId) ||
+ serverData.isNullOrEmpty(groupId)) {
+ filteredDataItems.push(data);
+ continue;
+ }
+ // We already have a match for this group, so move onto the next item
+ if (filledGroupIds.has(groupId)) {
+ continue;
+ }
+ // If the data has a match all user segment, we keep this in our result set.
+ const dataUserSegments = rawDataUserSegments.split(",");
+ if (dataUserSegments.includes(alwaysMatchUserSegment)) {
+ filteredDataItems.push(data);
+ filledGroupIds.add(groupId);
+ }
+ }
+ // Finally, if we have a preferredResultCount which is smaller than our result set, trim our results down to this count
+ if (serverData.isDefinedNonNull(preferredResultCount) && filteredDataItems.length > preferredResultCount) {
+ filteredDataItems = filteredDataItems.slice(0, preferredResultCount);
+ }
+ return {
+ personalizedData: filteredDataItems,
+ processingType: null,
+ };
+}
+//# sourceMappingURL=on-device-personalization-processing.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js
new file mode 100644
index 0000000..02003e9
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js
@@ -0,0 +1,134 @@
+import * as onDevicePersonalizationGroupingProcessing from "./on-device-personalization-grouping-processing";
+import * as onDevicePersonalizationProcessing from "./on-device-personalization-processing";
+import * as serverData from "../../foundation/json-parsing/server-data";
+/**
+ * Accepts an array of data blobs, and returns a subset of those original data blobs which is personalized to the user.
+ * In the case where personalization is disabled, or any personalized data blobs will instead be filtered out.
+ *
+ * @param placement Placement of the personalized items for on-device personalization
+ * @param dataItems The input list of data blobs.
+ * @param includeItemsWithNoPersonalizationData Whether to include data which no personalizationData meta is present.
+ * @param personalizationDataContainer The data container to use for personalizing the data.
+ * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount.
+ * @param preferredResultCount The preferred number of items to be included in the results.
+ * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search.
+ * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
+ * @returns The relevant list of data blobs.
+ */
+export function personalizeDataItems(objectGraph, placement, dataItems, includeItemsWithNoPersonalizationData, personalizationDataContainer, allowUnmatchedFallbackResults = false, preferredResultCount, parentAppId, diversify) {
+ if (isPersonalizationAvailable(objectGraph)) {
+ switch (placement) {
+ case "groupingAppEvent":
+ return personalizeGroupingDataItems(objectGraph, dataItems, includeItemsWithNoPersonalizationData, personalizationDataContainer, diversify);
+ default:
+ return onDevicePersonalizationProcessing.personalizeDataItems(objectGraph, dataItems, personalizationDataContainer, includeItemsWithNoPersonalizationData, allowUnmatchedFallbackResults, preferredResultCount, parentAppId, diversify);
+ }
+ }
+ else {
+ return onDevicePersonalizationProcessing.removePersonalizedDataItems(objectGraph, dataItems, preferredResultCount);
+ }
+}
+/**
+ * Accepts an array of data blobs, and returns a subset of those original data blobs which is personalized to the user.
+ * In the case where personalization is disabled, or any personalized data blobs will instead be filtered out.
+ *
+ * @param dataItems The input list of data blobs.
+ * @param includeItemsWithNoPersonalizationData Whether to include data which no personalizationData meta is present.
+ * @param personalizationDataContainer The data container to use for personalizing the data.
+ * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking
+ * @returns The relevant list of data blobs.
+ */
+function personalizeGroupingDataItems(objectGraph, dataItems, includeItemsWithNoPersonalizationData, personalizationDataContainer, diversify) {
+ var _a, _b;
+ // We must filter out non app-event items in order to avoid moving their positions
+ // when contingent_offers_personalization is turned off
+ let appEventsOnlyDataItems = dataItems;
+ let wereItemsRemoved = false;
+ const nonAppEventIndexes = [];
+ if (!objectGraph.featureFlags.isEnabled("contingent_offers_personalization")) {
+ appEventsOnlyDataItems = dataItems.filter((item, index) => {
+ if (serverData.isDefinedNonNullNonEmpty(item.type) && item.type !== "app-events") {
+ nonAppEventIndexes.push(index);
+ return false;
+ }
+ return true;
+ });
+ wereItemsRemoved = appEventsOnlyDataItems.length !== dataItems.length;
+ }
+ // We fetch the information regarding the segment optimizer flow from the personalization container
+ const personalizedMetricsData = personalizationDataContainer === null || personalizationDataContainer === void 0 ? void 0 : personalizationDataContainer.metricsData;
+ const useSegScores = (_a = personalizedMetricsData["use_segment_scores"]) !== null && _a !== void 0 ? _a : false;
+ const useOnDeviceSignals = (_b = personalizedMetricsData["use_signals"]) !== null && _b !== void 0 ? _b : false;
+ let personalizedResults;
+ if (useSegScores || useOnDeviceSignals) {
+ personalizedResults = onDevicePersonalizationGroupingProcessing.personalizeDataItems(objectGraph, appEventsOnlyDataItems, personalizationDataContainer, diversify);
+ }
+ else {
+ personalizedResults = onDevicePersonalizationProcessing.personalizeDataItems(objectGraph, appEventsOnlyDataItems, personalizationDataContainer, includeItemsWithNoPersonalizationData, null, null, null, diversify);
+ }
+ // We re-add non app-event items back into their original positions
+ if (wereItemsRemoved) {
+ const resultsArray = personalizedResults.personalizedData;
+ nonAppEventIndexes.forEach((index) => {
+ const item = dataItems[index];
+ if (index < resultsArray.length) {
+ resultsArray.splice(index, 0, item);
+ }
+ else {
+ resultsArray.push(item);
+ }
+ });
+ personalizedResults = {
+ personalizedData: resultsArray,
+ processingType: personalizedResults.processingType,
+ };
+ }
+ return personalizedResults;
+}
+/**
+ * Convenience function for determining if data personalization is available.
+ */
+export function isPersonalizationAvailable(objectGraph) {
+ return (objectGraph.client.isiOS &&
+ objectGraph.user.isOnDevicePersonalizationEnabled &&
+ objectGraph.bag.enableOnDevicePersonalization);
+}
+/**
+ * Reaches down to the native client to return the current set of on device personalization data,
+ * restricted to a set of app IDs.
+ *
+ * @param appIds A set of appIds to restrict the personalization data to.
+ * @returns The relevant set of personalization data
+ */
+export function personalizationDataContainerForAppIds(objectGraph, appIds) {
+ if (!isPersonalizationAvailable(objectGraph)) {
+ return null;
+ }
+ if (objectGraph.host.platform === "iOS") {
+ return objectGraph.user.onDevicePersonalizationDataContainerForAppIds(Array.from(appIds));
+ }
+ else {
+ return {
+ personalizationData: {},
+ metricsData: null,
+ };
+ }
+ return null;
+}
+/**
+ * Reaches down to the native client to return the current metrics data.
+ *
+ * @returns The current AMDClient metrics data
+ */
+export function metricsData(objectGraph) {
+ if (!isPersonalizationAvailable(objectGraph)) {
+ return null;
+ }
+ if (objectGraph.host.platform === "iOS") {
+ return objectGraph.user.onDevicePersonalizationDataContainerForAppIds([]).metricsData;
+ }
+ else {
+ return null;
+ }
+}
+//# sourceMappingURL=on-device-personalization.js.map \ No newline at end of file
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js
new file mode 100644
index 0000000..c03b729
--- /dev/null
+++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js
@@ -0,0 +1,190 @@
+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 * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common";
+import { Parameters } from "../../foundation/network/url-constants";
+export class PersonalizedData {
+}
+export async function recommendedAppsForUseCase(objectGraph, useCase, displayContext) {
+ const displayContextLogString = displayContext === "shelf" ? "OnDeviceRecommendationsShelfController" : "OnDeviceRecommendationsPageController";
+ return await new Promise((resolve, reject) => {
+ if (!objectGraph.host.isiOS) {
+ const errorMessage = `${displayContextLogString}: On device personalization is only enabled on iOS devices.`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new Error(errorMessage));
+ return;
+ }
+ if (serverData.isNullOrEmpty(objectGraph.user.dsid)) {
+ const errorMessage = `${displayContextLogString}: User is currently not signed in.`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new Error(errorMessage));
+ return;
+ }
+ if (serverData.isNullOrEmpty(useCase)) {
+ const errorMessage = `${displayContextLogString}: Missing valid useCase for ODP: ${useCase}`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new Error(errorMessage));
+ return;
+ }
+ objectGraph.onDeviceRecommendationsManager
+ .performRequest({
+ type: "fetchRecommendations",
+ dsId: objectGraph.user.dsid,
+ useCase: useCase,
+ })
+ .then((recoResponse) => {
+ const candidates = serverData.asArrayOrEmpty(recoResponse["candidates"]);
+ const recoMetrics = serverData.asJSONData(recoResponse["metrics"]);
+ if (serverData.isNullOrEmpty(candidates)) {
+ const errorMessage = `${displayContextLogString}: ODP returned no candidate ids for useCase: ${useCase}`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new NoODPCandidatesError(errorMessage));
+ return;
+ }
+ if (serverData.isNullOrEmpty(recoMetrics)) {
+ const errorMessage = `${displayContextLogString}: ODP returned no metrics for useCase: ${useCase}`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new Error(errorMessage));
+ return;
+ }
+ const candidatesData = [];
+ for (const candidateId of candidates) {
+ if (serverData.isDefinedNonNullNonEmpty(candidateId)) {
+ candidatesData.push({
+ id: candidateId,
+ type: "apps",
+ });
+ }
+ }
+ // MAPI Request
+ const mediaApiRequest = new mediaDataFetching.Request(objectGraph, candidatesData)
+ .withFilter("apps:recommendable", "true")
+ .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase);
+ groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest);
+ mediaNetwork
+ .fetchData(objectGraph, mediaApiRequest)
+ .then((recoDataContainer) => {
+ resolve({
+ candidates: candidates,
+ recoMetrics: recoMetrics,
+ dataContainer: recoDataContainer,
+ });
+ })
+ .catch((error) => {
+ const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for candidates: ${candidates}`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new Error(errorMessage));
+ });
+ })
+ .catch((error) => {
+ const errorMessage = `${displayContextLogString}: Failed to perform ODP for useCase: ${useCase}, ${error}`;
+ validation.unexpectedType("defaultValue", errorMessage, null);
+ reject(new Error(errorMessage));
+ });
+ });
+}
+/***
+ * Gives the list of app ids in the order in which they appear in the passed response
+ * @param personalizedDataItems The data items to be processed
+ * @return List of ordered app ids
+ */
+export function getOrderedAppIds(personalizedDataItems) {
+ const seenAppIds = new Set();
+ const orderedAppIds = [];
+ personalizedDataItems.forEach((dataItem, index) => {
+ if (!seenAppIds.has(dataItem.appId)) {
+ orderedAppIds.push(dataItem.appId);
+ seenAppIds.add(dataItem.appId);
+ }
+ });
+ return orderedAppIds;
+}
+/***
+ * This function reranks events based on a calculated on-device score
+ * that can factor in recency, frequency, or usage of an app
+ * @param personalizedDataItems List of personalized data items
+ * @param metricsData Metrics data with hyperparameter values to use
+ * @return Personalized data items after reranking using on-device signals
+ */
+export function getUpdatedScoreAfterBoosting(personalizedDataItems, metricsData) {
+ var _a;
+ const weightParam = Number((_a = metricsData["weight_parameter"]) !== null && _a !== void 0 ? _a : 0.0);
+ for (const personalizedDataItem of personalizedDataItems) {
+ const serverScore = personalizedDataItem.score;
+ const onDeviceScore = personalizedDataItem.onDeviceScore;
+ personalizedDataItem.modifiedScore = weightParam * onDeviceScore + (1 - weightParam) * serverScore;
+ }
+ personalizedDataItems.sort((a, b) => {
+ return b.modifiedScore - a.modifiedScore;
+ });
+ return personalizedDataItems;
+}
+/***
+ * This function will diversify the personalized items on the basis of ordering of apps passed.
+ * In case there are pinned items, we will remove them from diversification bucket and keep them in their position and add diversified items in order
+ * This can lead to duplicates next to the pinned items. But the pinning and duplicate app events case scenario is rare so we avoid complicated logic here.
+ * Explanation ->
+ * If the original data items list contains appId in this order -> [1(a), 1(b), 2(a), 3(a), 2(b), 2(c), 3(b), 4(a), 1(c), 4(b)]
+ * and the ordering of the apps used for relative ranking is -> [1, 2, 3, 4]
+ * Then the updated ordering of the data items should look like -> [1(a), 2(a), 3(a), 4(a), 1(b), 2(b), 3(b), 4(b), 1(c), 2(c)]
+ * @param personalizedDataItems List of personalized data items for diversification
+ * @param orderedAppIds List of ordered app ids used for relative ordering between in-app events
+ * @return The diversified list of data items.
+ */
+export function diversifyDataItems(personalizedDataItems, orderedAppIds) {
+ const pinnedItems = personalizedDataItems.filter((item) => serverData.isDefinedNonNull(item.pinnedPosition));
+ personalizedDataItems = personalizedDataItems.filter((item) => serverData.isNull(item.pinnedPosition));
+ const appIdGroups = new Map();
+ // Insert the elements in a map in reverse order of their appearance in dataItems so that we can later use pop() instead of shift()
+ // 1 -> [1(c), 1(b), 1(a)], 2 -> [2(c), 2(b), 2(a)], 3 -> [3(b), 3(a)], 4-> [4(b), 4(a)]
+ personalizedDataItems.reverse();
+ personalizedDataItems.forEach((dataItem, index) => {
+ if (dataItem.appId in appIdGroups) {
+ appIdGroups[dataItem.appId].push(dataItem);
+ }
+ else {
+ appIdGroups[dataItem.appId] = [dataItem];
+ }
+ });
+ const diversifiedDataItems = [];
+ // We find the max number of appItems for our appIds, to determine the iteration count
+ const maxAppEventsForAppId = Math.max(...Object.values(appIdGroups).map((a) => a.length));
+ for (let index = 0; index < maxAppEventsForAppId; index++) {
+ const reducedServerSideAppIdsOrdering = [];
+ orderedAppIds.forEach((appId) => {
+ if (appId in appIdGroups && appIdGroups[appId].length > 0) {
+ diversifiedDataItems.push(appIdGroups[appId].pop());
+ if (appIdGroups[appId].length !== 0) {
+ reducedServerSideAppIdsOrdering.push(appId);
+ }
+ }
+ });
+ orderedAppIds = reducedServerSideAppIdsOrdering;
+ }
+ // Merge the pinned items and diversified items
+ const finalizedResponse = new Array(pinnedItems.length + diversifiedDataItems.length);
+ // Sort pinned items by pinned position to be able to handle the pinned items that exceed the final list length
+ // For all the pinned items that have position greater than input list size, we just add them to end of the list
+ pinnedItems.sort((a, b) => a.pinnedPosition - b.pinnedPosition);
+ for (const dataItem of pinnedItems) {
+ if (dataItem.pinnedPosition < finalizedResponse.length) {
+ // Possible edgecase - Sanity check in case filtering reduces the length of items
+ finalizedResponse[dataItem.pinnedPosition] = dataItem;
+ }
+ else {
+ // Extremely rare case scenario: Push these items to diversifiedDataItemsList
+ diversifiedDataItems.push(dataItem);
+ }
+ }
+ diversifiedDataItems.reverse(); // To allow popping for first element fetch
+ for (const [index, finalizedItem] of finalizedResponse.entries()) {
+ if (serverData.isNull(finalizedItem) && diversifiedDataItems.length) {
+ finalizedResponse[index] = diversifiedDataItems.pop();
+ }
+ }
+ return finalizedResponse;
+}
+export class NoODPCandidatesError extends Error {
+}
+//# sourceMappingURL=on-device-recommendations-common.js.map \ No newline at end of file
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