import { actionChannel, call, fork, put, select, take, } from "redux-saga/effects"; import { EvaluationReduxAction, ReduxAction, ReduxActionErrorTypes, ReduxActionTypes, } from "constants/ReduxActionConstants"; import { getDataTree, getUnevaluatedDataTree, } from "selectors/dataTreeSelectors"; import WidgetFactory, { WidgetTypeConfigMap } from "../utils/WidgetFactory"; import { GracefulWorkerService } from "../utils/WorkerUtil"; import Worker from "worker-loader!../workers/evaluation.worker"; import { EVAL_WORKER_ACTIONS, EvalError, EvalErrorTypes, } from "../utils/DynamicBindingUtils"; import log from "loglevel"; import { WidgetType } from "../constants/WidgetConstants"; import { WidgetProps } from "../widgets/BaseWidget"; import PerformanceTracker, { PerformanceTransactionName, } from "../utils/PerformanceTracker"; import { Variant } from "components/ads/common"; import { Toaster } from "components/ads/Toast"; import * as Sentry from "@sentry/react"; import { EXECUTION_PARAM_KEY } from "../constants/ActionConstants"; import { Action } from "redux"; import _ from "lodash"; let widgetTypeConfigMap: WidgetTypeConfigMap; const worker = new GracefulWorkerService(Worker); const evalErrorHandler = (errors: EvalError[]) => { if (!errors) return; errors.forEach((error) => { if (error.type === EvalErrorTypes.DEPENDENCY_ERROR) { Toaster.show({ text: error.message, variant: Variant.danger, }); } if (error.type === EvalErrorTypes.EVAL_TREE_ERROR) { Toaster.show({ text: "Unexpected error occurred while evaluating the app", variant: Variant.danger, }); } if (error.type === EvalErrorTypes.BAD_UNEVAL_TREE_ERROR) { Sentry.captureException(error); } log.debug(error); }); }; function* postEvalActionDispatcher(actions: ReduxAction[]) { for (const action of actions) { yield put(action); } } function* evaluateTreeSaga(postEvalActions?: ReduxAction[]) { PerformanceTracker.startAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, ); const unevalTree = yield select(getUnevaluatedDataTree); log.debug({ unevalTree }); const workerResponse = yield call( worker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { unevalTree, widgetTypeConfigMap, }, ); const { errors, dataTree, dependencies, logs } = workerResponse; log.debug({ dataTree: dataTree }); logs.forEach((evalLog: any) => log.debug(evalLog)); evalErrorHandler(errors); yield put({ type: ReduxActionTypes.SET_EVALUATED_TREE, payload: dataTree, }); yield put({ type: ReduxActionTypes.SET_EVALUATION_DEPENDENCY_MAP, payload: dependencies, }); PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, ); if (postEvalActions && postEvalActions.length) { yield call(postEvalActionDispatcher, postEvalActions); } } export function* evaluateSingleValue( binding: string, executionParams: Record = {}, ) { const dataTree = yield select(getDataTree); const workerResponse = yield call( worker.request, EVAL_WORKER_ACTIONS.EVAL_SINGLE, { dataTree: Object.assign({}, dataTree, { [EXECUTION_PARAM_KEY]: executionParams, }), binding, }, ); const { errors, value } = workerResponse; evalErrorHandler(errors); return value; } export function* evaluateDynamicTrigger( dynamicTrigger: string, callbackData?: Array, ) { const unEvalTree = yield select(getUnevaluatedDataTree); const workerResponse = yield call( worker.request, EVAL_WORKER_ACTIONS.EVAL_TRIGGER, { dataTree: unEvalTree, dynamicTrigger, callbackData }, ); const { errors, triggers } = workerResponse; evalErrorHandler(errors); return triggers; } export function* clearEvalCache() { yield call(worker.request, EVAL_WORKER_ACTIONS.CLEAR_CACHE); return true; } export function* clearEvalPropertyCache(propertyPath: string) { yield call(worker.request, EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE, { propertyPath, }); } /** * clears all cache keys of a widget * * @param widgetName */ export function* clearEvalPropertyCacheOfWidget(widgetName: string) { yield call( worker.request, EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE_OF_WIDGET, { widgetName, }, ); } export function* validateProperty( widgetType: WidgetType, property: string, value: any, props: WidgetProps, ) { return yield call(worker.request, EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY, { widgetTypeConfigMap, widgetType, property, value, props, }); } const FIRST_EVAL_REDUX_ACTIONS = [ // Pages ReduxActionTypes.FETCH_PAGE_SUCCESS, ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS, ]; const EVALUATE_REDUX_ACTIONS = [ ...FIRST_EVAL_REDUX_ACTIONS, // Actions ReduxActionTypes.FETCH_ACTIONS_SUCCESS, ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS, ReduxActionErrorTypes.FETCH_ACTIONS_ERROR, ReduxActionErrorTypes.FETCH_ACTIONS_VIEW_MODE_ERROR, ReduxActionTypes.FETCH_ACTIONS_FOR_PAGE_SUCCESS, ReduxActionTypes.SUBMIT_CURL_FORM_SUCCESS, ReduxActionTypes.CREATE_ACTION_SUCCESS, ReduxActionTypes.UPDATE_ACTION_PROPERTY, ReduxActionTypes.DELETE_ACTION_SUCCESS, ReduxActionTypes.COPY_ACTION_SUCCESS, ReduxActionTypes.MOVE_ACTION_SUCCESS, ReduxActionTypes.RUN_ACTION_REQUEST, ReduxActionTypes.RUN_ACTION_SUCCESS, ReduxActionErrorTypes.RUN_ACTION_ERROR, ReduxActionTypes.EXECUTE_API_ACTION_REQUEST, ReduxActionTypes.EXECUTE_API_ACTION_SUCCESS, ReduxActionErrorTypes.EXECUTE_ACTION_ERROR, // App Data ReduxActionTypes.SET_APP_MODE, ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, ReduxActionTypes.UPDATE_APP_STORE, // Widgets ReduxActionTypes.UPDATE_LAYOUT, ReduxActionTypes.UPDATE_WIDGET_PROPERTY, ReduxActionTypes.UPDATE_WIDGET_NAME_SUCCESS, // Widget Meta ReduxActionTypes.SET_META_PROP, ReduxActionTypes.RESET_WIDGET_META, // Batches ReduxActionTypes.BATCH_UPDATES_SUCCESS, ]; const shouldProcessAction = (action: ReduxAction) => { // debugger; if ( action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS && Array.isArray(action.payload) ) { const batchedActionTypes = action.payload.map( (batchedAction) => batchedAction.type, ); return ( _.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length > 0 ); } return true; }; function evalQueueBuffer() { let canTake = false; let postEvalActions: any = []; const take = () => { if (canTake) { const resp = postEvalActions; postEvalActions = []; canTake = false; return { postEvalActions: resp, type: "BUFFERED_ACTION" }; } }; const flush = () => { if (canTake) { return [take() as Action]; } return []; }; const put = (action: EvaluationReduxAction) => { if (!shouldProcessAction(action)) { return; } canTake = true; // TODO: If the action is the same as before, we can send only one and ignore duplicates. if (action.postEvalActions) { postEvalActions.push(...action.postEvalActions); } }; return { take, put, isEmpty: () => { return !canTake; }, flush, }; } function* evaluationChangeListenerSaga() { // Explicitly shutdown old worker if present yield call(worker.shutdown); yield call(worker.start); widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap(); const initAction = yield take(FIRST_EVAL_REDUX_ACTIONS); yield fork(evaluateTreeSaga, initAction.postEvalActions); const evtActionChannel = yield actionChannel( EVALUATE_REDUX_ACTIONS, evalQueueBuffer(), ); while (true) { const action: EvaluationReduxAction = yield take( evtActionChannel, ); if (shouldProcessAction(action)) { yield call(evaluateTreeSaga, action.postEvalActions); } } // TODO(hetu) need an action to stop listening and evaluate (exit app) } export default function* evaluationSagaListeners() { yield take(ReduxActionTypes.START_EVALUATION); while (true) { try { yield call(evaluationChangeListenerSaga); } catch (e) { log.error(e); Sentry.captureException(e); } } }