Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Now under "new rendering" idioms, this is no longer good enough. onCreate is far too late to influence model-based rendering actions, and so we need to be much more specific about when exactly we expect these actions to occur.

...

In the pre-FLUID-6148 framework this would have been unjustifiable abuse of the framework since it was advertised that users should not attempt to fire events before a component's onCreate (although this probably worked in practice under many conditions). After FLUID-6148, the much stronger emphasis on construct-time coding has implied that the use of construct-time events is a blessed technique. However, this is still fundamentally abuse of the intention of the framework and the meaning of its primitives - we have a piece of model state which only ever holds the value undefined whose only purpose is to schedule some side-effects before other values which are known to be in the model are consumed. It's a deeply unsatisfactory compromise.

A few phenomena surround this particular situation - firstly, one might ask, why didn't we simply put the map itself into the model? But in practice, the map itself is quite un-model-like material, it actually represents a "view" element from another framework, and this would have made little improvement on our situation, although we could perhaps instead got the advantage of abusing some other framework primitive such as a modelListener.

A better answer to this question might have stepped back and considered - at what level does Infusion actually solve problems like this at all? And whilst there is some small assistance for dealing with these in-place issues where they occur in fine-grained elements such as free-form component options or model material, these problems are in practice only solved at the level of entire components. This implies that a more appropriate solution would be to upgrade the leaflet "Map" into a "mapHolder" component of its own, whose only purpose was to bear constructional responsibility for it. Which of course is what hortis.leafletMap originally was, before it got larded with additional design responsibility. We could then have shifted the workflow into the onCreate of that component.

...

But in practice this "mapHolder" doesn't even clearly solve our original problem. These workflow functions in practice interact with the map at the "view" level of our own design, and it still becomes no clearer that we should expect the "map" member  member of the held component to not be effectively addressable before "onCreate" of  of the child component has finished. We could perhaps arrange this to be so by ensuring that the mapHolder was not a modelComponent but again this represents expecting a totally unrealistic insight into the framework's scheduling primitives on the part of the designer.

...

Here we have a nested value, "arrowGeometry.length" whose  whose value depends on "polar.length" - but in order to consume the latter, we first need to consume "arrowGeometry.start"  - which necessarily restarts the process of evaluating "arrowGeometry" at  at a trunk node in the options, hence the name "retrunking". This involves a version of our "in-place" conundrum. If "arrowGeometry" were  were a classic, functional value, until we had computed all of its children, it would have to be said to be "in evaluation" - and so in this geometry it is impossible that there could be a classical function computing its value and then returning it.

The framework has been resistant to elementary versions of retrunking bugs like the above for many years (although we should return to how it manages this in detail shortly), but a more elaborate version of the bug surfaced recently after the FLUID-6145 refactoring6148 refactoring

Code Block
fluid.defaults("fluid.tests.retrunkingII", {
    gradeNames: "fluid.component",
    dom: "@expand:fluid.identity({that}.options.selectors)",
    selectors: {
        svg: ".flc-bagatelle-svg",
        taxonDisplay: ".fld-bagatelle-taxonDisplay",
        autocomplete: ".fld-bagatelle-autocomplete",
        segment: ".fld-bagatelle-segment",
        phyloPic: ".fld-bagatelle-phyloPic",
        mousable: "@expand:fluid.tests.FLUID4930combine({that}.options.selectors.segment, {that}.options.selectors.phyloPic)"
    }
});

The setup is extremely similar to the above, but we have left a placeholder for the evaluation of a member "dom" whose dataflow mirrors that of a standard fluid.viewComponent. This sets in motion the process of consuming the entire options tree at "selectors", which amongst itself contains a cyclic retrunking, that is, a member who consumes a sibling via an expander. This ends up confusing the framework's workflow, since the first data demand for the trunk value "selectors" marks the entire tree as "in evaluation", and any further attempts to revisit it encounter this marker rather than the currently populating trunk value.

All this raises rather deep philosophical issues about what it means for a value to be "fully evaluated", especially if that value classically appears to be a container for other values. This clearly has a different meaning depending on the nature of the consumer. If this consumer is a classical function, it naturally expects it to be in the classical sense, and indeed we have a JIRA about this too - FLUID-5981. But if the consumer of the value is the framework's own machinery, the answer is clearly different, otherwise no variety of "retrunking" could be supported. This directly feeds into the "containment without dependency" [footnote - first cited in PPIG 2015 paper at https://github.com/amb26/papers/tree/master/ppig-2015, this is the central insight of the 2010 Pupusa Queue Conversation] notion that founded the entire framework. From an OO or FP view of a structure, the structure "depends on" its members, and from our point of view it doesn't.

