UNPKG

infusion

Version:

Infusion is an application framework for developing flexible stuff with JavaScript

955 lines (888 loc) 123 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 **/ /** 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 a accepts a vertex and returns a list of vertices connected by edges * @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) { delete vertex.lowIndex; delete vertex.tarjanIndex; delete vertex.onStack; }); 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) { // This simply has the effect of returning the fluid.inEvaluationMarker marker if an access is somehow requested // early, which will then trigger the circular evaluation failure return that.model; }; fluid.findInitModelTransaction = function (that) { var transRec = fluid.currentTreeTransaction(); if (transRec && fluid.isComponent(that)) { return transRec.initModelTransaction[that.id]; } }; // Enlist this model component as part of the "initial transaction" wave - note that "special transaction" init // is indexed by component id, not by applier, and has special record type (completeOnInit + initModel), in addition to transaction fluid.enlistModelComponent = function (that) { var treeTransaction = fluid.currentTreeTransaction(); var transId = treeTransaction.initModelTransactionId; var initModelTransaction = treeTransaction.initModelTransaction; var enlist = initModelTransaction[that.id]; if (!enlist) { var shadow = fluid.shadowForComponent(that); enlist = { that: that, applier: fluid.getForComponent(that, "applier"), initModels: [], completeOnInit: !!shadow.initTransactionId, transaction: that.applier.initiate(null, "init", transId) }; initModelTransaction[that.id] = enlist; var transRec = fluid.getModelTransactionRec(fluid.rootComponent, transId); transRec[that.applier.applierId] = {transaction: enlist.transaction}; fluid.registerMaterialisationListener(that, that.applier); } return enlist; }; fluid.clearTransactions = function () { var instantiator = fluid.globalInstantiator; fluid.clear(instantiator.modelTransactions); }; 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} initModelTransaction - Hash of component id to enlisted component record. * @return {Object} - Hash of component id to list of enlisted component records which are connected by edges */ fluid.computeInitialOutArcs = function (transacs, initModelTransaction) { return fluid.transform(initModelTransaction, 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 initModelTransaction[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); }; fluid.subscribeResourceModelUpdates = function (that, resourceMapEntry) { var treeTransaction = fluid.currentTreeTransaction(); var resourceSpec = resourceMapEntry.resourceSpec; var resourceUpdateListener = function () { // We can't go for currentTreeTransaction() in this listener because we in the "dead space" between workflow // functions where it has not been restored by the waitIO listener. Isn't the stack a sod. var initTransaction = fluid.getImmediate(treeTransaction, ["initModelTransaction", that.id]); var trans = initTransaction ? initTransaction.transaction : that.applier.initiate(); resourceMapEntry.listeners.forEach(function (oneListener) { var innerValue = fluid.getImmediate(resourceSpec, oneListener.resourceSegs); var segs = oneListener.segs; trans.change(segs, null, "DELETE"); trans.change(segs, innerValue); }); if (!initTransaction) { trans.commit(); } else { var transRec = fluid.getModelTransactionRec(fluid.rootComponent, trans.id); fluid.clearLinkCounts(transRec, true); that.applier.preCommit.fire(trans, that); } }; resourceSpec.onFetched.addListener(resourceUpdateListener); fluid.recordListener(resourceSpec.onFetched, resourceUpdateListener, fluid.shadowForComponent(that)); }; fluid.resolveResourceModelWorkflow = function (shadows, treeTransaction) { var initModelTransaction = treeTransaction.initModelTransaction; // TODO: Original comment from when this action was in operateInitialTransaction, now incomprehensible // Do this afterwards so that model listeners can be fired by concludeComponentInit shadows.forEach(function (shadow) { var that = shadow.that; fluid.registerMergedModelListeners(that, that.options.modelListeners); fluid.each(shadow.modelSourcedDynamicComponents, function (componentRecord, key) { fluid.constructLensedComponents(shadow, initModelTransaction[that.id], componentRecord.sourcesParsed, key); }); }); }; // Condense the resource map so that it is indexed by resource id, so that all model paths affected by the same // resource can be updated in a single transaction fluid.condenseResourceMap = function (resourceMap) { var byId = {}; resourceMap.forEach(function (resourceMapEntry) { var resourceSpec = resourceMapEntry.fetchOne.resourceSpec; var id = resourceSpec.transformEvent.eventId; var existing = byId[id]; if (!existing) { existing = byId[id] = { resourceSpec: resourceSpec, listeners: [] }; } existing.listeners.push({ resourceSegs: resourceMapEntry.fetchOne.segs, segs: resourceMapEntry.segs }); }); return byId; }; /** Operate all coordinated transactions by bringing models to their respective initial values, and then commit them all * @param {Object} initModelTransaction - The 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`, `completeOnInit`. * @param {String} transId - The id of the model transaction corresponding to the init model transaction */ fluid.operateInitialTransaction = function (initModelTransaction, transId) { var transacs = fluid.transform(initModelTransaction, function (recel) { /* var transac = recel.that.applier.initiate(null, "init", transId); // Note that here we (probably unnecessarily) trash any old transactions since all we are after is newHolder transRec[recel.that.applier.applierId] = {transaction: transac}; // Also store it in the init transaction record so it can be easily globbed onto in applier.fireChangeRequest recel.transaction = transac; */ return recel.transaction; }); // 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, initModelTransaction); var arcAccessor = function (initTransactionRecord) { return outArcs[initTransactionRecord.that.id]; }; var recs = fluid.values(initModelTransaction); var components = fluid.stronglyConnected(recs, arcAccessor); var priorityIndex = 0; components.forEach(function (component) { component.forEach(function (recel) { recel.initPriority = recel.completeOnInit ? Infinity : priorityIndex++; }); }); recs.sort(function (reca, recb) { return reca.initPriority - recb.initPriority; }); var transRec = fluid.getModelTransactionRec(fluid.rootComponent, transId); // Pass 1: Apply all raw new (initial) values to their respective models in the correct dependency order, before attempting to apply any // relay rules in case these end up mutually overwriting (especially likely with freshly constructed lensed components) recs.forEach(function applyInitialModelTransactionValues(recel) { var that = recel.that, applier = recel.that.applier, transac = transacs[that.id]; if (recel.completeOnInit) { // Play the stabilised model value of previously complete components into the relay network fluid.notifyModelChanges(applier.listeners.sortedListeners, "ADD", transac.oldHolder, fluid.emptyHolder, null, transac, applier, that); } else { fluid.each(recel.initModels, function (oneInitModel) { if (oneInitModel !== undefined) { transac.fireChangeRequest({type: "ADD", segs: [], value: oneInitModel}); } fluid.clearLinkCounts(transRec, true); }); } }); // Pass 2: Apply all relay rules and fetch any extra values resolved from resources whose values were themselves model-dependent recs.forEach(function updateInitialModelTransactionRelays(recel) { var that = recel.that, applier = recel.that.applier, transac = transacs[that.id]; applier.preCommit.fire(transac, that); if (!recel.completeOnInit) { var resourceMapById = fluid.condenseResourceMap(applier.resourceMap); fluid.each(resourceMapById, function (resourceMapEntry) { fluid.subscribeResourceModelUpdates(that, resourceMapEntry); }); // Repeatedly flush arrived values through relays to ensure that the rest of the model is maximally contextualised applier.earlyModelResolved.fire(that.model); applier.preCommit.fire(transac, that); // Note that if there is a further operateInitialTransaction for this same init transaction, next time we should treat it as stabilised recel.completeOnInit = true; var shadow = fluid.shadowForComponent(that); if (shadow && !shadow.initTransactionId) { // Fix for FLUID-5869 - the component may have been destroyed during its own init transaction // read in fluid.enlistModelComponent in order to compute the completeOnInit flag shadow.initTransactionId = transId; } } }); }; fluid.parseModelReference = function (that, ref) { var parsed = fluid.parseContextReference(ref); parsed.segs = fluid.pathUtil.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} permitNonModel - <code>true</code> If `true`, 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 {ParsedModelReference} - A structure holding: * that {Component|Any} 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. * in the case of a reference to "local record" material such as {arguments} or {source}, `that` may exceptionally be a non-component. * applier {ChangeApplier|Undefined} 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[]|Undefined} An array of path segments into the model of the component if this is a model reference * 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, permitNonModel) { var localRecord = fluid.shadowForComponent(that).localRecord; 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 (permitNonModel) { 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.expandImmediate(ref.segs, that, localRecord) }; fluid.each(parsed.modelSegs, function (seg, index) { if (!fluid.isValue(seg)) { reject(" did not resolve path segment reference " + ref.segs[index] + " at index " + index); } }); } var contextTarget, target; // resolve target component, which defaults to "that" if (parsed.context) { // cf. logic in fluid.makeStackFetcher if (localRecord && parsed.context in localRecord) { // It's a "source" reference for a lensed component if (parsed.context === "source" && localRecord.sourceModelReference) { target = localRecord.sourceModelReference.that; parsed.modelSegs = localRecord.sourceModelReference.modelSegs.concat(parsed.segs); parsed.nonModel = false; } else { // It's an ordinary reference to localRecord material - FLUID-5912 case target = localRecord[parsed.context]; } } else { 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; // Note this might not be a component (see FLUID-6729) parsed.applier = target && target.applier; if (!parsed.path && parsed.applier) { // ChangeToApplicable amongst others rely on this parsed.path = target && parsed.applier.composeSegments.apply(null, parsed.modelSegs); } return parsed; }; fluid.registerNamespace("fluid.materialiserRegistry"); // Currently see FluidView-browser.js // Naturally this will be put into component grades, along with workflows, once we get rid of our insufferably inefficient options system fluid.matchMaterialiserSpec = function (record, segs) { var trundle = record; var routedPath = null; for (var i = 0; i < segs.length; ++i) { var seg = segs[i]; var wildcard = trundle["*"]; if (wildcard) { routedPath = wildcard; } trundle = trundle[seg] || wildcard; if (!trundle) { break; } else if (trundle.materialiser) { return trundle; } } if (routedPath) { fluid.fail("Materialised DOM path ", segs, " did not match any registered materialiser - available paths are " + Object.keys(routedPath).join(", ")); } return null; }; /** Given a path into a component's model, look up a "materialiser" in the materialiser registry that will bind it onto * some environmental source or sink of state (these are initially just DOM-based, but in future will include things such * as resource-based models backed by DataSources etc.) * This is intended to be called during early component startup as we parse modelRelay, modelListener and changePath records * found in the component's configuration. * This method is idempotent - the same path may be materialised any number of times during startup with no further effect * after the first call. * @param {Component} that - The component holding the model path * @param {String[]} segs - Array of path segments into the component's model to be materialised */ fluid.materialiseModelPath = function (that, segs) { var shadow = fluid.shadowForComponent(that); var materialisedPath = ["materialisedPaths"].concat(segs); if (!fluid.getImmediate(shadow, materialisedPath)) { fluid.each(fluid.materialiserRegistry, function (gradeRecord, grade) { if (fluid.componentHasGrade(that, grade)) { var record = fluid.matchMaterialiserSpec(gradeRecord, segs); if (record && record.materialiser) { fluid.model.setSimple(shadow, materialisedPath, {}); var args = fluid.makeArray(record.args); // Copy segs since the materialiser will close over it and fluid.parseImplicitRelay will pop it fluid.invokeGlobalFunction(record.materialiser, [that, fluid.makeArray(segs)].concat(args)); } } }); } }; // Hard to imagine this becoming more of a bottleneck than the rest of the ChangeApplier but it is pretty // inefficient. There are many more materialisers than we expect to ever be used at one component - // we should reorganise the registry so that it exposes a single giant listener to "" fluid.materialiseAgainstValue = function (that, newValue, segs) { if (fluid.isPlainObject(newValue)) { fluid.each(newValue, function (inner, seg) { segs.push(seg); fluid.materialiseAgainstValue(that, inner, segs); segs.pop(); }); } else { fluid.materialiseModelPath(that, segs); } }; /** Register a listener global to this changeApplier that reacts to all changes by attempting to materialise their * paths. This is a kind of "halfway house" strategy since it will trigger on every change, but it at least filters * by the component grade and the model root in the materialiser registry to avoid excess triggering. The listener * is non-transactional so that we can ensure to get the listener in before it triggers for real in notifyExternal - * an awkward kind of listener-registering listener. This is invoked very early in fluid.enlistModel. * @param {Component} that - The component for which the materialisation listener is to be registered * @param {ChangeApplier} applier - The applier for the component */ fluid.registerMaterialisationListener = function (that, applier) { fluid.each(fluid.materialiserRegistry, function (gradeRecord, grade) { if (fluid.componentHasGrade(that, grade)) { fluid.each(gradeRecord, function (rest, root) { applier.modelChanged.addListener({ transactional: false, path: root }, function (newValue) { var segs = [root]; fluid.materialiseAgainstValue(that, newValue, segs); }); }); } }); }; // 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 // The supplied component is actually irrelevant for now, implementation merely looks up the instantiator's modelTransaction fluid.getModelTransactionRec = function (that, transId) { if (!transId) { fluid.fail("Cannot get transaction record without transaction id"); } var instantiator = fluid.isComponent(that) && !fluid.isDestroyed(that, true) ? fluid.globalInstantiator : null; 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. *actually not null* * @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 if (!options.targetApplier) { fluid.materialiseModelPath(target, targetSegs); } if (!options.sourceApplier) { fluid.materialiseModelPath(source, sourceSegs); } 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 transEl = 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 (!transEl) { transEl = fluid.registerRelayTransaction(transRec, targetApplier, transId, options, npOptions); } if (transducer && !options.targetApplier) { // TODO: This censoring of newValue 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" fluid.pushActivity("relayTransducer", "computing modelRelay output for rule with target path \"%targetSegs\" and namespace \"%namespace\"", {targetSegs: targetSegs, namespace: npOptions.namespace}); transducer(transEl.transaction, options.sourceApplier ? undefined : newValue, source, sourceSegs, targetSegs, changeRequest); fluid.popActivity(); } else { if (changeRequest && changeRequest.type === "DELETE") { // Rebase the incoming DELETE with respect to this relay - whilst "newValue" is correct and we could honour this by // asking fluid.notifyModelChanges to decompose this into a DELETE plus ADD, this is more efficient var deleteSegs = targetSegs.concat(changeRequest.segs.slice(sourceSegs.length)); transEl.transaction.fireChangeRequest({type: "DELETE", segs: deleteSegs}); } else if (newValue !== undefined) { transEl.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); } // TODO: Actually, source is never null - the case ii) driver below passes it on - check the effect of this registration 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 * docume