UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

1,024 lines (932 loc) 174 kB
/* 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