infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
1,024 lines (932 loc) • 174 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/
"use strict";
/** NOTE: The contents of this file are by default NOT PART OF THE PUBLIC FLUID API unless explicitly annotated before the function **/
/* The Fluid "IoC System proper" - resolution of references and
* completely automated instantiation of declaratively defined
* component trees */
fluid.visitComponentChildren = function (that, visitor, options, segs) {
segs = segs || [];
var shadow = fluid.shadowForComponent(that);
for (var name in shadow.childComponents) {
var component = shadow.childComponents[name];
if (options.visited && options.visited[component.id]) {
continue;
}
segs.push(name);
if (options.visited) { // recall that this is here because we may run into a component that has been cross-injected which might otherwise cause cyclicity
options.visited[component.id] = true;
}
if (visitor(component, name, segs, segs.length - 1)) {
return true;
}
if (!options.flat) {
fluid.visitComponentChildren(component, visitor, options, segs);
}
segs.pop();
}
};
fluid.getContextHash = function (instantiator, that) {
var shadow = instantiator.idToShadow[that.id];
return shadow && shadow.contextHash;
};
fluid.componentHasGrade = function (that, gradeName) {
var contextHash = fluid.getContextHash(fluid.globalInstantiator, that);
return !!(contextHash && contextHash[gradeName]);
};
// A variant of fluid.visitComponentChildren that supplies the signature expected for fluid.matchIoCSelector
// this is: thatStack, contextHashes, memberNames, i - note, the supplied arrays are NOT writeable and shared through the iteration
fluid.visitComponentsForMatching = function (that, options, visitor) {
var instantiator = fluid.getInstantiator(that);
options = $.extend({
visited: {},
instantiator: instantiator
}, options);
var thatStack = [that];
var contextHashes = [fluid.getContextHash(instantiator, that)];
var visitorWrapper = function (component, name, segs) {
thatStack.length = 1;
contextHashes.length = 1;
for (var i = 0; i < segs.length; ++i) {
var child = thatStack[i][segs[i]];
thatStack[i + 1] = child;
contextHashes[i + 1] = fluid.getContextHash(instantiator, child) || {};
}
return visitor(component, thatStack, contextHashes, segs, segs.length);
};
fluid.visitComponentChildren(that, visitorWrapper, options, []);
};
fluid.getMemberNames = function (instantiator, thatStack) {
if (thatStack.length === 0) { // Odd edge case for FLUID-6126 from fluid.computeDistributionPriority
return [];
} else {
var path = instantiator.idToPath(fluid.peek(thatStack).id);
var segs = instantiator.parseEL(path);
// TODO: we should now have no longer shortness in the stack
segs.unshift.apply(segs, fluid.generate(thatStack.length - segs.length, ""));
return segs;
}
};
// thatStack contains an increasing list of MORE SPECIFIC thats.
// this visits all components starting from the current location (end of stack)
// in visibility order UP the tree.
fluid.visitComponentsForVisibility = function (instantiator, thatStack, visitor, options) {
options = options || {
visited: {},
flat: true,
instantiator: instantiator
};
var memberNames = fluid.getMemberNames(instantiator, thatStack);
for (var i = thatStack.length - 1; i >= 0; --i) {
var that = thatStack[i];
// explicitly visit the direct parent first
options.visited[that.id] = true;
if (visitor(that, memberNames[i], memberNames, i)) {
return;
}
if (fluid.visitComponentChildren(that, visitor, options, memberNames)) {
return;
}
memberNames.pop();
}
};
fluid.mountStrategy = function (prefix, root, toMount) {
var offset = prefix.length;
return function (target, name, i, segs) {
if (i <= prefix.length) { // Avoid OOB to not trigger deoptimisation!
return;
}
for (var j = 0; j < prefix.length; ++j) {
if (segs[j] !== prefix[j]) {
return;
}
}
return toMount(target, name, i - prefix.length, segs.slice(offset));
};
};
fluid.invokerFromRecord = function (invokerec, name, that) {
fluid.pushActivity("makeInvoker", "beginning instantiation of invoker with name %name and record %record as child of %that",
{name: name, record: invokerec, that: that});
var invoker = invokerec ? fluid.makeInvoker(that, invokerec, name) : undefined;
fluid.popActivity();
return invoker;
};
fluid.memberFromRecord = function (memberrecs, name, that) {
var shadow = fluid.shadowForComponent(that);
var togo;
for (var i = 0; i < memberrecs.length; ++i) { // memberrecs is the special "fluid.mergingArray" type which is not Arrayable
var expanded = fluid.expandImmediate(memberrecs[i], that, shadow.localRecord);
if (!fluid.isPlainObject(togo)) { // poor man's "merge" algorithm to hack FLUID-5668 for now
togo = expanded;
} else {
togo = $.extend(true, togo, expanded);
}
}
return togo;
};
fluid.resourceFromRecord = function (resourceRec, name, that) {
var resourceFetcher = fluid.getForComponent(that, "resourceFetcher");
var resourceSpec = resourceFetcher.resourceSpecs[name];
var oneFetcher = new fluid.fetchResources.FetchOne(resourceSpec, resourceFetcher);
var existing = that.resources[name];
if (existing && existing !== fluid.inEvaluationMarker) { // Resolve FLUID-6706 by returning an existing synchronously resolved resources
return existing;
} else {
var promise = oneFetcher.resourceSpec.promise;
if (!promise.disposition) {
var transRec = fluid.currentTreeTransaction();
transRec.pendingIO.push(promise);
} // No error handling here since the error handler added in workflows will abort the whole transaction
return oneFetcher;
}
};
/** Produce a "strategy" object which mechanises the work of converting a block of options material into a
* a live piece of component machinery to be mounted onto the component - e.g. an invoker, event, member or resource
* @param {Component} that - The component currently instantiating
* @param {Object} options - The component's currently evaluating options structure
* @param {Strategy} optionsStrategy - A "strategy" function which can drive further evaluation of the options structure
* @param {String} recordPath - A single path segment into the options structure which indexes the options records to be consumed
* @param {Function} recordMaker - A function converting an evaluated block of options into the material to be mounted,
* e.g. `fluid.invokerFromRecord`. Signature to this function is (Object options, String key, Component that).
* @param {String} prefix - Any prefix to be added to the path into options in order to generate the path into the final mounted material
* @param {Object} [exceptions] - Hack for FLUID-5668. Some exceptions to not undergo "flood" initialisation during `initter` since they
* self-initialise by some customised scheme
* @return {RecordStrategy} - A structure with two function members -
* {Strategy} strategy: A upstream function strategy by which evaluation of the mounted material can itself be driven
* {Function} initter: A function which can be used to trigger final "flood" initialisation of all material which has not so far been
* referenced.
*/
fluid.recordStrategy = function (that, options, optionsStrategy, recordPath, recordMaker, prefix, exceptions) {
prefix = prefix || [];
var fullyEvaluated = false;
return {
strategy: function (target, name, i) {
if (i !== 1 || fullyEvaluated) { // i !== -1 is strange hack added for forgotten reason
return;
}
var record = fluid.driveStrategy(options, [recordPath, name], optionsStrategy);
if (record === undefined) {
if (prefix.length > 0) {
fluid.fail("Reference to " + recordPath + " record with name " + name + " which is not registered for component " + fluid.dumpComponentAndPath(that));
} else {
return;
}
}
fluid.set(target, [name], fluid.inEvaluationMarker);
var member = recordMaker(record, name, that);
fluid.set(target, [name], member);
return member;
},
initter: function () {
var records = fluid.driveStrategy(options, recordPath, optionsStrategy) || {};
for (var name in records) {
if (!exceptions || !exceptions[name]) {
fluid.getForComponent(that, prefix.concat([name]));
}
}
fullyEvaluated = true;
}
};
};
fluid.makeDistributionRecord = function (contextThat, sourceRecord, sourcePath, targetSegs, exclusions, sourceType) {
sourceType = sourceType || "distribution";
fluid.pushActivity("makeDistributionRecord", "Making distribution record from source record %sourceRecord path %sourcePath to target path %targetSegs", {sourceRecord: sourceRecord, sourcePath: sourcePath, targetSegs: targetSegs});
var source = fluid.copy(fluid.get(sourceRecord, sourcePath));
fluid.each(exclusions, function (exclusion) {
fluid.model.applyChangeRequest(source, {segs: exclusion, type: "DELETE"});
});
var record = {options: {}};
fluid.model.applyChangeRequest(record, {segs: targetSegs, type: "ADD", value: source});
fluid.checkComponentRecord(record, fluid.componentRecordExpected);
fluid.popActivity();
return $.extend(record, {contextThat: contextThat, recordType: sourceType});
};
// Part of the early "distributeOptions" workflow. Given the description of the blocks to be distributed, assembles "canned" records
// suitable to be either registered into the shadow record for later or directly pushed to an existing component, as well as honouring
// any "removeSource" annotations by removing these options from the source block.
fluid.filterBlocks = function (contextThat, sourceBlocks, sourceSegs, targetSegs, exclusions, removeSource) {
var togo = [];
fluid.each(sourceBlocks, function (block) {
var source = fluid.get(block.source, sourceSegs);
if (source !== undefined) {
togo.push(fluid.makeDistributionRecord(contextThat, block.source, sourceSegs, targetSegs, exclusions, "distribution"));
var rescued = $.extend({}, source);
if (removeSource) {
fluid.model.applyChangeRequest(block.source, {segs: sourceSegs, type: "DELETE"});
}
fluid.each(exclusions, function (exclusion) {
var orig = fluid.get(rescued, exclusion);
fluid.set(block.source, sourceSegs.concat(exclusion), orig);
});
}
});
return togo;
};
// Use this peculiar signature since the actual component and shadow itself may not exist yet. Perhaps clean up with FLUID-4925
fluid.noteCollectedDistribution = function (parentShadow, memberName, distribution) {
fluid.model.setSimple(parentShadow, ["collectedDistributions", memberName, distribution.id], true);
};
fluid.isCollectedDistribution = function (parentShadow, memberName, distribution) {
return fluid.model.getSimple(parentShadow, ["collectedDistributions", memberName, distribution.id]);
};
fluid.clearCollectedDistributions = function (parentShadow, memberName) {
fluid.model.applyChangeRequest(parentShadow, {segs: ["collectedDistributions", memberName], type: "DELETE"});
};
fluid.collectDistributions = function (distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i) {
var lastMember = fluid.peek(memberNames);
if (!fluid.isCollectedDistribution(parentShadow, lastMember, distribution) &&
fluid.matchIoCSelector(distribution.selector, thatStack, contextHashes, memberNames, i)) {
distributedBlocks.push.apply(distributedBlocks, fluid.copy(distribution.blocks));
fluid.noteCollectedDistribution(parentShadow, lastMember, distribution);
}
};
// Slightly silly function to clean up the "appliedDistributions" records. In general we need to be much more aggressive both
// about clearing instantiation garbage (e.g. onCreate and most of the shadow)
// as well as caching frequently-used records such as the "thatStack" which
// would mean this function could be written in a sensible way
fluid.registerCollectedClearer = function (shadow, parentShadow, memberName) {
if (!shadow.collectedClearer && parentShadow) {
shadow.collectedClearer = function () {
fluid.clearCollectedDistributions(parentShadow, memberName);
};
}
};
fluid.receiveDistributions = function (parentThat, gradeNames, memberName, that) {
var instantiator = fluid.getInstantiator(parentThat || that);
var thatStack = instantiator.getThatStack(parentThat || that); // most specific is at end
thatStack.unshift(fluid.rootComponent);
var memberNames = fluid.getMemberNames(instantiator, thatStack);
var shadows = fluid.transform(thatStack, function (thisThat) {
return instantiator.idToShadow[thisThat.id];
});
var parentShadow = shadows[shadows.length - (parentThat ? 1 : 2)];
var contextHashes = fluid.getMembers(shadows, "contextHash");
if (parentThat) { // if called before construction of component from initComponentShell
memberNames.push(memberName);
contextHashes.push(fluid.gradeNamesToHash(gradeNames));
thatStack.push(that);
} else {
fluid.registerCollectedClearer(fluid.peek(shadows), parentShadow, fluid.peek(memberNames));
}
var distributedBlocks = [];
for (var i = 0; i < thatStack.length - 1; ++i) {
fluid.each(shadows[i].distributions, function (distribution) { // eslint-disable-line no-loop-func
fluid.collectDistributions(distributedBlocks, parentShadow, distribution, thatStack, contextHashes, memberNames, i);
});
}
return distributedBlocks;
};
fluid.computeTreeDistance = function (path1, path2) {
var i = 0;
while (i < path1.length && i < path2.length && path1[i] === path2[i]) {
++i;
}
return path1.length + path2.length - 2*i; // eslint-disable-line space-infix-ops
};
// Called from applyDistributions (immediate application route) as well as mergeRecordsToList (pre-instantiation route) AS WELL AS assembleCreatorArguments (pre-pre-instantiation route)
fluid.computeDistributionPriority = function (targetThat, distributedBlock) {
if (!distributedBlock.priority) {
var instantiator = fluid.getInstantiator(targetThat);
var targetStack = instantiator.getThatStack(targetThat);
var targetPath = fluid.getMemberNames(instantiator, targetStack);
var sourceStack = instantiator.getThatStack(distributedBlock.contextThat);
var sourcePath = fluid.getMemberNames(instantiator, sourceStack);
var distance = fluid.computeTreeDistance(targetPath, sourcePath);
distributedBlock.priority = fluid.mergeRecordTypes.distribution - distance;
}
return distributedBlock;
};
// convert "preBlocks" as produced from fluid.filterBlocks into "real blocks" suitable to be used by the expansion machinery.
fluid.applyDistributions = function (that, preBlocks, targetShadow) {
var distributedBlocks = fluid.transform(preBlocks, function (preBlock) {
return fluid.generateExpandBlock(preBlock, that, targetShadow.mergePolicy);
}, function (distributedBlock) {
return fluid.computeDistributionPriority(that, distributedBlock);
});
var mergeOptions = targetShadow.mergeOptions;
mergeOptions.mergeBlocks.push.apply(mergeOptions.mergeBlocks, distributedBlocks);
mergeOptions.updateBlocks();
return distributedBlocks;
};
// TODO: This implementation is obviously poor and has numerous flaws - in particular it does no backtracking as well as matching backwards through the selector
/** Match a parsed IoC selector against a selection of data structures representing a component's tree context.
* @param {ParsedSelector} selector - A parsed selector structure as returned from `fluid.parseSelector`.
* @param {Component[]} thatStack - An array of components ascending up the tree from the component being matched,
* which will be held in the last position.
* @param {Object[]} contextHashes - An array of context hashes as cached in the component's shadows - a hash to
* `true`/"memberName" depending on the reason the context matches
* @param {String[]} [memberNames] - An array of member names of components in their parents. This is only used in the distributeOptions route.
* @param {Number} i - One plus the index of the IoCSS head component within `thatStack` - all components before this
* index will be ignored for matching. Will have value `1` in the queryIoCSelector route.
* @return {Boolean} `true` if the selector matches the leaf component at the end of `thatStack`
*/
fluid.matchIoCSelector = function (selector, thatStack, contextHashes, memberNames, i) {
var thatpos = thatStack.length - 1;
var selpos = selector.length - 1;
while (true) {
var isChild = selector[selpos].child;
var mustMatchHere = thatpos === thatStack.length - 1 || isChild;
var that = thatStack[thatpos];
var selel = selector[selpos];
var match = true;
for (var j = 0; j < selel.predList.length; ++j) {
var pred = selel.predList[j];
var context = pred.context;
if (context && context !== "*" && !(contextHashes[thatpos][context] || memberNames[thatpos] === context)) {
match = false;
break;
}
if (pred.id && that.id !== pred.id) {
match = false;
break;
}
}
if (selpos === 0 && thatpos > i && mustMatchHere && isChild) {
match = false; // child selector must exhaust stack completely - FLUID-5029
}
if (match) {
if (selpos === 0) {
return true;
}
--thatpos;
--selpos;
}
else {
if (mustMatchHere) {
return false;
}
else {
--thatpos;
}
}
if (thatpos < i) {
return false;
}
}
return false;
};
// supported, PUBLIC API function
/** Query for all components matching a selector in a particular tree
* @param {Component} root - The root component at which to start the search
* @param {String} selector - An IoCSS selector, in form of a string. Note that since selectors supplied to this function implicitly
* match downwards, they do not contain the "head context" followed by whitespace required in the distributeOptions form. E.g.
* simply <code>"fluid.viewComponent"</code> will match all viewComponents below the root.
* @param {Boolean} flat - [Optional] <code>true</code> if the search should just be performed at top level of the component tree
* Note that with <code>flat=false</code> this search will scan every component in the tree and may well be very slow.
* @return {Component[]} An array holding all components matching the selector
*/
fluid.queryIoCSelector = function (root, selector, flat) {
var parsed = fluid.parseSelector(selector, fluid.IoCSSMatcher);
var togo = [];
fluid.visitComponentsForMatching(root, {flat: flat}, function (that, thatStack, contextHashes) {
if (fluid.matchIoCSelector(parsed, thatStack, contextHashes, [], 1)) {
togo.push(that);
}
});
return togo;
};
fluid.isIoCSSSelector = function (context) {
return context.indexOf(" ") !== -1; // simple-minded check for an IoCSS reference
};
fluid.pushDistributions = function (targetHead, selector, target, blocks) {
var targetShadow = fluid.shadowForComponent(targetHead);
var id = fluid.allocateGuid();
var distribution = {
id: id, // This id is used in clearDistributions
target: target, // Here for improved debuggability - info is duplicated in "selector"
selector: selector,
blocks: blocks
};
Object.freeze(distribution);
Object.freeze(distribution.blocks);
distribution.blocks.forEach(function (block) {
fluid.freezeRecursive(block.options);
});
fluid.pushArray(targetShadow, "distributions", distribution);
return id;
};
fluid.clearDistribution = function (targetHeadId, id) {
var targetHeadShadow = fluid.globalInstantiator.idToShadow[targetHeadId];
// By FLUID-6193, the head component may already have been destroyed, in which case the distributions are gone,
// and we have leaked only its id. In theory we may want to re-establish the distribution if the head is
// re-created, but that is a far wider issue.
if (targetHeadShadow) {
fluid.remove_if(targetHeadShadow.distributions, function (distribution) {
return distribution.id === id;
});
}
};
fluid.clearDistributions = function (shadow) {
fluid.each(shadow.outDistributions, function (outDist) {
fluid.clearDistribution(outDist.targetHeadId, outDist.distributionId);
});
};
// Modifies a parsed selector to extract and remove its head context which will be matched upwards
fluid.extractSelectorHead = function (parsedSelector) {
var predList = parsedSelector[0].predList;
var context = predList[0].context;
predList.length = 0;
return context;
};
fluid.parseExpectedOptionsPath = function (path, role) {
var segs = fluid.model.parseEL(path);
if (segs[0] !== "options") {
fluid.fail("Error in options distribution path ", path, " - only " + role + " paths beginning with \"options\" are supported");
}
return segs.slice(1);
};
fluid.replicateProperty = function (source, property, targets) {
if (source[property] !== undefined) {
fluid.each(targets, function (target) {
target[property] = source[property];
});
}
};
fluid.undistributableOptions = ["gradeNames", "distributeOptions", "argumentMap", "mergePolicy"]; // automatically added to "exclusions" of every distribution
fluid.distributeOptionsOne = function (that, record, targetRef, selector, context) {
fluid.pushActivity("distributeOptions", "parsing distributeOptions block %record %that ", {that: that, record: record});
var targetHead = fluid.resolveContext(context, that);
if (!targetHead) {
fluid.fail("Error in options distribution record ", record, " - could not resolve context {" + context + "} to a head component");
}
var thatShadow = fluid.shadowForComponent(that);
var targetSegs = fluid.model.parseEL(targetRef.path);
var preBlocks;
if (record.record !== undefined) {
preBlocks = [(fluid.makeDistributionRecord(that, record.record, [], targetSegs, []))];
}
else {
var source = fluid.parseContextReference(record.source);
if (source.context !== "that") {
fluid.fail("Error in options distribution record ", record, " only a source context of {that} is supported");
}
var sourceSegs = fluid.parseExpectedOptionsPath(source.path, "source");
var fullExclusions = fluid.makeArray(record.exclusions).concat(sourceSegs.length === 0 ? fluid.undistributableOptions : []);
var exclusions = fluid.transform(fullExclusions, function (exclusion) {
return fluid.model.parseEL(exclusion);
});
preBlocks = fluid.filterBlocks(that, thatShadow.mergeOptions.mergeBlocks, sourceSegs, targetSegs, exclusions, record.removeSource);
thatShadow.mergeOptions.updateBlocks(); // perhaps unnecessary
}
fluid.replicateProperty(record, "priority", preBlocks);
fluid.replicateProperty(record, "namespace", preBlocks);
// TODO: inline material has to be expanded in its original context!
if (selector) {
var distributionId = fluid.pushDistributions(targetHead, selector, record.target, preBlocks);
thatShadow.outDistributions = thatShadow.outDistributions || [];
thatShadow.outDistributions.push({
targetHeadId: targetHead.id,
distributionId: distributionId
});
}
else { // The component exists now, we must rebalance it
var targetShadow = fluid.shadowForComponent(targetHead);
fluid.applyDistributions(that, preBlocks, targetShadow);
}
fluid.popActivity();
};
/* Evaluate the `distributeOptions` block in the options of a component, and mount the distribution in the appropriate
* shadow for components yet to be constructed, or else apply it immediately to the merge blocks of any target
* which is currently in evaluation.
* This occurs early during the evaluation phase of the source component, during `fluid.computeComponentAccessor`
*/
fluid.distributeOptions = function (that, optionsStrategy) {
var records = fluid.driveStrategy(that.options, "distributeOptions", optionsStrategy);
fluid.each(records, function distributeOptionsOne(record) {
if (typeof(record.target) !== "string") {
fluid.fail("Error in options distribution record ", record, " a member named \"target\" must be supplied holding an IoC reference");
}
if (typeof(record.source) === "string" ^ record.record === undefined) {
fluid.fail("Error in options distribution record ", record, ": must supply either a member \"source\" holding an IoC reference or a member \"record\" holding a literal record");
}
var targetRef = fluid.parseContextReference(record.target);
var selector, context;
if (fluid.isIoCSSSelector(targetRef.context)) {
selector = fluid.parseSelector(targetRef.context, fluid.IoCSSMatcher);
context = fluid.extractSelectorHead(selector);
}
else {
context = targetRef.context;
}
if (context === "/" || context === "that") {
fluid.distributeOptionsOne(that, record, targetRef, selector, context);
} else {
var transRec = fluid.currentTreeTransaction();
transRec.deferredDistributions.push({that: that, record: record, targetRef: targetRef, selector: selector, context: context});
}
});
};
// Bitmapped constants holding reason for context name to be in scope within contextHash and childrenScope
fluid.contextName = 1;
fluid.memberName = 2;
fluid.gradeNamesToHash = function (gradeNames) {
var contextHash = {};
fluid.each(gradeNames, function (gradeName) {
if (!fluid.isReferenceOrExpander(gradeName)) {
contextHash[gradeName] = fluid.contextName;
contextHash[fluid.computeNickName(gradeName)] = fluid.contextName;
}
});
return contextHash;
};
fluid.applyToContexts = function (hash, key, disposition) {
var existing = hash[key];
hash[key] = (existing || 0) | disposition; // Resolve part of FLUID-6433
};
fluid.applyToScope = function (scope, key, value, disposition) {
var existing = scope[key];
if (!existing || (disposition & fluid.memberName)) {
scope[key] = value;
}
};
fluid.cacheShadowGrades = function (that, shadow) {
var contextHash = fluid.gradeNamesToHash(that.options && that.options.gradeNames || [that.typeName]);
// This is filtered out again in recordComponent - TODO: Ensure that ALL resolution uses the scope chain eventually
fluid.applyToContexts(contextHash, shadow.memberName, fluid.memberName);
shadow.contextHash = contextHash;
fluid.each(contextHash, function (disposition, context) {
shadow.ownScope[context] = that;
if (shadow.parentShadow && shadow.parentShadow.that.typeName !== "fluid.rootComponent") {
fluid.applyToScope(shadow.parentShadow.childrenScope, context, that, disposition);
}
});
};
// First sequence point where the mergeOptions strategy is delivered from Fluid.js - here we take care
// of both receiving and transmitting options distributions
fluid.deliverOptionsStrategy = function (that, target, mergeOptions) {
var shadow = fluid.shadowForComponent(that, shadow);
fluid.cacheShadowGrades(that, shadow);
shadow.mergeOptions = mergeOptions;
};
/** Dynamic grade closure algorithm - the following 4 functions share access to a small record structure "rec" which is
* constructed at the start of fluid.computeDynamicGrades
*/
fluid.collectDistributedGrades = function (rec) {
// Receive distributions first since these may cause arrival of more contextAwareness blocks.
var distributedBlocks = fluid.receiveDistributions(null, null, null, rec.that);
if (distributedBlocks.length > 0) {
var readyBlocks = fluid.applyDistributions(rec.that, distributedBlocks, rec.shadow);
var gradeNamesList = fluid.transform(fluid.getMembers(readyBlocks, ["source", "gradeNames"]), fluid.makeArray);
fluid.accumulateDynamicGrades(rec, fluid.flatten(gradeNamesList));
}
};
// Apply a batch of freshly acquired plain dynamic grades to the target component and recompute its options
fluid.applyDynamicGrades = function (rec) {
rec.oldGradeNames = fluid.makeArray(rec.gradeNames);
// Note that this crude algorithm doesn't allow us to determine which grades are "new" and which not // TODO: can no longer interpret comment
var newDefaults = fluid.copy(fluid.getMergedDefaults(rec.that.typeName, rec.gradeNames));
rec.gradeNames.length = 0; // acquire derivatives of dynamic grades (FLUID-5054)
rec.gradeNames.push.apply(rec.gradeNames, newDefaults.gradeNames);
fluid.each(rec.gradeNames, function (gradeName) {
if (!fluid.isReferenceOrExpander(gradeName)) {
rec.seenGrades[gradeName] = true;
}
});
var shadow = rec.shadow;
fluid.cacheShadowGrades(rec.that, shadow);
// This cheap strategy patches FLUID-5091 for now - some more sophisticated activity will take place
// at this site when we have a full fix for FLUID-5028
shadow.mergeOptions.destroyValue(["mergePolicy"]);
// TODO: Why do we do this given as we decided we are not responsive to it?
shadow.mergeOptions.destroyValue(["components"]);
shadow.mergeOptions.destroyValue(["invokers"]);
rec.defaultsBlock.source = newDefaults;
shadow.mergeOptions.updateBlocks();
shadow.mergeOptions.computeMergePolicy(); // TODO: we should really only do this if its content changed - this implies moving all options evaluation over to some (cheap) variety of the ChangeApplier
fluid.accumulateDynamicGrades(rec, newDefaults.gradeNames);
};
// Filter some newly discovered grades into their plain and dynamic queues
fluid.accumulateDynamicGrades = function (rec, newGradeNames) {
fluid.each(newGradeNames, function (gradeName) {
var flatGradeName = fluid.flattenGradeName(gradeName);
if (!rec.seenGrades[flatGradeName]) {
if (fluid.isReferenceOrExpander(gradeName)) {
rec.rawDynamic.push(gradeName);
rec.seenGrades[flatGradeName] = true;
} else if (!rec.oldGradeNames.includes(gradeName)) {
rec.plainDynamic.push(gradeName);
}
}
});
};
fluid.accumulateContextAwareGrades = function (that, rec) {
var newContextAware = [];
if (rec.gradeNames.includes("fluid.contextAware")) {
var contextAwarenessOptions = fluid.getForComponent(that, ["options", "contextAwareness"]);
newContextAware = fluid.contextAware.check(that, contextAwarenessOptions);
var lostGrade = fluid.find_if(rec.contextAware, function (gradeName) {
return !newContextAware.includes(gradeName);
});
if (lostGrade) { // The user really deserves a prize if they achieve this diagnostic
fluid.fail("Failure operating contextAwareness definition ", contextAwarenessOptions, " for component " + fluid.dumpComponentAndPath(that)
+ ": grade name " + lostGrade + " returned by an earlier round of checks was lost through a context change caused by a raw dynamic grade");
}
rec.contextAware = newContextAware;
}
return newContextAware;
};
fluid.computeDynamicGrades = function (that, shadow, strategy) {
delete that.options.gradeNames; // Recompute gradeNames for FLUID-5012 and others
var gradeNames = fluid.driveStrategy(that.options, "gradeNames", strategy); // Just acquire the reference and force eval of mergeBlocks "target", contents are wrong
gradeNames.length = 0;
// TODO: In complex distribution cases, a component might end up with multiple default blocks
var defaultsBlock = fluid.findMergeBlocks(shadow.mergeOptions.mergeBlocks, "defaults")[0];
var rec = {
that: that,
shadow: shadow,
defaultsBlock: defaultsBlock,
gradeNames: gradeNames, // remember that this array is globally shared
seenGrades: {},
plainDynamic: [], // unshared, accumulates any directly seen grades and their derivatives seen on one cycle
contextAware: [],
rawDynamic: []
};
fluid.each(shadow.mergeOptions.mergeBlocks, function (block) { // acquire parents of earlier blocks before applying later ones
gradeNames.push.apply(gradeNames, fluid.makeArray(block.target && block.target.gradeNames));
fluid.applyDynamicGrades(rec);
});
fluid.collectDistributedGrades(rec);
var checkContextAware = true; // Make a fresh check for contextAware grades after every contribution from raw dynamics
while (true) {
while (rec.plainDynamic.length > 0) {
gradeNames.push.apply(gradeNames, rec.plainDynamic);
rec.plainDynamic.length = 0;
fluid.applyDynamicGrades(rec);
fluid.collectDistributedGrades(rec);
}
if (checkContextAware) {
var newContextAware = fluid.accumulateContextAwareGrades(that, rec);
rec.plainDynamic = rec.plainDynamic.concat(newContextAware);
checkContextAware = false;
} else if (rec.rawDynamic.length > 0) {
var toexpand = rec.rawDynamic.shift();
var expanded = fluid.expandImmediate(toexpand, that, shadow.localRecord);
if (typeof(expanded) === "function") {
expanded = expanded();
}
if (expanded) {
rec.plainDynamic = rec.plainDynamic.concat(expanded);
}
checkContextAware = true;
} else {
break;
}
}
fluid.remove_if(gradeNames, fluid.isReferenceOrExpander);
if (shadow.collectedClearer) {
shadow.collectedClearer();
delete shadow.collectedClearer;
}
};
/* Second sequence point for mergeComponentOptions from Fluid.js - here we construct all further
* strategies required on the IoC side and mount them into the shadow's getConfig for universal use
* We also evaluate and broadcast any options distributions from the options' `distributeOptions`
*/
fluid.computeComponentAccessor = function (that, localRecord) {
var instantiator = fluid.globalInstantiator;
var shadow = fluid.shadowForComponent(that);
shadow.localRecord = localRecord;
// TODO: Presumably we can now simply resolve this from within the shadow potentia itself
var options = that.options;
var strategy = shadow.mergeOptions.strategy;
var optionsStrategy = fluid.mountStrategy(["options"], options, strategy);
shadow.invokerStrategy = fluid.recordStrategy(that, options, strategy, "invokers", fluid.invokerFromRecord);
shadow.eventStrategyBlock = fluid.recordStrategy(that, options, strategy, "events", fluid.eventFromRecord, ["events"]);
var eventStrategy = fluid.mountStrategy(["events"], that, shadow.eventStrategyBlock.strategy);
shadow.memberStrategy = fluid.recordStrategy(that, options, strategy, "members", fluid.memberFromRecord, null, {model: true, modelRelay: true});
// TODO: this is all hugely inefficient since we query every scheme for every path, whereas
// we should know perfectly well what kind of scheme there will be for a path, especially once we have resolved
// FLUID-5761, FLUID-5244
// Note that first two entries were swapped for FLUID-6580, since "dom.locate" was triggering resolution into the DOM binder -
// what an ancient historical blunder masked by DOM binder's faulty "not found" policy
shadow.getConfig = {strategies: [fluid.concreteStrategy, fluid.model.funcResolverStrategy,
optionsStrategy, shadow.invokerStrategy.strategy, shadow.memberStrategy.strategy, eventStrategy]};
fluid.computeDynamicGrades(that, shadow, strategy, shadow.mergeOptions.mergeBlocks);
if (shadow.contextHash["fluid.resourceLoader"]) {
shadow.resourceStrategyBlock = fluid.recordStrategy(that, options, strategy, "resources", fluid.resourceFromRecord, ["resources"]);
var resourceStrategy = fluid.mountStrategy(["resources"], that, shadow.resourceStrategyBlock.strategy);
shadow.getConfig.strategies.push(resourceStrategy);
that.resources = {};
}
fluid.distributeOptions(that, strategy);
if (shadow.contextHash["fluid.resolveRoot"]) {
var memberName;
if (shadow.contextHash["fluid.resolveRootSingle"]) {
var singleRootType = fluid.getForComponent(that, ["options", "singleRootType"]);
if (!singleRootType) {
fluid.fail("Cannot register object with grades " + Object.keys(shadow.contextHash).join(", ") + " as fluid.resolveRootSingle since it has not defined option singleRootType");
}
memberName = fluid.typeNameToMemberName(singleRootType);
} else {
memberName = fluid.computeGlobalMemberName(that.typeName, that.id);
}
var parent = fluid.resolveRootComponent;
if (parent[memberName]) {
instantiator.clearComponent(parent, memberName);
}
instantiator.recordKnownComponent(parent, that, memberName, false);
}
return shadow.getConfig;
};
// About the SHADOW:
// This holds a record of IoC information for each instantiated component.
// It is allocated at: instantiator's "recordComponent"
// It is destroyed at: instantiator's "clearConcreteComponent"
// Contents:
// path {String} Principal allocated path (point of construction) in tree
// that {Component} The component itself
// contextHash {String to Boolean} Map of context names which this component matches
// mergePolicy, mergeOptions: Machinery for last phase of options merging
// localRecord: The "local record" of special contexts for local resolution, e.g. {arguments}, {source}, etc.
// invokerStrategy, eventStrategyBlock, memberStrategy, getConfig: Junk required to operate the accessor
// listeners: Listeners registered during this component's construction, to be cleared during clearListeners
// distributions, collectedClearer: Managing options distributions
// outDistributions: A list of distributions registered from this component, signalling from distributeOptions to clearDistributions
// ownScope: A hash of names to components which are in scope from this component - populated in cacheShadowGrades
// childrenScope: A hash of names to components which are in scope because they are children of this component (BELOW own ownScope in resolution order)
// potentia: The original potentia record as supplied to registerPotentia - populated in fluid.processComponentShell
// createdTransactionId: The tree transaction id in which this component was created - populated in fluid.processComponentShell
// childComponents: Hash of key names to subcomponents
// lightMergeComponents, lightMergeDynamicComponents: signalling between fluid.processComponentShell and fluid.concludeComponentObservation
// modelSourcedDynamicComponents: signalling between fluid.processComponentShell and fluid.initModel
// From the DataBinding side:
// modelRelayEstablished: anticorruption check in fluid.establishModelRelay
// modelComplete: self-guard in notifyInitModelWorkflow
// initTransactionId: signalling from fluid.operateInitialTransaction to fluid.enlistModelComponent
// materialisedPaths: self-guard in fluid.materialiseModelPath
fluid.shadowForComponent = function (component) {
var instantiator = fluid.getInstantiator(component);
return instantiator && component ? instantiator.idToShadow[component.id] : null;
};
// Hack for FLUID-4930 - every test so far other than FLUID-4930 retrunking IV can pass without a single instance
// of the path, but apparently as a result of the options distribution this test requires to visit the expansion site twice
fluid.pathInEvaluation = function (path, paths) {
var index = paths.indexOf(path);
return index !== -1 && index !== paths.length - 1;
};
// Access the member at a particular path in a component, forcing it to be constructed gingerly if necessary
// supported, PUBLIC API function
fluid.getForComponent = function (component, path) {
var segs = fluid.model.pathToSegments(path);
if (segs.length === 0) {
return component;
}
var instantiator = fluid.globalInstantiator;
var shadow = fluid.shadowForComponent(component);
if (shadow) {
var next = fluid.get(component, segs[0], shadow.getConfig);
// Remove this appalling travesty when we eliminate fluid.get, merging, etc. in the FLUID-6143 rewrite
if (fluid.isComponent(next)) {
return fluid.getForComponent(next, segs.slice(1));
} else {
var inEvaluationPaths = instantiator.inEvaluationPaths;
var inEvaluationPath = component.id + ":" + path; // TODO: Need to reconstitute from segs since path may be segs
if (fluid.pathInEvaluation(inEvaluationPath, inEvaluationPaths)) {
fluid.fail("Error in component configuration - a circular reference was found during evaluation of path " + path + " for component " + fluid.dumpComponentAndPath(component) +
": a circular set of references was found - for more details, see the activity records following this message in the console");
} else {
inEvaluationPaths.push(inEvaluationPath);
var togo = fluid.get(component, path, shadow.getConfig);
inEvaluationPaths.pop();
return togo;
}
}
} else {
return fluid.get(component, path);
}
};
// The EL segment resolver strategy for resolving concrete members
fluid.concreteStrategy = function (component, thisSeg, index, segs) {
var atval = component[thisSeg];
if (atval === fluid.inEvaluationMarker) { // Only remaining user is currently modelRelay return, since inEvaluation hack covers ordinary material
fluid.fail("Error in component configuration - a circular reference was found during evaluation of path segment \"" + thisSeg + " of path ", segs,
"\": for more details, see the activity records following this message in the console, or issue fluid.setLogging(fluid.logLevel.TRACE) when running your application");
}
if (index > 1) {
return atval;
}
if (atval === undefined && component.hasOwnProperty(thisSeg)) { // avoid recomputing properties that have been explicitly evaluated to undefined
return fluid.NO_VALUE;
}
return atval;
};
// Listed in dependence order
fluid.frameworkGrades = ["fluid.component", "fluid.modelComponent", "fluid.viewComponent", "fluid.rendererComponent"];
fluid.filterBuiltinGrades = function (gradeNames) {
return fluid.remove_if(fluid.makeArray(gradeNames), function (gradeName) {
return fluid.frameworkGrades.indexOf(gradeName) !== -1;
});
};
fluid.dumpGradeNames = function (that) {
return that.options && that.options.gradeNames ?
" gradeNames: " + JSON.stringify(fluid.filterBuiltinGrades(that.options.gradeNames)) : "";
};
fluid.dumpThat = function (that) {
return "{ typeName: \"" + that.typeName + " id: " + that.id + "\"" + fluid.dumpGradeNames(that) + "}";
};
fluid.dumpThatStack = function (thatStack, instantiator) {
var togo = fluid.transform(thatStack, function (that) {
var path = instantiator.idToPath(that.id);
return fluid.dumpThat(that) + (path ? (" - path: " + path) : "");
});
return togo.join("\n");
};
fluid.dumpComponentPath = function (that) {
var path = fluid.pathForComponent(that);
return path ? fluid.pathUtil.composeSegments.apply(null, path) : "** no path registered for component **";
};
fluid.dumpComponentAndPath = function (that) {
return "component " + fluid.dumpThat(that) + " at path " + fluid.dumpComponentPath(that);
};
fluid.resolveContext = function (context, that, fast) {
if (context === "that") {
return that;
} else if (context === "/") {
return fluid.rootComponent;
}
// TODO: Check performance impact of this type check introduced for FLUID-5903 in a very sensitive corner
if (typeof(context) === "object") {
var innerContext = fluid.resolveContext(context.context, that, fast);
if (!fluid.isComponent(innerContext)) {
fluid.triggerMismatchedPathError(context.context, that);
}
var rawValue = fluid.getForComponent(innerContext, context.path);
// TODO: Terrible, slow dispatch for this route
var expanded = fluid.expandOptions(rawValue, that);
if (!fluid.isComponent(expanded)) {
fluid.fail("Unable to resolve recursive context expression " + fluid.renderContextReference(context) + ": the directly resolved value of " + rawValue +
" did not resolve to a component in the scope of " + fluid.dumpComponentAndPath(that) + ": got ", expanded);
}
return expanded;
} else {
var foundComponent;
var instantiator = fluid.globalInstantiator; // fluid.getInstantiator(that); // this hash lookup takes over 1us!
if (fast) {
var shadow = instantiator.idToShadow[that.id];
return shadow.ownScope[context];
} else {
var thatStack = instantiator.getFullStack(that);
fluid.visitComponentsForVisibility(instantiator, thatStack, function (component, memberName) {
var shadow = fluid.shadowForComponent(component);
var scopeValue = shadow.contextHash[context];
// Replace "memberName" member of contextHash from original site with memberName from injection site -
// need to mirror "fast" action of recordComponent in composing childrenScope
if (scopeValue && (scopeValue !== fluid.memberName) || context === memberName) {
foundComponent = component;
return true; // YOUR VISIT IS AT AN END!!
}
});
return foundComponent;
}
}
};
fluid.triggerMismatchedPathError = function (parsed, parentThat) {
var ref = fluid.renderContextReference(parsed);
fluid.fail("Failed to resolve reference " + ref + " - could not match context with name " +
parsed.context + " from " + fluid.dumpComponentAndPath(parentThat));
};
fluid.makeStackFetcher = function (parentThat, localRecord, fast) {
var fetcher = function (parsed) {
if (parentThat && parentThat.lifecycleStatus === "destroyed") {
fluid.fail("Cannot resolve reference " + fluid.renderContextReference(parsed) + " from component " +
fluid.dumpComponentAndPath(parentThat) + " which has been destroyed");
}
var context = parsed.context;
if (localRecord && context in localRecord) {
return fluid.get(localRecord[context], parsed.path);
}
var foundComponent = fluid.resolveContext(context, parentThat, fast);
if (!foundComponent && parsed.path !== "") {
fluid.triggerMismatchedPathError(parsed, parentThat);
}
return fluid.getForComponent(foundComponent, parsed.path);
};
return fetcher;
};
// TODO: Hoist all calls to this to a single expander per shadow
fluid.makeStackResolverOptions = function (parentThat, localRecord, fast) {
return $.extend(fluid.copy(fluid.rawDefaults("fluid.makeExpandOptions")), {
ELstyle: "{}",
localRecord: localRecord || {},
fetcher: fluid.makeStackFetcher(parentThat, localRecord, fast),
contextThat: parentThat,
exceptions: {members: {model: true, modelRelay: true}}
});
};
fluid.clearListeners = function (shadow) {
// TODO: bug here - "afterDestroy" listeners will be unregistered already unless they come from this component
fluid.each(shadow.listeners, function (rec) {
rec.event.removeListener(rec.listenerId || rec.listener);
});
delete shadow.listeners;
};
fluid.recordListener = function (event, listener, shadow, listenerId) {
if (eve