diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js | |
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js | 190 |
1 files changed, 190 insertions, 0 deletions
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 |
