diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js new file mode 100644 index 0000000..1655d6d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js @@ -0,0 +1,321 @@ +/** + * Created by km on 11/17/16. + */ +/* This file DOES NOT target nodejs usage. */ +import { isNothing, isSome } from "@jet/environment"; +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Parameters } from "../../foundation/network/url-constants"; +import * as routingComponents from "../../foundation/routing/routing-components"; +import * as objects from "../../foundation/util/objects"; +import { allOptional, tryAwait } from "../../foundation/util/promise-util"; +import * as productPageCommon from "../product-page/product-page-common"; +// region Routers +export const pageRouter = makeMetatype("app-store:page-router"); +export class PageRouter { + constructor() { + this.registeredBuilders = new Set(); + this.pageRouter = new routingComponents.UrlRouter(); + this.shelfRouter = new routingComponents.UrlRouter(); + this.paginationRouter = new routingComponents.UrlRouter(); + // endregion + } + /** + * Register a page builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} pageBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerPageBuilder(objectGraph, pageBuilder) { + if (this.registeredBuilders.has(pageBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${pageBuilder.builderClass}`); + } + this.registeredBuilders.add(pageBuilder.builderClass); + this.pageRouter.associate(pageBuilder.pageRoute(objectGraph), pageBuilder); + this.shelfRouter.associate(pageBuilder.shelfRoute(objectGraph), pageBuilder); + this.paginationRouter.associate(pageBuilder.paginationRoute(objectGraph), pageBuilder); + } + /** + * Register a shelf builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} shelfBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerShelfBuilder(objectGraph, shelfBuilder) { + if (this.registeredBuilders.has(shelfBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${shelfBuilder.builderClass}`); + } + this.registeredBuilders.add(shelfBuilder.builderClass); + this.shelfRouter.associate(shelfBuilder.shelfRoute(objectGraph), shelfBuilder); + } + /** + * Register a pagination builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} paginationBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerPaginationBuilder(objectGraph, paginationBuilder) { + if (this.registeredBuilders.has(paginationBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${paginationBuilder.builderClass}`); + } + this.registeredBuilders.add(paginationBuilder.builderClass); + this.paginationRouter.associate(paginationBuilder.paginationRoute(objectGraph), paginationBuilder); + } + // endregion + // region Exported API + /** + * Determine the type of destination for given `url`. + * @param {string} url The url to fetch `flowPage` for. + * @returns {FlowPage} The FlowPage for given `url` + */ + fetchFlowPage(url) { + const routerResult = this.pageRouter.routedObjectForUrl(url); + if (!routerResult.object) { + return `unknown`; + } + const builder = routerResult.object; + // Product URLs can go to `writeReview` via deep links in iOS, so the page type may not necessary be `product` + // So check that they are not routed to the reviews sections + if (builder.builderClass === "ProductBuilder" && + routerResult.parameters[Parameters.action] !== productPageCommon.reviewsAction && + routerResult.parameters[Parameters.action] !== productPageCommon.writeReviewAction) { + return "product"; + } + return builder.pageType(); + } + /** + * Fetch the contents of a page. + * @param objectGraph + * @param url The URL to determine what kind of page to load. + * @param pageType The meta type of the expected page, e.g. `models.ProductPage`. + * This is used to perform a runtime type check to prevent hard to track bugs. + * @returns A promise which will resolve into a page of `pageType`. + */ + async fetchPage(objectGraph, url, pageType) { + return await this.fetchAction(objectGraph, url, null, false).then(async (pageAction) => { + return await new Promise((resolve, reject) => { + if (!pageAction) { + throw new Error(`Promise resolved to null action for: ${url}`); + } + if (pageAction.actionClass === "FlowAction") { + const pageData = pageAction.pageData; + if (!objects.isTypeOf(pageData, pageType)) { + // As we have no data of the correct type, check if we have a redirect + const pageUrl = pageAction.pageUrl; + const isRedirectingToSelf = pageUrl === url; + if (pageUrl && !isRedirectingToSelf) { + // Re-process this fetch with the new URL. + this.fetchPage(objectGraph, pageUrl, pageType) + .then((page) => { + resolve(page); + }) + .catch((error) => { + reject(error); + }); + return; + } + reject(new Error(`pageData is not expected type ${pageType.name}, ${JSON.stringify(pageData)}`)); + return; + } + resolve(pageData); + return; + } + else if (pageAction.actionClass === "TabChangeAction") { + const tabChangeAction = pageAction; + if (tabChangeAction.actions.length === 1 && + tabChangeAction.actions[0].actionClass === "FlowAction") { + const pageData = tabChangeAction.actions[0].pageData; + if (!objects.isTypeOf(pageData, pageType)) { + reject(new Error(`pageData is not expected type ${pageType.name}, ${JSON.stringify(pageData)}`)); + return; + } + resolve(pageData); + return; + } + } + reject(new Error("Action is not a flowAction or a tabChangeAction that contains a single flowAction.")); + }); + }); + } + /** + * Fetch an action for a given url, including page data if there is any to return. + * @param url The URL to determine which actions to take. + * @param referrerData Optional incoming deep link referrer data. + * @param isIncomingURL Whether the fetch is for deep link. + * @param visitedUrls Optional set of URLs already visited in this redirect chain to prevent cycles. + * @returns A promise that will resolve to an Action. + */ + async fetchAction(objectGraph, url, referrerData, isIncomingURL, visitedUrls = new Set()) { + var _a; + const routerResult = this.pageRouter.routedObjectForUrl(url); + if (!routerResult.object) { + // Urls fed into `fetchAction` can redirect to supported routes, thus we attempt to + // chain the redirect and pipe it back into `fetchAction`. + return await this.redirectAndRefetchActionIfPossible(objectGraph, routerResult.normalizedUrl, visitedUrls); + } + const builder = routerResult.object; + return await builder.handlePage(objectGraph, routerResult.normalizedUrl, (_a = routerResult.parameters) !== null && _a !== void 0 ? _a : {}, routerResult.matchedRuleIdentifier, referrerData, isIncomingURL); + } + /** + * Fetch the next page of a page. The returned page will have its `shelves`, `nextPage` properties set. + * @param pageToken The page token to use. + * @returns A promise that will resolve to a page. + */ + async fetchMoreOfPage(objectGraph, pageToken) { + const url = pageToken.url; + if (!url) { + throw new Error("Page token missing required `url` property"); + } + const routerResult = this.paginationRouter.routedObjectForUrl(url); + if (!routerResult.object) { + throw new Error(`fetchMoreOfPage: Unhandled pagination url: ${url}`); + } + const builder = routerResult.object; + return await builder.handlePagination(objectGraph, routerResult.normalizedUrl, routerResult.parameters, routerResult.matchedRuleIdentifier, pageToken); + } + /** + * Fetch the contents of multiple shelves, producing a shelf batch object. + * @param requests A hash of request keys to URL strings. + * @returns A promise that will resolve into multiple shelves. + */ + async fetchShelves(objectGraph, requests) { + // eslint-disable-next-line promise/param-names + const shelfRequestKeys = Object.keys(requests); + const shelfUrls = shelfRequestKeys.map((key) => requests[key]); + // Guaranteed never to throw an error + const shelfForUrl = async (shelfUrl) => { + if (isNothing(shelfUrl)) { + throw new Error(`fetchShelves: Null shelf url`); + } + const routerResult = this.shelfRouter.routedObjectForUrl(shelfUrl); + if (isNothing(routerResult.object)) { + throw new Error(`fetchShelves: Unhandled shelf url: ${shelfUrl}`); + } + const builder = routerResult.object; + const shelf = await builder.handleShelf(objectGraph, routerResult.normalizedUrl, routerResult.parameters, routerResult.matchedRuleIdentifier); + return shelf; + }; + // Map each URL to a promise and make them all optional + const resolvedShelves = await allOptional(shelfUrls.map(shelfForUrl)); + const batch = { + shelves: {}, + errors: {}, + }; + for (const [index, resolvedShelf] of resolvedShelves.entries()) { + const requestKey = shelfRequestKeys[index]; + if (isNothing(requestKey)) { + // This should never happen as we create a shelf request for each key + continue; + } + if (resolvedShelf.success) { + batch.shelves[requestKey] = resolvedShelf.value; + } + else { + // For rejected promises, access the error property + batch.errors[requestKey] = resolvedShelf.error; + } + } + const hasResults = Object.keys(batch.shelves).length > 0; + if (hasResults) { + return batch; + } + else { + const messages = Object.keys(batch.errors).map((key) => batch.errors[key].message); + if (messages.length === 0) { + throw new Error(`Could not load any shelves: ${JSON.stringify(requests)}`); + } + else { + throw new Error(messages.join("\n")); + } + } + } + // endregion + // region Redirect + /** + * Given an `url`, it will fire a GET request and then try to refetch the action for url if + * it was redirected. This is used for urls that are unknown, but redirects to a supported url. + * + * @param objectGraph + * @param {string} url to attempt to redirect with. This is expected to be normalized. + * @param {PromiseResolveFunction<models.Action>} resolve function to resolve promise to an `Action`. + * @param {PromiseRejectFunction} reject function to reject promise to `Error`. + * @param {Set<string>} visitedUrls Set of URLs already visited in this redirect chain to prevent cycles. + */ + async redirectAndRefetchActionIfPossible(objectGraph, url, visitedUrls = new Set()) { + // Check for redirect cycle + const urlString = url.toString(); + if (visitedUrls.has(urlString)) { + throw new Error(`redirectAndRefetchActionIfPossible: Redirect cycle detected for URL: ${urlString}`); + } + // Add current URL to visited set + visitedUrls.add(urlString); + const fetchResult = await tryAwait(objectGraph.network.fetch({ + url: urlString, + method: "GET", + })); + if (!fetchResult.success) { + throw new Error(`redirectAndRefetchActionIfPossible: Failed to fetch page at url: ${url}`); + } + const response = fetchResult.value; + if (this.hasGotoURLInResponse(objectGraph, response)) { + // If the response body contains a goto url fail silently as we will natively try to load the URL. + return new models.BlankAction(); + } + else if (response.status === 200 && response.redirected && response.url) { + // Only route if this was a redirect; otherwise we don't know how to handle this page + return await this.fetchAction(objectGraph, response.url, null, false, visitedUrls); + } + else { + throw new Error(`redirectAndRefetchActionIfPossible: Unhandled page url: ${url}`); + } + } + /** + * Given a `response`, it will check to see if the response body contains a goto URL + * A goto URL is returned by the server in a plist as a way of navigating to a new page + * AMSURLSession which is the native response handler looks for these URLs and tries to load them natively + * @param objectGraph + * @param {FetchResponse} the response body from the server + */ + hasGotoURLInResponse(objectGraph, response) { + if (serverData.isNullOrEmpty(response.body)) { + return false; + } + try { + const responseData = serverData.asJSONValue(objectGraph.plist.parse(response.body)); + const responseActionKind = serverData.asString(responseData, "action.kind"); + const responseActionUrl = serverData.asString(responseData, "action.url"); + if (responseActionKind === "Goto" && isSome(responseActionUrl)) { + return true; + } + return false; + } + catch { + return false; + } + } + // endregion + // region Testing + /** + * Fetch the builder for a given url. + * @param url The URL that the builder is associated with. + * @returns A builder if any was associated, or null if no match was found. + * @note FOR TESTING ONLY. + */ + fetchBuilder(url) { + const routerResult = this.pageRouter.routedObjectForUrl(url) || + this.shelfRouter.routedObjectForUrl(url) || + this.paginationRouter.routedObjectForUrl(url); + return routerResult ? routerResult.object : null; + } +} +//# sourceMappingURL=routing.js.map
\ No newline at end of file |
