Skip to content

Commit f96f581

Browse files
authored
Merge pull request #41108 from appsmithorg/release
14/07 Daily Promotion
2 parents 81edda8 + e5b2a26 commit f96f581

File tree

36 files changed

+1271
-522
lines changed

36 files changed

+1271
-522
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import WidgetFactory from "../index";
2+
import { clearAllWidgetFactoryCache } from "../decorators";
3+
import type BaseWidget from "widgets/BaseWidget";
4+
5+
describe("WidgetFactory Cache Tests", () => {
6+
beforeAll(() => {
7+
// Clear the widget factory state before each test
8+
WidgetFactory.widgetsMap.clear();
9+
clearAllWidgetFactoryCache();
10+
});
11+
12+
afterAll(() => {
13+
// Clean up after each test
14+
WidgetFactory.widgetsMap.clear();
15+
clearAllWidgetFactoryCache();
16+
});
17+
18+
it("should return stale data after widget registration until cache is cleared", () => {
19+
// Initial state - no widgets
20+
let widgetTypes = WidgetFactory.getWidgetTypes();
21+
22+
expect(widgetTypes).toEqual([]);
23+
24+
// Add a widget to the map
25+
WidgetFactory.widgetsMap.set("TEST_WIDGET", {} as typeof BaseWidget);
26+
27+
// getWidgetTypes should still return empty array (stale cache)
28+
widgetTypes = WidgetFactory.getWidgetTypes();
29+
expect(widgetTypes).toEqual([]);
30+
31+
// Clear the cache
32+
clearAllWidgetFactoryCache();
33+
34+
// Now getWidgetTypes should return the updated widget type
35+
widgetTypes = WidgetFactory.getWidgetTypes();
36+
expect(widgetTypes).toContain("TEST_WIDGET");
37+
expect(widgetTypes).toHaveLength(1);
38+
});
39+
});

app/client/src/WidgetProvider/factory/decorators.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,47 @@
11
import memo from "micro-memoize";
22

3+
type AnyFn = (...args: unknown[]) => unknown;
4+
5+
interface MemoizedWithClear {
6+
(...args: unknown[]): unknown;
7+
clearCache: () => void;
8+
}
9+
10+
// Track all memoized functions
11+
const memoizedFunctions = new Set<MemoizedWithClear>();
12+
13+
// Function to clear memoized cache
14+
function clearMemoizedCache(fn: {
15+
cache: { keys: unknown[]; values: unknown[] };
16+
}) {
17+
fn.cache.keys.length = fn.cache.values.length = 0;
18+
}
19+
20+
// Create a memoize wrapper that adds cache clearing capability
21+
function memoizeWithClear(fn: AnyFn): MemoizedWithClear {
22+
const memoized = memo(fn, {
23+
maxSize: 100,
24+
}) as unknown as MemoizedWithClear;
25+
26+
// Add clearCache method to the memoized function
27+
memoized.clearCache = () => {
28+
clearMemoizedCache(
29+
memoized as unknown as { cache: { keys: unknown[]; values: unknown[] } },
30+
);
31+
};
32+
33+
// Add to tracked functions
34+
memoizedFunctions.add(memoized);
35+
36+
return memoized;
37+
}
38+
339
export function memoize(
440
target: unknown,
541
methodName: unknown,
642
descriptor: PropertyDescriptor,
743
) {
8-
descriptor.value = memo(descriptor.value, {
9-
maxSize: 100,
10-
});
44+
descriptor.value = memoizeWithClear(descriptor.value);
1145
}
1246

1347
export function freeze(
@@ -25,3 +59,8 @@ export function freeze(
2559
return Object.freeze(result);
2660
};
2761
}
62+
63+
// Function to clear all memoized caches
64+
export function clearAllWidgetFactoryCache() {
65+
memoizedFunctions.forEach((fn) => fn.clearCache());
66+
}

app/client/src/WidgetProvider/factory/registrationHelper.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { CanvasWidgetStructure } from "WidgetProvider/types";
33
import type BaseWidget from "widgets/BaseWidget";
44
import WidgetFactory from ".";
55
import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
6+
import { incrementWidgetConfigsVersion } from "./widgetConfigVersion";
67

78
/*
89
* Function to create builder for the widgets and register them in widget factory
@@ -11,28 +12,31 @@ import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
1112
* extracted this into a seperate file to break the circular reference.
1213
*
1314
*/
15+
1416
export const registerWidgets = (widgets: (typeof BaseWidget)[]) => {
15-
const widgetAndBuilders = widgets.map((widget) => {
16-
const { eagerRender = false, needsMeta = false } = widget.getConfig();
17+
widgets.forEach((widget) => {
18+
registerWidget(widget);
19+
});
20+
// Increment version to trigger selectors that depend on widget configs
21+
incrementWidgetConfigsVersion();
22+
};
1723

18-
// TODO: Fix this the next time the file is edited
19-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
20-
const ProfiledWidget: any = withBaseWidgetHOC(
21-
widget,
22-
needsMeta,
23-
eagerRender,
24-
);
24+
export const registerWidget = (widget: typeof BaseWidget) => {
25+
const { eagerRender = false, needsMeta = false } = widget.getConfig();
2526

26-
return [
27-
widget,
28-
(widgetProps: CanvasWidgetStructure) => (
29-
<ProfiledWidget {...widgetProps} key={widgetProps.widgetId} />
30-
),
31-
] as [
32-
typeof BaseWidget,
33-
(widgetProps: CanvasWidgetStructure) => React.ReactNode,
34-
];
35-
});
27+
// TODO: Fix this the next time the file is edited
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
const ProfiledWidget: any = withBaseWidgetHOC(widget, needsMeta, eagerRender);
30+
31+
const widgetAndBuilder: [
32+
typeof BaseWidget,
33+
(widgetProps: CanvasWidgetStructure) => React.ReactNode,
34+
] = [
35+
widget,
36+
(widgetProps: CanvasWidgetStructure) => (
37+
<ProfiledWidget {...widgetProps} key={widgetProps.widgetId} />
38+
),
39+
];
3640

37-
WidgetFactory.initialize(widgetAndBuilders);
41+
WidgetFactory.initialize([widgetAndBuilder]);
3842
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Global version counter that increments when widgets are registered
2+
let widgetConfigsVersion = 0;
3+
4+
// Export getter for selectors to depend on
5+
export const getWidgetConfigsVersion = () => widgetConfigsVersion;
6+
7+
// Export incrementer for registration helper to use
8+
export const incrementWidgetConfigsVersion = () => {
9+
widgetConfigsVersion++;
10+
};

app/client/src/actions/JSLibraryActions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export function fetchJSLibraries(
1212
};
1313
}
1414

