Skip to content

Commit

Permalink
[Feat] Support async mergers (#2129)
Browse files Browse the repository at this point in the history
* upgrade react-palm
* move code to merger-handler
* handle mergeStateFromMergers
* allow merge prop as an array
* add test
* add waitToFinish prop

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Co-authored-by: Shan He <heshan0131@gmail.com>
  • Loading branch information
igorDykhta and heshan0131 committed Mar 1, 2023
1 parent 28c3490 commit 9d568af
Show file tree
Hide file tree
Showing 11 changed files with 485 additions and 86 deletions.
2 changes: 1 addition & 1 deletion src/actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@types/react-redux": "^7.1.23",
"@types/redux-actions": "^2.6.2",
"lodash.curry": "^4.1.1",
"react-palm": "^3.3.7",
"react-palm": "^3.3.8",
"react-redux": "^7.1.3",
"redux": "^4.0.5",
"redux-actions": "^2.2.1"
Expand Down
2 changes: 1 addition & 1 deletion src/reducers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"lodash.pick": "^4.4.0",
"lodash.uniq": "^4.0.1",
"lodash.xor": "^4.5.0",
"react-palm": "^3.3.7",
"react-palm": "^3.3.8",
"redux": "^4.0.5",
"redux-actions": "^2.2.1",
"reselect": "^4.0.0"
Expand Down
128 changes: 128 additions & 0 deletions src/reducers/src/merger-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import {getGlobalTaskQueue} from 'react-palm/tasks';
import {isObject, toArray} from '@kepler.gl/utils';
import {ValueOf} from '@kepler.gl/types';
import {VisState, Merger, PostMergerPayload} from '@kepler.gl/schemas';

export function isValidMerger(merger: Merger<any>): boolean {
return (
isObject(merger) &&
typeof merger.merge === 'function' &&
(typeof merger.prop === 'string' || Array.isArray(merger.prop))
);
}

/**
* Call state updater, return the tasks created by the state update with withTask()
*/
function callFunctionGetTask(fn: () => any): [any, any] {
const before = getGlobalTaskQueue();
const ret = fn();
const after = getGlobalTaskQueue();
const diff = after.filter(t => !before.includes(t));
return [ret, diff];
}

export function mergeStateFromMergers<State extends VisState>(
state: State,
initialState: State,
mergers: Merger<any>[],
postMergerPayload: PostMergerPayload
): {
mergedState: State;
allMerged: boolean;
} {
// const newDataIds = Object.keys(postMergerPayload.newDataEntries);
let mergedState = state;
// merge state with config to be merged
const mergerQueue = [...mergers];

while (mergerQueue.length) {
const merger = mergerQueue.shift();

if (
merger &&
isValidMerger(merger) &&
merger.toMergeProp &&
hasPropsToMerge(state, merger.toMergeProp)
) {
const mergerActionPayload = {
mergers: mergerQueue,
postMergerPayload
};
// reset toMerge
const toMerge = getPropValueToMerger(mergedState, merger.toMergeProp, merger.toMergeProp);

mergedState = resetStateToMergeProps(mergedState, initialState, merger.toMergeProp);
// call merger
// eslint-disable-next-line no-loop-func
const mergeFunc = () => merger.merge(mergedState, toMerge, false, mergerActionPayload);
const [updatedState, newTasks] = callFunctionGetTask(mergeFunc);

mergedState = updatedState;
// check if asyncTask was created
if (newTasks.length && merger.waitToFinish) {
// skip rest
return {mergedState, allMerged: false};
}
}
}

// if we merged all mergers in the queue we can call post merger
return {mergedState, allMerged: true};
}

export function hasPropsToMerge<State extends {}>(
state: State,
mergerProps: string | string[]
): boolean {
return Array.isArray(mergerProps)
? Boolean(mergerProps.some(p => state.hasOwnProperty(p)))
: typeof mergerProps === 'string' && state.hasOwnProperty(mergerProps);
}

export function getPropValueToMerger<State extends {}>(
state: State,
mergerProps: string | string[],
toMergeProps?: string | string[]
): Partial<State> | ValueOf<State> {
return Array.isArray(mergerProps)
? mergerProps.reduce((accu, p, i) => {
if (!toMergeProps) return accu;
return {...accu, [toMergeProps[i]]: state[p]};
}, {})
: state[mergerProps];
}

export function resetStateToMergeProps<State extends VisState>(
state: State,
initialState: State,
mergerProps: string | string[]
) {
return toArray(mergerProps).reduce(
(accu, prop) => ({
...accu,
[prop]: initialState[prop]
}),
state
);
}
27 changes: 15 additions & 12 deletions src/reducers/src/vis-state-merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import uniq from 'lodash.uniq';
import pick from 'lodash.pick';
import flattenDeep from 'lodash.flattendeep';
import {
isObject,
arrayInsert,
getInitialMapLayersForSplitMap,
applyFiltersToDatasets,
Expand All @@ -33,7 +32,6 @@ import {LayerColumns, LayerColumn, Layer} from '@kepler.gl/layers';
import {LAYER_BLENDINGS, OVERLAY_BLENDINGS} from '@kepler.gl/constants';
import {
CURRENT_VERSION,
Merger,
VisState,
VisStateMergers,
visStateSchema,
Expand Down Expand Up @@ -121,17 +119,26 @@ export function mergeLayers<S extends VisState>(
fromConfig?: boolean
): S {
const preserveLayerOrder = fromConfig ? layersToMerge.map(l => l.id) : state.preserveLayerOrder;

if (!Array.isArray(layersToMerge) || !layersToMerge.length) {
return state;
}
// don't merge layer if dataset is being merged
const unmerged: ParsedLayer[] = [];
const toMerge: ParsedLayer[] = [];
layersToMerge.forEach((l: ParsedLayer) => {
if (l?.config?.dataId && state.isMergingDatasets[l.config.dataId]) {
unmerged.push(l);
} else {
toMerge.push(l);
}
});

const {validated: mergedLayer, failed: unmerged} = validateLayersByDatasets(
const {validated: mergedLayer, failed} = validateLayersByDatasets(
state.datasets,
state.layerClasses,
layersToMerge
toMerge
);

unmerged.push(...failed);
// put new layers in front of current layers
const {newLayerOrder, newLayers} = insertLayerAtRightOrder(
state.layers,
Expand Down Expand Up @@ -330,7 +337,7 @@ export function mergeInteractionTooltipConfig(
}

for (const dataId in tooltipConfig.fieldsToShow) {
if (!state.datasets[dataId]) {
if (!state.datasets[dataId] || state.isMergingDatasets[dataId]) {
// is not yet loaded
unmergedTooltip[dataId] = tooltipConfig.fieldsToShow[dataId];
} else {
Expand Down Expand Up @@ -633,11 +640,7 @@ export function mergeEditor<S extends VisState>(state: S, savedEditor: SavedEdit
};
}

export function isValidMerger(merger: Merger): boolean {
return isObject(merger) && typeof merger.merge === 'function' && typeof merger.prop === 'string';
}

export const VIS_STATE_MERGERS: VisStateMergers = [
export const VIS_STATE_MERGERS: VisStateMergers<any> = [
{merge: mergeLayers, prop: 'layers', toMergeProp: 'layerToBeMerged'},
{merge: mergeFilters, prop: 'filters', toMergeProp: 'filterToBeMerged'},
{merge: mergeInteractions, prop: 'interactionConfig', toMergeProp: 'interactionToBeMerged'},
Expand Down

0 comments on commit 9d568af

Please sign in to comment.