chore: add dsl version validation for app computation cache (#40301)
## Description This PR adds dsl version to the app computation cache. If there is a mismatch in dsl version the cache is updated with the new value. Change also includes error handling and reporting for cases when there are exceptions while fetching the cache. Fixes #`Issue Number` _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## 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/14533292018> > Commit: fadb0c528b85b846799db2cbbd9da4d01834627f > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14533292018&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Fri, 18 Apr 2025 11:07:11 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 - **New Features** - Added explicit DSL version tracking to application state and evaluation context for improved cache validation. - **Bug Fixes** - Improved error handling for cache operations, ensuring errors are logged and recorded without disrupting core functionality. - Enhanced cache validation to prevent usage of invalid or mismatched cache entries. - **Tests** - Expanded and updated test coverage for cache validity, error scenarios, and DSL version handling. - **Refactor** - Strengthened cache property validation and error propagation for more robust caching behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
041d189c5f
commit
c580cfd9b4
|
|
@ -1804,3 +1804,7 @@ export const getUpcomingPlugins = createSelector(
|
|||
(state: AppState) => state.entities.plugins.upcomingPlugins,
|
||||
(upcomingPlugins) => upcomingPlugins.list,
|
||||
);
|
||||
|
||||
export const getCurrentPageDSLVersion = (state: AppState) => {
|
||||
return state.entities.canvasWidgets[0]?.version || null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -327,6 +327,11 @@ export function* evalErrorHandler(
|
|||
});
|
||||
break;
|
||||
}
|
||||
case EvalErrorTypes.CACHE_ERROR: {
|
||||
log.error(error);
|
||||
captureException(error, { errorName: "CacheError" });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
log.error(error);
|
||||
captureException(reconstructedError, { errorName: "UnknownEvalError" });
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import { expectSaga, testSaga } from "redux-saga-test-plan";
|
|||
import { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
|
||||
import { select } from "redux-saga/effects";
|
||||
import { getMetaWidgets, getWidgets, getWidgetsMeta } from "./selectors";
|
||||
import { getAllActionValidationConfig } from "ee//selectors/entitiesSelector";
|
||||
import {
|
||||
getAllActionValidationConfig,
|
||||
getCurrentPageDSLVersion,
|
||||
} from "ee//selectors/entitiesSelector";
|
||||
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
|
||||
import { getAppMode } from "ee/selectors/applicationSelectors";
|
||||
import * as log from "loglevel";
|
||||
|
|
@ -59,6 +62,7 @@ describe("evaluateTreeSaga", () => {
|
|||
select(getApplicationLastDeployedAt),
|
||||
new Date("11 September 2024").toISOString(),
|
||||
],
|
||||
[select(getCurrentPageDSLVersion), 1],
|
||||
])
|
||||
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
||||
cacheProps: {
|
||||
|
|
@ -67,6 +71,7 @@ describe("evaluateTreeSaga", () => {
|
|||
pageId: "pageId",
|
||||
appMode: false,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
dslVersion: 1,
|
||||
},
|
||||
unevalTree: unEvalAndConfigTree,
|
||||
widgetTypeConfigMap: undefined,
|
||||
|
|
@ -105,6 +110,7 @@ describe("evaluateTreeSaga", () => {
|
|||
select(getApplicationLastDeployedAt),
|
||||
new Date("11 September 2024").toISOString(),
|
||||
],
|
||||
[select(getCurrentPageDSLVersion), 1],
|
||||
])
|
||||
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
||||
cacheProps: {
|
||||
|
|
@ -113,6 +119,7 @@ describe("evaluateTreeSaga", () => {
|
|||
pageId: "pageId",
|
||||
appMode: false,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
dslVersion: 1,
|
||||
},
|
||||
unevalTree: unEvalAndConfigTree,
|
||||
widgetTypeConfigMap: undefined,
|
||||
|
|
@ -160,6 +167,7 @@ describe("evaluateTreeSaga", () => {
|
|||
select(getApplicationLastDeployedAt),
|
||||
new Date("11 September 2024").toISOString(),
|
||||
],
|
||||
[select(getCurrentPageDSLVersion), 1],
|
||||
])
|
||||
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
||||
cacheProps: {
|
||||
|
|
@ -168,6 +176,7 @@ describe("evaluateTreeSaga", () => {
|
|||
pageId: "pageId",
|
||||
appMode: false,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
dslVersion: 1,
|
||||
},
|
||||
unevalTree: unEvalAndConfigTree,
|
||||
widgetTypeConfigMap: undefined,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import { resetWidgetsMetaState, updateMetaState } from "actions/metaActions";
|
|||
import {
|
||||
getAllActionValidationConfig,
|
||||
getAllJSActionsData,
|
||||
getCurrentPageDSLVersion,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import type { WidgetEntityConfig } from "ee/entities/DataTree/types";
|
||||
import type {
|
||||
|
|
@ -269,6 +270,7 @@ export function* evaluateTreeSaga(
|
|||
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
|
||||
const widgetsMeta: ReturnType<typeof getWidgetsMeta> =
|
||||
yield select(getWidgetsMeta);
|
||||
const dslVersion: number | null = yield select(getCurrentPageDSLVersion);
|
||||
|
||||
const shouldRespondWithLogs = log.getLevel() === log.levels.DEBUG;
|
||||
|
||||
|
|
@ -279,6 +281,7 @@ export function* evaluateTreeSaga(
|
|||
pageId,
|
||||
timestamp: lastDeployedAt,
|
||||
instanceId,
|
||||
dslVersion,
|
||||
},
|
||||
unevalTree: unEvalAndConfigTree,
|
||||
widgetTypeConfigMap,
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ export enum EvalErrorTypes {
|
|||
CLONE_ERROR = "CLONE_ERROR",
|
||||
SERIALIZATION_ERROR = "SERIALIZATION_ERROR",
|
||||
UPDATE_DATA_TREE_ERROR = "UPDATE_DATA_TREE_ERROR",
|
||||
CACHE_ERROR = "CACHE_ERROR",
|
||||
}
|
||||
|
||||
export interface EvalError {
|
||||
|
|
|
|||
|
|
@ -588,6 +588,7 @@ describe("DataTreeEvaluator", () => {
|
|||
timestamp: "timestamp",
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
instanceId: "instanceId",
|
||||
dslVersion: 1,
|
||||
},
|
||||
);
|
||||
evaluator.evalAndValidateFirstTree();
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ describe("evaluateAndGenerateResponse", () => {
|
|||
timestamp: "timestamp",
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
instanceId: "instanceId",
|
||||
dslVersion: 1,
|
||||
},
|
||||
);
|
||||
evaluator.evalAndValidateFirstTree();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import { EComputationCacheName, type ICacheProps } from "./types";
|
||||
import {
|
||||
EComputationCacheName,
|
||||
type ICacheProps,
|
||||
type IValidatedCacheProps,
|
||||
} from "./types";
|
||||
import { APP_MODE } from "entities/App";
|
||||
import localforage from "localforage";
|
||||
import loglevel from "loglevel";
|
||||
|
|
@ -71,12 +75,13 @@ describe("AppComputationCache", () => {
|
|||
|
||||
describe("generateCacheKey", () => {
|
||||
test("should generate the correct cache key", () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -102,14 +107,15 @@ describe("AppComputationCache", () => {
|
|||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const result = appComputationCache.isComputationCached({
|
||||
const result = appComputationCache.shouldComputationBeCached(
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
|
@ -121,14 +127,15 @@ describe("AppComputationCache", () => {
|
|||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const result = appComputationCache.isComputationCached({
|
||||
const result = appComputationCache.shouldComputationBeCached(
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
|
@ -139,14 +146,15 @@ describe("AppComputationCache", () => {
|
|||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const result = appComputationCache.isComputationCached({
|
||||
const result = appComputationCache.shouldComputationBeCached(
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
|
@ -158,14 +166,35 @@ describe("AppComputationCache", () => {
|
|||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const result = appComputationCache.isComputationCached({
|
||||
const result = appComputationCache.shouldComputationBeCached(
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if dslVersion is undefined", () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: null,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const result = appComputationCache.shouldComputationBeCached(
|
||||
cacheName,
|
||||
cacheProps,
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
|
@ -173,12 +202,13 @@ describe("AppComputationCache", () => {
|
|||
|
||||
describe("getCachedComputationResult", () => {
|
||||
test("should call getItemMock and return null if cache miss", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -205,12 +235,13 @@ describe("AppComputationCache", () => {
|
|||
});
|
||||
|
||||
test("should call deleteInvalidCacheEntries on cache miss after 10 seconds", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -240,13 +271,14 @@ describe("AppComputationCache", () => {
|
|||
expect(keysMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should call getItemMock and return cached value if cache hit", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
test("should call deleteInvalidCacheEntries on dsl version mismatch after 10 seconds", async () => {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 2,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -256,7 +288,43 @@ describe("AppComputationCache", () => {
|
|||
cacheProps,
|
||||
});
|
||||
|
||||
getItemMock.mockResolvedValue({ value: "cachedValue" });
|
||||
getItemMock.mockResolvedValue({ value: "cachedValue", dslVersion: 1 });
|
||||
|
||||
const result = await appComputationCache.getCachedComputationResult({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
|
||||
expect(getItemMock).toHaveBeenCalledWith(cacheKey);
|
||||
expect(result).toBe(null);
|
||||
|
||||
jest.advanceTimersByTime(2500);
|
||||
expect(keysMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
jest.advanceTimersByTime(2500);
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(keysMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should call getItemMock and return cached value if cache hit", async () => {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const cacheKey = appComputationCache.generateCacheKey({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
|
||||
getItemMock.mockResolvedValue({ value: "cachedValue", dslVersion: 1 });
|
||||
|
||||
const result = await appComputationCache.getCachedComputationResult({
|
||||
cacheName,
|
||||
|
|
@ -270,12 +338,13 @@ describe("AppComputationCache", () => {
|
|||
|
||||
describe("cacheComputationResult", () => {
|
||||
test("should store computation result and call trackCacheUsage when shouldCache is true", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -300,6 +369,7 @@ describe("AppComputationCache", () => {
|
|||
|
||||
expect(setItemMock).toHaveBeenCalledWith(cacheKey, {
|
||||
value: computationResult,
|
||||
dslVersion: 1,
|
||||
});
|
||||
expect(trackCacheUsageSpy).toHaveBeenCalledWith(cacheKey);
|
||||
|
||||
|
|
@ -313,6 +383,30 @@ describe("AppComputationCache", () => {
|
|||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const computationResult = "computedValue";
|
||||
|
||||
await appComputationCache.cacheComputationResult({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
computationResult,
|
||||
});
|
||||
|
||||
expect(setItemMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should not store computation result when dsl version is invalid", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: null,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -331,12 +425,13 @@ describe("AppComputationCache", () => {
|
|||
|
||||
describe("fetchOrCompute", () => {
|
||||
test("should return cached result if available", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -346,7 +441,7 @@ describe("AppComputationCache", () => {
|
|||
cacheProps,
|
||||
});
|
||||
|
||||
getItemMock.mockResolvedValue({ value: "cachedValue" });
|
||||
getItemMock.mockResolvedValue({ value: "cachedValue", dslVersion: 1 });
|
||||
|
||||
const computeFn = jest.fn(() => "computedValue");
|
||||
|
||||
|
|
@ -364,12 +459,13 @@ describe("AppComputationCache", () => {
|
|||
test("should compute, cache, and return result if not in cache", async () => {
|
||||
getItemMock.mockResolvedValue(null);
|
||||
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -413,12 +509,13 @@ describe("AppComputationCache", () => {
|
|||
|
||||
loglevel.setLevel("SILENT");
|
||||
|
||||
const cacheProps: ICacheProps = {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
|
@ -427,39 +524,54 @@ describe("AppComputationCache", () => {
|
|||
|
||||
const computeFn = jest.fn(() => computationResult);
|
||||
|
||||
const cacheComputationResultSpy = jest.spyOn(
|
||||
appComputationCache,
|
||||
"cacheComputationResult",
|
||||
);
|
||||
try {
|
||||
await appComputationCache.fetchOrCompute({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
computeFn,
|
||||
});
|
||||
fail("Expected error to be thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toContain("Cache access error");
|
||||
}
|
||||
|
||||
const result = await appComputationCache.fetchOrCompute({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
computeFn,
|
||||
});
|
||||
|
||||
expect(getItemMock).toHaveBeenCalled();
|
||||
expect(computeFn).toHaveBeenCalled();
|
||||
expect(cacheComputationResultSpy).toHaveBeenCalledWith({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
computationResult,
|
||||
});
|
||||
expect(result).toBe(computationResult);
|
||||
|
||||
cacheComputationResultSpy.mockRestore();
|
||||
loglevel.setLevel(defaultLogLevel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteInvalidCacheEntries", () => {
|
||||
test("should delete old cache entries", async () => {
|
||||
test("should not cache result when dsl version is invalid", async () => {
|
||||
const cacheProps: ICacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: null,
|
||||
};
|
||||
|
||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||
|
||||
const computationResult = "computedValue";
|
||||
|
||||
await appComputationCache.cacheComputationResult({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
computationResult,
|
||||
});
|
||||
|
||||
expect(setItemMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteInvalidCacheEntries", () => {
|
||||
test("should delete old cache entries", async () => {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date("11 September 2024").toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
const currentTimestamp = new Date(cacheProps.timestamp).getTime();
|
||||
|
|
@ -516,4 +628,105 @@ describe("AppComputationCache", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAppModeValid", () => {
|
||||
test("should return true for valid app modes", () => {
|
||||
expect(appComputationCache.isAppModeValid(APP_MODE.PUBLISHED)).toBe(true);
|
||||
expect(appComputationCache.isAppModeValid(APP_MODE.EDIT)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for invalid app modes", () => {
|
||||
expect(appComputationCache.isAppModeValid(undefined)).toBe(false);
|
||||
expect(appComputationCache.isAppModeValid(null)).toBe(false);
|
||||
expect(appComputationCache.isAppModeValid("invalid")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTimestampValid", () => {
|
||||
test("should return true for valid timestamps", () => {
|
||||
const validTimestamp = new Date().toISOString();
|
||||
|
||||
expect(appComputationCache.isTimestampValid(validTimestamp)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for invalid timestamps", () => {
|
||||
expect(appComputationCache.isTimestampValid(undefined)).toBe(false);
|
||||
expect(appComputationCache.isTimestampValid(null)).toBe(false);
|
||||
expect(appComputationCache.isTimestampValid("invalid")).toBe(false);
|
||||
expect(appComputationCache.isTimestampValid("2024-01-01")).toBe(false);
|
||||
expect(appComputationCache.isTimestampValid("2024-01-01T00")).toBe(false);
|
||||
expect(appComputationCache.isTimestampValid("2024-01-01T00:00")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(appComputationCache.isTimestampValid("2024-01-01T00:00:00")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
appComputationCache.isTimestampValid("2024-01-01T00:00:00.000"),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDSLVersionValid", () => {
|
||||
test("should return true for valid dsl versions", () => {
|
||||
expect(appComputationCache.isDSLVersionValid(1)).toBe(true);
|
||||
expect(appComputationCache.isDSLVersionValid(90)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for invalid dsl versions", () => {
|
||||
expect(appComputationCache.isDSLVersionValid(0)).toBe(false);
|
||||
expect(appComputationCache.isDSLVersionValid(null)).toBe(false);
|
||||
expect(appComputationCache.isDSLVersionValid("invalid")).toBe(false);
|
||||
expect(appComputationCache.isDSLVersionValid(undefined)).toBe(false);
|
||||
expect(appComputationCache.isDSLVersionValid(NaN)).toBe(false);
|
||||
expect(appComputationCache.isDSLVersionValid(Infinity)).toBe(false);
|
||||
expect(appComputationCache.isDSLVersionValid(-1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCacheValid", () => {
|
||||
const cacheProps: IValidatedCacheProps = {
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
timestamp: new Date().toISOString(),
|
||||
appId: "appId",
|
||||
instanceId: "instanceId",
|
||||
pageId: "pageId",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
test("should return true for valid cache", () => {
|
||||
const cachedValue = {
|
||||
value: "cachedValue",
|
||||
dslVersion: 1,
|
||||
};
|
||||
|
||||
expect(appComputationCache.isCacheValid(cachedValue, cacheProps)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return false null cache", () => {
|
||||
expect(appComputationCache.isCacheValid(null, cacheProps)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if dsl version is not present", () => {
|
||||
expect(
|
||||
appComputationCache.isCacheValid(
|
||||
{
|
||||
value: "cachedValue",
|
||||
},
|
||||
cacheProps,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if dsl version mismatch", () => {
|
||||
expect(
|
||||
appComputationCache.isCacheValid(
|
||||
{ value: "cachedValue", dslVersion: 2 },
|
||||
cacheProps,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@ import { APP_MODE } from "entities/App";
|
|||
import localforage from "localforage";
|
||||
import isNull from "lodash/isNull";
|
||||
import loglevel from "loglevel";
|
||||
import { EComputationCacheName, type ICacheProps } from "./types";
|
||||
import {
|
||||
EComputationCacheName,
|
||||
type IValidatedCacheProps,
|
||||
type ICacheProps,
|
||||
} from "./types";
|
||||
import debounce from "lodash/debounce";
|
||||
import { isFinite, isNumber, isString } from "lodash";
|
||||
|
||||
interface ICachedData<T> {
|
||||
value: T;
|
||||
dslVersion?: number;
|
||||
}
|
||||
|
||||
interface ICacheLog {
|
||||
|
|
@ -52,6 +58,25 @@ export class AppComputationCache {
|
|||
return AppComputationCache.instance;
|
||||
}
|
||||
|
||||
isAppModeValid(appMode: unknown) {
|
||||
return appMode === APP_MODE.PUBLISHED || appMode === APP_MODE.EDIT;
|
||||
}
|
||||
|
||||
isTimestampValid(timestamp: unknown) {
|
||||
const isoStringRegex =
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z$/;
|
||||
|
||||
if (isString(timestamp) && !!timestamp.trim()) {
|
||||
return isoStringRegex.test(timestamp);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isDSLVersionValid(dslVersion: unknown) {
|
||||
return isNumber(dslVersion) && isFinite(dslVersion) && dslVersion > 0;
|
||||
}
|
||||
|
||||
debouncedDeleteInvalidCacheEntries = debounce(
|
||||
this.deleteInvalidCacheEntries,
|
||||
5000,
|
||||
|
|
@ -61,16 +86,17 @@ export class AppComputationCache {
|
|||
* Check if the computation result should be cached based on the app mode configuration
|
||||
* @returns - A boolean indicating whether the cache should be enabled for the given app mode
|
||||
*/
|
||||
isComputationCached({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
}: {
|
||||
cacheName: EComputationCacheName;
|
||||
cacheProps: ICacheProps;
|
||||
}) {
|
||||
const { appMode, timestamp } = cacheProps;
|
||||
shouldComputationBeCached(
|
||||
cacheName: EComputationCacheName,
|
||||
cacheProps: ICacheProps,
|
||||
): cacheProps is IValidatedCacheProps {
|
||||
const { appMode, dslVersion, timestamp } = cacheProps;
|
||||
|
||||
if (!appMode || !timestamp) {
|
||||
if (
|
||||
!this.isAppModeValid(appMode) ||
|
||||
!this.isTimestampValid(timestamp) ||
|
||||
!this.isDSLVersionValid(dslVersion)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +107,7 @@ export class AppComputationCache {
|
|||
* Checks if the value should be cached based on the app mode configuration and
|
||||
* caches the computation result if it should be cached. It also tracks the cache usage
|
||||
* @returns - A promise that resolves when the computation result is cached
|
||||
* @throws - Logs an error if the computation result cannot be cached and throws the error
|
||||
*/
|
||||
async cacheComputationResult<T>({
|
||||
cacheName,
|
||||
|
|
@ -91,31 +118,31 @@ export class AppComputationCache {
|
|||
cacheName: EComputationCacheName;
|
||||
computationResult: T;
|
||||
}) {
|
||||
const shouldCache = this.isComputationCached({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
|
||||
if (!shouldCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheKey = this.generateCacheKey({ cacheProps, cacheName });
|
||||
|
||||
try {
|
||||
const isCacheable = this.shouldComputationBeCached(cacheName, cacheProps);
|
||||
|
||||
if (!isCacheable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheKey = this.generateCacheKey({ cacheProps, cacheName });
|
||||
|
||||
await this.store.setItem<ICachedData<T>>(cacheKey, {
|
||||
value: computationResult,
|
||||
dslVersion: cacheProps.dslVersion,
|
||||
});
|
||||
|
||||
await this.trackCacheUsage(cacheKey);
|
||||
} catch (error) {
|
||||
loglevel.debug("Error caching computation result:", error);
|
||||
loglevel.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cached computation result if it exists and is valid
|
||||
* @returns - A promise that resolves with the cached computation result or null if it does not exist
|
||||
* @throws - Logs an error if the computation result cannot be fetched and throws the error
|
||||
*/
|
||||
async getCachedComputationResult<T>({
|
||||
cacheName,
|
||||
|
|
@ -124,24 +151,21 @@ export class AppComputationCache {
|
|||
cacheProps: ICacheProps;
|
||||
cacheName: EComputationCacheName;
|
||||
}): Promise<T | null> {
|
||||
const shouldCache = this.isComputationCached({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
|
||||
if (!shouldCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = this.generateCacheKey({
|
||||
cacheProps,
|
||||
cacheName,
|
||||
});
|
||||
|
||||
try {
|
||||
const isCacheable = this.shouldComputationBeCached(cacheName, cacheProps);
|
||||
|
||||
if (!isCacheable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = this.generateCacheKey({
|
||||
cacheProps,
|
||||
cacheName,
|
||||
});
|
||||
|
||||
const cached = await this.store.getItem<ICachedData<T>>(cacheKey);
|
||||
|
||||
if (isNull(cached)) {
|
||||
if (!this.isCacheValid(cached, cacheProps)) {
|
||||
// Cache miss
|
||||
// Delete invalid cache entries when thread is idle
|
||||
setTimeout(async () => {
|
||||
|
|
@ -155,12 +179,30 @@ export class AppComputationCache {
|
|||
|
||||
return cached.value;
|
||||
} catch (error) {
|
||||
loglevel.error("Error getting cache result:", error);
|
||||
|
||||
return null;
|
||||
loglevel.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cached value is valid
|
||||
* @returns - A boolean indicating whether the cached value is valid
|
||||
*/
|
||||
isCacheValid<T>(
|
||||
cachedValue: ICachedData<T> | null,
|
||||
cacheProps: IValidatedCacheProps,
|
||||
): cachedValue is ICachedData<T> {
|
||||
if (isNull(cachedValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cachedValue.dslVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cachedValue.dslVersion === cacheProps.dslVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cache key from the index parts
|
||||
* @returns - The generated cache key
|
||||
|
|
@ -169,7 +211,7 @@ export class AppComputationCache {
|
|||
cacheName,
|
||||
cacheProps,
|
||||
}: {
|
||||
cacheProps: ICacheProps;
|
||||
cacheProps: IValidatedCacheProps;
|
||||
cacheName: EComputationCacheName;
|
||||
}) {
|
||||
const { appId, appMode, instanceId, pageId, timestamp } = cacheProps;
|
||||
|
|
@ -201,16 +243,13 @@ export class AppComputationCache {
|
|||
computeFn: () => Promise<T> | T;
|
||||
cacheName: EComputationCacheName;
|
||||
}) {
|
||||
const shouldCache = this.isComputationCached({
|
||||
cacheName,
|
||||
cacheProps,
|
||||
});
|
||||
|
||||
if (!shouldCache) {
|
||||
return computeFn();
|
||||
}
|
||||
|
||||
try {
|
||||
const isCacheable = this.shouldComputationBeCached(cacheName, cacheProps);
|
||||
|
||||
if (!isCacheable) {
|
||||
return computeFn();
|
||||
}
|
||||
|
||||
const cachedResult = await this.getCachedComputationResult<T>({
|
||||
cacheProps,
|
||||
cacheName,
|
||||
|
|
@ -230,10 +269,8 @@ export class AppComputationCache {
|
|||
|
||||
return computationResult;
|
||||
} catch (error) {
|
||||
loglevel.error("Error getting cache result:", error);
|
||||
const fallbackResult = await computeFn();
|
||||
|
||||
return fallbackResult;
|
||||
loglevel.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,6 +296,7 @@ export class AppComputationCache {
|
|||
/**
|
||||
* Delete invalid cache entries
|
||||
* @returns - A promise that resolves when the invalid cache entries are deleted
|
||||
* @throws - Logs an error if the invalid cache entries cannot be deleted
|
||||
*/
|
||||
|
||||
async deleteInvalidCacheEntries(cacheProps: ICacheProps) {
|
||||
|
|
@ -271,6 +309,10 @@ export class AppComputationCache {
|
|||
const keyParts = key.split(AppComputationCache.CACHE_KEY_DELIMITER);
|
||||
const cacheKeyTimestamp = parseInt(keyParts[4], 10);
|
||||
|
||||
if (!cacheProps.timestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
keyParts[0] === cacheProps.instanceId &&
|
||||
keyParts[1] === cacheProps.appId &&
|
||||
|
|
@ -295,6 +337,9 @@ export class AppComputationCache {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the singleton instance
|
||||
*/
|
||||
static resetInstance() {
|
||||
AppComputationCache.instance = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@ export interface ICacheProps {
|
|||
appId: string;
|
||||
pageId: string;
|
||||
appMode?: APP_MODE;
|
||||
timestamp?: string;
|
||||
instanceId: string;
|
||||
dslVersion: number | null;
|
||||
}
|
||||
|
||||
export interface IValidatedCacheProps {
|
||||
appId: string;
|
||||
pageId: string;
|
||||
appMode: APP_MODE;
|
||||
timestamp: string;
|
||||
instanceId: string;
|
||||
dslVersion: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,6 +290,7 @@ describe("DataTreeEvaluator", () => {
|
|||
timestamp: "timestamp",
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
instanceId: "instanceId",
|
||||
dslVersion: 1,
|
||||
},
|
||||
);
|
||||
dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
|
|
@ -391,6 +392,7 @@ describe("DataTreeEvaluator", () => {
|
|||
timestamp: "timestamp",
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
instanceId: "instanceId",
|
||||
dslVersion: 1,
|
||||
},
|
||||
);
|
||||
dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
|
|
@ -454,6 +456,7 @@ describe("DataTreeEvaluator", () => {
|
|||
timestamp: new Date().toISOString(),
|
||||
appMode: APP_MODE.PUBLISHED,
|
||||
instanceId: "instanceId",
|
||||
dslVersion: 1,
|
||||
},
|
||||
);
|
||||
dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
|
|
|
|||
|
|
@ -302,11 +302,21 @@ export default class DataTreeEvaluator {
|
|||
);
|
||||
const allKeysGenerationStartTime = performance.now();
|
||||
|
||||
this.allKeys = await appComputationCache.fetchOrCompute({
|
||||
cacheProps,
|
||||
cacheName: EComputationCacheName.ALL_KEYS,
|
||||
computeFn: () => getAllPaths(unEvalTreeWithStrigifiedJSFunctions),
|
||||
});
|
||||
try {
|
||||
this.allKeys = await appComputationCache.fetchOrCompute({
|
||||
cacheProps,
|
||||
cacheName: EComputationCacheName.ALL_KEYS,
|
||||
computeFn: () => getAllPaths(unEvalTreeWithStrigifiedJSFunctions),
|
||||
});
|
||||
} catch (error) {
|
||||
this.errors.push({
|
||||
type: EvalErrorTypes.CACHE_ERROR,
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
});
|
||||
|
||||
this.allKeys = getAllPaths(unEvalTreeWithStrigifiedJSFunctions);
|
||||
}
|
||||
|
||||
const allKeysGenerationEndTime = performance.now();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ import type {
|
|||
DataTreeEntityObject,
|
||||
} from "ee/entities/DataTree/types";
|
||||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeTypes";
|
||||
import { getEntityId, getEvalErrorPath } from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
EvalErrorTypes,
|
||||
getEntityId,
|
||||
getEvalErrorPath,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import { convertArrayToObject, extractInfoFromBindings } from "./utils";
|
||||
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
||||
import { get, isEmpty, set } from "lodash";
|
||||
|
|
@ -59,13 +63,23 @@ export async function createDependencyMap(
|
|||
);
|
||||
});
|
||||
|
||||
const dependencyMapCache =
|
||||
await appComputationCache.getCachedComputationResult<
|
||||
let dependencyMapCache: Record<string, string[]> | null = null;
|
||||
|
||||
try {
|
||||
dependencyMapCache = await appComputationCache.getCachedComputationResult<
|
||||
Record<string, string[]>
|
||||
>({
|
||||
cacheProps,
|
||||
cacheName: EComputationCacheName.DEPENDENCY_MAP,
|
||||
});
|
||||
} catch (error) {
|
||||
dataTreeEvalRef.errors.push({
|
||||
type: EvalErrorTypes.CACHE_ERROR,
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
});
|
||||
dependencyMapCache = null;
|
||||
}
|
||||
|
||||
if (dependencyMapCache) {
|
||||
profileFn("createDependencyMap.addDependency", {}, webworkerSpans, () => {
|
||||
|
|
@ -104,11 +118,19 @@ export async function createDependencyMap(
|
|||
DependencyMapUtils.makeParentsDependOnChildren(dependencyMap);
|
||||
|
||||
if (shouldCache) {
|
||||
await appComputationCache.cacheComputationResult({
|
||||
cacheProps,
|
||||
cacheName: EComputationCacheName.DEPENDENCY_MAP,
|
||||
computationResult: dependencyMap.dependencies,
|
||||
});
|
||||
try {
|
||||
await appComputationCache.cacheComputationResult({
|
||||
cacheProps,
|
||||
cacheName: EComputationCacheName.DEPENDENCY_MAP,
|
||||
computationResult: dependencyMap.dependencies,
|
||||
});
|
||||
} catch (error) {
|
||||
dataTreeEvalRef.errors.push({
|
||||
type: EvalErrorTypes.CACHE_ERROR,
|
||||
message: (error as Error).message,
|
||||
stack: (error as Error).stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user