PromucFlow_constructor/app/client/src/ce/utils/serviceWorkerUtils.ts
Diljit 9d1f515d6d
chore: make branchName a query param in consolidated api (#38851)
## Description
- Move `branchName` header to `branchName` query param for consolidated
api
- Update the service worker prefetching logic to reflect the new url
signature changes for consolidated api.

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13005671168>
> Commit: 45b23c18a097f8bc667a89b629c9d339d73ad040
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13005671168&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Tue, 28 Jan 2025 08:54:43 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


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

## Release Notes

- **New Features**
- Added support for branch-specific data retrieval across multiple
components.
- Enhanced API endpoint configuration to dynamically handle branch
parameters.

- **Improvements**
- Refactored URL generation and parameter handling for more flexible API
calls.
- Updated server-side controllers to support branch-related query
parameters.

- **Technical Updates**
- Modified several API and saga functions to include optional branch
information.
- Improved type safety and parameter management in consolidated API
services.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-01-28 16:05:11 +05:30

271 lines
7.3 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 "../constants/routes/appRoutes";
import { ConsolidatedApiUtils } from "api/services/ConsolidatedPageLoadApi/url";
interface TMatchResult {
basePageId?: string;
baseApplicationId?: string;
applicationSlug?: string;
}
export interface TApplicationParams {
origin: string;
basePageId?: string;
baseApplicationId?: string;
branchName: string;
appMode: APP_MODE;
applicationSlug?: string;
}
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 = (
urlParams: Pick<URL, "origin" | "pathname" | "search">,
): TApplicationParamsOrNull => {
const { origin, pathname, search } = urlParams;
// Get the branch name from the query string
const branchName = getSearchQuery(search, "branch");
const matchedBuilder: Match<TMatchResult> = matchBuilderPath(pathname, {
end: false,
});
const matchedViewer: Match<TMatchResult> = matchViewerPath(pathname);
if (matchedBuilder) {
return {
origin,
basePageId: matchedBuilder.params.basePageId,
baseApplicationId: matchedBuilder.params.baseApplicationId,
branchName,
appMode: APP_MODE.EDIT,
applicationSlug: matchedBuilder.params.applicationSlug,
};
}
if (matchedViewer) {
return {
origin,
basePageId: matchedViewer.params.basePageId,
baseApplicationId: matchedViewer.params.baseApplicationId,
branchName,
appMode: APP_MODE.PUBLISHED,
applicationSlug: matchedViewer.params.applicationSlug,
};
}
return null;
};
/**
* Function to get the prefetch request for consolidated api
*/
export const getConsolidatedApiPrefetchRequest = (
applicationProps: TApplicationParams,
) => {
const { appMode, baseApplicationId, basePageId, branchName, origin } =
applicationProps;
if (!basePageId) {
return null;
}
// If the URL matches the builder path
if (appMode === APP_MODE.EDIT) {
const requestUrl = ConsolidatedApiUtils.getEditUrl({
defaultPageId: basePageId,
applicationId: baseApplicationId,
branchName,
});
const request = new Request(`${origin}/api/${requestUrl}`, {
method: "GET",
});
return request;
}
// If the URL matches the viewer path
if (appMode === APP_MODE.PUBLISHED) {
const requestUri = ConsolidatedApiUtils.getViewUrl({
defaultPageId: basePageId,
applicationId: baseApplicationId,
branchName,
});
const request = new Request(`${origin}/api/${requestUri}`, {
method: "GET",
});
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>();
constructor() {}
// Function to get the request key
getRequestKey = (request: Request) => {
const requestKey = `${request.method}:${request.url}`;
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;
}
}