dash-renderer
Version:
render dash components in react
1,202 lines (1,177 loc) • 49 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.addAllResolvedFromOutputs = addAllResolvedFromOutputs;
exports.computeGraphs = computeGraphs;
exports.getUnfilteredLayoutCallbacks = getUnfilteredLayoutCallbacks;
exports.getWatchedKeys = getWatchedKeys;
exports.idMatch = idMatch;
exports.isMultiOutputProp = void 0;
exports.isMultiValued = isMultiValued;
exports.parseIfWildcard = parseIfWildcard;
exports.splitIdAndProp = splitIdAndProp;
exports.stringifyId = stringifyId;
exports.validateCallbacksToLayout = validateCallbacksToLayout;
var _dependencyGraph = require("dependency-graph");
var _fastIsnumeric = _interopRequireDefault(require("fast-isnumeric"));
var _ramda = require("ramda");
var _dependencies_ts = require("./dependencies_ts");
var _paths = require("./paths");
var _utils = require("./utils");
var _registry = _interopRequireDefault(require("../registry"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
/*
* If this update is for multiple outputs, then it has
* starting & trailing `..` and each propId pair is separated
* by `...`, e.g.
* "..output-1.value...output-2.value...output-3.value...output-4.value.."
*/
var isMultiOutputProp = idAndProp => idAndProp.startsWith('..');
exports.isMultiOutputProp = isMultiOutputProp;
var ALL = {
wild: 'ALL',
multi: 1
};
var MATCH = {
wild: 'MATCH'
};
var ALLSMALLER = {
wild: 'ALLSMALLER',
multi: 1,
expand: 1
};
var wildcards = {
ALL,
MATCH,
ALLSMALLER
};
var allowedWildcards = {
Output: {
ALL,
MATCH
},
Input: wildcards,
State: wildcards
};
var wildcardValTypes = ['string', 'number', 'boolean'];
var idInvalidChars = ['.', '{'];
/*
* If this ID is a wildcard, it is a stringified JSON object
* the "{" character is disallowed from regular string IDs
*/
var isWildcardId = idStr => idStr.startsWith('{');
/*
* Turn stringified wildcard IDs into objects.
* Wildcards are encoded as single-item arrays containing the wildcard name
* as a string.
*/
function parseWildcardId(idStr) {
return (0, _ramda.map)(val => Array.isArray(val) && wildcards[val[0]] || val, JSON.parse(idStr));
}
/*
* If this update is for multiple outputs, then it has
* starting & trailing `..` and each propId pair is separated
* by `...`, e.g.
* "..output-1.value...output-2.value...output-3.value...output-4.value.."
*/
function parseMultipleOutputs(outputIdAndProp) {
return outputIdAndProp.substr(2, outputIdAndProp.length - 4).split('...');
}
function splitIdAndProp(idAndProp) {
// since wildcard ids can have . in them but props can't,
// look for the last . in the string and split there
var dotPos = idAndProp.lastIndexOf('.');
var idStr = idAndProp.substr(0, dotPos);
return {
id: parseIfWildcard(idStr),
property: idAndProp.substr(dotPos + 1)
};
}
/*
* Check if this ID is a stringified object, and if so parse it to that object
*/
function parseIfWildcard(idStr) {
return isWildcardId(idStr) ? parseWildcardId(idStr) : idStr;
}
/*
* JSON.stringify - for the object form - but ensuring keys are sorted
*/
function stringifyId(id) {
if (typeof id !== 'object') {
return id;
}
var stringifyVal = v => v && v.wild || JSON.stringify(v);
var parts = Object.keys(id).sort().map(k => JSON.stringify(k) + ':' + stringifyVal(id[k]));
return '{' + parts.join(',') + '}';
}
/*
* id dict values can be numbers, strings, and booleans.
* We need a definite ordering that will work across types,
* even if sane users would not mix types.
* - numeric strings are treated as numbers
* - booleans come after numbers, before strings. false, then true.
* - non-numeric strings come last
*/
function idValSort(a, b) {
var bIsNumeric = (0, _fastIsnumeric.default)(b);
if ((0, _fastIsnumeric.default)(a)) {
if (bIsNumeric) {
var aN = Number(a);
var bN = Number(b);
return aN > bN ? 1 : aN < bN ? -1 : 0;
}
return -1;
}
if (bIsNumeric) {
return 1;
}
var aIsBool = typeof a === 'boolean';
if (aIsBool !== (typeof b === 'boolean')) {
return aIsBool ? -1 : 1;
}
return a > b ? 1 : a < b ? -1 : 0;
}
/*
* Provide a value known to be before or after v, according to idValSort
*/
var valBefore = v => (0, _fastIsnumeric.default)(v) ? v - 1 : 0;
var valAfter = v => typeof v === 'string' ? v + 'z' : 'z';
function addMap(depMap, id, prop, dependency) {
var idMap = depMap[id] = depMap[id] || {};
var callbacks = idMap[prop] = idMap[prop] || [];
callbacks.push(dependency);
}
// Patterns are stored in a nested Map structure to avoid the overhead of
// stringifying ids for every callback.
function addPattern(patterns, idSpec, prop, dependency) {
var keys = Object.keys(idSpec).sort();
var keyStr = keys.join(',');
var values = (0, _ramda.props)(keys, idSpec);
var valuesKey = values.map(v => typeof v === 'object' && v !== null ? v.wild ? v.wild : JSON.stringify(v) : String(v)).join('|');
if (!patterns.has(keyStr)) {
patterns.set(keyStr, new Map());
}
var propMap = patterns.get(keyStr);
if (!propMap.has(prop)) {
propMap.set(prop, new Map());
}
var valueMap = propMap.get(prop);
var valMatch = valueMap.get(valuesKey);
if (!valMatch) {
valMatch = {
keys,
values,
callbacks: []
};
valueMap.set(valuesKey, valMatch);
}
valMatch.callbacks.push(dependency);
}
// Convert the nested Map structure of patterns into the plain nested object structure
// expected by the rest of the code, with stringified id keys.
// This is only done once per pattern, at the end of graph construction,
// to minimize the overhead of stringifying ids.
function offloadPatterns(patternsMap, targetMap) {
var _iterator = _createForOfIteratorHelper(patternsMap.entries()),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _step$value = _slicedToArray(_step.value, 2),
keyStr = _step$value[0],
propMap = _step$value[1];
targetMap[keyStr] = {};
var _iterator2 = _createForOfIteratorHelper(propMap.entries()),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _step2$value = _slicedToArray(_step2.value, 2),
prop = _step2$value[0],
valueMap = _step2$value[1];
targetMap[keyStr][prop] = Array.from(valueMap.values());
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
function validateDependencies(parsedDependencies, dispatchError) {
var outStrs = {};
var outObjs = [];
parsedDependencies.forEach(dep => {
var inputs = dep.inputs,
outputs = dep.outputs,
state = dep.state;
var hasOutputs = true;
if (outputs.length === 1 && !outputs[0].id && !outputs[0].property) {
hasOutputs = false;
}
var head = 'In the callback for output(s):\n ' + outputs.map(_dependencies_ts.combineIdAndProp).join('\n ');
if (!inputs.length) {
dispatchError('A callback is missing Inputs', [head, 'there are no `Input` elements.', 'Without `Input` elements, it will never get called.', '', 'Subscribing to `Input` components will cause the', 'callback to be called whenever their values change.']);
}
var spec = [[outputs, 'Output'], [inputs, 'Input'], [state, 'State']];
spec.forEach(_ref => {
var _ref2 = _slicedToArray(_ref, 2),
args = _ref2[0],
cls = _ref2[1];
if (cls === 'Output' && !hasOutputs) {
// just a quirk of how we pass & parse outputs - if you don't
// provide one, it looks like a single blank output. This is
// actually useful for graceful failure, so we work around it.
return;
}
if (!Array.isArray(args)) {
dispatchError("Callback ".concat(cls, "(s) must be an Array"), [head, "For ".concat(cls, "(s) we found:"), JSON.stringify(args), 'but we expected an Array.']);
}
args.forEach((idProp, i) => {
validateArg(idProp, head, cls, i, dispatchError);
});
});
if (hasOutputs) {
findDuplicateOutputs(outputs, head, dispatchError, outStrs, outObjs);
findMismatchedWildcards(outputs, inputs, state, head, dispatchError);
}
});
}
function validateArg(_ref3, head, cls, i, dispatchError) {
var id = _ref3.id,
property = _ref3.property;
if (typeof property !== 'string' || !property) {
dispatchError('Callback property error', [head, "".concat(cls, "[").concat(i, "].property = ").concat(JSON.stringify(property)), 'but we expected `property` to be a non-empty string.']);
}
if (typeof id === 'object') {
if ((0, _ramda.isEmpty)(id)) {
dispatchError('Callback item missing ID', [head, "".concat(cls, "[").concat(i, "].id = {}"), 'Every item linked to a callback needs an ID']);
}
(0, _ramda.forEachObjIndexed)((v, k) => {
if (!k) {
dispatchError('Callback wildcard ID error', [head, "".concat(cls, "[").concat(i, "].id has key \"").concat(k, "\""), 'Keys must be non-empty strings.']);
}
if (typeof v === 'object' && v.wild) {
if (allowedWildcards[cls][v.wild] !== v) {
dispatchError('Callback wildcard ID error', [head, "".concat(cls, "[").concat(i, "].id[\"").concat(k, "\"] = ").concat(v.wild), "Allowed wildcards for ".concat(cls, "s are:"), (0, _ramda.keys)(allowedWildcards[cls]).join(', ')]);
}
} else if (!(0, _ramda.includes)(typeof v, wildcardValTypes)) {
dispatchError('Callback wildcard ID error', [head, "".concat(cls, "[").concat(i, "].id[\"").concat(k, "\"] = ").concat(JSON.stringify(v)), 'Wildcard callback ID values must be either wildcards', 'or constants of one of these types:', wildcardValTypes.join(', ')]);
}
}, id);
} else if (typeof id === 'string') {
if (!id) {
dispatchError('Callback item missing ID', [head, "".concat(cls, "[").concat(i, "].id = \"").concat(id, "\""), 'Every item linked to a callback needs an ID']);
}
var invalidChars = idInvalidChars.filter(c => (0, _ramda.includes)(c, id));
if (invalidChars.length) {
dispatchError('Callback invalid ID string', [head, "".concat(cls, "[").concat(i, "].id = '").concat(id, "'"), "characters '".concat(invalidChars.join("', '"), "' are not allowed.")]);
}
} else {
dispatchError('Callback ID type error', [head, "".concat(cls, "[").concat(i, "].id = ").concat(JSON.stringify(id)), 'IDs must be strings or wildcard-compatible objects.']);
}
}
function findDuplicateOutputs(outputs, head, dispatchError, outStrs, outObjs) {
var newOutputStrs = {};
var newOutputObjs = [];
outputs.forEach((_ref4, i) => {
var id = _ref4.id,
property = _ref4.property;
if (typeof id === 'string') {
var idProp = (0, _dependencies_ts.combineIdAndProp)({
id,
property
});
if (newOutputStrs[idProp]) {
dispatchError('Duplicate callback Outputs', [head, "Output ".concat(i, " (").concat(idProp, ") is already used by this callback.")]);
} else if (outStrs[idProp]) {
dispatchError('Duplicate callback outputs', [head, "Output ".concat(i, " (").concat(idProp, ") is already in use."), 'To resolve this, set `allow_duplicate=True` on', 'duplicate outputs, or combine the outputs into', 'one callback function, distinguishing the trigger', 'by using `dash.callback_context` if necessary.']);
} else {
newOutputStrs[idProp] = 1;
}
} else {
var idObj = {
id,
property
};
var selfOverlap = wildcardOverlap(idObj, newOutputObjs);
var otherOverlap = selfOverlap || wildcardOverlap(idObj, outObjs);
if (selfOverlap || otherOverlap) {
var _idProp = (0, _dependencies_ts.combineIdAndProp)(idObj);
var idProp2 = (0, _dependencies_ts.combineIdAndProp)(selfOverlap || otherOverlap);
dispatchError('Overlapping wildcard callback outputs', [head, "Output ".concat(i, " (").concat(_idProp, ")"), "overlaps another output (".concat(idProp2, ")"), "used in ".concat(selfOverlap ? 'this' : 'a different', " callback.")]);
} else {
newOutputObjs.push(idObj);
}
}
});
(0, _ramda.keys)(newOutputStrs).forEach(k => {
outStrs[k] = 1;
});
newOutputObjs.forEach(idObj => {
outObjs.push(idObj);
});
}
function checkInOutOverlap(out, inputs) {
var outId = out.id,
outProp = out.property;
return inputs.some(in_ => {
var inId = in_.id,
inProp = in_.property;
if (outProp !== inProp || typeof outId !== typeof inId) {
return false;
}
if (typeof outId === 'string') {
if (outId === inId) {
return true;
}
} else if (wildcardOverlap(in_, [out])) {
return true;
}
return false;
});
}
function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) {
var _findWildcardKeys = findWildcardKeys(outputs.length ? outputs[0].id : undefined),
out0MatchKeys = _findWildcardKeys.matchKeys;
outputs.forEach((out, i) => {
if (i && !(0, _ramda.equals)(findWildcardKeys(out.id).matchKeys, out0MatchKeys)) {
dispatchError('Mismatched `MATCH` wildcards across `Output`s', [head, "Output ".concat(i, " (").concat((0, _dependencies_ts.combineIdAndProp)(out), ")"), 'does not have MATCH wildcards on the same keys as', "Output 0 (".concat((0, _dependencies_ts.combineIdAndProp)(outputs[0]), ")."), 'MATCH wildcards must be on the same keys for all Outputs.', 'ALL wildcards need not match, only MATCH.']);
}
});
[[inputs, 'Input'], [state, 'State']].forEach(_ref5 => {
var _ref6 = _slicedToArray(_ref5, 2),
args = _ref6[0],
cls = _ref6[1];
args.forEach((arg, i) => {
var _findWildcardKeys2 = findWildcardKeys(arg.id),
matchKeys = _findWildcardKeys2.matchKeys,
allsmallerKeys = _findWildcardKeys2.allsmallerKeys;
var allWildcardKeys = matchKeys.concat(allsmallerKeys);
var diff = (0, _ramda.difference)(allWildcardKeys, out0MatchKeys);
if (diff.length) {
diff.sort();
dispatchError('`Input` / `State` wildcards not in `Output`s', [head, "".concat(cls, " ").concat(i, " (").concat((0, _dependencies_ts.combineIdAndProp)(arg), ")"), "has MATCH or ALLSMALLER on key(s) ".concat(diff.join(', ')), "where Output 0 (".concat((0, _dependencies_ts.combineIdAndProp)(outputs[0]), ")"), 'does not have a MATCH wildcard. Inputs and State do not', 'need every MATCH from the Output(s), but they cannot have', 'extras beyond the Output(s).']);
}
});
});
}
var matchWildKeys = _ref7 => {
var _ref8 = _slicedToArray(_ref7, 2),
a = _ref8[0],
b = _ref8[1];
var aWild = a && a.wild;
var bWild = b && b.wild;
if (aWild && bWild) {
// Every wildcard combination overlaps except MATCH<->ALLSMALLER
return !(a === MATCH && b === ALLSMALLER || a === ALLSMALLER && b === MATCH);
}
return a === b || aWild || bWild;
};
function wildcardOverlap(_ref9, objs) {
var id = _ref9.id,
property = _ref9.property;
var idKeys = (0, _ramda.keys)(id).sort();
var idVals = (0, _ramda.props)(idKeys, id);
var _iterator3 = _createForOfIteratorHelper(objs),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var obj = _step3.value;
var id2 = obj.id,
property2 = obj.property;
if (property2 === property && typeof id2 !== 'string' && (0, _ramda.equals)((0, _ramda.keys)(id2).sort(), idKeys) && (0, _ramda.all)(matchWildKeys, (0, _ramda.zip)(idVals, (0, _ramda.props)(idKeys, id2)))) {
return obj;
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
return false;
}
function validateCallbacksToLayout(state_, dispatchError) {
var config = state_.config,
graphs = state_.graphs,
layout_ = state_.layout,
paths_ = state_.paths;
var validateIds = !config.suppress_callback_exceptions;
var layout, paths;
if (validateIds && config.validation_layout) {
layout = config.validation_layout;
paths = (0, _paths.computePaths)(layout, [], null, paths_.events);
} else {
layout = layout_;
paths = paths_;
}
var outputMap = graphs.outputMap,
inputMap = graphs.inputMap,
outputPatterns = graphs.outputPatterns,
inputPatterns = graphs.inputPatterns;
function tail(callbacks) {
return 'This ID was used in the callback(s) for Output(s):\n ' + callbacks.map(_ref0 => {
var outputs = _ref0.outputs;
return outputs.map(_dependencies_ts.combineIdAndProp).join(', ');
}).join('\n ');
}
function missingId(id, cls, callbacks) {
dispatchError('ID not found in layout', ["Attempting to connect a callback ".concat(cls, " item to component:"), " \"".concat(stringifyId(id), "\""), 'but no components with that id exist in the layout.', '', 'If you are assigning callbacks to components that are', 'generated by other callbacks (and therefore not in the', 'initial layout), you can suppress this exception by setting', '`suppress_callback_exceptions=True`.', tail(callbacks)]);
}
function validateProp(id, idPath, rawProp, cls, callbacks) {
var prop = rawProp.split('@')[0];
var component = (0, _ramda.path)(idPath, layout);
var element = _registry.default.resolve(component);
// note: Flow components do not have propTypes, so we can't validate.
if (element && element.propTypes && !element.propTypes[prop]) {
// look for wildcard props (ie data-* etc)
for (var propName in element.propTypes) {
var last = propName.length - 1;
if (propName.charAt(last) === '*' && prop.substr(0, last) === propName.substr(0, last)) {
return;
}
}
var type = component.type,
namespace = component.namespace;
dispatchError('Invalid prop for this component', ["Property \"".concat(prop, "\" was used with component ID:"), " ".concat(JSON.stringify(id)), "in one of the ".concat(cls, " items of a callback."), "This ID is assigned to a ".concat(namespace, ".").concat(type, " component"), 'in the layout, which does not support this property.', tail(callbacks)]);
}
}
function validateIdPatternProp(id, property, cls, callbacks) {
(0, _dependencies_ts.resolveDeps)()(paths)({
id,
property
}).forEach(dep => {
var idResolved = dep.id,
idPath = dep.path;
validateProp(idResolved, idPath, property, cls, callbacks);
});
}
var callbackIdsCheckedForState = {};
function validateState(callback) {
var state = callback.state,
output = callback.output;
// ensure we don't check the same callback for state multiple times
if (callbackIdsCheckedForState[output]) {
return;
}
callbackIdsCheckedForState[output] = 1;
var cls = 'State';
state.forEach(_ref1 => {
var id = _ref1.id,
property = _ref1.property;
if (typeof id === 'string') {
var idPath = (0, _paths.getPath)(paths, id);
if (!idPath) {
if (validateIds) {
missingId(id, cls, [callback]);
}
} else {
validateProp(id, idPath, property, cls, [callback]);
}
}
// Only validate props for State object ids that we don't need to
// resolve them to specific inputs or outputs
else if (!(0, _ramda.intersection)([MATCH, ALLSMALLER], (0, _ramda.values)(id)).length) {
validateIdPatternProp(id, property, cls, [callback]);
}
});
}
function validateMap(map, cls, doState) {
var _loop = function _loop(id) {
var idProps = map[id];
var fcb = (0, _ramda.flatten)((0, _ramda.values)(idProps));
var optional = fcb.reduce((acc, cb) => {
if (acc === false || cb.optional) {
return acc;
}
var deps = (0, _ramda.concat)(cb.outputs, cb.inputs, cb.states).filter(dep => dep.id === id);
return !deps.length || (0, _ramda.all)(_ref10 => {
var allow_optional = _ref10.allow_optional;
return allow_optional;
}, deps);
}, true);
if (optional) {
return 1; // continue
}
var idPath = (0, _paths.getPath)(paths, id);
if (!idPath) {
if (validateIds) {
missingId(id, cls, fcb);
}
} else {
for (var property in idProps) {
var callbacks = idProps[property];
validateProp(id, idPath, property, cls, callbacks);
if (doState) {
// It would be redundant to check state on both inputs
// and outputs - so only set doState for outputs.
callbacks.forEach(validateState);
}
}
}
};
for (var id in map) {
if (_loop(id)) continue;
}
}
validateMap(outputMap, 'Output', true);
validateMap(inputMap, 'Input');
function validatePatterns(patterns, cls, doState) {
for (var keyStr in patterns) {
var keyPatterns = patterns[keyStr];
var _loop2 = function _loop2(property) {
keyPatterns[property].forEach(_ref11 => {
var keys = _ref11.keys,
values = _ref11.values,
callbacks = _ref11.callbacks;
var id = (0, _ramda.zipObj)(keys, values);
validateIdPatternProp(id, property, cls, callbacks);
if (doState) {
callbacks.forEach(validateState);
}
});
};
for (var property in keyPatterns) {
_loop2(property);
}
}
}
validatePatterns(outputPatterns, 'Output', true);
validatePatterns(inputPatterns, 'Input');
}
function computeGraphs(dependencies, dispatchError, config) {
// multiGraph is just for finding circular deps
var multiGraph = new _dependencyGraph.DepGraph();
var start = performance.now();
var wildcardPlaceholders = {};
var fixIds = (0, _ramda.map)((0, _ramda.evolve)({
id: parseIfWildcard
}));
var parsedDependencies = (0, _ramda.map)(dep => {
var output = dep.output,
no_output = dep.no_output;
var out = (0, _ramda.evolve)({
inputs: fixIds,
state: fixIds
}, dep);
if (no_output) {
// No output case
out.outputs = [];
out.noOutput = true;
} else {
out.outputs = (0, _ramda.map)(outi => (0, _ramda.assoc)('out', true, splitIdAndProp(outi)), isMultiOutputProp(output) ? parseMultipleOutputs(output) : [output]);
}
return out;
}, dependencies);
var hasError = false;
var wrappedDE = (message, lines) => {
hasError = true;
dispatchError(message, lines);
};
if (config.validate_callbacks) {
validateDependencies(parsedDependencies, wrappedDE);
}
/*
* For regular ids, outputMap and inputMap are:
* {[id]: {[prop]: [callback, ...]}}
* where callbacks are the matching specs from the original
* dependenciesRequest, but with outputs parsed to look like inputs,
* and a list matchKeys added if the outputs have MATCH wildcards.
* For outputMap there should only ever be one callback per id/prop
* but for inputMap there may be many.
*
* For wildcard ids, outputPatterns and inputPatterns are:
* {
* [keystr]: {
* [prop]: [
* {keys: [...], values: [...], callbacks: [callback, ...]},
* {...}
* ]
* }
* }
* keystr is a stringified ordered list of keys in the id
* keys is the same ordered list (just copied for convenience)
* values is an array of explicit or wildcard values for each key in keys
*/
var outputMap = {};
var inputMap = {};
var outputPatternMap = new Map();
var inputPatternMap = new Map();
var outputPatterns = {};
var inputPatterns = {};
var finalGraphs = {
MultiGraph: multiGraph,
outputMap,
inputMap,
outputPatterns,
inputPatterns,
callbacks: parsedDependencies
};
if (hasError) {
// leave the graphs empty if we found an error, so we don't try to
// execute the broken callbacks.
return finalGraphs;
}
// builds up wildcardPlaceholders with all the wildcard keys and values used in the callbacks, so we can generate the full list of ids that each callback depends on.
parsedDependencies.forEach(dependency => {
var outputs = dependency.outputs,
inputs = dependency.inputs;
outputs.concat(inputs).filter(item => typeof item.id === 'object').forEach(item => {
(0, _ramda.forEachObjIndexed)((val, key) => {
if (!wildcardPlaceholders[key]) {
wildcardPlaceholders[key] = {
exact: [],
expand: 0
};
}
var keyPlaceholders = wildcardPlaceholders[key];
if (val && val.wild) {
if (val.expand) {
keyPlaceholders.expand += 1;
}
} else if (keyPlaceholders.exact.indexOf(val) === -1) {
keyPlaceholders.exact.push(val);
}
}, item.id);
});
});
// Efficiently build wildcardPlaceholders.vals arrays
(0, _ramda.forEachObjIndexed)(keyPlaceholders => {
var exact = keyPlaceholders.exact,
expand = keyPlaceholders.expand;
var vals = exact.slice().sort(idValSort);
if (expand) {
for (var i = 0; i < expand; i++) {
if (exact.length) {
vals.splice(0, 0, [valBefore(vals[0])]);
vals.push(valAfter(vals[vals.length - 1]));
} else {
vals.push(i);
}
}
} else if (!exact.length) {
// only MATCH/ALL - still need a value
vals.push(0);
}
keyPlaceholders.vals = vals;
}, wildcardPlaceholders);
function makeAllIds(idSpec, outIdFinal) {
var idList = [{}];
(0, _ramda.forEachObjIndexed)((val, key) => {
var testVals = wildcardPlaceholders[key].vals;
var outValIndex = testVals.indexOf(outIdFinal[key]);
var newVals = [val];
if (val && val.wild) {
if (val === ALLSMALLER) {
if (outValIndex > 0) {
newVals = testVals.slice(0, outValIndex);
} else {
// no smaller items - delete all outputs.
newVals = [];
}
} else {
// MATCH or ALL
// MATCH *is* ALL for outputs, ie we don't already have a
// value specified in `outIdFinal`
newVals = outValIndex === -1 || val === ALL ? testVals : [outIdFinal[key]];
}
}
// replicates everything in idList once for each item in
// newVals, attaching each value at key.
idList = (0, _ramda.ap)((0, _ramda.ap)([(0, _ramda.assoc)(key)], newVals), idList);
}, idSpec);
return idList;
}
/* multiGraph is used only for testing circularity
*
* Each component+property that is used as an input or output is added as a node
* to a directed graph with a dependency from each input to each output. The
* function triggerDefaultState in index.js then checks this graph for circularity.
*
* In order to allow the same component+property to be both an input and output
* of the same callback, a two pass approach is used.
*
* In the first pass, the graph is built up normally with the exception that
* in cases where an output is also an input to the same callback a special
* "output" node is added and the dependencies target this output node instead.
* For example, if `slider.value` is both an input and an output, then the a new
* node `slider.value__output` will be added with a dependency from `slider.value`
* to `slider.value__output`. Splitting the input and output into separate nodes
* removes the circularity.
*
* In order to still detect other forms of circularity, it is necessary to do a
* second pass and add the new output nodes as a dependency in any *other* callbacks
* where the original node was an input. Continuing the example, any other callback
* that had `slider.value` as an input dependency also needs to have
* `slider.value__output` as a dependency. To make this efficient, all the inputs
* and outputs for each callback are stored during the first pass.
*/
var outputTag = '__output';
var duplicateOutputs = [];
var cbIn = [];
var cbOut = [];
function addInputToMulti(inIdProp, outIdProp) {
var firstPass = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
if (!config.validate_callbacks) return;
multiGraph.addNode(inIdProp);
multiGraph.addDependency(inIdProp, outIdProp);
// only store callback inputs and outputs during the first pass
if (firstPass) {
cbIn[cbIn.length - 1].push(inIdProp);
cbOut[cbOut.length - 1].push(outIdProp);
}
}
parsedDependencies.forEach(dependency => {
var outputs = dependency.outputs,
inputs = dependency.inputs;
// new callback, add an empty array for its inputs and outputs
cbIn.push([]);
cbOut.push([]);
function addOutputToMulti(outIdFinal, outIdProp) {
if (!config.validate_callbacks) return;
multiGraph.addNode(outIdProp);
inputs.forEach(inObj => {
var inId = inObj.id,
property = inObj.property;
if (typeof inId === 'object') {
var inIdList = makeAllIds(inId, outIdFinal);
inIdList.forEach(id => {
addInputToMulti((0, _dependencies_ts.combineIdAndProp)({
id,
property
}), outIdProp);
});
} else {
addInputToMulti((0, _dependencies_ts.combineIdAndProp)(inObj), outIdProp);
}
});
}
// We'll continue to use dep.output as its id, but add outputs as well
// for convenience and symmetry with the structure of inputs and state.
// Also collect MATCH keys in the output (all outputs must share these)
// and ALL keys in the first output (need not be shared but we'll use
// the first output for calculations) for later convenience.
var _findWildcardKeys3 = findWildcardKeys(outputs.length ? outputs[0].id : undefined),
matchKeys = _findWildcardKeys3.matchKeys;
var firstSingleOutput = (0, _ramda.findIndex)(o => !isMultiValued(o.id), outputs);
var finalDependency = (0, _ramda.mergeRight)({
matchKeys,
firstSingleOutput,
outputs
}, dependency);
outputs.forEach(outIdProp => {
var outId = outIdProp.id,
property = outIdProp.property;
// check if this output is also an input to the same callback
var alsoInput;
if (config.validate_callbacks) {
alsoInput = checkInOutOverlap(outIdProp, inputs);
}
if (typeof outId === 'object') {
if (config.validate_callbacks) {
var outIdList = makeAllIds(outId, {});
outIdList.forEach(id => {
var tempOutIdProp = {
id,
property
};
var outIdName = (0, _dependencies_ts.combineIdAndProp)(tempOutIdProp);
// if this output is also an input, add `outputTag` to the name
if (alsoInput) {
duplicateOutputs.push(tempOutIdProp);
outIdName += outputTag;
}
addOutputToMulti(id, outIdName);
});
}
addPattern(outputPatternMap, outId, property, finalDependency);
} else {
if (config.validate_callbacks) {
var outIdName = (0, _dependencies_ts.combineIdAndProp)(outIdProp);
// if this output is also an input, add `outputTag` to the name
if (alsoInput) {
duplicateOutputs.push(outIdProp);
outIdName += outputTag;
}
addOutputToMulti({}, outIdName);
}
addMap(outputMap, outId, property, finalDependency);
}
});
inputs.forEach(inputObject => {
var inId = inputObject.id,
inProp = inputObject.property;
if (typeof inId === 'object') {
addPattern(inputPatternMap, inId, inProp, finalDependency);
} else {
addMap(inputMap, inId, inProp, finalDependency);
}
});
});
outputPatterns = offloadPatterns(outputPatternMap, outputPatterns);
inputPatterns = offloadPatterns(inputPatternMap, inputPatterns);
// second pass for adding new output nodes as dependencies where needed
duplicateOutputs.forEach(dupeOutIdProp => {
var originalName = (0, _dependencies_ts.combineIdAndProp)(dupeOutIdProp);
var newName = originalName.concat(outputTag);
for (var cnt = 0; cnt < cbIn.length; cnt++) {
// check if input to the callback
if (cbIn[cnt].some(inName => inName === originalName)) {
/* make sure it's not also an output of the callback
* (this will be the original callback)
*/
if (!cbOut[cnt].some(outName => outName === newName)) {
cbOut[cnt].forEach(outName => {
addInputToMulti(newName, outName, false);
});
}
}
}
});
var end = performance.now();
if (!window.dash_component_api) {
window.dash_component_api = {};
}
window.dash_component_api.callbackGraphTime = (end - start).toFixed(2);
return finalGraphs;
}
function findWildcardKeys(id) {
var matchKeys = [];
var allsmallerKeys = [];
if (typeof id === 'object') {
(0, _ramda.forEachObjIndexed)((val, key) => {
if (val === MATCH) {
matchKeys.push(key);
} else if (val === ALLSMALLER) {
allsmallerKeys.push(key);
}
}, id);
matchKeys.sort();
allsmallerKeys.sort();
}
return {
matchKeys,
allsmallerKeys
};
}
/*
* Do the given id values `vals` match the pattern `patternVals`?
* `keys`, `patternVals`, and `vals` are all arrays, and we already know that
* we're only looking at ids with the same keys as the pattern.
*
* Optionally, include another reference set of the same - to ensure the
* correct matching of MATCH or ALLSMALLER between input and output items.
*/
function idMatch(keys, vals, patternVals, refKeys, refVals, refPatternVals) {
for (var i = 0; i < keys.length; i++) {
var val = vals[i];
var patternVal = patternVals[i];
if (patternVal.wild) {
// If we have a second id, compare the wildcard values.
// Without a second id, all wildcards pass at this stage.
if (refKeys && patternVal !== ALL) {
var refIndex = refKeys.indexOf(keys[i]);
var refPatternVal = refPatternVals[refIndex];
// Sanity check. Shouldn't ever fail this, if the back end
// did its job validating callbacks.
// You can't resolve an input against an input, because
// two ALLSMALLER's wouldn't make sense!
if (patternVal === ALLSMALLER && refPatternVal === ALLSMALLER) {
throw new Error('invalid wildcard id pair: ' + JSON.stringify({
keys,
patternVals,
vals,
refKeys,
refPatternVals,
refVals
}));
}
if (idValSort(val, refVals[refIndex]) !== (patternVal === ALLSMALLER ? -1 : refPatternVal === ALLSMALLER ? 1 : 0)) {
return false;
}
}
} else if (val !== patternVal) {
return false;
}
}
return true;
}
function getAnyVals(patternVals, vals) {
var matches = [];
for (var i = 0; i < patternVals.length; i++) {
if (patternVals[i] === MATCH) {
matches.push(vals[i]);
}
}
return matches.length ? JSON.stringify(matches) : '';
}
/*
* Does this item (input / output / state) support multiple values?
* string IDs do not; wildcard IDs only do if they contain ALL or ALLSMALLER
*/
function isMultiValued(_ref12) {
var id = _ref12.id;
return typeof id === 'object' && (0, _ramda.any)(v => v.multi, (0, _ramda.values)(id));
}
/*
* For a given output id and prop, find the callback generating it.
* If no callback is found, returns false.
* If one is found, returns:
* {
* callback: the callback spec {outputs, inputs, state etc}
* anyVals: stringified list of resolved MATCH keys we matched
* resolvedId: the "outputs" id string plus MATCH values we matched
* getOutputs: accessor function to give all resolved outputs of this
* callback. Takes `paths` as argument to apply when the callback is
* dispatched, in case a previous callback has altered the layout.
* The result is a list of {id (string or object), property (string)}
* getInputs: same for inputs
* getState: same for state
* changedPropIds: an object of {[idAndProp]: v} triggering this callback
* v = DIRECT (2): the prop was changed in the front end, so dependent
* callbacks *MUST* be executed.
* v = INDIRECT (1): the prop is expected to be changed by a callback,
* but if this is prevented, dependent callbacks may be pruned.
* initialCall: boolean, if true we don't require any changedPropIds
* to keep this callback around, as it's the initial call to populate
* this value on page load or changing part of the layout.
* By default this is true for callbacks generated by
* getCallbackByOutput, false from getCallbacksByInput.
* }
*/
function getCallbackByOutput(graphs, paths, id, prop) {
var resolve;
var callback;
var anyVals = '';
if (typeof id === 'string') {
// standard id version
var callbacks = (graphs.outputMap[id] || {})[prop];
if (callbacks) {
callback = callbacks[0];
resolve = (0, _dependencies_ts.resolveDeps)();
}
} else {
// wildcard version
var _keys = Object.keys(id).sort();
var vals = (0, _ramda.props)(_keys, id);
var keyStr = _keys.join(',');
var patterns = (graphs.outputPatterns[keyStr] || {})[prop];
if (patterns) {
for (var i = 0; i < patterns.length; i++) {
var patternVals = patterns[i].values;
if (idMatch(_keys, vals, patternVals)) {
callback = patterns[i].callbacks[0];
resolve = (0, _dependencies_ts.resolveDeps)(_keys, vals, patternVals);
anyVals = getAnyVals(patternVals, vals);
break;
}
}
}
}
if (!resolve) {
return false;
}
return (0, _dependencies_ts.makeResolvedCallback)(callback, resolve, anyVals);
}
function addResolvedFromOutputs(callback, outPattern, outs, matches) {
var out0Keys = Object.keys(outPattern.id).sort();
var out0PatternVals = (0, _ramda.props)(out0Keys, outPattern.id);
var foundCbIds = {};
outs.forEach(_ref13 => {
var outId = _ref13.id;
var outVals = (0, _ramda.props)(out0Keys, outId);
var resolved = (0, _dependencies_ts.makeResolvedCallback)(callback, (0, _dependencies_ts.resolveDeps)(out0Keys, outVals, out0PatternVals), getAnyVals(out0PatternVals, outVals));
var resolvedId = resolved.resolvedId;
if (!foundCbIds[resolvedId]) {
matches.push(resolved);
foundCbIds[resolvedId] = true;
}
});
}
function addAllResolvedFromOutputs(resolve, paths, matches) {
return callback => {
var matchKeys = callback.matchKeys,
firstSingleOutput = callback.firstSingleOutput,
outputs = callback.outputs;
if (matchKeys.length) {
var singleOutPattern = outputs[firstSingleOutput];
if (singleOutPattern) {
addResolvedFromOutputs(callback, singleOutPattern, resolve(paths)(singleOutPattern), matches);
} else {
/*
* If every output has ALL we need to reduce resolved set
* to one item per combination of MATCH values.
* That will give one result per callback invocation.
*/
var anySeen = {};
outputs.forEach(outPattern => {
var outSet = resolve(paths)(outPattern).filter(i => {
var matchStr = JSON.stringify((0, _ramda.props)(matchKeys, i.id));
if (!anySeen[matchStr]) {
anySeen[matchStr] = 1;
return true;
}
return false;
});
addResolvedFromOutputs(callback, outPattern, outSet, matches);
});
}
} else {
var cb = (0, _dependencies_ts.makeResolvedCallback)(callback, resolve, '');
matches.push(cb);
}
};
}
/*
* For a given id and prop find all callbacks it's an input of.
*
* Returns an array of objects:
* {callback, resolvedId, getOutputs, getInputs, getState}
* See getCallbackByOutput for details.
*
* Note that if the original input contains an ALLSMALLER wildcard,
* there may be many entries for the same callback, but any given output
* (with an MATCH corresponding to the input's ALLSMALLER) will only appear
* in one entry.
*/
function getWatchedKeys(id, newProps, graphs) {
if (!(id && graphs && newProps.length)) {
return [];
}
if (typeof id === 'string') {
var inputs = graphs.inputMap[id];
return inputs ? newProps.filter(newProp => inputs[newProp]) : [];
}
var keys = Object.keys(id).sort();
var vals = (0, _ramda.props)(keys, id);
var keyStr = keys.join(',');
var keyPatterns = graphs.inputPatterns[keyStr];
if (!keyPatterns) {
return [];
}
return newProps.filter(prop => {
var patterns = keyPatterns[prop];
return patterns && patterns.some(pattern => idMatch(keys, vals, pattern.values));
});
}
/*
* Return a list of all callbacks referencing a chunk of the layout,
* either as inputs or outputs.
*
* opts.outputsOnly: boolean, set true when crawling the *whole* layout,
* because outputs are enough to get everything.
* opts.removedArrayInputsOnly: boolean, set true to only look for inputs in
* wildcard arrays (ALL or ALLSMALLER), no outputs. This gets used to tell
* when the new *absence* of a given component should trigger a callback.
* opts.newPaths: paths object after the edit - to be used with
* removedArrayInputsOnly to determine if the callback still has its outputs
* opts.chunkPath: path to the new chunk - used to determine if any outputs are
* outside of this chunk, because this determines whether inputs inside the
* chunk count as having changed
*
* Returns an array of objects:
* {callback, resolvedId, getOutputs, getInputs, getState, ...etc}
* See getCallbackByOutput for details.
*/
function getUnfilteredLayoutCallbacks(graphs, paths, layoutChunk, opts) {
var outputsOnly = opts.outputsOnly,
removedArrayInputsOnly = opts.removedArrayInputsOnly,
newPaths = opts.newPaths,
chunkPath = opts.chunkPath;
var foundCbIds = {};
var callbacks = [];
function addCallback(callback) {
if (callback) {
var foundIndex = foundCbIds[callback.resolvedId];
if (foundIndex !== undefined) {
var foundCb = callbacks[foundIndex];
foundCb.changedPropIds = (0, _dependencies_ts.mergeMax)(foundCb.changedPropIds, callback.changedPropIds);
if (callback.initialCall) {
foundCb.initialCall = true;
}
} else {
foundCbIds[callback.resolvedId] = callbacks.length;
callbacks.push(callback);
}
}
}
function addCallbackIfArray(idStr) {
return cb => cb.getInputs(paths).some(ini => {
if (Array.isArray(ini) && ini.some(inij => stringifyId(inij.id) === idStr)) {
// This callback should trigger even with no changedProps,
// since the props that changed no longer exist.
// We're kind of abusing the `initialCall` flag here, it's
// more like a "final call" for the removed inputs, but
// this case is not subject to `prevent_initial_call`.
if ((0, _ramda.flatten)(cb.getOutputs(newPaths)).length) {
cb.initialCall = true;
cb.changedPropIds = {};
addCallback(cb);
}
return true;
}
return false;
});
}
function handleOneId(id, outIdCallbacks, inIdCallbacks) {
if (outIdCallbacks) {
for (var property in outIdCallbacks) {
var cb = getCallbackByOutput(graphs, paths, id, property);
if (cb) {
// callbacks found in the layout by output should always run
// unless specifically requested not to.
// ie this is the initial call of this callback even if it's
// not the page initialization but just a new layout chunk
if (!cb.callback.prevent_initial_call) {
cb.initialCall = true;
addCallback(cb);
}
}
}
}
if (!outputsOnly && inIdCallbacks) {
var maybeAddCallback = removedArrayInputsOnly ? addCallbackIfArray(stringifyId(id)) : addCallback;
var handleThisCallback = maybeAddCallback;
if (chunkPath) {
handleThisCallback = cb => {
if (!(0, _ramda.all)((0, _ramda.startsWith)(chunkPath), (0, _ramda.pluck)('path', (0, _ramda.flatten)(cb.getOutputs(paths))))) {
maybeAddCallback(cb);
}
};
}
for (var _property in inIdCallbacks) {
(0, _dependencies_ts.getCallbacksByInput)(graphs, paths, id, _property, _dependencies_ts.INDIRECT).forEach(handleThisCallback);
}
}
}
(0, _utils.crawlLayout)(layoutChunk, child => {
var id = (0, _ramda.path)(['props', 'id'], child);
if (id) {
if (typeof id === 'string' && !removedArrayInputsOnly) {
handleOneId(id, graphs.outputMap[id], graphs.inputMap[id]);
} else {
var keyStr = Object.keys(id).sort().join(',');
handleOneId(id, !removedArrayInputsOnly && graphs.outputPatterns[keyStr], graphs.inputPatterns[keyStr]);
}
}
});
return (0, _ramda.map)(cb => _objectSpread(_objectSpread({}, cb), {}, {
priority: (0, _dependencies_ts.getPriority)(graphs, paths, cb)
}), callbacks);
}