/** * Data Fetching for Search Results for: * - Initial search requests * - Content pagination requests */ import { isNothing } from "@jet/environment/types/optional"; import * as models from "../../api/models"; import { asBooleanOrFalse, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; import { defaultAdditionalPlatformsForClient, Request } from "../../foundation/media/data-fetching"; import { metricsFromMediaApiObject, } from "../../foundation/media/data-structure"; import { fetchData } from "../../foundation/media/network"; import { buildURLFromRequest } from "../../foundation/media/url-builder"; import * as constants from "../../foundation/util/constants"; import * as client from "../../foundation/wrappers/client"; import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; import * as categories from "../categories"; import { addVariantParametersToRequestForItems, shouldFetchCustomAttributes, } from "../product-page/product-page-variants"; import { setPreviewPlatform } from "../preview-platform"; import { SponsoredSearchRequestData } from "./search-ads"; import { searchMetricsDataSetID } from "./search-common"; import { fetchSponsoredSearchNativeAdvertData } from "./sponsored-search-fetching"; import { dateFlooredToHour } from "../../foundation/util/date-util"; import { shouldUsePrerenderedIconArtwork } from "../content/content"; import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; export async function fetchSegmentedSearchResults(objectGraph, options) { var _a; // Primary search request const searchRequest = (_a = createSearchRequest(objectGraph, options)) === null || _a === void 0 ? void 0 : _a.addingQuery("groupBy[search]", "platform"); if (isNothing(searchRequest)) { return null; } const fetchSearchResults = fetchData(objectGraph, searchRequest, undefined); return await fetchSearchResults.then((catalogResponse) => { renameDataSetIdKey(catalogResponse); const data = { catalogResponse: catalogResponse, requestMetadata: { requestDescriptor: options, searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url }, categoriesFilterData: null, sponsoredSearchRequestData: null, sponsoredSearchAdvertData: null, }; return data; }); } /** * Fetch a collection of data for displaying a single, sequential set of search results. * @param options Parameters for the search being performed. * @returns `Promise` for aggregate search result data, or `null` if `options` was invalid. */ export async function fetchSequentialSearchResultsData(objectGraph, options) { // Advert targeting data from client const sponsoredSearchRequestData = new SponsoredSearchRequestData(options.targetingData, objectGraph.random.nextUUID()); // Primary search request const searchRequest = createSearchRequest(objectGraph, options); if (searchRequest === null) { return null; } const fetchSearchResults = fetchData(objectGraph, searchRequest, createSearchFetchOptions(objectGraph, sponsoredSearchRequestData)); // Search History if (objectGraph.bag.mediaAPISearchFocusEnabled) { const historyItem = { term: options.term.trim(), entity: options.searchEntity, }; // ** MAINTAINER'S NOTE ** // Fire and forget this request to reduce delay showing search results on persisting recent searches that are not visible at this time. objectGraph.onDeviceSearchHistoryManager.saveRecentSearchWithLimit(historyItem, 30); } // Optional requests const fetchSponsoredSearchAds = fetchSponsoredSearchNativeAdvertData(objectGraph, sponsoredSearchRequestData, options.term, fetchSearchResults); const fetchCategoriesOrNull = fetchCategoryFiltersDataIfNeeded(objectGraph); return await Promise.all([fetchSearchResults, fetchSponsoredSearchAds, fetchCategoriesOrNull]).then(([catalogResponse, sponsoredSearchAdvertData, categoriesFilterData]) => { var _a, _b, _c, _d; renameDataSetIdKey(catalogResponse); // Remember the last time a natural language search was performed. if ((_c = (_b = (_a = catalogResponse.meta) === null || _a === void 0 ? void 0 : _a.results) === null || _b === void 0 ? void 0 : _b.search) === null || _c === void 0 ? void 0 : _c.naturalLanguage) { const previousNLSQueryDate = objectGraph.storage.retrieveString("lastNLSQueryDate"); const lastNLSQueryDate = dateFlooredToHour(new Date()).getTime().toString(); objectGraph.storage.storeString("lastNLSQueryDate", lastNLSQueryDate); // Notify ODJ on change using AMSEngagement. (_d = objectGraph.amsEngagement) === null || _d === void 0 ? void 0 : _d.enqueueData({ eventType: "lastNLSQueryDateChange", app: "com.apple.AppStore", oldState: previousNLSQueryDate, newState: lastNLSQueryDate, }); } const data = { catalogResponse: catalogResponse, categoriesFilterData: categoriesFilterData, sponsoredSearchRequestData: sponsoredSearchRequestData, sponsoredSearchAdvertData: sponsoredSearchAdvertData, requestMetadata: { requestDescriptor: options, searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url }, }; return data; }); } /** * Fetch a collection of data for displaying several, grouped set of search results. * @param options Parameters for the search being performed. * @returns `Promise` for aggregate search result data, or `Promise` of `null` if `options` was invalid. */ export async function fetchPlatformGroupedSearchResultsData(objectGraph, options) { // Primary search request const searchRequest = createPlatformGroupedSearchRequest(objectGraph, options); if (isNothing(searchRequest)) { return null; } const fetchSearchResults = fetchData(objectGraph, searchRequest, createSearchFetchOptions(objectGraph)); // Optional requests const fetchCategoriesOrNull = fetchCategoryFiltersDataIfNeeded(objectGraph); return await Promise.all([fetchSearchResults, fetchCategoriesOrNull]).then(([catalogResponse, categoriesFilterData]) => { renameDataSetIdKey(catalogResponse); const data = { catalogResponse: catalogResponse, categoriesFilterData: categoriesFilterData, sponsoredSearchRequestData: null, sponsoredSearchAdvertData: null, requestMetadata: { requestDescriptor: options, searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url }, }; return data; }); } /** * Fetch a set of items for appearing in search results * @param items Items to fetch. */ export async function fetchSearchResultItems(objectGraph, items) { const request = createCatalogRequestForItems(objectGraph, items); return await fetchData(objectGraph, request); } // endregion // region Request Header /** * Create fetch options, annotating with `SponsoredSearchRequestData` header fields. * @param adData Advert data to include in header. */ function createSearchFetchOptions(objectGraph, sponsoredSearchRequestData) { const searchRequestHeader = {}; if (sponsoredSearchRequestData && sponsoredSearchRequestData.validAdRequest()) { searchRequestHeader[constants.appStoreClientRequestIdName] = sponsoredSearchRequestData.appStoreClientRequestId; searchRequestHeader[constants.iAdRequestDataHeaderName] = sponsoredSearchRequestData.sponsoredSearchRequestData; searchRequestHeader[constants.iAdRoutingInfoHeaderName] = sponsoredSearchRequestData.routingInfo; } return { headers: searchRequestHeader, }; } // endregion // region Search Results Request /** * Set of relationships fetched as relation for search request / catalog requests */ const searchIncludeRelationships = ["apps", "top-apps"]; /** * Create the `Request` object to execute a search described by `options` * @param options Options for search being executed. * @returns `Request` configured for `options`, or `null` if `options` was invalid. */ export function createSearchRequest(objectGraph, options) { var _a; const term = (_a = options.term) === null || _a === void 0 ? void 0 : _a.trim(); if (isNullOrEmpty(term)) { return null; } const origin = options.origin; const source = options.source; const searchEntityOrNull = options.searchEntity; const facetsOrNull = options.facets; const selectedFacetOptionsOrNull = options.selectedFacetOptions; const spellCheckEnabled = options.spellCheckEnabled; const excludedTermsOrNull = options.excludedTerms; const clientIdentifier = objectGraph.host.clientIdentifier; const searchRequest = new Request(objectGraph) .withSearchTerm(term) .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) .includingAttributes(includeAttributesForSearchResults(objectGraph)) .includingScopedAttributes("editorial-items", ["showLabelInSearch"]) .includingRelationshipsForUpsell(true) .includingMacOSCompatibleIOSAppsWhenSupported() .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); setPreviewPlatform(objectGraph, searchRequest); if (!objectGraph.client.isWatch && !objectGraph.client.isVision) { searchRequest.includingRelationships(searchIncludeRelationships); } if (objectGraph.client.isVision) { // Temporarily increase the sparseLimit on Vision to workaround lack of filtering for bincompat. searchRequest.addingQuery("sparseLimit[developers:top-apps]", "12"); } if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { searchRequest.includingAssociateKeys("apps", ["app-events"]); searchRequest.includingScopedAttributes("app-events", AppEventsAttributes); searchRequest.includingScopedRelationships("editorial-items", ["primary-content"]); } /** * Tinker Filter */ if (asBooleanOrFalse(objectGraph.client.isTinkerWatch)) { searchRequest.withFilter("contexts", "tinker"); } /** * Guided Search */ if (objectGraph.host.isiOS) { // Feature enablers searchRequest.enablingFeature("guidedSearch"); searchRequest.enablingFeature("midScrollGuidedSearch"); // Selected facets, if any. if (isDefinedNonNullNonEmpty(options.guidedSearchTokens)) { searchRequest.addingQuery("selectedFacets", options.guidedSearchTokens.join(",")); } // Optimization term, if any were on request. if (isDefinedNonNullNonEmpty(options.guidedSearchOptimizationTerm)) { searchRequest.addingQuery("finalTerm", options.guidedSearchOptimizationTerm); } if (objectGraph.bag.isLLMSearchTagsEnabled) { searchRequest.includingAssociateKeys("results:apps", ["tags"]); } } if (objectGraph.featureFlags.isEnabled("voyager_bundles_2025A")) { searchRequest.includingScopedAttributes("apps", ["screenshotsByType"]); } /** * Entities in results */ if (searchEntityOrNull === "story") { searchRequest.searchingOverTypes(["editorial-items"]); } else if (searchEntityOrNull === "developer") { searchRequest.searchingOverTypes(["developers"]); } else if (searchEntityOrNull === "watch" || searchEntityOrNull === "arcade") { searchRequest.searchingOverTypes(["apps"]).withFilter("contexts", searchEntityOrNull); } else if (objectGraph.client.isTV) { searchRequest.searchingOverTypes(["apps", "developers", "groupings", "editorial-items"]); } else if (objectGraph.client.isVision) { searchRequest.searchingOverTypes([ "apps", "developers", "editorial-items", "app-bundles", "in-apps", "editorial-pages", ]); } else { searchRequest.searchingOverTypes([ "apps", "developers", "groupings", "editorial-items", "app-bundles", "in-apps", ]); } /** * Signal Rosetta unavailability. */ if (objectGraph.appleSilicon.isSupportEnabled && !objectGraph.appleSilicon.isRosettaAvailable) { searchRequest.addingQuery("restrict", "!requiresRosetta"); } /** * Facets / filters */ if (facetsOrNull) { for (const key of Object.keys(facetsOrNull)) { searchRequest.addingQuery(key, facetsOrNull[key]); } } if (selectedFacetOptionsOrNull) { for (const key of Object.keys(selectedFacetOptionsOrNull)) { const requestValues = models.PageFacets.requestValuesForSelectedFacetOptions(selectedFacetOptionsOrNull[key]); if (isDefinedNonNullNonEmpty(requestValues)) { searchRequest.addingQuery(key, requestValues.value); for (const additionalKey of Object.keys(requestValues.additionalKeyValuePairs)) { searchRequest.addingQuery(additionalKey, requestValues.additionalKeyValuePairs[additionalKey]); } } } } /** * natural language search availability */ const isNaturalLanguageSearchResultsEnabled = objectGraph.bag.isNaturalLanguageSearchEnabled || objectGraph.bag.isNaturalLanguageSearchResultsEnabled; /** * source attribution */ const sourceKey = isNaturalLanguageSearchResultsEnabled ? "source" : "src"; if (origin === "hints") { const hintSource = isNaturalLanguageSearchResultsEnabled && (source === null || source === void 0 ? void 0 : source.length) ? "hint:".concat(source) : "hint"; searchRequest.addingQuery(sourceKey, hintSource); } else if (origin === "recents") { searchRequest.addingQuery(sourceKey, "recent"); } else if (origin === "trending") { searchRequest.addingQuery(sourceKey, "trending"); } else if (origin === "undoSpellCorrection") { searchRequest.addingQuery(sourceKey, "searchInstead"); } else if (origin === "applySpellCorrection") { searchRequest.addingQuery(sourceKey, "didYouMean"); } else if (origin === "guidedToken") { searchRequest.addingQuery(sourceKey, "facet"); } /** * contexts */ switch (clientIdentifier) { case client.watchIdentifier: searchRequest.addingContext("watch"); break; case client.messagesIdentifier: searchRequest.addingContext("messages"); break; case client.arcadeIdentifier: searchRequest.addingContext("arcade"); break; default: break; } /** * Advert limit */ searchRequest.addingQuery("limit[ads-result]", objectGraph.bag.mediaAdvertRequestLimit.toString()); /** * Ads locale metadata. */ if (isDefinedNonNullNonEmpty(objectGraph.bag.adsOverrideLanguage)) { searchRequest.enablingFeature("adsLocaleMetadata"); } /** * Feature: Spellcheck */ if (spellCheckEnabled) { searchRequest.enablingFeature("spellCheck"); } /** * Feature: Natural Language */ if (isNaturalLanguageSearchResultsEnabled) { searchRequest.enablingFeature("naturalLanguage"); } /** * Feature: Organic CPPs */ if (objectGraph.host.isiOS) { searchRequest.enablingFeature("searchResultCpps"); } /** * Excluded terms */ if (excludedTermsOrNull) { searchRequest.addingQuery("excludeTerms", excludedTermsOrNull.join(",")); } return searchRequest; } /** * Create the `Request` object to execute a search described by `options`, with results grouped by platform * @param options Options for search being executed. * @returns `Request` configured for `options`, or `null` if `options` was invalid. */ export function createPlatformGroupedSearchRequest(objectGraph, options) { var _a; return (_a = createSearchRequest(objectGraph, options)) === null || _a === void 0 ? void 0 : _a.addingQuery("groupBy[search]", "platform").includingMacOSCompatibleIOSAppsWhenSupported(); } /** * Create a request for fetching set of `items` for appearing in search tab. Used for pagination. * @param items Items to fetch. */ function createCatalogRequestForItems(objectGraph, items) { const shouldUseMixedCatalogRequest = objectGraph.bag.isLLMSearchTagsEnabled || asBooleanOrFalse(objectGraph.bag.supportedMixedMediaRequestUsecases["search"]); const searchRequest = new Request(objectGraph, items, shouldUseMixedCatalogRequest, ["tags"]) .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) .includingScopedAttributes("editorial-items", ["showLabelInSearch"]) .includingAttributes(includeAttributesForSearchResults(objectGraph)) .includingRelationshipsForUpsell(true) .includingMacOSCompatibleIOSAppsWhenSupported() .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); addVariantParametersToRequestForItems(objectGraph, searchRequest, items); if (!objectGraph.client.isWatch) { searchRequest.includingRelationships(searchIncludeRelationships); } if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { searchRequest.includingAssociateKeys("apps", ["app-events"]); searchRequest.includingScopedAttributes("app-events", AppEventsAttributes); searchRequest.includingScopedRelationships("editorial-items", ["primary-content"]); } return searchRequest; } /** * Returns the include attributes used for: * 1. Initial search request * 2. Subsequent catalog request for pagination */ function includeAttributesForSearchResults(objectGraph) { const attributes = [ "screenshotsByType", "messagesScreenshots", "videoPreviewsByType", "requiredCapabilities", "editorialBadgeInfo", "supportsFunCamera", "minimumOSVersion", "customScreenshotsByTypeForAd", "customVideoPreviewsByTypeForAd", "secondaryGenreShortDisplayNames", "genreShortDisplayName", "editorialVideo", ]; if (objectGraph.appleSilicon.isSupportEnabled) { attributes.push("macRequiredCapabilities"); } if (objectGraph.client.isMac) { attributes.push("hasMacIPAPackage"); } if (objectGraph.client.isVision) { attributes.push("compatibilityControllerRequirement"); } if (objectGraph.bag.enableUpdatedAgeRatings) { attributes.push("ageRating"); } if (shouldUsePrerenderedIconArtwork(objectGraph)) { attributes.push("iconArtwork"); } /// We don't want to unnecessarily ask for clients that don't have metadata ribbon capability if (objectGraph.host.isOSAtLeast(15, 5, 0)) { attributes.push("remoteControllerRequirement"); } // Keeping custom creatives out of prod. if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { attributes.push("adCreativeArtwork"); attributes.push("adCreativeVideo"); } } return attributes; } // endregion // region Categories Filter Request /** * Fetches data to for category filters on devices that need them. */ async function fetchCategoryFiltersDataIfNeeded(objectGraph) { const deviceType = objectGraph.client.deviceType; if (deviceTypeSupportsCategoryFilters(deviceType)) { const categoriesRequest = categories.createRequest(objectGraph, null, null, defaultAdditionalPlatformsForClient(objectGraph)); if (categoriesRequest) { // Failable request. return await fetchData(objectGraph, categoriesRequest).catch(() => null); } } return null; } function deviceTypeSupportsCategoryFilters(deviceType) { switch (deviceType) { case "pad": case "mac": return true; default: return false; } } // endregion // region Search Result Modification /** * The search dataset id is set to the key `dataSetId` on `meta.metrics`, but needs to have `data.search.dataSetId` key. * Metrics: metrics blob in search response has incorrect field name */ function renameDataSetIdKey(searchResponse) { var _a; const searchMetrics = metricsFromMediaApiObject(searchResponse); const oldDataSetId = "dataSetId"; if (isDefinedNonNull(searchMetrics) && isDefinedNonNull(searchResponse.meta) && isDefinedNonNull((_a = searchResponse.meta) === null || _a === void 0 ? void 0 : _a.metrics)) { searchResponse.meta.metrics[searchMetricsDataSetID] = asString(searchMetrics, oldDataSetId); delete searchResponse.meta.metrics[oldDataSetId]; } } // endregion //# sourceMappingURL=search-results-fetching.js.map