269 lines
7.4 KiB
TypeScript
269 lines
7.4 KiB
TypeScript
import { Mutex } from "async-mutex";
|
|
import { APP_MODE } from "entities/App";
|
|
import type { Match, TokensToRegexpOptions } from "path-to-regexp";
|
|
import { match } from "path-to-regexp";
|
|
import {
|
|
BUILDER_PATH,
|
|
BUILDER_CUSTOM_PATH,
|
|
VIEWER_PATH,
|
|
VIEWER_CUSTOM_PATH,
|
|
BUILDER_PATH_DEPRECATED,
|
|
VIEWER_PATH_DEPRECATED,
|
|
} from "@appsmith/constants/routes/appRoutes";
|
|
|
|
interface TMatchResult {
|
|
basePageId?: string;
|
|
baseApplicationId?: string;
|
|
}
|
|
|
|
export interface TApplicationParams {
|
|
origin: string;
|
|
basePageId?: string;
|
|
baseApplicationId?: string;
|
|
branchName: string;
|
|
appMode: APP_MODE;
|
|
}
|
|
|
|
type TApplicationParamsOrNull = TApplicationParams | null;
|
|
|
|
export const cachedApiUrlRegex = new RegExp("/api/v1/consolidated-api/");
|
|
|
|
/**
|
|
* Function to match the path with the builder path
|
|
*/
|
|
export const matchBuilderPath = (
|
|
pathName: string,
|
|
options: TokensToRegexpOptions,
|
|
) =>
|
|
match<TMatchResult>(BUILDER_PATH, options)(pathName) ||
|
|
match<TMatchResult>(BUILDER_PATH_DEPRECATED, options)(pathName) ||
|
|
match<TMatchResult>(BUILDER_CUSTOM_PATH, options)(pathName);
|
|
|
|
/**
|
|
* Function to match the path with the viewer path
|
|
*/
|
|
export const matchViewerPath = (pathName: string) =>
|
|
match<TMatchResult>(VIEWER_PATH)(pathName) ||
|
|
match<TMatchResult>(VIEWER_PATH_DEPRECATED)(pathName) ||
|
|
match<TMatchResult>(VIEWER_CUSTOM_PATH)(pathName);
|
|
|
|
/**
|
|
* returns the value in the query string for a key
|
|
*/
|
|
export const getSearchQuery = (search = "", key: string) => {
|
|
const params = new URLSearchParams(search);
|
|
return decodeURIComponent(params.get(key) || "");
|
|
};
|
|
|
|
export const getApplicationParamsFromUrl = (
|
|
url: URL,
|
|
): TApplicationParamsOrNull => {
|
|
// Get the branch name from the query string
|
|
const branchName = getSearchQuery(url.search, "branch");
|
|
|
|
const matchedBuilder: Match<TMatchResult> = matchBuilderPath(url.pathname, {
|
|
end: false,
|
|
});
|
|
const matchedViewer: Match<TMatchResult> = matchViewerPath(url.pathname);
|
|
|
|
if (matchedBuilder) {
|
|
return {
|
|
origin: url.origin,
|
|
basePageId: matchedBuilder.params.basePageId,
|
|
baseApplicationId: matchedBuilder.params.baseApplicationId,
|
|
branchName,
|
|
appMode: APP_MODE.EDIT,
|
|
};
|
|
}
|
|
|
|
if (matchedViewer) {
|
|
return {
|
|
origin: url.origin,
|
|
basePageId: matchedViewer.params.basePageId,
|
|
baseApplicationId: matchedViewer.params.baseApplicationId,
|
|
branchName,
|
|
appMode: APP_MODE.PUBLISHED,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Function to get the prefetch request for consolidated api
|
|
*/
|
|
export const getConsolidatedApiPrefetchRequest = (
|
|
applicationProps: TApplicationParams,
|
|
) => {
|
|
const { appMode, baseApplicationId, basePageId, branchName, origin } =
|
|
applicationProps;
|
|
|
|
const headers = new Headers();
|
|
const searchParams = new URLSearchParams();
|
|
|
|
if (!basePageId) {
|
|
return null;
|
|
}
|
|
|
|
searchParams.append("defaultPageId", basePageId);
|
|
|
|
if (baseApplicationId) {
|
|
searchParams.append("applicationId", baseApplicationId);
|
|
}
|
|
|
|
// Add the branch name to the headers
|
|
if (branchName) {
|
|
headers.append("Branchname", branchName);
|
|
}
|
|
|
|
// If the URL matches the builder path
|
|
if (appMode === APP_MODE.EDIT) {
|
|
const requestUrl = `${origin}/api/${"v1/consolidated-api/edit"}?${searchParams.toString()}`;
|
|
const request = new Request(requestUrl, { method: "GET", headers });
|
|
return request;
|
|
}
|
|
|
|
// If the URL matches the viewer path
|
|
if (appMode === APP_MODE.PUBLISHED) {
|
|
const requestUrl = `${origin}/api/v1/consolidated-api/view?${searchParams.toString()}`;
|
|
const request = new Request(requestUrl, { method: "GET", headers });
|
|
return request;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Function to get the prefetch requests for an application
|
|
*/
|
|
export const getPrefetchRequests = (
|
|
applicationParams: TApplicationParams,
|
|
): Request[] => {
|
|
const prefetchRequests: Request[] = [];
|
|
const consolidatedApiPrefetchRequest =
|
|
getConsolidatedApiPrefetchRequest(applicationParams);
|
|
|
|
if (consolidatedApiPrefetchRequest) {
|
|
prefetchRequests.push(consolidatedApiPrefetchRequest);
|
|
}
|
|
|
|
return prefetchRequests;
|
|
};
|
|
|
|
/**
|
|
* Service to fetch and cache prefetch requests
|
|
*/
|
|
export class PrefetchApiService {
|
|
cacheName = "prefetch-cache-v1";
|
|
cacheMaxAge = 2 * 60 * 1000; // 2 minutes in milliseconds
|
|
// Mutex to lock the fetch and cache operation
|
|
prefetchFetchMutexMap = new Map<string, Mutex>();
|
|
// Header keys used to create the unique request key
|
|
headerKeys = ["branchname"];
|
|
|
|
constructor() {}
|
|
|
|
// Function to get the request key
|
|
getRequestKey = (request: Request) => {
|
|
let requestKey = `${request.method}:${request.url}`;
|
|
|
|
this.headerKeys.forEach((headerKey) => {
|
|
const headerValue = request.headers.get(headerKey);
|
|
if (headerValue) {
|
|
requestKey += `:${headerKey}:${headerValue}`;
|
|
}
|
|
});
|
|
|
|
return requestKey;
|
|
};
|
|
|
|
// Function to acquire the fetch mutex for a request
|
|
aqcuireFetchMutex = async (request: Request) => {
|
|
const requestKey = this.getRequestKey(request);
|
|
let mutex = this.prefetchFetchMutexMap.get(requestKey);
|
|
|
|
if (!mutex) {
|
|
mutex = new Mutex();
|
|
this.prefetchFetchMutexMap.set(requestKey, mutex);
|
|
}
|
|
|
|
return mutex.acquire();
|
|
};
|
|
|
|
// Function to wait for the lock to be released for a request
|
|
waitForUnlock = async (request: Request) => {
|
|
const requestKey = this.getRequestKey(request);
|
|
const mutex = this.prefetchFetchMutexMap.get(requestKey);
|
|
|
|
if (mutex) {
|
|
return mutex.waitForUnlock();
|
|
}
|
|
};
|
|
|
|
// Function to release the fetch mutex for a request
|
|
releaseFetchMutex = (request: Request) => {
|
|
const requestKey = this.getRequestKey(request);
|
|
const mutex = this.prefetchFetchMutexMap.get(requestKey);
|
|
|
|
if (mutex) {
|
|
mutex.release();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function to fetch and cache the consolidated API
|
|
*/
|
|
async cacheApi(request: Request) {
|
|
// Acquire the lock
|
|
await this.aqcuireFetchMutex(request);
|
|
const prefetchApiCache = await caches.open(this.cacheName);
|
|
try {
|
|
const response = await fetch(request);
|
|
|
|
if (response.ok) {
|
|
// Clone the response as the response can be consumed only once
|
|
const clonedResponse = response.clone();
|
|
// Put the response in the cache
|
|
await prefetchApiCache.put(request, clonedResponse);
|
|
}
|
|
} catch (error) {
|
|
// Delete the existing cache if the fetch fails
|
|
await prefetchApiCache.delete(request);
|
|
} finally {
|
|
// Release the lock
|
|
this.releaseFetchMutex(request);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Function to get the cached response for the request
|
|
*/
|
|
async getCachedResponse(request: Request) {
|
|
// Wait for the lock to be released
|
|
await this.waitForUnlock(request);
|
|
const prefetchApiCache = await caches.open(this.cacheName);
|
|
// Fetch the cached response for the request
|
|
// if it is a miss, assign null to the cachedResponse
|
|
let cachedResponse: Response | null =
|
|
(await prefetchApiCache.match(request)) || null;
|
|
|
|
if (cachedResponse) {
|
|
const dateHeader = cachedResponse.headers.get("date");
|
|
const cachedTime = dateHeader ? new Date(dateHeader).getTime() : 0;
|
|
const currentTime = Date.now();
|
|
// Check if the cache is valid
|
|
const isCacheValid = currentTime - cachedTime < this.cacheMaxAge;
|
|
|
|
// If the cache is not valid, assign null to the cachedResponse
|
|
if (!isCacheValid) {
|
|
cachedResponse = null;
|
|
}
|
|
|
|
// Delete the cache as this is a one time read cache
|
|
await prefetchApiCache.delete(request);
|
|
}
|
|
|
|
return cachedResponse;
|
|
}
|
|
}
|