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,
|
(state: AppState) => state.entities.plugins.upcomingPlugins,
|
||||||
(upcomingPlugins) => upcomingPlugins.list,
|
(upcomingPlugins) => upcomingPlugins.list,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getCurrentPageDSLVersion = (state: AppState) => {
|
||||||
|
return state.entities.canvasWidgets[0]?.version || null;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,11 @@ export function* evalErrorHandler(
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EvalErrorTypes.CACHE_ERROR: {
|
||||||
|
log.error(error);
|
||||||
|
captureException(error, { errorName: "CacheError" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
log.error(error);
|
log.error(error);
|
||||||
captureException(reconstructedError, { errorName: "UnknownEvalError" });
|
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 { EVAL_WORKER_ACTIONS } from "ee/workers/Evaluation/evalWorkerActions";
|
||||||
import { select } from "redux-saga/effects";
|
import { select } from "redux-saga/effects";
|
||||||
import { getMetaWidgets, getWidgets, getWidgetsMeta } from "./selectors";
|
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 { getSelectedAppTheme } from "selectors/appThemingSelectors";
|
||||||
import { getAppMode } from "ee/selectors/applicationSelectors";
|
import { getAppMode } from "ee/selectors/applicationSelectors";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
@ -59,6 +62,7 @@ describe("evaluateTreeSaga", () => {
|
||||||
select(getApplicationLastDeployedAt),
|
select(getApplicationLastDeployedAt),
|
||||||
new Date("11 September 2024").toISOString(),
|
new Date("11 September 2024").toISOString(),
|
||||||
],
|
],
|
||||||
|
[select(getCurrentPageDSLVersion), 1],
|
||||||
])
|
])
|
||||||
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
||||||
cacheProps: {
|
cacheProps: {
|
||||||
|
|
@ -67,6 +71,7 @@ describe("evaluateTreeSaga", () => {
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
appMode: false,
|
appMode: false,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
unevalTree: unEvalAndConfigTree,
|
unevalTree: unEvalAndConfigTree,
|
||||||
widgetTypeConfigMap: undefined,
|
widgetTypeConfigMap: undefined,
|
||||||
|
|
@ -105,6 +110,7 @@ describe("evaluateTreeSaga", () => {
|
||||||
select(getApplicationLastDeployedAt),
|
select(getApplicationLastDeployedAt),
|
||||||
new Date("11 September 2024").toISOString(),
|
new Date("11 September 2024").toISOString(),
|
||||||
],
|
],
|
||||||
|
[select(getCurrentPageDSLVersion), 1],
|
||||||
])
|
])
|
||||||
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
||||||
cacheProps: {
|
cacheProps: {
|
||||||
|
|
@ -113,6 +119,7 @@ describe("evaluateTreeSaga", () => {
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
appMode: false,
|
appMode: false,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
unevalTree: unEvalAndConfigTree,
|
unevalTree: unEvalAndConfigTree,
|
||||||
widgetTypeConfigMap: undefined,
|
widgetTypeConfigMap: undefined,
|
||||||
|
|
@ -160,6 +167,7 @@ describe("evaluateTreeSaga", () => {
|
||||||
select(getApplicationLastDeployedAt),
|
select(getApplicationLastDeployedAt),
|
||||||
new Date("11 September 2024").toISOString(),
|
new Date("11 September 2024").toISOString(),
|
||||||
],
|
],
|
||||||
|
[select(getCurrentPageDSLVersion), 1],
|
||||||
])
|
])
|
||||||
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
.call(evalWorker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, {
|
||||||
cacheProps: {
|
cacheProps: {
|
||||||
|
|
@ -168,6 +176,7 @@ describe("evaluateTreeSaga", () => {
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
appMode: false,
|
appMode: false,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
unevalTree: unEvalAndConfigTree,
|
unevalTree: unEvalAndConfigTree,
|
||||||
widgetTypeConfigMap: undefined,
|
widgetTypeConfigMap: undefined,
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ import { resetWidgetsMetaState, updateMetaState } from "actions/metaActions";
|
||||||
import {
|
import {
|
||||||
getAllActionValidationConfig,
|
getAllActionValidationConfig,
|
||||||
getAllJSActionsData,
|
getAllJSActionsData,
|
||||||
|
getCurrentPageDSLVersion,
|
||||||
} from "ee/selectors/entitiesSelector";
|
} from "ee/selectors/entitiesSelector";
|
||||||
import type { WidgetEntityConfig } from "ee/entities/DataTree/types";
|
import type { WidgetEntityConfig } from "ee/entities/DataTree/types";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -269,6 +270,7 @@ export function* evaluateTreeSaga(
|
||||||
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
|
const appMode: ReturnType<typeof getAppMode> = yield select(getAppMode);
|
||||||
const widgetsMeta: ReturnType<typeof getWidgetsMeta> =
|
const widgetsMeta: ReturnType<typeof getWidgetsMeta> =
|
||||||
yield select(getWidgetsMeta);
|
yield select(getWidgetsMeta);
|
||||||
|
const dslVersion: number | null = yield select(getCurrentPageDSLVersion);
|
||||||
|
|
||||||
const shouldRespondWithLogs = log.getLevel() === log.levels.DEBUG;
|
const shouldRespondWithLogs = log.getLevel() === log.levels.DEBUG;
|
||||||
|
|
||||||
|
|
@ -279,6 +281,7 @@ export function* evaluateTreeSaga(
|
||||||
pageId,
|
pageId,
|
||||||
timestamp: lastDeployedAt,
|
timestamp: lastDeployedAt,
|
||||||
instanceId,
|
instanceId,
|
||||||
|
dslVersion,
|
||||||
},
|
},
|
||||||
unevalTree: unEvalAndConfigTree,
|
unevalTree: unEvalAndConfigTree,
|
||||||
widgetTypeConfigMap,
|
widgetTypeConfigMap,
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,7 @@ export enum EvalErrorTypes {
|
||||||
CLONE_ERROR = "CLONE_ERROR",
|
CLONE_ERROR = "CLONE_ERROR",
|
||||||
SERIALIZATION_ERROR = "SERIALIZATION_ERROR",
|
SERIALIZATION_ERROR = "SERIALIZATION_ERROR",
|
||||||
UPDATE_DATA_TREE_ERROR = "UPDATE_DATA_TREE_ERROR",
|
UPDATE_DATA_TREE_ERROR = "UPDATE_DATA_TREE_ERROR",
|
||||||
|
CACHE_ERROR = "CACHE_ERROR",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EvalError {
|
export interface EvalError {
|
||||||
|
|
|
||||||
|
|
@ -588,6 +588,7 @@ describe("DataTreeEvaluator", () => {
|
||||||
timestamp: "timestamp",
|
timestamp: "timestamp",
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
evaluator.evalAndValidateFirstTree();
|
evaluator.evalAndValidateFirstTree();
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ describe("evaluateAndGenerateResponse", () => {
|
||||||
timestamp: "timestamp",
|
timestamp: "timestamp",
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
evaluator.evalAndValidateFirstTree();
|
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 { APP_MODE } from "entities/App";
|
||||||
import localforage from "localforage";
|
import localforage from "localforage";
|
||||||
import loglevel from "loglevel";
|
import loglevel from "loglevel";
|
||||||
|
|
@ -71,12 +75,13 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
describe("generateCacheKey", () => {
|
describe("generateCacheKey", () => {
|
||||||
test("should generate the correct cache key", () => {
|
test("should generate the correct cache key", () => {
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -102,14 +107,15 @@ describe("AppComputationCache", () => {
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
||||||
const result = appComputationCache.isComputationCached({
|
const result = appComputationCache.shouldComputationBeCached(
|
||||||
cacheName,
|
cacheName,
|
||||||
cacheProps,
|
cacheProps,
|
||||||
});
|
);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -121,14 +127,15 @@ describe("AppComputationCache", () => {
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
||||||
const result = appComputationCache.isComputationCached({
|
const result = appComputationCache.shouldComputationBeCached(
|
||||||
cacheName,
|
cacheName,
|
||||||
cacheProps,
|
cacheProps,
|
||||||
});
|
);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -139,14 +146,15 @@ describe("AppComputationCache", () => {
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
||||||
const result = appComputationCache.isComputationCached({
|
const result = appComputationCache.shouldComputationBeCached(
|
||||||
cacheName,
|
cacheName,
|
||||||
cacheProps,
|
cacheProps,
|
||||||
});
|
);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -158,14 +166,35 @@ describe("AppComputationCache", () => {
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
||||||
const result = appComputationCache.isComputationCached({
|
const result = appComputationCache.shouldComputationBeCached(
|
||||||
cacheName,
|
cacheName,
|
||||||
cacheProps,
|
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);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -173,12 +202,13 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
describe("getCachedComputationResult", () => {
|
describe("getCachedComputationResult", () => {
|
||||||
test("should call getItemMock and return null if cache miss", async () => {
|
test("should call getItemMock and return null if cache miss", async () => {
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -205,12 +235,13 @@ describe("AppComputationCache", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should call deleteInvalidCacheEntries on cache miss after 10 seconds", async () => {
|
test("should call deleteInvalidCacheEntries on cache miss after 10 seconds", async () => {
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -240,13 +271,14 @@ describe("AppComputationCache", () => {
|
||||||
expect(keysMock).toHaveBeenCalledTimes(1);
|
expect(keysMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should call getItemMock and return cached value if cache hit", async () => {
|
test("should call deleteInvalidCacheEntries on dsl version mismatch after 10 seconds", async () => {
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -256,7 +288,43 @@ describe("AppComputationCache", () => {
|
||||||
cacheProps,
|
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({
|
const result = await appComputationCache.getCachedComputationResult({
|
||||||
cacheName,
|
cacheName,
|
||||||
|
|
@ -270,12 +338,13 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
describe("cacheComputationResult", () => {
|
describe("cacheComputationResult", () => {
|
||||||
test("should store computation result and call trackCacheUsage when shouldCache is true", async () => {
|
test("should store computation result and call trackCacheUsage when shouldCache is true", async () => {
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -300,6 +369,7 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
expect(setItemMock).toHaveBeenCalledWith(cacheKey, {
|
expect(setItemMock).toHaveBeenCalledWith(cacheKey, {
|
||||||
value: computationResult,
|
value: computationResult,
|
||||||
|
dslVersion: 1,
|
||||||
});
|
});
|
||||||
expect(trackCacheUsageSpy).toHaveBeenCalledWith(cacheKey);
|
expect(trackCacheUsageSpy).toHaveBeenCalledWith(cacheKey);
|
||||||
|
|
||||||
|
|
@ -313,6 +383,30 @@ describe("AppComputationCache", () => {
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
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;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -331,12 +425,13 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
describe("fetchOrCompute", () => {
|
describe("fetchOrCompute", () => {
|
||||||
test("should return cached result if available", async () => {
|
test("should return cached result if available", async () => {
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -346,7 +441,7 @@ describe("AppComputationCache", () => {
|
||||||
cacheProps,
|
cacheProps,
|
||||||
});
|
});
|
||||||
|
|
||||||
getItemMock.mockResolvedValue({ value: "cachedValue" });
|
getItemMock.mockResolvedValue({ value: "cachedValue", dslVersion: 1 });
|
||||||
|
|
||||||
const computeFn = jest.fn(() => "computedValue");
|
const computeFn = jest.fn(() => "computedValue");
|
||||||
|
|
||||||
|
|
@ -364,12 +459,13 @@ describe("AppComputationCache", () => {
|
||||||
test("should compute, cache, and return result if not in cache", async () => {
|
test("should compute, cache, and return result if not in cache", async () => {
|
||||||
getItemMock.mockResolvedValue(null);
|
getItemMock.mockResolvedValue(null);
|
||||||
|
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -413,12 +509,13 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
loglevel.setLevel("SILENT");
|
loglevel.setLevel("SILENT");
|
||||||
|
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: IValidatedCacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
pageId: "pageId",
|
||||||
|
dslVersion: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheName = EComputationCacheName.ALL_KEYS;
|
const cacheName = EComputationCacheName.ALL_KEYS;
|
||||||
|
|
@ -427,39 +524,54 @@ describe("AppComputationCache", () => {
|
||||||
|
|
||||||
const computeFn = jest.fn(() => computationResult);
|
const computeFn = jest.fn(() => computationResult);
|
||||||
|
|
||||||
const cacheComputationResultSpy = jest.spyOn(
|
try {
|
||||||
appComputationCache,
|
await appComputationCache.fetchOrCompute({
|
||||||
"cacheComputationResult",
|
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);
|
loglevel.setLevel(defaultLogLevel);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("deleteInvalidCacheEntries", () => {
|
test("should not cache result when dsl version is invalid", async () => {
|
||||||
test("should delete old cache entries", async () => {
|
|
||||||
const cacheProps: ICacheProps = {
|
const cacheProps: ICacheProps = {
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
timestamp: new Date("11 September 2024").toISOString(),
|
timestamp: new Date("11 September 2024").toISOString(),
|
||||||
appId: "appId",
|
appId: "appId",
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
pageId: "pageId",
|
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();
|
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 localforage from "localforage";
|
||||||
import isNull from "lodash/isNull";
|
import isNull from "lodash/isNull";
|
||||||
import loglevel from "loglevel";
|
import loglevel from "loglevel";
|
||||||
import { EComputationCacheName, type ICacheProps } from "./types";
|
import {
|
||||||
|
EComputationCacheName,
|
||||||
|
type IValidatedCacheProps,
|
||||||
|
type ICacheProps,
|
||||||
|
} from "./types";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
import { isFinite, isNumber, isString } from "lodash";
|
||||||
|
|
||||||
interface ICachedData<T> {
|
interface ICachedData<T> {
|
||||||
value: T;
|
value: T;
|
||||||
|
dslVersion?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICacheLog {
|
interface ICacheLog {
|
||||||
|
|
@ -52,6 +58,25 @@ export class AppComputationCache {
|
||||||
return AppComputationCache.instance;
|
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(
|
debouncedDeleteInvalidCacheEntries = debounce(
|
||||||
this.deleteInvalidCacheEntries,
|
this.deleteInvalidCacheEntries,
|
||||||
5000,
|
5000,
|
||||||
|
|
@ -61,16 +86,17 @@ export class AppComputationCache {
|
||||||
* Check if the computation result should be cached based on the app mode configuration
|
* 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
|
* @returns - A boolean indicating whether the cache should be enabled for the given app mode
|
||||||
*/
|
*/
|
||||||
isComputationCached({
|
shouldComputationBeCached(
|
||||||
cacheName,
|
cacheName: EComputationCacheName,
|
||||||
cacheProps,
|
cacheProps: ICacheProps,
|
||||||
}: {
|
): cacheProps is IValidatedCacheProps {
|
||||||
cacheName: EComputationCacheName;
|
const { appMode, dslVersion, timestamp } = cacheProps;
|
||||||
cacheProps: ICacheProps;
|
|
||||||
}) {
|
|
||||||
const { appMode, timestamp } = cacheProps;
|
|
||||||
|
|
||||||
if (!appMode || !timestamp) {
|
if (
|
||||||
|
!this.isAppModeValid(appMode) ||
|
||||||
|
!this.isTimestampValid(timestamp) ||
|
||||||
|
!this.isDSLVersionValid(dslVersion)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,6 +107,7 @@ export class AppComputationCache {
|
||||||
* Checks if the value should be cached based on the app mode configuration and
|
* 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
|
* 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
|
* @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>({
|
async cacheComputationResult<T>({
|
||||||
cacheName,
|
cacheName,
|
||||||
|
|
@ -91,31 +118,31 @@ export class AppComputationCache {
|
||||||
cacheName: EComputationCacheName;
|
cacheName: EComputationCacheName;
|
||||||
computationResult: T;
|
computationResult: T;
|
||||||
}) {
|
}) {
|
||||||
const shouldCache = this.isComputationCached({
|
|
||||||
cacheName,
|
|
||||||
cacheProps,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shouldCache) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheKey = this.generateCacheKey({ cacheProps, cacheName });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const isCacheable = this.shouldComputationBeCached(cacheName, cacheProps);
|
||||||
|
|
||||||
|
if (!isCacheable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheKey = this.generateCacheKey({ cacheProps, cacheName });
|
||||||
|
|
||||||
await this.store.setItem<ICachedData<T>>(cacheKey, {
|
await this.store.setItem<ICachedData<T>>(cacheKey, {
|
||||||
value: computationResult,
|
value: computationResult,
|
||||||
|
dslVersion: cacheProps.dslVersion,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.trackCacheUsage(cacheKey);
|
await this.trackCacheUsage(cacheKey);
|
||||||
} catch (error) {
|
} 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
|
* 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
|
* @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>({
|
async getCachedComputationResult<T>({
|
||||||
cacheName,
|
cacheName,
|
||||||
|
|
@ -124,24 +151,21 @@ export class AppComputationCache {
|
||||||
cacheProps: ICacheProps;
|
cacheProps: ICacheProps;
|
||||||
cacheName: EComputationCacheName;
|
cacheName: EComputationCacheName;
|
||||||
}): Promise<T | null> {
|
}): Promise<T | null> {
|
||||||
const shouldCache = this.isComputationCached({
|
|
||||||
cacheName,
|
|
||||||
cacheProps,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shouldCache) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheKey = this.generateCacheKey({
|
|
||||||
cacheProps,
|
|
||||||
cacheName,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
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);
|
const cached = await this.store.getItem<ICachedData<T>>(cacheKey);
|
||||||
|
|
||||||
if (isNull(cached)) {
|
if (!this.isCacheValid(cached, cacheProps)) {
|
||||||
// Cache miss
|
// Cache miss
|
||||||
// Delete invalid cache entries when thread is idle
|
// Delete invalid cache entries when thread is idle
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|
@ -155,12 +179,30 @@ export class AppComputationCache {
|
||||||
|
|
||||||
return cached.value;
|
return cached.value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loglevel.error("Error getting cache result:", error);
|
loglevel.error(error);
|
||||||
|
throw error;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Generates a cache key from the index parts
|
||||||
* @returns - The generated cache key
|
* @returns - The generated cache key
|
||||||
|
|
@ -169,7 +211,7 @@ export class AppComputationCache {
|
||||||
cacheName,
|
cacheName,
|
||||||
cacheProps,
|
cacheProps,
|
||||||
}: {
|
}: {
|
||||||
cacheProps: ICacheProps;
|
cacheProps: IValidatedCacheProps;
|
||||||
cacheName: EComputationCacheName;
|
cacheName: EComputationCacheName;
|
||||||
}) {
|
}) {
|
||||||
const { appId, appMode, instanceId, pageId, timestamp } = cacheProps;
|
const { appId, appMode, instanceId, pageId, timestamp } = cacheProps;
|
||||||
|
|
@ -201,16 +243,13 @@ export class AppComputationCache {
|
||||||
computeFn: () => Promise<T> | T;
|
computeFn: () => Promise<T> | T;
|
||||||
cacheName: EComputationCacheName;
|
cacheName: EComputationCacheName;
|
||||||
}) {
|
}) {
|
||||||
const shouldCache = this.isComputationCached({
|
|
||||||
cacheName,
|
|
||||||
cacheProps,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shouldCache) {
|
|
||||||
return computeFn();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const isCacheable = this.shouldComputationBeCached(cacheName, cacheProps);
|
||||||
|
|
||||||
|
if (!isCacheable) {
|
||||||
|
return computeFn();
|
||||||
|
}
|
||||||
|
|
||||||
const cachedResult = await this.getCachedComputationResult<T>({
|
const cachedResult = await this.getCachedComputationResult<T>({
|
||||||
cacheProps,
|
cacheProps,
|
||||||
cacheName,
|
cacheName,
|
||||||
|
|
@ -230,10 +269,8 @@ export class AppComputationCache {
|
||||||
|
|
||||||
return computationResult;
|
return computationResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loglevel.error("Error getting cache result:", error);
|
loglevel.error(error);
|
||||||
const fallbackResult = await computeFn();
|
throw error;
|
||||||
|
|
||||||
return fallbackResult;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,6 +296,7 @@ export class AppComputationCache {
|
||||||
/**
|
/**
|
||||||
* Delete invalid cache entries
|
* Delete invalid cache entries
|
||||||
* @returns - A promise that resolves when the invalid cache entries are deleted
|
* @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) {
|
async deleteInvalidCacheEntries(cacheProps: ICacheProps) {
|
||||||
|
|
@ -271,6 +309,10 @@ export class AppComputationCache {
|
||||||
const keyParts = key.split(AppComputationCache.CACHE_KEY_DELIMITER);
|
const keyParts = key.split(AppComputationCache.CACHE_KEY_DELIMITER);
|
||||||
const cacheKeyTimestamp = parseInt(keyParts[4], 10);
|
const cacheKeyTimestamp = parseInt(keyParts[4], 10);
|
||||||
|
|
||||||
|
if (!cacheProps.timestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
keyParts[0] === cacheProps.instanceId &&
|
keyParts[0] === cacheProps.instanceId &&
|
||||||
keyParts[1] === cacheProps.appId &&
|
keyParts[1] === cacheProps.appId &&
|
||||||
|
|
@ -295,6 +337,9 @@ export class AppComputationCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the singleton instance
|
||||||
|
*/
|
||||||
static resetInstance() {
|
static resetInstance() {
|
||||||
AppComputationCache.instance = null;
|
AppComputationCache.instance = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,16 @@ export interface ICacheProps {
|
||||||
appId: string;
|
appId: string;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
appMode?: APP_MODE;
|
appMode?: APP_MODE;
|
||||||
|
timestamp?: string;
|
||||||
|
instanceId: string;
|
||||||
|
dslVersion: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IValidatedCacheProps {
|
||||||
|
appId: string;
|
||||||
|
pageId: string;
|
||||||
|
appMode: APP_MODE;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
|
dslVersion: number;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,7 @@ describe("DataTreeEvaluator", () => {
|
||||||
timestamp: "timestamp",
|
timestamp: "timestamp",
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
dataTreeEvaluator.evalAndValidateFirstTree();
|
dataTreeEvaluator.evalAndValidateFirstTree();
|
||||||
|
|
@ -391,6 +392,7 @@ describe("DataTreeEvaluator", () => {
|
||||||
timestamp: "timestamp",
|
timestamp: "timestamp",
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
dataTreeEvaluator.evalAndValidateFirstTree();
|
dataTreeEvaluator.evalAndValidateFirstTree();
|
||||||
|
|
@ -454,6 +456,7 @@ describe("DataTreeEvaluator", () => {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
appMode: APP_MODE.PUBLISHED,
|
appMode: APP_MODE.PUBLISHED,
|
||||||
instanceId: "instanceId",
|
instanceId: "instanceId",
|
||||||
|
dslVersion: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
dataTreeEvaluator.evalAndValidateFirstTree();
|
dataTreeEvaluator.evalAndValidateFirstTree();
|
||||||
|
|
|
||||||
|
|
@ -302,11 +302,21 @@ export default class DataTreeEvaluator {
|
||||||
);
|
);
|
||||||
const allKeysGenerationStartTime = performance.now();
|
const allKeysGenerationStartTime = performance.now();
|
||||||
|
|
||||||
this.allKeys = await appComputationCache.fetchOrCompute({
|
try {
|
||||||
cacheProps,
|
this.allKeys = await appComputationCache.fetchOrCompute({
|
||||||
cacheName: EComputationCacheName.ALL_KEYS,
|
cacheProps,
|
||||||
computeFn: () => getAllPaths(unEvalTreeWithStrigifiedJSFunctions),
|
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();
|
const allKeysGenerationEndTime = performance.now();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ import type {
|
||||||
DataTreeEntityObject,
|
DataTreeEntityObject,
|
||||||
} from "ee/entities/DataTree/types";
|
} from "ee/entities/DataTree/types";
|
||||||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeTypes";
|
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 { convertArrayToObject, extractInfoFromBindings } from "./utils";
|
||||||
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
||||||
import { get, isEmpty, set } from "lodash";
|
import { get, isEmpty, set } from "lodash";
|
||||||
|
|
@ -59,13 +63,23 @@ export async function createDependencyMap(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const dependencyMapCache =
|
let dependencyMapCache: Record<string, string[]> | null = null;
|
||||||
await appComputationCache.getCachedComputationResult<
|
|
||||||
|
try {
|
||||||
|
dependencyMapCache = await appComputationCache.getCachedComputationResult<
|
||||||
Record<string, string[]>
|
Record<string, string[]>
|
||||||
>({
|
>({
|
||||||
cacheProps,
|
cacheProps,
|
||||||
cacheName: EComputationCacheName.DEPENDENCY_MAP,
|
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) {
|
if (dependencyMapCache) {
|
||||||
profileFn("createDependencyMap.addDependency", {}, webworkerSpans, () => {
|
profileFn("createDependencyMap.addDependency", {}, webworkerSpans, () => {
|
||||||
|
|
@ -104,11 +118,19 @@ export async function createDependencyMap(
|
||||||
DependencyMapUtils.makeParentsDependOnChildren(dependencyMap);
|
DependencyMapUtils.makeParentsDependOnChildren(dependencyMap);
|
||||||
|
|
||||||
if (shouldCache) {
|
if (shouldCache) {
|
||||||
await appComputationCache.cacheComputationResult({
|
try {
|
||||||
cacheProps,
|
await appComputationCache.cacheComputationResult({
|
||||||
cacheName: EComputationCacheName.DEPENDENCY_MAP,
|
cacheProps,
|
||||||
computationResult: dependencyMap.dependencies,
|
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