## 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 -->
271 lines
7.3 KiB
TypeScript
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;
|
|
}
|
|
}
|