From f9ca248ebc091fdbad923be7d2b5efb4f5bb95db Mon Sep 17 00:00:00 2001 From: Alexandros Tzimas Date: Tue, 8 Oct 2024 20:20:46 +0300 Subject: [PATCH] Add a routing cache interface to the common/lib --- portal/common/lib/page_fetching.ts | 41 +++++++++++++++----- portal/common/lib/routing_cache_interface.ts | 18 +++++++++ portal/common/lib/types/index.ts | 12 ++++++ portal/server/next-env.d.ts | 2 - 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 portal/common/lib/routing_cache_interface.ts diff --git a/portal/common/lib/page_fetching.ts b/portal/common/lib/page_fetching.ts index f14c1f69..aef22879 100644 --- a/portal/common/lib/page_fetching.ts +++ b/portal/common/lib/page_fetching.ts @@ -3,7 +3,7 @@ import { getFullnodeUrl, SuiClient } from "@mysten/sui/client"; import { NETWORK } from "./constants"; -import { DomainDetails, isResource } from "./types/index"; +import { DomainDetails, isResource, isRoutes } from "./types/index"; import { subdomainToObjectId, HEXtoBase36 } from "./objectId_operations"; import { resolveSuiNsAddress, hardcodedSubdmains } from "./suins"; import { fetchResource } from "./resource"; @@ -17,29 +17,50 @@ import { aggregatorEndpoint } from "./aggregator"; import { toB64 } from "@mysten/bcs"; import { sha256 } from "./crypto"; import { getRoutes, matchPathToRoute } from "./routing"; +import { RoutingCacheInterface } from "./routing_cache_interface"; +import { Routes, Empty, isEmpty } from "./types/index"; /** * Resolves the subdomain to an object ID, and gets the corresponding resources. */ -export async function resolveAndFetchPage(parsedUrl: DomainDetails): Promise { +export async function resolveAndFetchPage( + parsedUrl: DomainDetails, cache?: RoutingCacheInterface +): Promise { const rpcUrl = getFullnodeUrl(NETWORK); const client = new SuiClient({ url: rpcUrl }); const resolveObjectResult = await resolveObjectId(parsedUrl, client); const isObjectId = typeof resolveObjectResult == "string"; if (isObjectId) { - console.log("Object ID: ", resolveObjectResult); + console.log("Object ID: ", resolveObjectResult); console.log("Base36 version of the object ID: ", HEXtoBase36(resolveObjectResult)); // Rerouting based on the contents of the routes object, // constructed using the ws-resource.json. - const routes = await getRoutes(client, resolveObjectResult); - if (!routes) { - console.warn("No routes found for the object ID"); - return fetchPage(client, resolveObjectResult, parsedUrl.path); + let routes: Routes | Empty | undefined; + if (cache) { + routes = await cache.get(resolveObjectResult); + if (!routes) { + // The routes object was not found in the cache, so we need to fetch it. + routes = await getRoutes(client, resolveObjectResult); + await cache.set(resolveObjectResult, routes ? routes : {}); + } + if (isEmpty(routes)) { + console.warn("The routes object was already fetched, but it was empty."); + return fetchPage(client, resolveObjectResult, parsedUrl.path); + } + } else { + console.warn("No cache provided, fetching the routes object on every request."); + routes = await getRoutes(client, resolveObjectResult); + if (!routes) { + console.warn("No routes found for the object ID"); + return fetchPage(client, resolveObjectResult, parsedUrl.path); + } } let matchingRoute: string | undefined; - matchingRoute = matchPathToRoute(parsedUrl.path, routes) - if (!matchingRoute) { - console.warn(`No matching route found for ${parsedUrl.path}`); + if (isRoutes(routes)) { + matchingRoute = matchPathToRoute(parsedUrl.path, routes) + if (!matchingRoute) { + console.warn(`No matching route found for ${parsedUrl.path}`); + } } return fetchPage(client, resolveObjectResult, matchingRoute ?? parsedUrl.path); } diff --git a/portal/common/lib/routing_cache_interface.ts b/portal/common/lib/routing_cache_interface.ts new file mode 100644 index 00000000..51562ec0 --- /dev/null +++ b/portal/common/lib/routing_cache_interface.ts @@ -0,0 +1,18 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { Routes, Empty } from "./types"; + +/** + * Abstract class for a cache, to be able to swap out different + * caches between different portal implementations. + */ +export interface RoutingCacheInterface { + get(key: string): Promise; + /// A note on Empty: + /// This is used to indicate that the cache does not have a value for the key. + /// It is used to differentiate between a cache miss and a cache hit with an empty value. + /// Cache hit with empty values should not trigger a fetch from the fullnode. + set(key: string, value: Routes | Empty): Promise; + delete(key: string): Promise; +} diff --git a/portal/common/lib/types/index.ts b/portal/common/lib/types/index.ts index 1dd0c621..e38f27e9 100644 --- a/portal/common/lib/types/index.ts +++ b/portal/common/lib/types/index.ts @@ -72,3 +72,15 @@ export function isRoutes(obj: any): obj is Routes { obj.routes_list instanceof Map ); } + +/// An empty object that cannot have any properties. +export type Empty = Record; + +/** + * Checks if an object is of type Empty. + * @param obj The object to check. + * @returns True if the object is Empty, false otherwise. + */ +export function isEmpty(obj: any): obj is Empty { + return Object.keys(obj).length === 0; +} diff --git a/portal/server/next-env.d.ts b/portal/server/next-env.d.ts index 80dca4e5..d5d7d918 100644 --- a/portal/server/next-env.d.ts +++ b/portal/server/next-env.d.ts @@ -5,5 +5,3 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript -// for more information.