import { isNothing, isSome } from "@jet/environment/types/optional"; import * as serverData from "../json-parsing/server-data"; import * as client from "../wrappers/client"; export function allPlatforms(objectGraph, platformsToExclude) { const platforms = new Set(); platforms.add("iphone"); platforms.add("ipad"); platforms.add("appletv"); platforms.add("mac"); platforms.add("watch"); if (objectGraph.client.isVision || objectGraph.bag.enableVisionPlatform) { platforms.add("realityDevice"); } if (isSome(platformsToExclude)) { for (const platform of platformsToExclude) { platforms.delete(platform); } } return Array.from(platforms); } export function defaultPlatformForClient(objectGraph) { if (objectGraph.client.isCompanionVisionApp) { // The Vision companion app always prefers visionOS assets return "realityDevice"; } switch (objectGraph.client.deviceType) { case "phone": return "iphone"; case "pad": return "ipad"; case "tv": return "appletv"; case "mac": return "mac"; case "watch": return "watch"; case "vision": return "realityDevice"; case "web": return "web"; default: return null; } } /** * Returns the layout size controlling the number of rows to display. * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. * @returns {number} Layout size. */ export function defaultLayoutSize(objectGraph) { return objectGraph.client.isPhone ? 2 : 1; } /** * Returns the sparse count controlling the number of shelves to hydrate. * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. * @returns {number} Sparse count. */ export function defaultSparseCountForClient(objectGraph) { switch (objectGraph.client.deviceType) { case "phone": return 4; case "pad": return 5; case "tv": return 6; case "mac": return 5; case "watch": return 10; default: return null; } } /** * Returns the sparse limit controlling the number of items to hydrate per shelve. * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. * @returns {number} Sparse limit. */ export function defaultSparseLimitForClient(objectGraph) { switch (objectGraph.client.deviceType) { case "phone": return 9; case "pad": return 12; case "tv": return 15; case "mac": return 15; case "watch": return 3; case "web": return 12; default: return null; } } /** * Get the list of all platforms excluding the current platform * @returns {Platform[]} */ export function defaultAdditionalPlatformsForClient(objectGraph) { switch (objectGraph.host.clientIdentifier) { case "com.apple.TVAppStore.AppStoreTopShelfExtension": case "com.apple.Arcade.ArcadeTopShelfExtension": case "com.apple.AppStore.Widgets": // Skip additional platforms for top shelf extension due to memory constraint. return []; default: { const currentPlatform = defaultPlatformForClient(objectGraph); return allPlatforms(objectGraph, isSome(currentPlatform) ? new Set([currentPlatform]) : undefined); } } } /** * Get the product page reviews limit for a client. * @returns {number} The limit to use. */ export function defaultProductPageReviewsLimitForClient(objectGraph) { switch (objectGraph.client.deviceType) { case "phone": return 6; case "pad": return 10; case "mac": return 12; case "vision": return 10; default: return 8; } } /// Returns the context param to use for a given grouping request. /// This is leveraged for switching grouping tab root in specific configurations. export function defaultGroupingContextForClient(objectGraph) { let context = null; if (objectGraph.host.clientIdentifier === client.messagesIdentifier) { // Messages Grouping (iOS) context = "messages"; } else if (objectGraph.host.clientIdentifier === client.watchIdentifier) { // Bridge App Store (iOS) context = "watch"; } else if (objectGraph.client.isWatch && objectGraph.client.isTinkerWatch) { // Tinker App Store (watchOS) context = "tinker"; } return context; } // Nothing on the App Store is rated 1000+, so a value this high essentially // indicates that content restrictions are disabled. export const ageRestrictionsDisabledThreshold = 1000; export class Request { constructor(objectGraph, param, enableMixedCatalog, supplementaryMetadataAssociations) { var _a; /// The resource types used in a mixed contents call this.contentsResourceTypes = new Set(); /// The ID(s) associated with the resource type this.ids = new Set(); /// IDs associated with specific resource types, used for mixed catalog requests this.idsByResourceType = new Map(); /// The original ordering of the ids in this reqeust if there is a mixed catalog request this.originalOrdering = []; this.relationshipIncludes = new Set(); // Contents of `extend` param this.attributeIncludes = new Set(); this.platform = null; /// The paths to additional metadata that need to be fetched for mixed media requests this.supplementaryMetadataAssociations = []; this.additionalPlatforms = new Set(); this.additionalQuery = {}; this.relationshipLimits = {}; this.searchTerm = null; this.searchTypes = []; /// Whether we are searching watch or messages store. this.context = null; /// Whether or not to use custom extend attributes, instead of using `attributeIncludes` as-is. this.useCustomAttributes = false; /// An optional country code override for constructing the URL. Used in very specific regulatory scenarios where the bag country code cannot be relied upon. this.countryCodeOverride = undefined; this.objectGraph = objectGraph; this.platform = defaultPlatformForClient(objectGraph); this.isMixedMediaRequest = enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false; this.supplementaryMetadataAssociations = supplementaryMetadataAssociations !== null && supplementaryMetadataAssociations !== void 0 ? supplementaryMetadataAssociations : []; this.includeAppBinaryTraitsAttribute = objectGraph.client.isiOS; // By default, the `platform` for web defaults to whatever the `activeIntent` is (e.g. "mac"), // but this can be overridden when calling `setPreviewPlatform` with the request, which // will set the `platform` to `web` and `previewPlatform` will be the `activeIntent` platform. if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) { this.platform = objectGraph.activeIntent.platform; } if (serverData.isNullOrEmpty(param)) { return; } if (typeof param === "string") { this.href = param; } else if (Array.isArray(param)) { this.withDataItems(param, supplementaryMetadataAssociations, enableMixedCatalog); } } /** * Adds a data object to an idsByResourceType mapping. * @param data The data object * @returns An updated {resource-type: IDs} map, which includes the data object */ addDataToIDsByResourceType(data) { const resourceType = data.type; const id = data.id; let ids = this.idsByResourceType.get(resourceType); if (isNothing(ids)) { ids = new Set(); } ids.add(id); this.idsByResourceType.set(resourceType, ids); } forType(type) { this.resourceType = type; return this; } /** * This method is used to add data items to the request. It is used to build the request for the mixed catalog / catalog * * @param dataItems The data items to add to the request * @param supplementaryMetadataAssociations The metadata paths to add to the request * @param enableMixedCatalog Whether to allow mixed catalog requests * @returns The modified Request */ withDataItems(dataItems, supplementaryMetadataAssociations, enableMixedCatalog) { if (dataItems.length === 0) { return this; } this.isMixedMediaRequest = this.isMixedMediaRequest || (enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false); for (const data of dataItems) { // Track all IDs in a single set, for the case where we use the contents endpoint. this.ids.add(data.id); // Track all IDs by resource type, for either the case where we ultimately have just // one resource type, or where we used the mixed catalog endpoint. this.addDataToIDsByResourceType(data); // If we have any additional metadata paths that need fetching, we now add them // as additional resource types for the mixed catalog endpoint. if (isSome(enableMixedCatalog) && enableMixedCatalog && isSome(supplementaryMetadataAssociations) && supplementaryMetadataAssociations.length > 0) { for (const association of supplementaryMetadataAssociations) { const metadataItems = extractMetaAssociationFromData(association, data); if (isSome(metadataItems) && metadataItems.length > 0) { metadataItems.forEach((metadataItem) => { this.addDataToIDsByResourceType(metadataItem); }); } } } } // Now that we have collated all IDs and resource types, we can assign them to different // properties depending on the number of resource types we need. if (this.idsByResourceType.size === 1) { // We only have one list of IDs for a single resource type, so we just take the first one this.resourceType = this.idsByResourceType.keys().next().value; this.isMixedMediaRequest = false; } else if (this.idsByResourceType.size > 1 && !this.isMixedMediaRequest) { this.resourceType = "contents"; this.contentsResourceTypes = new Set(Array.from(this.idsByResourceType.keys())); } this.originalOrdering.push(...[...dataItems]); return this; } /** * Add a single id to this request, this should only be used when fetching a single resource * @param id The single ID to add to the request * @param type The type of the resource * @returns The modified Request */ withIdOfType(id, type) { return this.withDataItems([{ id, type }]); } /** * Add a list of ids to this request, this will add these ids to the mapping of resource types * @param ids The IDs to add to the request * @param type The type of the resource * @returns The modified Request */ withIdsOfType(ids, type) { return this.withDataItems(ids.map((id) => ({ id, type }))); } includingRelationships(relationships) { for (const relationship of relationships) { this.relationshipIncludes.add(relationship); } return this; } includingScopedRelationships(scopedDataType, relationshipsToAdd) { // Lazy init top-level property if (!this.scopedRelationshipIncludes) { this.scopedRelationshipIncludes = new Map(); } // Retrieve existing scoped relationship. Lazy init if needed. let scopedRelationship = this.scopedRelationshipIncludes.get(scopedDataType); if (!scopedRelationship) { scopedRelationship = new Set(); } // Update for (const newRelationship of relationshipsToAdd) { scopedRelationship.add(newRelationship); } this.scopedRelationshipIncludes.set(scopedDataType, scopedRelationship); return this; } includingMetaKeys(scopedDataType, keysToAdd) { // Lazy init top-level property if (!this.metaIncludes) { this.metaIncludes = new Map(); } // Retrieve existing inclusion. Lazy init if needed. let scopedMeta = this.metaIncludes.get(scopedDataType); if (!scopedMeta) { scopedMeta = new Set(); } // Update for (const newKey of keysToAdd) { scopedMeta.add(newKey); } this.metaIncludes.set(scopedDataType, scopedMeta); return this; } includingViews(viewsToAdd) { // Lazy init top-level property if (!this.viewsIncludes) { this.viewsIncludes = new Set(); } for (const view of viewsToAdd) { this.viewsIncludes.add(view); } return this; } includingKindsKeys(scopedDataType, keysToAdd) { // Lazy init top-level property if (!this.kindIncludes) { this.kindIncludes = new Map(); } // Retrieve existing inclusion. Lazy init if needed. let scopedMeta = this.kindIncludes.get(scopedDataType); if (!scopedMeta) { scopedMeta = new Set(); } // Update for (const newKey of keysToAdd) { scopedMeta.add(newKey); } this.kindIncludes.set(scopedDataType, scopedMeta); return this; } includingAssociateKeys(scopedDataType, keysToAdd) { // Lazy init top-level property if (!this.associateIncludes) { this.associateIncludes = new Map(); } // Retrieve existing inclusion. Lazy init if needed. let scopedAssociate = this.associateIncludes.get(scopedDataType); if (!scopedAssociate) { scopedAssociate = new Set(); } // Update for (const newKey of keysToAdd) { scopedAssociate.add(newKey); } this.associateIncludes.set(scopedDataType, scopedAssociate); return this; } /** * Include the relationships needed for an upsell. * @param requiresScopedInclude A flag indicating whether the include should be "scoped". This will need to be the * case when the relationships we are including is on a request for a separate primary resource type. For example, * if we are looking to have the upsell relationship included in an EI (Today card, for example), this needs to be * scoped. However, if we are fetching a grouping directly and need the upsell included, this should *not* be scoped. */ includingRelationshipsForUpsell(requiresScopedRelationshipInclude) { const relationship = "marketing-items"; if (requiresScopedRelationshipInclude) { // Lazy init top-level property if (!this.scopedRelationshipIncludes) { this.scopedRelationshipIncludes = new Map(); } // Retrieve existing scoped relationship. Lazy init if needed. let scopedRelationship = this.scopedRelationshipIncludes.get("editorial-items"); if (!scopedRelationship) { scopedRelationship = new Set(); } // Update scopedRelationship.add(relationship); this.scopedRelationshipIncludes.set("editorial-items", scopedRelationship); } else { this.relationshipIncludes.add(relationship); } // In order to get metrics metadata stiched in to the marketing item relationship, we have to include it in our // request. if (relationship === "marketing-items") { if (!this.metaIncludes) { this.metaIncludes = new Map(); } // Retrieve existing inclusion. Lazy init if needed. let scopedMeta = this.metaIncludes.get("marketing-items"); if (!scopedMeta) { scopedMeta = new Set(); } scopedMeta.add("metrics"); this.metaIncludes.set("marketing-items", scopedMeta); } return this; } includingAttributes(attributes) { for (const attribute of attributes) { this.attributeIncludes.add(attribute); } return this; } includingScopedAttributes(resourceType, attributesToAdd) { // Lazy init top-level property if (!this.scopedAttributeIncludes) { this.scopedAttributeIncludes = new Map(); } // Retrieve existing scoped relationship. Lazy init if needed. let attributesForResourceType = this.scopedAttributeIncludes.get(resourceType); if (!attributesForResourceType) { attributesForResourceType = new Set(); } // Update for (const attribute of attributesToAdd) { attributesForResourceType.add(attribute); } this.scopedAttributeIncludes.set(resourceType, attributesForResourceType); return this; } /** * Adds an age restriction to the request. ManagedConfiguration provides * this value on the client, which maps to JRPurpleRating.clientIdentifier. * * @returns {Request} The updated request */ includingAgeRestrictions() { const maxAppContentRating = this.objectGraph.client.maxAppContentRating; if (maxAppContentRating < ageRestrictionsDisabledThreshold) { this.ageRestriction = maxAppContentRating; } return this; } includingAdditionalPlatforms(additionalPlatforms) { for (const platform of additionalPlatforms) { this.additionalPlatforms.add(platform); } return this; } includingScopedAvailableIn(resourceType, valuesToAdd) { // Lazy init top-level property if (!this.scopedAvailableInIncludes) { this.scopedAvailableInIncludes = new Map(); } // Retrieve existing scoped relationship. Lazy init if needed. let valuesForResourceType = this.scopedAvailableInIncludes.get(resourceType); if (!valuesForResourceType) { valuesForResourceType = new Set(); } // Update for (const value of valuesToAdd) { valuesForResourceType.add(value); } this.scopedAvailableInIncludes.set(resourceType, valuesForResourceType); return this; } /** * Include the sparse limit needed for a specific resource type. * @param {media.Type} resourceType Resource type to use for the sparse limit. * @param {number} value Value to set as the sparse limit. * @returns {Request} Updated request. */ includingScopedSparseLimit(resourceType, value) { // Lazy init top-level property if (!this.scopedSparseLimit) { this.scopedSparseLimit = new Map(); } this.scopedSparseLimit.set(resourceType, value); return this; } addingQuery(key, value) { if (isSome(value)) { this.additionalQuery[key] = value; } else { delete this.addingQuery[key]; } return this; } /** * @param query The additional query values to add * @returns The modified Reqeust */ addingQueryValues(query) { this.additionalQuery = { ...this.addingQuery, ...query, }; return this; } addingRelationshipLimit(relationship, limit) { this.relationshipLimits[relationship] = limit; return this; } withSearchTerm(term) { this.searchTerm = term; return this; } searchingOverTypes(types) { for (const type of types) { this.searchTypes.push(type); } return this; } addingContext(context) { this.context = context; return this; } includingMacOSCompatibleIOSAppsWhenSupported(verifiedBadgeOnly = false) { if (this.objectGraph.appleSilicon.isSupportEnabled) { if (!verifiedBadgeOnly) { this.enablingFeature("macOSCompatibleIOSApps"); } this.includingScopedAttributes("apps", ["isVerifiedForAppleSiliconMac"]); } return this; } /// Mark a request to include (or exclude) appBinaryTraits attributes. includingAppBinaryTraitsAttribute(includeAppBinaryTraitsAttribute = true) { this.includeAppBinaryTraitsAttribute = includeAppBinaryTraitsAttribute; return this; } /** * Mark a request to use custom attributes. * This triggers usage of `customXYZ` extend attributes in url-builder for specific extend params * e.g. `artwork`, `customArtwork`. */ usingCustomAttributes(useCustomAttributes) { this.useCustomAttributes = useCustomAttributes; return this; } alwaysUseIdsAsQueryParam(value) { this.useIdsAsQueryParam = value; return this; } attributingTo(canonicalUrl) { this.canonicalUrl = canonicalUrl; return this; } withFilter(type, value) { this.filterType = type; this.filterValue = value; return this; } withLimit(limit) { this.limit = limit; return this; } withSparseLimit(sparseLimit) { if (sparseLimit !== null) { this.sparseLimit = sparseLimit; } return this; } withSparseCount(sparseCount) { if (sparseCount !== null) { this.sparseCount = sparseCount; } return this; } enablingFeature(feature) { if (!this.enabledFeatures) { this.enabledFeatures = []; } this.enabledFeatures.push(feature); return this; } enablingFeatures(features) { if (!this.enabledFeatures) { this.enabledFeatures = []; } this.enabledFeatures.push(...features); return this; } /** * Limit the request to include only certain fields. * This should only be used in very select use-cases, where: * - There is no possibility of data being used for other purposes, e.g. sidpack * - All consumer of this data use it in the same narrow scope, e.g. requesting a set of icons for displaying without metadata only. * @param fields Set of fields to limit to */ asPartialResponseLimitedToFields(fields) { this.fields = fields; return this; } includesResourceType(resourceType) { if (this.resourceType === resourceType) { return true; } if (serverData.isDefinedNonNull(this.contentsResourceTypes)) { return this.contentsResourceTypes.has(resourceType); } return false; } withCountryCodeOverride(countryCodeOverride) { this.countryCodeOverride = countryCodeOverride; return this; } } /** * Extracts the metadata association from the data. * @param association The association to extract * @param data The data to extract from */ export function extractMetaAssociationFromData(association, data) { if (serverData.isNullOrEmpty(data)) { return null; } const associationData = serverData.asArrayOrEmpty(data, `meta.associations.${association}.data`); if (isNothing(associationData)) { return null; } return [...associationData]; } //# sourceMappingURL=data-fetching.js.map