summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js
init commit
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.js321
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