infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
885 lines (813 loc) • 99 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/master/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/master/Infusion-LICENSE.txt
*/
var fluid_3_0_0 = fluid_3_0_0 || {};
(function ($, fluid) {
"use strict";
/** NOTE: The contents of this file are by default NOT PART OF THE PUBLIC FLUID API unless explicitly annotated before the function **/
/** MODEL ACCESSOR ENGINE **/
/** Standard strategies for resolving path segments **/
fluid.model.makeEnvironmentStrategy = function (environment) {
return function (root, segment, index) {
return index === 0 && environment[segment] ?
environment[segment] : undefined;
};
};
fluid.model.defaultCreatorStrategy = function (root, segment) {
if (root[segment] === undefined) {
root[segment] = {};
return root[segment];
}
};
fluid.model.defaultFetchStrategy = function (root, segment) {
return root[segment];
};
fluid.model.funcResolverStrategy = function (root, segment) {
if (root.resolvePathSegment) {
return root.resolvePathSegment(segment);
}
};
fluid.model.traverseWithStrategy = function (root, segs, initPos, config, uncess) {
var strategies = config.strategies;
var limit = segs.length - uncess;
for (var i = initPos; i < limit; ++i) {
if (!root) {
return root;
}
var accepted;
for (var j = 0; j < strategies.length; ++j) {
accepted = strategies[j](root, segs[i], i + 1, segs);
if (accepted !== undefined) {
break; // May now short-circuit with stateless strategies
}
}
if (accepted === fluid.NO_VALUE) {
accepted = undefined;
}
root = accepted;
}
return root;
};
/* Returns both the value and the path of the value held at the supplied EL path */
fluid.model.getValueAndSegments = function (root, EL, config, initSegs) {
return fluid.model.accessWithStrategy(root, EL, fluid.NO_VALUE, config, initSegs, true);
};
// Very lightweight remnant of trundler, only used in resolvers
fluid.model.makeTrundler = function (config) {
return function (valueSeg, EL) {
return fluid.model.getValueAndSegments(valueSeg.root, EL, config, valueSeg.segs);
};
};
fluid.model.getWithStrategy = function (root, EL, config, initSegs) {
return fluid.model.accessWithStrategy(root, EL, fluid.NO_VALUE, config, initSegs);
};
fluid.model.setWithStrategy = function (root, EL, newValue, config, initSegs) {
fluid.model.accessWithStrategy(root, EL, newValue, config, initSegs);
};
fluid.model.accessWithStrategy = function (root, EL, newValue, config, initSegs, returnSegs) {
// This function is written in this unfortunate style largely for efficiency reasons. In many cases
// it should be capable of running with 0 allocations (EL is preparsed, initSegs is empty)
if (!fluid.isPrimitive(EL) && !fluid.isArrayable(EL)) {
var key = EL.type || "default";
var resolver = config.resolvers[key];
if (!resolver) {
fluid.fail("Unable to find resolver of type " + key);
}
var trundler = fluid.model.makeTrundler(config); // very lightweight trundler for resolvers
var valueSeg = {root: root, segs: initSegs};
valueSeg = resolver(valueSeg, EL, trundler);
if (EL.path && valueSeg) { // every resolver supports this piece of output resolution
valueSeg = trundler(valueSeg, EL.path);
}
return returnSegs ? valueSeg : (valueSeg ? valueSeg.root : undefined);
}
else {
return fluid.model.accessImpl(root, EL, newValue, config, initSegs, returnSegs, fluid.model.traverseWithStrategy);
}
};
// Implementation notes: The EL path manipulation utilities here are equivalents of the simpler ones
// that are provided in Fluid.js and elsewhere - they apply escaping rules to parse characters .
// as \. and \ as \\ - allowing us to process member names containing periods. These versions are mostly
// in use within model machinery, whereas the cheaper versions based on String.split(".") are mostly used
// within the IoC machinery.
// Performance testing in early 2015 suggests that modern browsers now allow these to execute slightly faster
// than the equivalent machinery written using complex regexps - therefore they will continue to be maintained
// here. However, there is still a significant performance gap with respect to the performance of String.split(".")
// especially on Chrome, so we will continue to insist that component member names do not contain a "." character
// for the time being.
// See http://jsperf.com/parsing-escaped-el for some experiments
fluid.registerNamespace("fluid.pathUtil");
fluid.pathUtil.getPathSegmentImpl = function (accept, path, i) {
var segment = null;
if (accept) {
segment = "";
}
var escaped = false;
var limit = path.length;
for (; i < limit; ++i) {
var c = path.charAt(i);
if (!escaped) {
if (c === ".") {
break;
}
else if (c === "\\") {
escaped = true;
}
else if (segment !== null) {
segment += c;
}
}
else {
escaped = false;
if (segment !== null) {
segment += c;
}
}
}
if (segment !== null) {
accept[0] = segment;
}
return i;
};
var globalAccept = []; // TODO: reentrancy risk here. This holder is here to allow parseEL to make two returns without an allocation.
/* A version of fluid.model.parseEL that apples escaping rules - this allows path segments
* to contain period characters . - characters "\" and "}" will also be escaped. WARNING -
* this current implementation is EXTREMELY slow compared to fluid.model.parseEL and should
* not be used in performance-sensitive applications */
// supported, PUBLIC API function
fluid.pathUtil.parseEL = function (path) {
var togo = [];
var index = 0;
var limit = path.length;
while (index < limit) {
var firstdot = fluid.pathUtil.getPathSegmentImpl(globalAccept, path, index);
togo.push(globalAccept[0]);
index = firstdot + 1;
}
return togo;
};
// supported, PUBLIC API function
fluid.pathUtil.composeSegment = function (prefix, toappend) {
toappend = toappend.toString();
for (var i = 0; i < toappend.length; ++i) {
var c = toappend.charAt(i);
if (c === "." || c === "\\" || c === "}") {
prefix += "\\";
}
prefix += c;
}
return prefix;
};
/* Escapes a single path segment by replacing any character ".", "\" or "}" with itself prepended by \ */
// supported, PUBLIC API function
fluid.pathUtil.escapeSegment = function (segment) {
return fluid.pathUtil.composeSegment("", segment);
};
/*
* Compose a prefix and suffix EL path, where the prefix is already escaped.
* Prefix may be empty, but not null. The suffix will become escaped.
*/
// supported, PUBLIC API function
fluid.pathUtil.composePath = function (prefix, suffix) {
if (prefix.length !== 0) {
prefix += ".";
}
return fluid.pathUtil.composeSegment(prefix, suffix);
};
/*
* Compose a set of path segments supplied as arguments into an escaped EL expression. Escaped version
* of fluid.model.composeSegments
*/
// supported, PUBLIC API function
fluid.pathUtil.composeSegments = function () {
var path = "";
for (var i = 0; i < arguments.length; ++i) {
path = fluid.pathUtil.composePath(path, arguments[i]);
}
return path;
};
/* Helpful utility for use in resolvers - matches a path which has already been parsed into segments */
fluid.pathUtil.matchSegments = function (toMatch, segs, start, end) {
if (end - start !== toMatch.length) {
return false;
}
for (var i = start; i < end; ++i) {
if (segs[i] !== toMatch[i - start]) {
return false;
}
}
return true;
};
fluid.model.unescapedParser = {
parse: fluid.model.parseEL,
compose: fluid.model.composeSegments
};
// supported, PUBLIC API record
fluid.model.defaultGetConfig = {
parser: fluid.model.unescapedParser,
strategies: [fluid.model.funcResolverStrategy, fluid.model.defaultFetchStrategy]
};
// supported, PUBLIC API record
fluid.model.defaultSetConfig = {
parser: fluid.model.unescapedParser,
strategies: [fluid.model.funcResolverStrategy, fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
};
fluid.model.escapedParser = {
parse: fluid.pathUtil.parseEL,
compose: fluid.pathUtil.composeSegments
};
// supported, PUBLIC API record
fluid.model.escapedGetConfig = {
parser: fluid.model.escapedParser,
strategies: [fluid.model.defaultFetchStrategy]
};
// supported, PUBLIC API record
fluid.model.escapedSetConfig = {
parser: fluid.model.escapedParser,
strategies: [fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
};
/** CONNECTED COMPONENTS AND TOPOLOGICAL SORTING **/
// Following "tarjan" at https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
/** Compute the strongly connected components of a graph, specified as a list of vertices and an accessor function.
* Returns an array of arrays of strongly connected vertices, with each component in topologically sorted order.
* @param {Vertex[]} vertices - An array of vertices of the graph to be processed. Each vertex object will be polluted
* with three extra fields: `tarjanIndex`, `lowIndex` and `onStack`.
* @param {Function} accessor - A function that returns the accessor vertex or vertices.
* @return {Array.<Vertex[]>} - An array of arrays of vertices.
*/
fluid.stronglyConnected = function (vertices, accessor) {
var that = {
stack: [],
accessor: accessor,
components: [],
index: 0
};
vertices.forEach(function (vertex) {
if (vertex.tarjanIndex === undefined) {
fluid.stronglyConnectedOne(vertex, that);
}
});
return that.components;
};
// Perform one round of the Tarjan search algorithm using the state structure generated in fluid.stronglyConnected
fluid.stronglyConnectedOne = function (vertex, that) {
vertex.tarjanIndex = that.index;
vertex.lowIndex = that.index;
++that.index;
that.stack.push(vertex);
vertex.onStack = true;
var outEdges = that.accessor(vertex);
outEdges.forEach(function (outVertex) {
if (outVertex.tarjanIndex === undefined) {
// Successor has not yet been visited; recurse on it
fluid.stronglyConnectedOne(outVertex, that);
vertex.lowIndex = Math.min(vertex.lowIndex, outVertex.lowIndex);
} else if (outVertex.onStack) {
// Successor is on the stack and hence in the current component
vertex.lowIndex = Math.min(vertex.lowIndex, outVertex.tarjanIndex);
}
});
// If vertex is a root node, pop the stack back as far as it and generate a component
if (vertex.lowIndex === vertex.tarjanIndex) {
var component = [], outVertex;
do {
outVertex = that.stack.pop();
outVertex.onStack = false;
component.push(outVertex);
} while (outVertex !== vertex);
that.components.push(component);
}
};
/** MODEL COMPONENT HIERARCHY AND RELAY SYSTEM **/
fluid.initRelayModel = function (that) {
fluid.deenlistModelComponent(that);
return that.model;
};
// TODO: This utility compensates for our lack of control over "wave of explosions" initialisation - we may
// catch a model when it is apparently "completely initialised" and that's the best we can do, since we have
// missed its own initial transaction
fluid.isModelComplete = function (that) {
return "model" in that && that.model !== fluid.inEvaluationMarker;
};
// Enlist this model component as part of the "initial transaction" wave - note that "special transaction" init
// is indexed by component, not by applier, and has special record type (complete + initModel), not transaction
fluid.enlistModelComponent = function (that) {
var instantiator = fluid.getInstantiator(that);
var enlist = instantiator.modelTransactions.init[that.id];
if (!enlist) {
enlist = {
that: that,
applier: fluid.getForComponent(that, "applier"), // required for FLUID-5504 even though currently unused
complete: fluid.isModelComplete(that)
};
instantiator.modelTransactions.init[that.id] = enlist;
}
return enlist;
};
fluid.clearTransactions = function () {
var instantiator = fluid.globalInstantiator;
fluid.clear(instantiator.modelTransactions);
instantiator.modelTransactions.init = {};
};
fluid.failureEvent.addListener(fluid.clearTransactions, "clearTransactions", "before:fail");
// Utility to coordinate with our crude "oscillation prevention system" which limits each link to 2 updates (presumably
// in opposite directions). In the case of the initial transaction, we need to reset the count given that genuine
// changes are arising in the system with each new enlisted model. TODO: if we ever get users operating their own
// transactions, think of a way to incorporate this into that workflow
fluid.clearLinkCounts = function (transRec, relaysAlso) {
// TODO: Separate this record out into different types of records (relays are already in their own area)
fluid.each(transRec, function (value, key) {
if (typeof(value) === "number") {
transRec[key] = 0;
} else if (relaysAlso && value.options && typeof(value.relayCount) === "number") {
value.relayCount = 0;
}
});
};
/** Compute relay dependency out arcs for a group of initialising components.
* @param {Object} transacs - Hash of component id to local ChangeApplier transaction.
* @param {Object} mrec - Hash of component id to enlisted component record.
* @return {Object} - Hash of component id to list of enlisted component record.
*/
fluid.computeInitialOutArcs = function (transacs, mrec) {
return fluid.transform(mrec, function (recel, id) {
var oneOutArcs = {};
var listeners = recel.that.applier.listeners.sortedListeners;
fluid.each(listeners, function (listener) {
if (listener.isRelay && !fluid.isExcludedChangeSource(transacs[id], listener.cond)) {
var targetId = listener.targetId;
if (targetId !== id) {
oneOutArcs[targetId] = true;
}
}
});
var oneOutArcList = Object.keys(oneOutArcs);
var togo = oneOutArcList.map(function (id) {
return mrec[id];
});
// No edge if the component is not enlisted - it will sort to the end via "completeOnInit"
fluid.remove_if(togo, function (rec) {
return rec === undefined;
});
return togo;
});
};
fluid.sortCompleteLast = function (reca, recb) {
return (reca.completeOnInit ? 1 : 0) - (recb.completeOnInit ? 1 : 0);
};
/** Operate all coordinated transactions by bringing models to their respective initial values, and then commit them all
* @param {Component} that - A representative component of the collection for which the initial transaction is to be operated
* @param {Object} mrec - The global model transaction record for the init transaction. This is a hash indexed by component id
* to a model transaction record, as registered in `fluid.enlistModelComponent`. This has members `that`, `applier`, `complete`.
*/
fluid.operateInitialTransaction = function (that, mrec) {
var transId = fluid.allocateGuid();
var transRec = fluid.getModelTransactionRec(that, transId);
var transac;
var transacs = fluid.transform(mrec, function (recel) {
transac = recel.that.applier.initiate(null, "init", transId);
transRec[recel.that.applier.applierId] = {transaction: transac};
return transac;
});
// TODO: This sort has very little effect in any current test (can be replaced by no-op - see FLUID-5339) - but
// at least can't be performed in reverse order ("FLUID-3674 event coordination test" will fail) - need more cases
// Compute the graph of init transaction relays for FLUID-6234 - one day we will have to do better than this, since there
// may be finer structure than per-component - it may be that each piece of model area participates in this relation
// differently. But this will require even more ambitious work such as fragmenting all the initial model values along
// these boundaries.
var outArcs = fluid.computeInitialOutArcs(transacs, mrec);
var arcAccessor = function (mrec) {
return outArcs[mrec.that.id];
};
var recs = fluid.values(mrec);
var components = fluid.stronglyConnected(recs, arcAccessor);
var priorityIndex = 0;
components.forEach(function (component) {
component.forEach(function (recel) {
recel.initPriority = recel.completeOnInit ? Math.Infinity : priorityIndex++;
});
});
recs.sort(function (reca, recb) {
return reca.initPriority - recb.initPriority;
});
recs.forEach(function (recel) {
var that = recel.that;
var transac = transacs[that.id];
if (recel.completeOnInit) {
fluid.initModelEvent(that, that.applier, transac, that.applier.listeners.sortedListeners);
} else {
fluid.each(recel.initModels, function (initModel) {
transac.fireChangeRequest({type: "ADD", segs: [], value: initModel});
fluid.clearLinkCounts(transRec, true);
});
}
var shadow = fluid.shadowForComponent(that);
if (shadow) { // Fix for FLUID-5869 - the component may have been destroyed during its own init transaction
shadow.modelComplete = true; // technically this is a little early, but this flag is only read in fluid.connectModelRelay
}
});
transac.commit(); // committing one representative transaction will commit them all
};
// This modelComponent has now concluded initialisation - commit its initialisation transaction if it is the last such in the wave
fluid.deenlistModelComponent = function (that) {
var instantiator = fluid.getInstantiator(that);
var mrec = instantiator.modelTransactions.init;
if (!mrec[that.id]) { // avoid double evaluation through currently hacked "members" implementation
return;
}
that.model = undefined; // Abuse of the ginger system - in fact it is "currently in evaluation" - we need to return a proper initial model value even if no init occurred yet
mrec[that.id].complete = true; // flag means - "complete as in ready to participate in this transaction"
var incomplete = fluid.find_if(mrec, function (recel) {
return recel.complete !== true;
});
if (!incomplete) {
try { // For FLUID-6195 ensure that exceptions during init relay don't leave the framework unusable
fluid.operateInitialTransaction(that, mrec);
} catch (e) {
fluid.clearTransactions();
throw e;
}
// NB: Don't call fluid.concludeTransaction since "init" is not a standard record - this occurs in commitRelays for the corresponding genuine record as usual
instantiator.modelTransactions.init = {};
}
};
fluid.parseModelReference = function (that, ref) {
var parsed = fluid.parseContextReference(ref);
parsed.segs = that.applier.parseEL(parsed.path);
return parsed;
};
/** Given a string which may represent a reference into a model, parses it into a structure holding the coordinates for resolving the reference. It specially
* detects "references into model material" by looking for the first path segment in the path reference which holds the value "model". Some of its workflow is bypassed
* in the special case of a reference representing an implicit model relay. In this case, ref will definitely be a String, and if it does not refer to model material, rather than
* raising an error, the return structure will include a field <code>nonModel: true</code>
* @param {Component} that - The component holding the reference
* @param {String} name - A human-readable string representing the type of block holding the reference - e.g. "modelListeners"
* @param {String|ModelReference} ref - The model reference to be parsed. This may have already been partially parsed at the original site - that is, a ModelReference is a
* structure containing
* segs: {String[]} An array of model path segments to be dereferenced in the target component (will become `modelSegs` in the final return)
* context: {String} An IoC reference to the component holding the model
* @param {Boolean} implicitRelay - <code>true</code> if the reference was being resolved for an implicit model relay - that is,
* whether it occured within the `model` block itself. In this case, references to non-model material are not a failure and will simply be resolved
* (by the caller) onto their targets (as constants). Otherwise, this function will issue a failure on discovering a reference to non-model material.
* @return {Object} - A structure holding:
* that {Component} The component whose model is the target of the reference. This may end up being constructed as part of the act of resolving the reference
* applier {Component} The changeApplier for the component <code>that</code>. This may end up being constructed as part of the act of resolving the reference
* modelSegs {String[]} An array of path segments into the model of the component
* path {String} the value of <code>modelSegs</code> encoded as an EL path (remove client uses of this in time)
* nonModel {Boolean} Set if <code>implicitRelay</code> was true and the reference was not into a model (modelSegs/path will not be set in this case)
* segs {String[]} Holds the full array of path segments found by parsing the original reference - only useful in <code>nonModel</code> case
*/
fluid.parseValidModelReference = function (that, name, ref, implicitRelay) {
var reject = function () {
var failArgs = ["Error in " + name + ": ", ref].concat(fluid.makeArray(arguments));
fluid.fail.apply(null, failArgs);
};
var rejectNonModel = function (value) {
reject(" must be a reference to a component with a ChangeApplier (descended from fluid.modelComponent), instead got ", value);
};
var parsed; // resolve ref into context and modelSegs
if (typeof(ref) === "string") {
if (fluid.isIoCReference(ref)) {
parsed = fluid.parseModelReference(that, ref);
var modelPoint = parsed.segs.indexOf("model");
if (modelPoint === -1) {
if (implicitRelay) {
parsed.nonModel = true;
} else {
reject(" must be a reference into a component model via a path including the segment \"model\"");
}
} else {
parsed.modelSegs = parsed.segs.slice(modelPoint + 1);
parsed.contextSegs = parsed.segs.slice(0, modelPoint);
delete parsed.path;
}
} else {
parsed = {
path: ref,
modelSegs: that.applier.parseEL(ref)
};
}
} else {
if (!fluid.isArrayable(ref.segs)) {
reject(" must contain an entry \"segs\" holding path segments referring a model path within a component");
}
parsed = {
context: ref.context,
modelSegs: fluid.expandOptions(ref.segs, that)
};
}
var contextTarget, target; // resolve target component, which defaults to "that"
if (parsed.context) {
contextTarget = fluid.resolveContext(parsed.context, that);
if (!contextTarget) {
reject(" context must be a reference to an existing component");
}
target = parsed.contextSegs ? fluid.getForComponent(contextTarget, parsed.contextSegs) : contextTarget;
} else {
target = that;
}
if (!parsed.nonModel) {
if (!fluid.isComponent(target)) {
rejectNonModel(target);
}
if (!target.applier) {
fluid.getForComponent(target, ["applier"]);
}
if (!target.applier) {
rejectNonModel(target);
}
}
parsed.that = target;
parsed.applier = target && target.applier;
if (!parsed.path) { // ChangeToApplicable amongst others rely on this
parsed.path = target && target.applier.composeSegments.apply(null, parsed.modelSegs);
}
return parsed;
};
// Gets global record for a particular transaction id, allocating if necessary - looks up applier id to transaction,
// as well as looking up source id (linkId in below) to count/true
// Through poor implementation quality, not every access passes through this function - some look up instantiator.modelTransactions directly
fluid.getModelTransactionRec = function (that, transId) {
var instantiator = fluid.getInstantiator(that);
if (!transId) {
fluid.fail("Cannot get transaction record without transaction id");
}
if (!instantiator) {
return null;
}
var transRec = instantiator.modelTransactions[transId];
if (!transRec) {
transRec = instantiator.modelTransactions[transId] = {
relays: [], // sorted array of relay elements (also appear at top level index by transaction id)
sources: {}, // hash of the global transaction sources (includes "init" but excludes "relay" and "local")
externalChanges: {} // index by applierId to changePath to listener record
};
}
return transRec;
};
fluid.recordChangeListener = function (component, applier, sourceListener, listenerId) {
var shadow = fluid.shadowForComponent(component);
fluid.recordListener(applier.modelChanged, sourceListener, shadow, listenerId);
};
/** Called when a relay listener registered using `fluid.registerDirectChangeRelay` enlists in a transaction. Opens a local
* representative of this transaction on `targetApplier`, creates and stores a "transaction element" within the global transaction
* record keyed by the target applier's id. The transaction element is also pushed onto the `relays` member of the global transaction record - they
* will be sorted by priority here when changes are fired.
* @param {TransactionRecord} transRec - The global record for the current ChangeApplier transaction as retrieved from `fluid.getModelTransactionRec`
* @param {ChangeApplier} targetApplier - The ChangeApplier to which outgoing changes will be applied. A local representative of the transaction will be opened on this applier and returned.
* @param {String} transId - The global id of this transaction
* @param {Object} options - The `options` argument supplied to `fluid.registerDirectChangeRelay`. This will be stored in the returned transaction element
* - note that only the member `update` is ever used in `fluid.model.updateRelays` - TODO: We should thin this out
* @param {Object} npOptions - Namespace and priority options
* namespace {String} [optional] The namespace attached to this relay definition
* priority {String} [optional] The (unparsed) priority attached to this relay definition
* @return {Object} A "transaction element" holding information relevant to this relay's enlistment in the current transaction. This includes fields:
* transaction {Transaction} The local representative of this transaction created on `targetApplier`
* relayCount {Integer} The number of times this relay has been activated in this transaction
* namespace {String} [optional] Namespace for this relay definition
* priority {Priority} The parsed priority definition for this relay
*/
fluid.registerRelayTransaction = function (transRec, targetApplier, transId, options, npOptions) {
var newTrans = targetApplier.initiate("relay", null, transId); // non-top-level transaction will defeat postCommit
var transEl = transRec[targetApplier.applierId] = {transaction: newTrans, relayCount: 0, namespace: npOptions.namespace, priority: npOptions.priority, options: options};
transEl.priority = fluid.parsePriority(transEl.priority, transRec.relays.length, false, "model relay");
transRec.relays.push(transEl);
return transEl;
};
// Configure this parameter to tweak the number of relays the model will attempt per transaction before bailing out with an error
fluid.relayRecursionBailout = 100;
// Used with various arg combinations from different sources. For standard "implicit relay" or fully lensed relay,
// the first 4 args will be set, and "options" will be empty
// For a model-dependent relay, this will be used in two halves - firstly, all of the model
// sources will bind to the relay transform document itself. In this case the argument "targetApplier" within "options" will be set.
// In this case, the component known as "target" is really the source - it is a component reference discovered by parsing the
// relay document.
// Secondly, the relay itself will schedule an invalidation (as if receiving change to "*" of its source - which may in most
// cases actually be empty) and play through its transducer. "Source" component itself is never empty, since it is used for listener
// degistration on destruction (check this is correct for external model relay). However, "sourceSegs" may be empty in the case
// there is no "source" component registered for the link. This change is played in a "half-transactional" way - that is, we wait
// for all other changes in the system to settle before playing the relay document, in order to minimise the chances of multiple
// firing and corruption. This is done via the "preCommit" hook registered at top level in establishModelRelay. This listener
// is transactional but it does not require the transaction to conclude in order to fire - it may be reused as many times as
// required within the "overall" transaction whilst genuine (external) changes continue to arrive.
// TODO: Vast overcomplication and generation of closure garbage. SURELY we should be able to convert this into an externalised, arg-ist form
/** Registers a listener operating one leg of a model relay relation, connecting the source and target. Called once or twice from `fluid.connectModelRelay` -
* see the comment there for the three cases involved. Note that in its case iii)B) the applier to bind to is not the one attached to `target` but is instead
* held in `options.targetApplier`.
* @param {Object} target - The target component at the end of the relay.
* @param {String[]} targetSegs - String segments representing the path in the target where outgoing changes are to be fired
* @param {Component|null} source - The source component from where changes will be listened to. May be null if the change source is a relay document.
* @param {String[]} sourceSegs - String segments representing the path in the source component's model at which changes will be listened to
* @param {String} linkId - The unique id of this relay arc. This will be used as a key within the active transaction record to look up dynamic information about
* activation of the link within that transaction (currently just an activation count)
* @param {Function|null} transducer - A function which will be invoked when a change is to be relayed. This is one of the adapters constructed in "makeTransformPackage"
* and is set in all cases other than iii)B) (collecting changes to contextualised relay). Note that this will have a member `cond` as returned from
* `fluid.model.parseRelayCondition` encoding the condition whereby changes should be excluded from the transaction. The rule encoded by the condition
* will be applied by the function within `transducer`.
* @param {Object} options -
* transactional {Boolean} `true` in case iii) - although this only represents `half-transactions`, `false` in others since these are resolved immediately with no granularity
* targetApplier {ChangeApplier} [optional] in case iii)B) holds the applier for the contextualised relay document which outgoing changes should be applied to
* sourceApplier {ChangeApplier} [optional] in case ii) holds the applier for the contextualised relay document on which we listen for outgoing changes
* @param {Object} npOptions - Namespace and priority options
* namespace {String} [optional] The namespace attached to this relay definition
* priority {String} [optional] The (unparsed) priority attached to this relay definition
*/
fluid.registerDirectChangeRelay = function (target, targetSegs, source, sourceSegs, linkId, transducer, options, npOptions) {
var targetApplier = options.targetApplier || target.applier; // first branch implies the target is a relay document
var sourceApplier = options.sourceApplier || source.applier; // first branch implies the source is a relay document - listener will be transactional
var applierId = targetApplier.applierId;
targetSegs = fluid.makeArray(targetSegs);
sourceSegs = fluid.makeArray(sourceSegs); // take copies since originals will be trashed
var sourceListener = function (newValue, oldValue, path, changeRequest, trans, applier) {
var transId = trans.id;
var transRec = fluid.getModelTransactionRec(target, transId);
if (applier && trans && !transRec[applier.applierId]) { // don't trash existing record which may contain "options" (FLUID-5397)
transRec[applier.applierId] = {transaction: trans}; // enlist the outer user's original transaction
}
var existing = transRec[applierId];
transRec[linkId] = transRec[linkId] || 0;
// Crude "oscillation prevention" system limits each link to maximum of 2 operations per cycle (presumably in opposite directions)
var relay = true; // TODO: See FLUID-5303 - we currently disable this check entirely to solve FLUID-5293 - perhaps we might remove link counts entirely
if (relay) {
++transRec[linkId];
if (transRec[linkId] > fluid.relayRecursionBailout) {
fluid.fail("Error in model relay specification at component ", target, " - operated more than " + fluid.relayRecursionBailout + " relays without model value settling - current model contents are ", trans.newHolder.model);
}
if (!existing) {
existing = fluid.registerRelayTransaction(transRec, targetApplier, transId, options, npOptions);
}
if (transducer && !options.targetApplier) {
// TODO: This is just for safety but is still unusual and now abused. The transducer doesn't need the "newValue" since all the transform information
// has been baked into the transform document itself. However, we now rely on this special signalling value to make sure we regenerate transforms in
// the "forwardAdapter"
transducer(existing.transaction, options.sourceApplier ? undefined : newValue, sourceSegs, targetSegs, changeRequest);
} else {
if (changeRequest && changeRequest.type === "DELETE") {
existing.transaction.fireChangeRequest({type: "DELETE", segs: targetSegs});
}
if (newValue !== undefined) {
existing.transaction.fireChangeRequest({type: "ADD", segs: targetSegs, value: newValue});
}
}
}
};
var spec = sourceApplier.modelChanged.addListener({
isRelay: true,
cond: transducer && transducer.cond,
targetId: target.id, // these two fields for debuggability
targetApplierId: targetApplier.id,
segs: sourceSegs,
transactional: options.transactional
}, sourceListener);
if (fluid.passLogLevel(fluid.logLevel.TRACE)) {
fluid.log(fluid.logLevel.TRACE, "Adding relay listener with listenerId " + spec.listenerId + " to source applier with id " +
sourceApplier.applierId + " from target applier with id " + applierId + " for target component with id " + target.id);
}
if (source) { // TODO - we actually may require to register on THREE sources in the case modelRelay is attached to a
// component which is neither source nor target. Note there will be problems if source, say, is destroyed and recreated,
// and holder is not - relay will in that case be lost. Need to integrate relay expressions with IoCSS.
fluid.recordChangeListener(source, sourceApplier, sourceListener, spec.listenerId);
if (target !== source) {
fluid.recordChangeListener(target, sourceApplier, sourceListener, spec.listenerId);
}
}
};
/** Connect a model relay relation between model material. This is called in three scenarios:
* i) from `fluid.parseModelRelay` when parsing an uncontextualised model relay (one with a static transform document), to
* directly connect the source and target of the relay
* ii) from `fluid.parseModelRelay` when parsing a contextualised model relay (one whose transform document depends on other model
* material), to connect updates emitted from the transform document's applier onto the relay ends (both source and target)
* iii) from `fluid.parseImplicitRelay` when parsing model references found within contextualised model relay to bind changes emitted
* from the target of the reference onto the transform document's applier. These may apply directly to another component's model (in its case
* A) or apply to a relay document (in its case B)
*
* This function will make one or two calls to `fluid.registerDirectChangeRelay` in order to set up each leg of any required relay.
* Note that in case iii)B) the component referred to as our argument `target` is actually the "source" of the changes (that is, the one encountered
* while traversing the transform document), and our argument `source` is the component holding the transform, and so
* the call to `fluid.registerDirectChangeRelay` will have `source` and `target` reversed (`fluid.registerDirectChangeRelay` will bind to the `targetApplier`
* in the options rather than source's applier).
* @param {Component} source - The component holding the material giving rise to the relay, or the one referred to by the `source` member
* of the configuration in case ii), if there is one
* @param {Array|null} sourceSegs - An array of parsed string segments of the `source` relay reference in case i), or the offset into the transform
* document of the reference component in case iii), otherwise `null` (case ii))
* @param {Component} target - The component holding the model relay `target` in cases i) and ii), or the component at the other end of
* the model reference in case iii) (in this case in fact a "source" for the changes.
* @param {Array} targetSegs - An array of parsed string segments of the `target` reference in cases i) and ii), or of the model reference in
* case iii)
* @param {Object} options - A structure describing the relay, allowing discrimination of the various cases above. This is derived from the return from
* `fluid.makeTransformPackage` but will have some members filtered in different cases. This contains members:
* update {Function} A function to be called at the end of a "half-transaction" when all pending updates have been applied to the document's applier.
* This discriminates case iii)
* targetApplier {ChangeApplier} The ChangeApplier for the relay document, in case iii)B)
* forwardApplier (ChangeApplier} The ChangeApplier for the relay document, in cases ii) and iii)B) (only used in latter case)
* forwardAdapter {Adapter} A function accepting (transaction, newValue) to pass through the forward leg of the relay. Contains a member `cond` holding the parsed relay condition.
* backwardAdapter {Adapter} A function accepting (transaction, newValue) to pass through the backward leg of the relay. Contains a member `cond` holding the parsed relay condition.
* namespace {String} Namespace for any relay definition
* priority {String} Priority for any relay definition or synthetic "first" for iii)A)
*/
fluid.connectModelRelay = function (source, sourceSegs, target, targetSegs, options) {
var linkId = fluid.allocateGuid();
function enlistComponent(component) {
var enlist = fluid.enlistModelComponent(component);
if (enlist.complete) {
var shadow = fluid.shadowForComponent(component);
if (shadow.modelComplete) {
enlist.completeOnInit = true;
}
}
}
enlistComponent(target);
enlistComponent(source); // role of "source" and "target" are swapped in case iii)B)
var npOptions = fluid.filterKeys(options, ["namespace", "priority"]);
if (options.update) { // it is a call for a relay document - ii) or iii)B)
if (options.targetApplier) { // case iii)B)
// We are in the middle of parsing a contextualised relay, and this call has arrived via its parseImplicitRelay.
// register changes from the target model onto changes to the model relay document
fluid.registerDirectChangeRelay(source, sourceSegs, target, targetSegs, linkId, null, {
transactional: false,
targetApplier: options.targetApplier,
update: options.update
}, npOptions);
} else { // case ii), contextualised relay overall output
// Rather than bind source-source, instead register the "half-transactional" listener which binds changes
// from the relay document itself onto the target
fluid.registerDirectChangeRelay(target, targetSegs, source, [], linkId + "-transform", options.forwardAdapter, {transactional: true, sourceApplier: options.forwardApplier}, npOptions);
}
} else { // case i) or iii)A): more efficient, old-fashioned branch where relay is uncontextualised
fluid.registerDirectChangeRelay(target, targetSegs, source, sourceSegs, linkId, options.forwardAdapter, {transactional: false}, npOptions);
fluid.registerDirectChangeRelay(source, sourceSegs, target, targetSegs, linkId, options.backwardAdapter, {transactional: false}, npOptions);
}
};
fluid.parseSourceExclusionSpec = function (targetSpec, sourceSpec) {
targetSpec.excludeSource = fluid.arrayToHash(fluid.makeArray(sourceSpec.excludeSource || (sourceSpec.includeSource ? "*" : undefined)));
targetSpec.includeSource = fluid.arrayToHash(fluid.makeArray(sourceSpec.includeSource));
return targetSpec;
};
/** Determines whether the supplied transaction should have changes not propagated into it as a result of being excluded by a
* condition specification.
* @param {Transaction} transaction - A local ChangeApplier transaction, with member `fullSources` holding all currently active sources
* @param {ConditionSpec} spec - A parsed relay condition specification, as returned from `fluid.model.parseRelayCondition`.
* @return {Boolean} `true` if changes should be excluded from the supplied transaction according to the supplied specification
*/
fluid.isExcludedChangeSource = function (transaction, spec) {
if (!spec || !spec.excludeSource) { // mergeModelListeners initModelEvent fabricates a fake spec that bypasses processing
return false;
}
var excluded = spec.excludeSource["*"];
for (var source in transaction.fullSources) {
if (spec.excludeSource[source]) {
excluded = true;
}
if (spec.includeSource[source]) {
excluded = false;
}
}
return excluded;
};
fluid.model.guardedAdapter = function (transaction, cond, func, args) {
if (!fluid.isExcludedChangeSource(transaction, cond) && func !== fluid.model.transform.uninvertibleTransform) {
func.apply(null, args);
}
};
// TODO: This rather crummy function is the only site with a hard use of "path" as String
fluid.transformToAdapter = function (transform, targetPath) {
var basedTransform = {};
basedTransform[targetPath] = transform; // TODO: Faulty with respect to escaping rules
return function (trans, newValue, sourceSegs, targetSegs, changeRequest) {
if (changeRequest && changeRequest.type === "DELETE") {
trans.fireChangeRequest({type: "DELETE", path: targetPath}); // avoid mouse droppings in target document for FLUID-5585
}
// TODO: More efficient model that can only run invalidated portion of transform (need to access changeMap of source transaction)
fluid.model.transformWithRules(newValue, basedTransform, {finalApplier: trans});
};
};
// TODO: sourcePath and targetPath should really be converted to segs to avoid excess work in parseValidModelReference
fluid.makeTransformPackage = function (componentThat, transform, sourcePath, targetPath, forwardCond, backwardCond, namespace, priority) {
var that = {
forwardHolder: {model: transform},
backwardHolder: {model: null}
};
that.generateAdapters = function (trans) {
// can't commit "half-transaction" or events will fire - violate encapsulation in this way
that.forwardAdapterImpl = fluid.transformToAdapter(trans ? trans.newHolder.model : that.forwardHolder.model, targetPath);
if (sourcePath !== null) {
var inverted = fluid.model.transform.invertConfiguration(transform);
if (inverted !== fluid.model.transform.uninvertibleTransform) {
that.backwardHolder.model = inverted;
that.backwardAdapterImpl = fluid.transformToAdapter(that.backwardHolder.model, sourcePath);
} else {
that.backwardAdapterImpl = inverted;
}
}
};
that.forwardAdapter = function (transaction, newValue) { // create a stable function reference for this possibly changing adapter
if (newValue === undefined) {
that.generateAdapters(); // TODO: Quick fix for incorrect scheduling of invalidation/transducing
// "it so happens" that fluid