Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import WidgetFactory from "../index";
import { clearAllWidgetFactoryCache } from "../decorators";
import type BaseWidget from "widgets/BaseWidget";

describe("WidgetFactory Cache Tests", () => {
beforeAll(() => {
// Clear the widget factory state before each test
WidgetFactory.widgetsMap.clear();
clearAllWidgetFactoryCache();
});

afterAll(() => {
// Clean up after each test
WidgetFactory.widgetsMap.clear();
clearAllWidgetFactoryCache();
});

it("should return stale data after widget registration until cache is cleared", () => {
// Initial state - no widgets
let widgetTypes = WidgetFactory.getWidgetTypes();

expect(widgetTypes).toEqual([]);

// Add a widget to the map
WidgetFactory.widgetsMap.set("TEST_WIDGET", {} as typeof BaseWidget);

// getWidgetTypes should still return empty array (stale cache)
widgetTypes = WidgetFactory.getWidgetTypes();
expect(widgetTypes).toEqual([]);

// Clear the cache
clearAllWidgetFactoryCache();

// Now getWidgetTypes should return the updated widget type
widgetTypes = WidgetFactory.getWidgetTypes();
expect(widgetTypes).toContain("TEST_WIDGET");
expect(widgetTypes).toHaveLength(1);
});
});
45 changes: 42 additions & 3 deletions app/client/src/WidgetProvider/factory/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
import memo from "micro-memoize";

type AnyFn = (...args: unknown[]) => unknown;

interface MemoizedWithClear {
(...args: unknown[]): unknown;
clearCache: () => void;
}

// Track all memoized functions
const memoizedFunctions = new Set<MemoizedWithClear>();

// Function to clear memoized cache
function clearMemoizedCache(fn: {
cache: { keys: unknown[]; values: unknown[] };
}) {
fn.cache.keys.length = fn.cache.values.length = 0;
}

// Create a memoize wrapper that adds cache clearing capability
function memoizeWithClear(fn: AnyFn): MemoizedWithClear {
const memoized = memo(fn, {
maxSize: 100,
}) as unknown as MemoizedWithClear;

// Add clearCache method to the memoized function
memoized.clearCache = () => {
clearMemoizedCache(
memoized as unknown as { cache: { keys: unknown[]; values: unknown[] } },
);
};

// Add to tracked functions
memoizedFunctions.add(memoized);

return memoized;
}

export function memoize(
target: unknown,
methodName: unknown,
descriptor: PropertyDescriptor,
) {
descriptor.value = memo(descriptor.value, {
maxSize: 100,
});
descriptor.value = memoizeWithClear(descriptor.value);
}

export function freeze(
Expand All @@ -25,3 +59,8 @@ export function freeze(
return Object.freeze(result);
};
}

// Function to clear all memoized caches
export function clearAllWidgetFactoryCache() {
memoizedFunctions.forEach((fn) => fn.clearCache());
}
44 changes: 24 additions & 20 deletions app/client/src/WidgetProvider/factory/registrationHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { CanvasWidgetStructure } from "WidgetProvider/types";
import type BaseWidget from "widgets/BaseWidget";
import WidgetFactory from ".";
import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
import { incrementWidgetConfigsVersion } from "./widgetConfigVersion";

/*
* Function to create builder for the widgets and register them in widget factory
Expand All @@ -11,28 +12,31 @@ import { withBaseWidgetHOC } from "widgets/BaseWidgetHOC/withBaseWidgetHOC";
* extracted this into a seperate file to break the circular reference.
*
*/

export const registerWidgets = (widgets: (typeof BaseWidget)[]) => {
const widgetAndBuilders = widgets.map((widget) => {
const { eagerRender = false, needsMeta = false } = widget.getConfig();
widgets.forEach((widget) => {
registerWidget(widget);
});
// Increment version to trigger selectors that depend on widget configs
incrementWidgetConfigsVersion();
};

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

return [
widget,
(widgetProps: CanvasWidgetStructure) => (
<ProfiledWidget {...widgetProps} key={widgetProps.widgetId} />
),
] as [
typeof BaseWidget,
(widgetProps: CanvasWidgetStructure) => React.ReactNode,
];
});
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ProfiledWidget: any = withBaseWidgetHOC(widget, needsMeta, eagerRender);

const widgetAndBuilder: [
typeof BaseWidget,
(widgetProps: CanvasWidgetStructure) => React.ReactNode,
] = [
widget,
(widgetProps: CanvasWidgetStructure) => (
<ProfiledWidget {...widgetProps} key={widgetProps.widgetId} />
),
];

WidgetFactory.initialize(widgetAndBuilders);
WidgetFactory.initialize([widgetAndBuilder]);
};
10 changes: 10 additions & 0 deletions app/client/src/WidgetProvider/factory/widgetConfigVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Global version counter that increments when widgets are registered
let widgetConfigsVersion = 0;

// Export getter for selectors to depend on
export const getWidgetConfigsVersion = () => widgetConfigsVersion;

