1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
|
/**
* 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
|