diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/personalization')
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 |
