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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
|
import { isNothing, isSome } from "@jet/environment";
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 { Parameters } from "../../foundation/network/url-constants";
import { allOptional } from "../../foundation/util/promise-util";
import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common";
import * as lottery from "../util/lottery";
import { startPromiseWithAdditionalTimeout } from "../util/timeout-manager-util";
import { isPersonalizationAvailable } from "./on-device-personalization";
export const todayTabODPTimeoutUseCase = "todayTabPersonalization";
const displayContextLogString = "OnDeviceRecommendationsTodayShelfController";
/**
* Convenience function for determining if Today tab Arcade personalization is available.
*/
export function isTodayTabArcadePersonalizationAvailable(objectGraph) {
const isDataPersonalizationAvailable = isPersonalizationAvailable(objectGraph);
const isiOS = objectGraph.client.isiOS;
const isFeatureEnabledForCurrentUser = lottery.isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.todayTabArcadePersonalizationRate);
// Personalization is enabled only IF
// - Data personalization is available AND
// - Client is iOS AND
// - Feature is enabled for current user
return isDataPersonalizationAvailable && isiOS && isFeatureEnabledForCurrentUser;
}
/**
* Fetches and returns Today recommendations result using on-device recommendations manager with timeout.
* @param timeout: Optional timeout in seconds.
* @returns Promise<Opt<TodayRecommendationsResult>>: Today recommendations result.
*/
export async function fetchTodayRecommendationsWithTimeout(objectGraph, timeout) {
return await startPromiseWithAdditionalTimeout(objectGraph, fetchTodayRecommendations(objectGraph), timeout, todayTabODPTimeoutUseCase);
}
/**
* Fetches and returns Today recommendations result using on-device recommendations manager.
* @returns Promise<Opt<TodayRecommendationsResult>>: Today recommendations result.
*/
async function fetchTodayRecommendations(objectGraph) {
try {
const useCases = await fetchUseCasesForTab("today", objectGraph);
const recommendationPromises = useCases.map(async (useCase) => await fetchTodayRecommendationForUseCase(objectGraph, useCase));
const recommendationPromiseResults = await allOptional(recommendationPromises);
const recommendations = recommendationPromiseResults
.map((promiseResult) => {
if (promiseResult.success) {
return promiseResult.value;
}
else {
return undefined;
}
})
.filter(isSome);
return new TodayRecommendationsResult(recommendations);
}
catch (error) {
objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP for Today recommendations: ${error}`);
return undefined;
}
}
/**
* Fetches Today Recommendation for a use case using on-device recommendations manager.
* - useCase: On-device personalization use case.
* @returns Promise<TodayRecommendation | undefined>: Today recommendation.
*/
async function fetchTodayRecommendationForUseCase(objectGraph, useCase) {
try {
const odrResponse = await fetchTodayRecommendation(useCase, objectGraph);
const recommendedCandidatesAndMetrics = await makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse);
const recoMetrics = recommendedCandidatesAndMetrics.metrics;
const recommendedCandidates = recommendedCandidatesAndMetrics.candidates;
if (recommendedCandidates.length === 0) {
return undefined;
}
// Select the first candidate as we support only one candidate for each use case.
const recommendedCandidate = recommendedCandidates[0];
const todayRecommendationPromise = await makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph);
return todayRecommendationPromise;
}
catch (error) {
objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`);
return undefined;
}
}
/**
* Fetches and returns use cases for given tab using on-device recommendations manager.
* - tab: Navigation tab.
* @returns Promise<string[]>: Use cases for given tab.
*/
export async function fetchUseCasesForTab(tab, objectGraph) {
if (serverData.isNullOrEmpty(objectGraph.user.dsid)) {
const errorMessage = `${displayContextLogString}: User is currently not signed in.`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
try {
const odrResponse = await objectGraph.onDeviceRecommendationsManager.performRequest({
type: "fetchUseCases",
tab: tab,
dsId: objectGraph.user.dsid,
});
const useCases = serverData.asArrayOrEmpty(odrResponse["useCases"]);
if (serverData.isNullOrEmpty(useCases)) {
const errorMessage = `${displayContextLogString}: ODP returned no use cases for tab: ${tab}`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
return useCases;
}
catch (error) {
const errorMessage = `${displayContextLogString}: Failed to fetch ODP use cases for tab: ${tab}, with error: ${error}`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
}
/**
* Fetches and returns Today recommendation response for given use case using on-device recommendations manager.
* - useCase: Use case.
* @returns Promise<JSONData>: Today recommendation response.
*/
export async function fetchTodayRecommendation(useCase, objectGraph) {
if (serverData.isNullOrEmpty(objectGraph.user.dsid)) {
const errorMessage = `${displayContextLogString}: User is currently not signed in.`;
throw new Error(errorMessage);
}
try {
return await objectGraph.onDeviceRecommendationsManager.performRequest({
type: "fetchRecommendations",
dsId: objectGraph.user.dsid,
useCase: useCase,
});
}
catch (error) {
const errorMessage = `${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
}
/**
* Makes and returns Today recommended candidates and metrics from given useCase and on-device recommendation response.
* - useCase: Use case.
* - odrResponse: On-device recommendation response.
* @returns { candidates: TodayRecommendedCandidate[]; metrics: JSONData }: Today recommended candidates and reco metrics.
*/
export async function makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse) {
const recoCandidates = serverData.asArrayOrEmpty(odrResponse["candidates"]);
if (serverData.isNullOrEmpty(recoCandidates)) {
const errorMessage = `${displayContextLogString}: ODP returned no candidates for useCase: ${useCase}`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
const recoMetrics = serverData.asJSONData(odrResponse["metrics"]);
const recommendedCandidates = recoCandidates.map((candidate) => makeRecommendedCandidate(candidate)).filter(isSome);
if (serverData.isNull(recoMetrics) || serverData.isNullOrEmpty(recommendedCandidates)) {
const errorMessage = `${displayContextLogString}: ODP candidates could not be parsed for useCase: ${useCase}`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
return { candidates: recommendedCandidates, metrics: recoMetrics };
}
/**
* Makes and returns Today recommendation for given use case and candidates using MAPI response.
* - useCase: Use case.
* - recommendedCandidate: Recommended candidate.
* - recoMetrics: Reco metrics.
* @returns Promise<TodayRecommendation>: Today recommendation.
*/
export async function makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph) {
const mediaApiRequest = new mediaDataFetching.Request(objectGraph, recommendedCandidate.data, true)
.addingQuery(Parameters.onDevicePersonalizationUseCase, useCase)
.addingQuery(Parameters.filterRecommendable, "true");
groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest);
try {
const mapiResponse = await mediaNetwork.fetchData(objectGraph, mediaApiRequest);
return new TodayRecommendation(useCase, [recommendedCandidate], recoMetrics, mapiResponse);
}
catch (error) {
const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for: ${recommendedCandidate.data}, with error: ${error}`;
validation.unexpectedType("defaultValue", errorMessage, null);
throw new Error(errorMessage);
}
}
/**
* Makes and returns Today recommended candidate using given candidate data.
* - candidate: Candidate data.
* @returns TodayRecommendedCandidate: Today recommended candidate.
*/
export function makeRecommendedCandidate(candidate) {
// Extract candidate ID and type from candidate data.
const candidateID = serverData.asString(candidate.id);
const candidateType = serverData.asString(candidate.type);
if (serverData.isNull(candidateID)) {
return undefined;
}
let storyIDs = [];
let mediaType;
switch (candidateType) {
case "editorialItemGroup":
// Exract story candidates to use within the story group.
const storyCandidates = serverData.asArrayOrEmpty(candidate.candidates);
// Create story IDs from story candidates.
storyIDs = storyCandidates
.map((storyCandidate) => serverData.asString(storyCandidate.id))
.filter((storyID) => isSome(storyID));
mediaType = "editorial-item-groups";
break;
case "editorialItem":
mediaType = "editorial-items";
break;
default:
return undefined;
}
let candidatesData = [];
candidatesData.push({
id: candidateID,
type: mediaType,
});
// If there are any story IDs, add story candidates to candidates data.
if (serverData.isDefinedNonNullNonEmpty(storyIDs)) {
const storyCandidates = storyIDs.map((storyID) => ({
id: storyID,
type: "editorial-items",
}));
candidatesData = candidatesData.concat(storyCandidates);
}
return new TodayRecommendedCandidate(candidateID, mediaType, storyIDs, candidatesData);
}
/**
* A container for Today recommendations that also makes story data and story group data.
*/
export class TodayRecommendationsResult {
/**
* Initializes today recommendations results.
* @param recommendations - Today recommendations.
*/
constructor(recommendations) {
this.recommendations = recommendations;
}
/**
* Returns story data from recommendations that has the given use case.
* @param useCase - Use case that is used to match a story.
* @returns Story data for given use case and type, or null if not found.
*/
storyData(useCase) {
var _a;
const recommendation = this.recommendationForUseCase(useCase);
const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-items");
if (isNothing(recommendation) || isNothing(candidate)) {
return undefined;
}
return (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === candidate.id);
}
/**
* Returns story group data from recommendations that has the given use case.
* @param useCase - Use case that is used to match a story group.
* @returns Story group data for given use case and type, or null if not found.
*/
storyGroupData(useCase) {
var _a, _b;
const recommendation = this.recommendationForUseCase(useCase);
const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-item-groups");
if (isNothing(recommendation) || isNothing(candidate)) {
return undefined;
}
const storyGroupData = (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === (candidate === null || candidate === void 0 ? void 0 : candidate.id));
const storiesData = (_b = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _b === void 0 ? void 0 : _b.data.filter((item) => candidate.candidateIDs.includes(item.id));
if (isNothing(storyGroupData) || isNothing(storiesData)) {
return undefined;
}
storyGroupData["meta"] = {
associations: {
recommendations: {
data: storiesData,
},
},
};
return storyGroupData;
}
/**
* Returns the first recommendation that has the given use case.
* @param useCase - Use case that is used to match a recommendation.
* @returns Recommendation for given use case or null if not found.
*/
recommendationForUseCase(useCase) {
return this.recommendations.find((recommendation) => recommendation.useCase === useCase);
}
}
/**
* Today recommendation with a use case, candidates, metrics and a data container.
*/
export class TodayRecommendation {
/**
* Initializes today recommendation.
* @param useCase - Use case that is used to match recommendations.
* @param candidates - Recommended candidates to use as replacements.
* @param recoMetrics - Metrics for given candidates.
* @param dataContainer - Data returned from MAPI for recommended candidates.
*/
constructor(useCase, candidates, recoMetrics, dataContainer) {
this.useCase = useCase;
this.candidates = candidates;
this.recoMetrics = recoMetrics;
this.dataContainer = dataContainer;
}
/// Returns the first candidate that has the given type.
candidate(type) {
return this.candidates.find((candidate) => candidate.type === type);
}
}
/**
* Today recommended candidate with its id, type, data
* and candidate IDs (only if it is a candidate for a story group).
*/
export class TodayRecommendedCandidate {
/**
* Initializes today recommended candidate.
* @param id - Candidate id i.e. story ID or story group ID.
* @param type - Candidate type i.e. "editorial-items" or "editorial-item-groups".
* @param candidateIDs - Candidate IDs.
* @param data - Data to use while fetching from MAPI.
*/
constructor(id, type, candidateIDs, data) {
this.id = id;
this.type = type;
this.candidateIDs = candidateIDs;
this.data = data;
}
}
//# sourceMappingURL=on-device-recommendations-today.js.map
|