...

But to return to the theme at the head of the page, implementing such a process of "discovery" in a conventional programming language leads our "functions" to have a very peculiar appearance, and also a rather stereotypical one - they necessarily must accept some kind of more or less massive "environment" as an argument, in order that they may deliver a freshly discovered value "in-place" into its appropriate spatial position at the very earliest possible moment. In terms of the base programming language, this is a huge weakness - the function as well as being massively impure, has a massive environmental dependency that greatly restricts its reuse. It is for this reason that taking the hit of "being an Infusion" is something that we want to incur as rarely as possible, despite our commitment to pluralities in Infusions.

Object Delivery Sites Through The Ages

Let's now survey what these sites actually look like within Infusion, starting at the small scale where individual options values are "delivered":

...

Delivery of Option Values

Our first tour takes in the site where individual options values are discovered. This part of the code has not changed substantially since the original "ginger world" rewrite of ca. 2012 described in FLUID-4330, but is due for a massive rewrite once we can afford to get going with the FLUID-5304 "Infusion Compiler" - currently at FluidIoC.js line 3099:

Code Block
languagejs
firstline3099
linenumberstrue
    fluid.expandSource = function (options, target, i, segs, deliverer, source, policy, recurse) {
        var expanded, isTrunk;
        var thisPolicy = fluid.derefMergePolicy(policy);
        if (typeof (source) === "string" && !thisPolicy.noexpand) {
            if (!options.defaultEL || source.charAt(0) === "{") { // hard-code this for performance
                fluid.pushActivity("expandContextValue", "expanding context value %source held at path %path", {source: source, path: fluid.path.apply(null, segs.slice(0, i))});
                expanded = fluid.copy(fluid.resolveContextValue(source, options));
                fluid.popActivity(1);
            } else {
                expanded = source;
            }
        }
        else if (thisPolicy.noexpand || fluid.isUnexpandable(source)) {
            expanded = fluid.copy(source);
        }
        else if (source.expander) {
            expanded = fluid.expandExpander(deliverer, source, options);
        }
        else {
            expanded = fluid.freshContainer(source);
            isTrunk = true;
        } // 2nd branch was another partial site for FLUID-6428 but no longer seems to be needed
        if (expanded !== fluid.NO_VALUE /* && source !== undefined */) {
            deliverer(expanded);
        }
        if (isTrunk) {
            recurse(expanded, source, i, segs, policy);
        }
        return expanded;
    };

Note the use of "deliverer" at line 3122. This classically fits the pattern we are describing - the fully expanded value must be delivered in-place immediately since we are just about to recurse on further nested values which may end up referring to it - yet this function can't be expected to know its address. This is by now one of the most hated code sites in the framework and one of the most classically optimised away once we build FLUID-5304.

Delivery of Component Values

Next we turn to the wider scale - exactly the same phenomenon occurs with respect to entire components. One of the key realisations in the (current) FLUID-6148 rewrite is that the site of component delivery needed to become completely focused - that is, the minimum time must elapse between construction of the actual component reference and its registration in place in the component tree. Here is the setup for this in the previous world (with the basic structure of the ca. 2012 framework as above, but tweaked in mid-2015, actually at the time of writing, March 2020, still in current master):

Code Block

    fluid.initDependent = function (that, name, localRecord) {
        if (that[name]) { return; }
 ....
        else if (component.type) {
            var type = fluid.expandImmediate(component.type, that, localDynamic);
            if (!type) {
                fluid.fail("Error in subcomponent record: ", component.type, " could not be resolved to a type for component ", name,
                    " of parent ", that);
            }
            var invokeSpec = fluid.assembleCreatorArguments(that, type, {componentRecord: component, memberName: name, localDynamic: localDynamic});
            instance = fluid.initSubcomponentImpl(that, {type: invokeSpec.funcName}, invokeSpec.args);
        }
        // NOTE: 2012 framework here classically had - that[name] = instance
        // but in 2015 this was found to be far too late and shifted halfway up the stack, see below
        return instance;
    };
