/** * Builder methods for building a collection of search results models for a series of search result data. */ import * as validation from "@jet/environment/json/validation"; import { isNothing } from "@jet/environment/types/optional"; import * as models from "../../api/models"; import * as serverData from "../../foundation/json-parsing/server-data"; import { isDataHydrated } from "../../foundation/media/data-structure"; import { shouldFilter } from "../filtering"; import { nextPosition, pushBasicLocation, popLocation } from "../metrics/helpers/location"; import * as onDevicePersonalization from "../personalization/on-device-personalization"; import { searchResultFromData } from "./content/search-results"; import * as searchAds from "./search-ads"; import * as guidedSearch from "./guided-search/guided-search"; import { addImpressionFields } from "../metrics/helpers/impressions"; import { searchAdMissedOpportunityFromId } from "../lockups/ad-lockups"; /** * Determines where to display guided search, e.g. as mid-scroll module or pinned to top. * @param meta The metadata from the MAPI response for search catalog request. * @param objectGraph The App Store object graph. * @returns The position of the mid-scroll guided search module in search results, or `undefined` for the pinned to top experience. */ export function guidedSearchPositionFromSearchResponseMeta(meta, objectGraph) { var _a, _b, _c; // Client debug override const guidedSearchPositionOverride = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.integer("GuidedSearchOverrides.position"); if (serverData.isNumber(guidedSearchPositionOverride) && guidedSearchPositionOverride > 1) { return guidedSearchPositionOverride; } // Server controlled by default return (_c = (_b = meta === null || meta === void 0 ? void 0 : meta.displayStyle) === null || _b === void 0 ? void 0 : _b.guidedSearch) === null || _c === void 0 ? void 0 : _c.position; } /** * Create a `SearchResultsBuildResult` containing set of built and deferred results from the top-level search data container. * This method supports prepending adverts. * @param objectGraph * @param requestMetadata Request metadata for search being performed. * @param searchResponseMetadata Response metadata for search that was performed. * @param metricsOptions Metrics options for built models. * @param resultsData Array of search results data. * @param advertData Data container with advert results. * @param facetData Array of guided search token response data. * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past */ export async function createSearchResults(objectGraph, requestMetadata, searchResponseMetadata, metricsOptions, resultsData, advertData = undefined, facetData = undefined, installedStates = undefined, appStates = undefined) { var _a, _b, _c, _d, _e, _f; /// Built models const builtResults = []; // Unhydrated items that will be fetched in pagination. const deferredResults = []; // Search Experiments Data const searchExperimentsData = searchResponseMetadata || null; // Generate the personalization data const appIds = resultsData .filter((resultData) => { return resultData.type === "apps"; }) .map((resultData) => { return resultData.id; }); const personalizationDataContainer = onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, new Set(appIds)); // Build Adverts let advertsSearchResult; let advertsDisplayStyle; if (searchAds.platformSupportsAdverts(objectGraph) && serverData.isDefinedNonNullNonEmpty(advertData)) { const adsResultAndDisplayStyle = searchAds.adsResultFromSearchResults(objectGraph, advertData, resultsData, requestMetadata, metricsOptions, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, searchExperimentsData, personalizationDataContainer); advertsSearchResult = adsResultAndDisplayStyle.result; advertsDisplayStyle = adsResultAndDisplayStyle.displayStyle; if (serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) { advertsSearchResult.searchAdOpportunity = advertsSearchResult.lockups[0].searchAdOpportunity; builtResults.push(advertsSearchResult); } } // Flag for Ad Media Deduping let isFirstResult = true; const guidedSearchPosition = guidedSearchPositionFromSearchResponseMeta(searchResponseMetadata, objectGraph); for (const [index, resultData] of resultsData.entries()) { // Inject the mid-scroll guided search module if we've reached the desired position. if (index === guidedSearchPosition) { const tokens = createGuidedSearchTokens(objectGraph, requestMetadata.requestDescriptor, facetData, metricsOptions); if (tokens.length > 0) { const title = (_c = (_b = (_a = searchResponseMetadata === null || searchResponseMetadata === void 0 ? void 0 : searchResponseMetadata.displayStyle) === null || _a === void 0 ? void 0 : _a.guidedSearch) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : objectGraph.loc.string("Search.Guided.Title.ExploreMore"); // static fallback query context const guidedSearchResult = new models.GuidedSearchResult(title, tokens); const impressionOptions = { ...metricsOptions, id: "midScrollGuidedSearch", kind: "grouping", targetType: "module", title: title, softwareType: null, }; addImpressionFields(objectGraph, guidedSearchResult, impressionOptions); builtResults.push(guidedSearchResult); nextPosition(metricsOptions.locationTracker); } } // Deferred items for subsequent pagination. if (!isDataHydrated(resultData)) { // On the first unhydrated item, attach the rest of the queue to `deferredResults` to preserve ordering. deferredResults.push(...resultsData.slice(index)); break; } // Filter if (shouldFilter(objectGraph, resultData, 10750 /* Filter.Search */)) { continue; } // Advert: Update CPP data on first organic result. // We must do this *after* the ad results are built, because we need to ensure we're picking the first lockup that will appear, // not just the first data (that may be filtered somehow). if (isFirstResult && serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) { searchAds.updateDupeOrganicResultCPPData(objectGraph, advertData !== null && advertData !== void 0 ? advertData : [], advertsSearchResult, resultData, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, metricsOptions, personalizationDataContainer); } // Build model const searchResult = searchResultFromData(objectGraph, resultData, searchResponseMetadata, personalizationDataContainer, metricsOptions, requestMetadata.requestDescriptor.isNetworkConstrained, requestMetadata.requestDescriptor.searchEntity, searchExperimentsData); if (!searchResult || !platformSupportsResultType(objectGraph, searchResult)) { continue; } /** * Advert: When first advert and result matches, modify media. */ if (isFirstResult && serverData.isDefinedNonNullNonEmpty(advertsSearchResult) && serverData.isDefinedNonNullNonEmpty(advertsSearchResult.lockups)) { searchAds.dedupeAdMediaFromMatchingResult(objectGraph, advertsSearchResult, searchResult, searchExperimentsData, advertsDisplayStyle); } /** * Advert: When advert isn't available, mark the organic as a missed opportunity slot */ if (isFirstResult && searchAds.platformSupportsAdverts(objectGraph) && serverData.isDefinedNonNull((_d = metricsOptions.pageInformation) === null || _d === void 0 ? void 0 : _d.iAdInfo) && (serverData.isNull(advertsSearchResult) || serverData.isNullOrEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups))) { searchResult.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, metricsOptions.pageInformation); (_e = searchResult.searchAdOpportunity) === null || _e === void 0 ? void 0 : _e.setMissedOpportunityReason("NOAD"); (_f = searchResult.searchAdOpportunity) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP"); } builtResults.push(searchResult); isFirstResult = false; nextPosition(metricsOptions.locationTracker); } return await applyClientFilteringToIAPs(objectGraph, builtResults).then((builtResultsFiltered) => { return { builtSearchResults: builtResultsFiltered, deferredSearchResults: deferredResults, }; }); } // endregion // region Internals /** * Create guided search tokens for the given search request descriptor and facet MAPI response data. * @param objectGraph The App Store object graph. * @param requestDescriptor The search request descriptor. * @param facetData The media API response for guided search facets. * @param metricsOptions The metrics options. * @returns The guided search tokens to display. */ function createGuidedSearchTokens(objectGraph, requestDescriptor, facetData, metricsOptions) { if (!objectGraph.host.isiOS || isNothing(facetData) || facetData.length === 0) { return []; } pushBasicLocation(objectGraph, { pageInformation: metricsOptions.pageInformation, locationTracker: metricsOptions.locationTracker, targetType: "SearchRevisions", }, ""); // Tokens from facet data const tokens = []; for (const data of facetData) { const token = guidedSearch.createGuidedSearchToken(objectGraph, "rewrite", requestDescriptor, data, metricsOptions); if (token) { tokens.push(token); nextPosition(metricsOptions.locationTracker); } } popLocation(metricsOptions.locationTracker); return tokens; } /** * Apply client-side iAP filtering to set of results. * @param objectGraph * @param resultsToFilter Results to apply iAP filtering on. */ async function applyClientFilteringToIAPs(objectGraph, resultsToFilter) { return await validation.context("applyClientFilteringToIAPs", async () => { const iAPProductIDToParentBundleID = {}; for (const result of resultsToFilter) { if (result.resultType === "inAppPurchase") { const inAppPurchaseResult = result; const inAppPurchaseLockup = inAppPurchaseResult.lockup; if (inAppPurchaseLockup.parent && inAppPurchaseLockup.productIdentifier && inAppPurchaseLockup.parent.bundleId) { iAPProductIDToParentBundleID[inAppPurchaseLockup.productIdentifier] = inAppPurchaseLockup.parent.bundleId; } else { validation.unexpectedNull("ignoredValue", "string", `required fields for ${inAppPurchaseLockup.adamId}`); } } } if (Object.keys(iAPProductIDToParentBundleID).length === 0) { return await Promise.resolve(resultsToFilter); } return await objectGraph.clientOrdering.visibilityForIAPs(iAPProductIDToParentBundleID).then((visibilities) => { const filteredResults = resultsToFilter.filter((result) => { if (result.resultType !== "inAppPurchase") { return true; } const inAppPurchaseResult = result; const inAppPurchaseLockup = inAppPurchaseResult.lockup; if (inAppPurchaseLockup.productIdentifier && visibilities[inAppPurchaseLockup.productIdentifier]) { return true; } else { return inAppPurchaseLockup.isVisibleByDefault; } }); return filteredResults; }); }); } /** * Whether or not current platform supports displaying given `searchResult`. */ function platformSupportsResultType(objectGraph, searchResult) { if (objectGraph.host.isTV) { switch (searchResult.resultType) { case "content": case "editorial": return true; default: return false; } } if (!objectGraph.host.isiOS && !objectGraph.client.isWeb) { switch (searchResult.resultType) { case "appEvent": return false; default: break; } } return true; } // endregion //# sourceMappingURL=search-results-pipeline.js.map