// Export incrementer for registration helper to use
export const incrementWidgetConfigsVersion = () => {
widgetConfigsVersion++;
};
10 changes: 10 additions & 0 deletions app/client/src/actions/JSLibraryActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ export function fetchJSLibraries(
};
}

export function deferLoadingJSLibraries(
applicationId: string,
customJSLibraries?: ApiResponse,
) {
return {
type: ReduxActionTypes.DEFER_LOADING_JS_LIBRARIES,
payload: { applicationId, customJSLibraries },
};
}

export function installLibraryInit(payload: Partial<JSLibrary>) {
return {
type: ReduxActionTypes.INSTALL_LIBRARY_INIT,
Expand Down
7 changes: 7 additions & 0 deletions app/client/src/actions/evaluationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ConditionalOutput,
DynamicValues,
} from "reducers/evaluationReducers/formEvaluationReducer";
import type { ReduxActionWithoutPayload } from "./ReduxActionTypes";

export const shouldTriggerEvaluation = (action: ReduxAction<unknown>) => {
return (
Expand Down Expand Up @@ -79,6 +80,12 @@ export const setDependencyMap = (
};
};

export const setIsFirstPageLoad = (): ReduxActionWithoutPayload => {
return {
type: ReduxActionTypes.IS_FIRST_PAGE_LOAD,
};
};

// These actions require the entire tree to be re-evaluated
const FORCE_EVAL_ACTIONS = {
[ReduxActionTypes.INSTALL_LIBRARY_SUCCESS]: true,
Expand Down
9 changes: 9 additions & 0 deletions app/client/src/ce/constants/ReduxActionConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const JSLibraryActionTypes = {
TOGGLE_INSTALLER: "TOGGLE_INSTALLER",
FETCH_JS_LIBRARIES_INIT: "FETCH_JS_LIBRARIES_INIT",
FETCH_JS_LIBRARIES_SUCCESS: "FETCH_JS_LIBRARIES_SUCCESS",
DEFER_LOADING_JS_LIBRARIES: "DEFER_LOADING_JS_LIBRARIES",
CLEAR_PROCESSED_INSTALLS: "CLEAR_PROCESSED_INSTALLS",
INSTALL_LIBRARY_INIT: "INSTALL_LIBRARY_INIT",
INSTALL_LIBRARY_START: "INSTALL_LIBRARY_START",
Expand Down Expand Up @@ -1288,7 +1289,15 @@ const PlatformActionErrorTypes = {
API_ERROR: "API_ERROR",
};

const DeferRenderingAppViewerActionTypes = {
HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE:
"HAS_DISPATCHED_FIRST_EVALUATION_MESSAGE",
RENDER_PAGE: "RENDER_PAGE",
IS_FIRST_PAGE_LOAD: "IS_FIRST_PAGE_LOAD",
};

export const ReduxActionTypes = {
...DeferRenderingAppViewerActionTypes,
...ActionActionTypes,
...AdminSettingsActionTypes,
...AnalyticsActionTypes,
Expand Down
2 changes: 2 additions & 0 deletions app/client/src/ce/reducers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConve
import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer";
import type { IDEState } from "reducers/uiReducers/ideReducer";
import type { PluginActionEditorState } from "PluginActionEditor";
import type { FirstEvaluationState } from "reducers/evaluationReducers/firstEvaluationReducer";

/* Reducers which are integrated into the core system when registering a pluggable module
or done so by a module that is designed to be eventually pluggable */
Expand Down Expand Up @@ -171,6 +172,7 @@ export interface AppState {
loadingEntities: LoadingEntitiesState;
formEvaluation: FormEvaluationState;
triggers: TriggerValuesEvaluationState;
firstEvaluation: FirstEvaluationState;
};
linting: {
errors: LintErrorsStore;
Expand Down
11 changes: 9 additions & 2 deletions app/client/src/ce/sagas/PageSagas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ import { apiFailureResponseInterceptor } from "api/interceptors/response";
import type { AxiosError } from "axios";
import { handleFetchApplicationError } from "./ApplicationSagas";
import { getCurrentUser } from "actions/authActions";
import { getIsFirstPageLoad } from "selectors/evaluationSelectors";

export interface HandleWidgetNameUpdatePayload {
newName: string;
Expand Down Expand Up @@ -370,8 +371,14 @@ export function* postFetchedPublishedPage(
response.data.userPermissions,
),
);
// Clear any existing caches
yield call(clearEvalCache);
const isFirstLoad: boolean = yield select(getIsFirstPageLoad);

// Only the first page load we defer the clearing of caches
if (!isFirstLoad) {
// Clear any existing caches
yield call(clearEvalCache);
}

// Set url params
yield call(setDataUrl);

Expand Down
3 changes: 3 additions & 0 deletions app/client/src/ce/selectors/moduleInstanceSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ export const getModuleInstanceJSCollectionById = (
): JSCollection | undefined => {
return undefined;
};
export const getAllUniqueWidgetTypesInUiModules = (state: DefaultRootState) => {
return [];
};
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function ToggleComponentToJsonHandler(props: HandlerProps) {
}

function ToggleComponentToJson(props: Props) {
return props.viewType === ViewTypes.JSON
return props.viewType === ViewTypes.JSON && props.renderCompFunction
? props.renderCompFunction({
...alternateViewTypeInputConfig(),
configProperty: props.configProperty,
Expand Down
2 changes: 1 addition & 1 deletion app/client/src/components/propertyControls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { SwitchControlProps } from "components/propertyControls/SwitchContr
import SwitchControl from "components/propertyControls/SwitchControl";
import OptionControl from "components/propertyControls/OptionControl";
import type { ControlProps } from "components/propertyControls/BaseControl";
import type BaseControl from "components/propertyControls/BaseControl";
import CodeEditorControl from "components/propertyControls/CodeEditorControl";
import type BaseControl from "components/propertyControls/BaseControl";
import type { DatePickerControlProps } from "components/propertyControls/DatePickerControl";
import DatePickerControl from "components/propertyControls/DatePickerControl";
import ChartDataControl from "components/propertyControls/ChartDataControl";
Expand Down
23 changes: 18 additions & 5 deletions app/client/src/entities/Engine/AppViewerEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import type { APP_MODE } from "entities/App";
import { call, put, spawn } from "redux-saga/effects";
import { call, put, select, spawn } from "redux-saga/effects";
import type { DeployConsolidatedApi } from "sagas/InitSagas";
import {
failFastApiCalls,
Expand All @@ -20,7 +20,10 @@ import {
} from "sagas/InitSagas";
import type { AppEnginePayload } from ".";
import AppEngine, { ActionsNotFoundError } from ".";
import { fetchJSLibraries } from "actions/JSLibraryActions";
import {
fetchJSLibraries,
deferLoadingJSLibraries,
} from "actions/JSLibraryActions";
import { waitForFetchUserSuccess } from "ee/sagas/userSagas";
import { fetchJSCollectionsForView } from "actions/jsActionActions";
import {
Expand All @@ -29,6 +32,7 @@ import {
} from "actions/appThemingActions";
import type { Span } from "instrumentation/types";
import { endSpan, startNestedSpan } from "instrumentation/generateTraces";
import { getIsFirstPageLoad } from "selectors/evaluationSelectors";

export default class AppViewerEngine extends AppEngine {
constructor(mode: APP_MODE) {
Expand Down Expand Up @@ -120,9 +124,18 @@ export default class AppViewerEngine extends AppEngine {
ReduxActionErrorTypes.SETUP_PUBLISHED_PAGE_ERROR,
];

initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries));
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
failureActionEffects.push(ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED);
const isFirstPageLoad = yield select(getIsFirstPageLoad);

if (isFirstPageLoad) {
// we are deferring the loading of JS libraries
yield put(deferLoadingJSLibraries(applicationId, customJSLibraries));
} else {
initActionsCalls.push(fetchJSLibraries(applicationId, customJSLibraries));
successActionEffects.push(ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS);
failureActionEffects.push(
ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
);
}

const resultOfPrimaryCalls: boolean = yield failFastApiCalls(
initActionsCalls,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { getWidgetHierarchy } from "layoutSystems/anvil/utils/paste/utils";
import type { AnvilGlobalDnDStates } from "../../canvas/hooks/useAnvilGlobalDnDStates";
import { getWidgets } from "sagas/selectors";
import { useMemo } from "react";
import { WDSZoneWidget } from "widgets/wds/WDSZoneWidget";
import { useAnvilWidgetElevation } from "../../canvas/providers/AnvilWidgetElevationProvider";
import { anvilWidgets } from "widgets/wds/constants";

interface AnvilDnDListenerStatesProps {
anvilGlobalDragStates: AnvilGlobalDnDStates;
Expand Down Expand Up @@ -148,7 +148,7 @@ export const useAnvilDnDListenerStates = ({
}, [widgetProps, allWidgets]);

const isElevatedWidget = useMemo(() => {
if (widgetProps.type === WDSZoneWidget.type) {
if (widgetProps.type === anvilWidgets.ZONE_WIDGET) {
const isAnyZoneElevated = allSiblingsWidgetIds.some(
(each) => !!elevatedWidgets[each],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ export const FixedLayoutViewerCanvas = (props: BaseWidgetProps) => {
!!props.noPad,
);
}, [
props.children,
props?.children,
props?.metaWidgetChildrenStructure,
props.positioning,
props.shouldScrollContents,
props.widgetId,
props.componentHeight,
props.componentWidth,
snapColumnSpace,
props.metaWidgetChildrenStructure,
props.noPad,
defaultWidgetProps,
layoutSystemProps,
]);
const snapRows = getCanvasSnapRows(props.bottomRow);

Expand Down
Loading
Loading