// This goes upstairs to fluid.initSubcomponentImpl, where we bizarrely remain in Fluid.js for quite a while, in deference to the mythical "non-IoC components"
    fluid.initSubcomponentImpl = function (that, entry, args) {
        var togo;
        if (typeof (entry) !== "function") {
            var entryType = typeof (entry) === "string" ? entry : entry.type;
            togo = entryType === "fluid.emptySubcomponent" ?
                null : fluid.invokeGlobalFunction(entryType, args);
        } else {
            togo = entry.apply(null, args);
        }
        return togo;
    };
// Which hits the creator function, via fluid.makeComponentCreator
    fluid.initComponent = function (componentName, initArgs) {
        var options = fluid.defaults(componentName);
        if (!options.gradeNames) {
            fluid.fail("Cannot initialise component " + componentName + " which has no gradeName registered");
        }
        var args = [componentName].concat(fluid.makeArray(initArgs));
        var that;
        fluid.pushActivity("initComponent", "constructing component of type %componentName with arguments %initArgs",
            {componentName: componentName, initArgs: initArgs});
        that = fluid.invokeGlobalFunction(options.initFunction, args);
...
        that.events.onCreate.fire(that);
        fluid.popActivity();
        return that;
    };
// Which hits an old-fashioned "concrete creator function" e.g. fluid.initLittleComponent - FINALLY WE GET TO THE POINT WHERE THE REFERENCE IS CONSTRUCTED!
    fluid.initLittleComponent = function (name, userOptions, localOptions, receiver) {
        var that = fluid.typeTag(name);
        that.lifecycleStatus = "constructing";
        localOptions = localOptions || {gradeNames: "fluid.component"};

        that.destroy = fluid.makeRootDestroy(that); // overwritten by FluidIoC for constructed subcomponents
        var mergeOptions = fluid.mergeComponentOptions(that, name, userOptions, localOptions);
        mergeOptions.exceptions = {members: {model: true, modelRelay: true}}; // don't evaluate these in "early flooding" - they must be fetched explicitly
        var options = that.options;
        that.events = {};
        // deliver to a non-IoC side early receiver of the component (currently only initView)
        (receiver || fluid.identity)(that, options, mergeOptions.strategy);
        fluid.computeDynamicComponents(that, mergeOptions);

        // TODO: ****THIS**** is the point we must deliver and suspend!! Construct the "component skeleton" first, and then continue
        // for as long as we can continue to find components.
        for (var i = 0; i < mergeOptions.mergeBlocks.length; ++i) {
            mergeOptions.mergeBlocks[i].initter();
        }
        mergeOptions.initter();
        delete options.mergePolicy;

        fluid.instantiateFirers(that, options);
        fluid.mergeListeners(that, that.events, options.listeners);

        return that;
    };
// Still in Fluid.js, we are about to bat back to FluidIoC.js for "expandComponentOptions"
    fluid.mergeComponentOptions = function (that, componentName, userOptions, localOptions) {
        var rawDefaults = fluid.rawDefaults(componentName);
        var defaults = fluid.getMergedDefaults(componentName, rawDefaults && rawDefaults.gradeNames ? null : localOptions.gradeNames);
        var sharedMergePolicy = {};

        var mergeBlocks = [];

        if (fluid.expandComponentOptions) {
            mergeBlocks = mergeBlocks.concat(fluid.expandComponentOptions(sharedMergePolicy, defaults, userOptions, that));
        }
        ....
    }
// Here we go - in fluid.expandComponentOptions we prepare to report the component's existence seemingly as an irrelevant side-effect
    // This is the initial entry point from the non-IoC side reporting the first presence of a new component - called from fluid.mergeComponentOptions
    fluid.expandComponentOptions = function (mergePolicy, defaults, userOptions, that) {
        var initRecord = userOptions; // might have been tunnelled through "userOptions" from "assembleCreatorArguments"
        var instantiator = userOptions && userOptions.marker === fluid.EXPAND ? userOptions.instantiator : null;
        fluid.pushActivity("expandComponentOptions", "expanding component options %options with record %record for component %that",
            {options: instantiator ? userOptions.mergeRecords.user : userOptions, record: initRecord, that: that});
        if (!instantiator) { // it is a top-level component which needs to be attached to the global root
            instantiator = fluid.globalInstantiator;
            initRecord = { // upgrade "userOptions" to the same format produced by fluid.assembleCreatorArguments via the subcomponent route
                mergeRecords: {user: {options: fluid.expandCompact(userOptions, true)}},
                memberName: fluid.computeGlobalMemberName(that),
                instantiator: instantiator,
                parentThat: fluid.rootComponent
            };
        }
        that.destroy = fluid.fabricateDestroyMethod(initRecord.parentThat, initRecord.memberName, instantiator, that);

        instantiator.recordKnownComponent(initRecord.parentThat, that, initRecord.memberName, true);
        var togo = expandComponentOptionsImpl(mergePolicy, defaults, initRecord, that);

        fluid.popActivity();
        return togo;
    };
