PromucFlow_constructor/app/client/src/ce/utils/serviceWorkerUtils.ts
Shrikant Sharat Kandula d64ddee67a
chore: Don't duplicate the URL parsing Regexes (#34788)
This code duplication meant that we missed updating the regexes to the
ones that support UUIDs, and so the caching functionality broke.

This PR removes the duplication and imports the regex constants from a
single place.

/test sanity


<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9850040595>
> Commit: d8261a1b5ed55c7258260695fea2f2ca32dc0484
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9850040595&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Tue, 09 Jul 2024 03:00:41 UTC
<!-- end of auto-generated comment: Cypress test results  -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Refactor**
- Improved maintainability by importing path constants from a
centralized location.
- Replaced hardcoded `pageId` and `applicationId` with variables for
better code clarity and flexibility.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-07-09 17:47:04 +05:30

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 {
pageId?: string;
applicationId?: string;
}
export interface TApplicationParams {
origin: string;
pageId?: string;
applicationId?: 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,
pageId: matchedBuilder.params.pageId,
applicationId: matchedBuilder.params.applicationId,
branchName,
appMode: APP_MODE.EDIT,
};
}
if (matchedViewer) {
return {
origin: url.origin,
pageId: matchedViewer.params.pageId,
applicationId: matchedViewer.params.applicationId,
branchName,
appMode: APP_MODE.PUBLISHED,
};
}
return null;
};
/**
* Function to get the prefetch request for consolidated api
*/
export const getConsolidatedApiPrefetchRequest = (
applicationProps: TApplicationParams,
) => {
const { applicationId, appMode, branchName, origin, pageId } =
applicationProps;
const headers = new Headers();
const searchParams = new URLSearchParams();
if (!pageId) {
return null;
}
searchParams.append("defaultPageId", pageId);
if (applicationId) {
searchParams.append("applicationId", applicationId);
}
// 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;
}
}