mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-29 02:18:11 +08:00
* Remove unused variables and imports
* Simplify conditional
* Fix typo
* Do not overprune var data cache
There is a bug in how the universe and world gene sets are constructed
and passed to `ControlsHelpers.pruneVarDataCache` that causes the var
data cache to be over-pruned. This commit fixes the issue.
Consider the following example from the node console:
```
❯ node
Welcome to Node.js v13.5.0.
Type ".help" for more information.
> new Set([1], [2], [3])
Set(1) { 1 }
```
What we really want is the set `Set(3) { 1, 2, 3 }`, which can be
constructed as:
```
> new Set([].concat([1], [2], [3]))
Set(3) { 1, 2, 3 }
```
291 lines
8.0 KiB
JavaScript
291 lines
8.0 KiB
JavaScript
import StateMachine from "../util/statemachine";
|
|
import createFsmTransitions from "./undoableFsm";
|
|
|
|
const actionKey = "@@undoable/filterAction";
|
|
const stateKey = "@@undoable/filterState";
|
|
|
|
/*
|
|
these actions will not affect history
|
|
*/
|
|
const skipOnActions = new Set([
|
|
"url changed",
|
|
"interface reset started",
|
|
"initial data load start",
|
|
"universe: column load success",
|
|
"universe exists, but loading is still in progress",
|
|
"configuration load complete",
|
|
"increment graph render counter",
|
|
"window resize",
|
|
"reset colorscale",
|
|
"reset centroid labels",
|
|
|
|
"graph brush change",
|
|
"continuous metadata histogram brush",
|
|
|
|
"expression load start",
|
|
"expression load success",
|
|
"expression load error",
|
|
|
|
"request user defined gene started",
|
|
"request user defined gene success",
|
|
"clear all user defined genes",
|
|
|
|
"get single gene expression for coloring started",
|
|
"get single gene expression for coloring error",
|
|
|
|
"category value mouse hover start",
|
|
"category value mouse hover end",
|
|
|
|
/* autosave annotations */
|
|
"writable obs annotations - save complete",
|
|
"writable obs annotations - save started",
|
|
"writable obs annotations - save error",
|
|
|
|
/* annotation component action */
|
|
"annotation: activate add new label mode",
|
|
"annotation: disable add new label mode",
|
|
"annotation: activate category edit mode",
|
|
"annotation: disable category edit mode",
|
|
"annotation: activate edit label mode",
|
|
"annotation: cancel edit label mode",
|
|
"set annotations collection name"
|
|
]);
|
|
|
|
/*
|
|
identical, repeated occurances of these action types will be debounced.
|
|
Entire action must be identical (all keys).
|
|
*/
|
|
const debounceOnActions = new Set([]);
|
|
|
|
/*
|
|
history will be cleared when these actions occur
|
|
*/
|
|
const clearOnActions = new Set([
|
|
"initial data load complete (universe exists)",
|
|
"reset World to eq Universe",
|
|
"initial data load error",
|
|
]);
|
|
|
|
/*
|
|
An immediate history save will be done for these
|
|
*/
|
|
const saveOnActions = new Set([
|
|
"categorical metadata filter select",
|
|
"categorical metadata filter deselect",
|
|
"categorical metadata filter all of these",
|
|
"categorical metadata filter none of these",
|
|
|
|
"color by categorical metadata",
|
|
"color by continuous metadata",
|
|
"color by expression",
|
|
|
|
"show centroid labels for category",
|
|
|
|
"set scatterplot x",
|
|
"set scatterplot y",
|
|
|
|
"store current cell selection as differential set 1",
|
|
"store current cell selection as differential set 2",
|
|
|
|
"set World to current selection",
|
|
"set clip quantiles",
|
|
|
|
"set layout choice",
|
|
"change graph interaction mode",
|
|
|
|
// user editable annotations
|
|
"annotation: create category",
|
|
"annotation: add new label to category",
|
|
"annotation: delete category",
|
|
"annotation: label edited",
|
|
"annotation: label current cell selection",
|
|
"annotation: delete label",
|
|
"annotation: category edited"
|
|
]);
|
|
|
|
/**
|
|
StateMachine - processing complex action handling - see FSM graph for
|
|
actual structure, in undoableFsm.js
|
|
**/
|
|
|
|
/*
|
|
Default FSM actions. Used to side-effect transitions in the graph.
|
|
See graph definition for the transitions that use each.
|
|
|
|
Signature: (fsm, transition, reducerState, reducerAction) => undoableAction
|
|
*/
|
|
const stashPending = fsm => ({
|
|
[actionKey]: "stashPending",
|
|
[stateKey]: { fsm }
|
|
});
|
|
const cancelPending = () => ({
|
|
[actionKey]: "cancelPending",
|
|
[stateKey]: { fsm: null }
|
|
});
|
|
const applyPending = () => ({
|
|
[actionKey]: "applyPending",
|
|
[stateKey]: { fsm: null }
|
|
});
|
|
const skip = (fsm, transition) => ({
|
|
[actionKey]: "skip",
|
|
[stateKey]: { fsm: transition.to !== "done" ? fsm : null }
|
|
});
|
|
const clear = () => ({ [actionKey]: "clear", [stateKey]: { fsm: null } });
|
|
const save = (fsm, transition) => ({
|
|
[actionKey]: "save",
|
|
[stateKey]: { fsm: transition.to !== "done" ? fsm : null }
|
|
});
|
|
|
|
/*
|
|
Error handler for state transitions that are unexpected. Called by
|
|
StateMachine when it doesn't know what to do.
|
|
|
|
Signature: (fsm, event, from) => undoableAction
|
|
*/
|
|
const onFsmError = (fsm, event, from) => {
|
|
console.error(`FSM error [event: "${event}", state: "${from}"]`, fsm);
|
|
// In production, try to recover gracefully if we have unexpected state
|
|
return clear();
|
|
};
|
|
|
|
/*
|
|
Definition of the transition graph mapping action types to history side effects.
|
|
*/
|
|
const fsmTransitions = createFsmTransitions(
|
|
stashPending,
|
|
cancelPending,
|
|
applyPending,
|
|
skip,
|
|
clear,
|
|
save
|
|
);
|
|
/* State machine we clone whenever we need to run it */
|
|
const seedFsm = new StateMachine("init", fsmTransitions, onFsmError);
|
|
|
|
/*
|
|
See undoable.js for description action filter interface description.
|
|
|
|
Basic approach:
|
|
* trivial handlers for skip, clear & save cases to keep config simple.
|
|
* only implement complex state machines where absolutely required (eg,
|
|
multi-event seleciton and the like)
|
|
*/
|
|
const actionFilter = debug => (state, action, prevFilterState) => {
|
|
const actionType = action.type;
|
|
const filterState = {
|
|
...prevFilterState,
|
|
prevAction: action
|
|
};
|
|
if (skipOnActions.has(actionType)) {
|
|
return { [actionKey]: "skip", [stateKey]: filterState };
|
|
}
|
|
if (
|
|
debounceOnActions.has(actionType) &&
|
|
shallowObjectEq(action, prevFilterState.prevAction)
|
|
) {
|
|
return { [actionKey]: "skip", [stateKey]: filterState };
|
|
}
|
|
if (clearOnActions.has(actionType)) {
|
|
return { [actionKey]: "clear", [stateKey]: filterState };
|
|
}
|
|
if (saveOnActions.has(actionType)) {
|
|
return { [actionKey]: "save", [stateKey]: filterState };
|
|
}
|
|
|
|
/*
|
|
Else, something more complex OR unknown to us....
|
|
*/
|
|
if (seedFsm.events.has(actionType)) {
|
|
let { fsm } = filterState;
|
|
if (!fsm) {
|
|
/* no active FSM, so create one in init state */
|
|
fsm = seedFsm.clone("init");
|
|
}
|
|
return fsm.next(action.type, { state, action });
|
|
}
|
|
|
|
/* else, we have no idea what this is - skip it */
|
|
if (debug) console.log("**** ACTION FILTER EVENT HANDLER MISS", actionType);
|
|
return { [actionKey]: "skip", [stateKey]: filterState };
|
|
};
|
|
|
|
/*
|
|
return true if objA and objB are ===, OR if:
|
|
- are both objects and not null
|
|
- have same own properties
|
|
- all values are strict equal (===)
|
|
*/
|
|
function shallowObjectEq(objA, objB) {
|
|
if (objA === objB) return true;
|
|
if (!objA || !objB) return false;
|
|
if (!shallowArrayEq(Object.keys(objA), Object.keys(objB))) return false;
|
|
if (!shallowArrayEq(Object.values(objA), Object.values(objB))) return false;
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
return true if arrA and arrB contain the same strict-equal values,
|
|
in the same order.
|
|
*/
|
|
function shallowArrayEq(arrA, arrB) {
|
|
if (arrA.length !== arrB.length) return false;
|
|
for (let i = 0, l = arrA.length; i < l; i += 1) {
|
|
if (arrA[i] !== arrB[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* configuration for the undoable meta reducer */
|
|
/*
|
|
debug: set to any falsish value to disable logging of helpful debugging information.
|
|
Set to true or 1 for base logging, high number for more verbosity (currently only 1/true
|
|
or 2).
|
|
*/
|
|
const debug = false;
|
|
const undoableConfig = {
|
|
debug,
|
|
historyLimit: 50, // maximum history size
|
|
actionFilter: actionFilter(debug)
|
|
};
|
|
|
|
/*
|
|
this code is strictly for sanity checking configuration, and is only
|
|
enabled when we are debugging the undoable configuration (ie, debug === true).
|
|
*/
|
|
if (debug) {
|
|
/*
|
|
Confirm no intersection between the various trivial rejection action filters
|
|
*/
|
|
if (
|
|
new Set([...skipOnActions].filter(x => clearOnActions.has(x))).size > 0 ||
|
|
new Set([...skipOnActions].filter(x => saveOnActions.has(x))).size > 0 ||
|
|
new Set([...clearOnActions].filter(x => saveOnActions.has(x))).size > 0
|
|
) {
|
|
console.error(
|
|
"Undoable misconfiguration - action filters have redundant events"
|
|
);
|
|
}
|
|
|
|
/*
|
|
Confirm that no FSM events are blocked by a trivial rejection filter.
|
|
If this occurs, the FSM can't ever see the events needed to process
|
|
state transitions.
|
|
*/
|
|
const trivialFilters = new Set([
|
|
...skipOnActions,
|
|
...clearOnActions,
|
|
...saveOnActions
|
|
]);
|
|
const trivialOverlapWithFsm = new Set(
|
|
[...trivialFilters].filter(x => seedFsm.events.has(x))
|
|
);
|
|
if (trivialOverlapWithFsm.size > 0) {
|
|
console.error(
|
|
"Undoable misconfiguration - trivival action filter blocking FSM filter",
|
|
[...trivialOverlapWithFsm]
|
|
);
|
|
}
|
|
}
|
|
|
|
export default undoableConfig;
|