// And HERE, 3 function calls deeper, we finally attach the component to its parent inside instantiator.recordKnownComponent
        that.recordKnownComponent = function (parent, component, name, created) {
            parent[name] = component;
            if (fluid.isComponent(component) || component.type === "instantiator") {
                var parentPath = that.idToShadow[parent.id].path;
                var path = that.composePath(parentPath, name);
                recordComponent(parent, component, path, name, created);
                that.events.onComponentAttach.fire(component, path, that, created);
            } else {
                fluid.fail("Cannot record non-component with value ", component, " at path \"" + name + "\" of parent ", parent);
            }
        };

The above trace shows an incredible rigmarole spanning 7 functions. Classically, the value of the component is demanded in fluid.initDependent at the base of the stack. The original 2012-era framework waited for the component to be completely constructed and control to return all the way back through the 7 functions until the reference was attached to its parent. This is what a well-behaved object-oriented framework would do. The 2015 rewrite was structurally conservative, and retained most of the function scaffolding intact, but it was morally progressive, since the value was now "delivered" in-place to the instantiator's records half-way up the stack, whilst it was partway through evaluation. However, the resulting structure is a shambolic and confused mass of old and new intentions - however, note that, crucially, we do succeed in attaching the component before "anything important" has happened to it, in particular the call to expandComponentOptionsImpl which will attempt to resolve references attached to it \[ footnote - Note that the crucial midpoint of this refactoring occurred during the following commit of Feb 11th 2015 - https://github.com/fluid-project/infusion/commit/a5a74f3f164db63641ef8c06c16a8f13a86efdb4#diff-50cd32a7983e921376821a0476122713 as part of the general rationalisation eliminating demands blocks (finally achieved in April) and the storage of records a single "global instantiator"]

Contrast this with the incredibly focused post-FLUID-6148 framework in which a component "shell" is constructed and attached immediately via instantiator.recordKnownComponent, before any further evaluation of any kind has begun:

Code Block
    /**
     * Creates the shell of a component, evaluating enough of its structure to determine its grade content but
     * without creating events or (hopefully) any side-effects
     *
     * @param {Potentia} potentia - Creation potentia for the component
     * @param {LightMerge} lightMerge - A set of lightly merged component options as returned from `fluid.lightMergeRecords`
     * @return {Component|Null} A component shell which has begun the process of construction, or `null` if the component
     * has been configured away by resolving to the type "fluid.emptySubcomponent"
     */
    fluid.initComponentShell = function (potentia, lightMerge) {
....
        var that = lightMerge.type === "fluid.emptySubcomponent" ? null : fluid.typeTag(lightMerge.type, potentia.componentId);
        if (that) {
            that.lifecycleStatus = "constructing";
            instantiator.recordKnownComponent(parentThat, that, memberName, true);
            // mergeComponentOptions computes distributeOptions which is essential for evaluating the meaning of shells everywhere
            var mergeOptions = fluid.mergeComponentOptions(that, potentia, lightMerge);
            that.events = {};
        }
        return that;
    };

In functional terms, it is still nuts, but at least much smaller nuts, and shows the very clearest expression of our notion that "place" logically precedes almost any other aspect of a component's identity - if the language permitted it, we would talk about the component's place before we even produced the object reference which corresponds to it. Our components "grow in place" like seeds planted in soil, rather than placeless values excreted into a void by atmospheric gasbags. Having "the same component" somewhere else other than where it was created has no meaning - if we want it somewhere else, we must tear down the original and reseed it in the new site, and have it grow in the new conditions - where it may well grow into something quite different.