15+
export function deferLoadingJSLibraries(
16+
applicationId: string,
17+
customJSLibraries?: ApiResponse,
18+
) {
19+
return {
20+
type: ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES,
21+
payload: { applicationId, customJSLibraries },
22+
};
23+
}
24+
1525
export function installLibraryInit(payload: Partial<JSLibrary>) {
1626
return {
1727
type: ReduxActionTypes.INSTALL_LIBRARY_INIT,

app/client/src/actions/evaluationActions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
ConditionalOutput,
1414
DynamicValues,
1515
} from "reducers/evaluationReducers/formEvaluationReducer";
16+
import type { ReduxActionWithoutPayload } from "./ReduxActionTypes";
1617

1718
export const shouldTriggerEvaluation = (action: ReduxAction<unknown>) => {
1819
return (
@@ -79,6 +80,12 @@ export const setDependencyMap = (
7980
};
8081
};
8182

83+
export const setIsFirstPageLoad = (): ReduxActionWithoutPayload => {
84+
return {
85+
type: ReduxActionTypes.IS_FIRST_PAGE_LOAD,
86+
};
87+
};
88+
8289
// These actions require the entire tree to be re-evaluated
8390
const FORCE_EVAL_ACTIONS = {
8491
[ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true,

app/client/src/ce/constants/ReduxActionConstants.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const JSLibraryActionTypes = {
1414
TOGGLE_INSTALLER: "TOGGLE_INSTALLER",
1515
FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT",
1616
FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS",
17+
DEFER_LOADING_JS_LIBRARIES: "DEFER_LOADING_JS_LIBRARIES",
1718
CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS",
1819
INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT",
1920
INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START",
@@ -1288,7 +1289,15 @@ const PlatformActionErrorTypes = {
12881289
API_ERROR: "API_ERROR",
12891290
};
12901291

1292+
const DeferRenderingAppViewerActionTypes = {
1293+
HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE:
1294+
"HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE",
1295+
RENDER_PAGE: "RENDER_PAGE",
1296+
IS_FIRST_PAGE_LOAD: "IS_FIRST_PAGE_LOAD",
1297+
};
1298+
12911299
export const ReduxActionTypes = {
1300+
...DeferRenderingAppViewerActionTypes,
12921301
...ActionActionTypes,
12931302
...AdminSettingsActionTypes,
12941303
...AnalyticsActionTypes,

app/client/src/ce/reducers/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConve
7070
import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer";
7171
import type { IDEState } from "reducers/uiReducers/ideReducer";
7272
import type { PluginActionEditorState } from "PluginActionEditor";
73+
import type { FirstEvaluationState } from "reducers/evaluationReducers/firstEvaluationReducer";
7374

7475
/* Reducers which are integrated into the core system when registering a pluggable module
7576
or done so by a module that is designed to be eventually pluggable */
@@ -171,6 +172,7 @@ export interface AppState {
171172
loadingEntities: LoadingEntitiesState;
172173
formEvaluation: FormEvaluationState;
173174
triggers: TriggerValuesEvaluationState;
175+
firstEvaluation: FirstEvaluationState;
174176
};
175177
linting: {
176178
errors: LintErrorsStore;

app/client/src/ce/sagas/PageSagas.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ import { apiFailureResponseInterceptor } from "api/interceptors/response";
157157
import type { AxiosError } from "axios";
158158
import { handleFetchApplicationError } from "./ApplicationSagas";
159159
import { getCurrentUser } from "actions/authActions";
160+
import { getIsFirstPageLoad } from "selectors/evaluationSelectors";
160161

161162
export interface HandleWidgetNameUpdatePayload {
162163
newName: string;
@@ -370,8 +371,14 @@ export function* postFetchedPublishedPage(
370371
response.data.userPermissions,
371372
),
372373
);
373-
// Clear any existing caches
374-
yield call(clearEvalCache);
374+
const isFirstLoad: boolean = yield select(getIsFirstPageLoad);
375+
376+
// Only the first page load we defer the clearing of caches
377+
if (!isFirstLoad) {
378+
// Clear any existing caches
379+
yield call(clearEvalCache);
380+
}
381+
375382
// Set url params
376383
yield call(setDataUrl);
377384

app/client/src/ce/selectors/moduleInstanceSelectors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ export const getModuleInstanceJSCollectionById = (
1414
): JSCollection | undefined => {
1515
return undefined;
1616
};
17+
export const getAllUniqueWidgetTypesInUiModules = (state: DefaultRootState) => {
18+
return [];
19+
};

0 commit comments

Comments
 (0)