diff --git a/core/event/event-manager.js b/core/event/event-manager.js index 5aacf9ddf..1d96f0d46 100644 --- a/core/event/event-manager.js +++ b/core/event/event-manager.js @@ -3305,8 +3305,9 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } mutableEventTarget = mutableEvent.target; - eventPath = this.eventPathForTarget(mutableEventTarget); - mutableEvent._composedPath = eventPath; + // eventPath = this.eventPathForTarget(mutableEventTarget); + // mutableEvent._composedPath = eventPath; + eventPath = mutableEvent.composedPath(); // Let the delegate handle the event first @@ -3681,136 +3682,136 @@ var EventManager = exports.EventManager = Montage.specialize(/** @lends EventMan } }, - _eventPathForTargetMap : { - value: undefined - }, - - eventPathForTarget: { - enumerable: false, - value: function (target) { - - if (this.isBrowser && (this.isElement(target) || target instanceof Document || target === window)) { - return this._eventPathForDomTarget(target); - } else { - return this._eventPathForTarget(target); - } - } - }, + // _eventPathForTargetMap : { + // value: undefined + // }, - /** - * Build the event target chain for the the specified Target - * @private - */ - _eventPathForTarget: { - enumerable: false, - value: function (target) { + // eventPathForTarget: { + // enumerable: false, + // value: function (target) { - if (!target) { - return this._emptyComposedPath; - } else if(target.composedPath) { - /* - If target has a commposedPath, it's likely cached. - So in case it's mutated by a listner, we're making a copy - to guard against that. - */ - return Array.from(target.composedPath); - } else { + // if (this.isBrowser && (this.isElement(target) || target instanceof Document || target === window)) { + // return this._eventPathForDomTarget(target); + // } else { + // return this._eventPathForTarget(target); + // } + // } + // }, - var targetCandidate = target, - application = this.application, - eventPath = [], - discoveredTargets = this._eventPathForTargetMap; + // /** + // * Build the event target chain for the the specified Target + // * @private + // */ + // _eventPathForTarget: { + // enumerable: false, + // value: function (target) { - discoveredTargets.clear(); + // if (!target) { + // return this._emptyComposedPath; + // } else if(target.composedPath()) { + // /* + // If target has a commposedPath, it's likely cached. + // So in case it's mutated by a listner, we're making a copy + // to guard against that. + // */ + // return Array.from(target.composedPath()); + // } else { - // Consider the target "discovered" for less specialized detection of cycles - // discoveredTargets.set(target,true); + // var targetCandidate = target, + // application = this.application, + // eventPath = [], + // discoveredTargets = this._eventPathForTargetMap; - do { - if (!discoveredTargets.has(targetCandidate)) { - eventPath.push(targetCandidate); - discoveredTargets.set(targetCandidate,true); - } + // discoveredTargets.clear(); - targetCandidate = targetCandidate.nextTarget; + // // Consider the target "discovered" for less specialized detection of cycles + // // discoveredTargets.set(target,true); - if (!targetCandidate || discoveredTargets.has(targetCandidate)) { - targetCandidate = application; - } + // do { + // if (!discoveredTargets.has(targetCandidate)) { + // eventPath.push(targetCandidate); + // discoveredTargets.set(targetCandidate,true); + // } - if (targetCandidate && discoveredTargets.has(targetCandidate)) { - targetCandidate = null; - } - } - while (targetCandidate); + // targetCandidate = targetCandidate.nextTarget; - return eventPath; - } + // if (!targetCandidate || discoveredTargets.has(targetCandidate)) { + // targetCandidate = application; + // } - } - }, + // if (targetCandidate && discoveredTargets.has(targetCandidate)) { + // targetCandidate = null; + // } + // } + // while (targetCandidate); - _emptyComposedPath: { - value: Object.freeze([]) - }, - /** - * Build the event target chain for the the specified DOM target - * @private - */ - _eventPathForDomTarget: { - enumerable: false, - value: function (target) { + // return eventPath; + // } - if (!target) { - return this._emptyComposedPath; - } + // } + // }, - var targetCandidate = target, - targetView = targetCandidate && targetCandidate.defaultView ? targetCandidate.defaultView : window, - targetDocument = targetView.document ? targetView.document : document, - targetApplication = this.application, - previousBubblingTarget, - eventPath = []; + // _emptyComposedPath: { + // value: Object.freeze([]) + // }, + // /** + // * Build the event target chain for the the specified DOM target + // * @private + // */ + // _eventPathForDomTarget: { + // enumerable: false, + // value: function (target) { - do { - // Include the target itself as the root of the event's compsoedPath - eventPath.push(targetCandidate); - - previousBubblingTarget = targetCandidate; - // use the structural DOM hierarchy until we run out of that and need - // to give listeners on document, window, and application a chance to respond - switch (targetCandidate) { - case targetApplication: - targetCandidate = targetCandidate.parentApplication; - if (targetCandidate) { - targetApplication = targetCandidate; - } - break; - case targetView: - targetCandidate = targetApplication; - break; - case targetDocument: - targetCandidate = targetView; - break; - case targetDocument.documentElement: - targetCandidate = targetDocument; - break; - default: - targetCandidate = targetCandidate.parentNode; + // if (!target) { + // return this._emptyComposedPath; + // } - // Run out of hierarchy candidates? go up to the application - if (!targetCandidate) { - targetCandidate = targetApplication; - } + // var targetCandidate = target, + // targetView = targetCandidate && targetCandidate.defaultView ? targetCandidate.defaultView : window, + // targetDocument = targetView.document ? targetView.document : document, + // targetApplication = this.application, + // previousBubblingTarget, + // eventPath = []; + + // do { + // // Include the target itself as the root of the event's compsoedPath + // eventPath.push(targetCandidate); + + // previousBubblingTarget = targetCandidate; + // // use the structural DOM hierarchy until we run out of that and need + // // to give listeners on document, window, and application a chance to respond + // switch (targetCandidate) { + // case targetApplication: + // targetCandidate = targetCandidate.parentApplication; + // if (targetCandidate) { + // targetApplication = targetCandidate; + // } + // break; + // case targetView: + // targetCandidate = targetApplication; + // break; + // case targetDocument: + // targetCandidate = targetView; + // break; + // case targetDocument.documentElement: + // targetCandidate = targetDocument; + // break; + // default: + // targetCandidate = targetCandidate.parentNode; + + // // Run out of hierarchy candidates? go up to the application + // if (!targetCandidate) { + // targetCandidate = targetApplication; + // } - break; - } - } - while (targetCandidate && previousBubblingTarget !== targetCandidate); + // break; + // } + // } + // while (targetCandidate && previousBubblingTarget !== targetCandidate); - return eventPath; - } - }, + // return eventPath; + // } + // }, /** * @private diff --git a/core/event/mutable-event.js b/core/event/mutable-event.js index 0e414b1ac..2aabd8e32 100644 --- a/core/event/mutable-event.js +++ b/core/event/mutable-event.js @@ -5,6 +5,10 @@ const Montage = require("../core").Montage, uuid = require("../../core/uuid"), Promise = require("../promise").Promise, + currentEnvironment = require("../environment").currentEnvironment, + Element = require("../extras/element").Element, + Document = global.Document, + window = global.window, console = require('../extras/console').console; var wrapPropertyGetter = function (key, storageKey) { @@ -389,16 +393,163 @@ var wrapPropertyGetter = function (key, storageKey) { this._detail = value; } }, + + isBrowser: { + value: currentEnvironment.isBrowser + }, + _emptyComposedPath: { + value: Object.freeze([]) + }, _composedPath: { value: void 0 }, + /** + * Build the event target chain for the the specified DOM target + * @private + */ + _composedPathForDomTarget: { + enumerable: false, + value: function (target) { + + if (!target) { + return this._emptyComposedPath; + } + + var targetCandidate = target, + targetView = targetCandidate && targetCandidate.defaultView ? targetCandidate.defaultView : window, + targetDocument = targetView.document ? targetView.document : document, + targetApplication = this.application, + previousBubblingTarget, + eventPath = []; + + do { + // Include the target itself as the root of the event's compsoedPath + eventPath.push(targetCandidate); + + previousBubblingTarget = targetCandidate; + // use the structural DOM hierarchy until we run out of that and need + // to give listeners on document, window, and application a chance to respond + switch (targetCandidate) { + case targetApplication: + targetCandidate = targetCandidate.parentApplication; + if (targetCandidate) { + targetApplication = targetCandidate; + } + break; + case targetView: + targetCandidate = targetApplication; + break; + case targetDocument: + targetCandidate = targetView; + break; + case targetDocument.documentElement: + targetCandidate = targetDocument; + break; + default: + targetCandidate = targetCandidate.parentNode; + + // Run out of hierarchy candidates? go up to the application + if (!targetCandidate) { + targetCandidate = targetApplication; + } + + break; + } + } + while (targetCandidate && previousBubblingTarget !== targetCandidate); + + return eventPath; + } + }, + /** + * Build the event target chain for the the specified Target + * @private + */ + _composedPathForTarget: { + enumerable: false, + value: function (target) { + + if (!target) { + return this._emptyComposedPath; + } else if(target.composedPath) { + /* + If target has a commposedPath, it's likely cached. + So in case it's mutated by a listner, we're making a copy + to guard against that. + */ + return Array.from(target.composedPath); + } else { + + var targetCandidate = target, + application = this.application, + eventPath = [], + discoveredTargets = this._composedPathForTargetMap; + + discoveredTargets.clear(); + + // Consider the target "discovered" for less specialized detection of cycles + // discoveredTargets.set(target,true); + + do { + if (!discoveredTargets.has(targetCandidate)) { + eventPath.push(targetCandidate); + discoveredTargets.set(targetCandidate,true); + } + + targetCandidate = targetCandidate.nextTarget; + + if (!targetCandidate || discoveredTargets.has(targetCandidate)) { + targetCandidate = application; + } + + if (targetCandidate && discoveredTargets.has(targetCandidate)) { + targetCandidate = null; + } + } + while (targetCandidate); + + return eventPath; + } + + } + }, + + isElement: { + value: currentEnvironment.isBrowser ? Element.isElement : (value) => false + }, + + /** + * @private * @type {Property} - * @default {Element} null + * @default {Map} singleton shared on prototype + * + * Cache for events whose target-based composed path doesn't change over time + */ + + _composedPathForTargetMap : { + value: new Map() + }, + + composedPathForTarget: { + enumerable: false, + value: function (target) { + + if (this.isBrowser && (this.isElement(target) || target instanceof Document || target === window)) { + return this._composedPathForDomTarget(target); + } else { + return this._composedPathForTarget(target); + } + } + }, + + /** + * @type {Property} + * @return {Array} */ composedPath: { value: function () { - return this._composedPath; + return this._composedPath || (this._composedPath = this.composedPathForTarget(this.target)); } } diff --git a/data/service/data-operation.js b/data/service/data-operation.js index 7fc214580..eebc7fedc 100644 --- a/data/service/data-operation.js +++ b/data/service/data-operation.js @@ -262,6 +262,213 @@ exports.DataOperationErrorNames = DataOperationErrorNames = new Enum().initWithM value: false }, + +/** + * Topologically sorts nodes and all their ancestors, + * guaranteeing each node appears before its nextTarget. + * + * @param {object[]} startingNodes - The leaf/starting nodes + * @returns {object[]} Sorted array: dependents before their nextTarget + */ + topologicallySortMergedComposedPaths: { + value: function (startingNodes, nextTargetsMap, inDegree = new Map()) { + + // 1. Collect every node reachable via nextTarget + + let visited; + if(startingNodes instanceof Set) { + visited = startingNodes; + } else { + visited = new Set(); + const allNodes = []; + + for (const node of startingNodes) { + let current = node; + while (current && !visited.has(current)) { + visited.add(current); + allNodes.push(current); + current = current.nextTarget; + } + } + } + + // 2. Kahn's algorithm — build in-degree map based on nextTarget edges + // unless we're provided one + // const inDegree = new Map(); + // if(inDegree.size === 0) { + // for (const node of allNodes) { + // let nodeNextTargets = nextTargetsMap.get(node); + + // if (!inDegree.has(node)) inDegree.set(node, 0); + // //nodeNextTargets is a Set + // for (const nodeNextTarget of nodeNextTargets.values()) { + // console.log(value); // Output: apple, banana, cherry + // } + + // if (nodeNextTarget && inDegree.has(nodeNextTarget)) { + // inDegree.set(nodeNextTarget, inDegree.get(nodeNextTarget) + 1); + // } + // } + // } + + // 3. Queue all nodes with no incoming edges (nothing points TO them) + const queue = []; + //degree is expected to be a Set + for (const [node, degree] of inDegree) { + if (degree.size === 0) queue.push(node); + } + + // 4. Process queue + const sorted = []; + while (queue.length > 0) { + const node = queue.shift(); + sorted.push(node); + + const nodeNextTargets = nextTargetsMap.get(node); + //nodeNextTargets is a Set + for (const nodeNextTarget of nodeNextTargets.values()) { + if (nodeNextTarget && inDegree.has(nodeNextTarget)) { + const nodeNextTargetInDegree = inDegree.get(nodeNextTarget); + nodeNextTargetInDegree.delete(node); + if (nodeNextTargetInDegree.size === 0) queue.push(nodeNextTarget); + } + } + } + + return sorted; + } + }, + + /** + * Overrides MutableEvent to provide a specialized version taking into account a ReadOperation's readExpressions + * When readExpressions end with a different type then the target of the read operation, + * like when fetching a type's relationships to other types, or traversing multiple relationships, + * the type of object to be returned is going to be of the type at the end of those read expressions. + * + * So in a scenario where multiple RawDataServices are involved, with some Mux-Services doing orchestration, + * a RawDataService handling one of the type at the end of an expression but not the read operation's targer + * isn't in MutableEvent's composedPath path and would never get to handle the readOperation's read expression it + * is supposed to. + * + * A solution is to make sure those RawDataServices are included in the composedPath by looping on readExpressions + * The following prepares aspects (indegree) needed for Kahn's Topological sorting algorithm + + * @type {Property} + * @return {Array} + */ + _includeReadExpressionEndpointsIncomposedPath: { + value: false + }, + composedPath: { + value: function () { + //This the default behavior, based on the target property + let composedPathForTarget = this.super(); + + + + if(this._includeReadExpressionEndpointsIncomposedPath && this.type === DataOperation.Type.ReadOperation && this.data?.readExpressions) { + + let mergedComposedPaths = new Set(), + readExpressions = this.data?.readExpressions, + target = this.target; + + //There could be readExpressions leading back to target, so we add it to the set. + //mergedComposedPaths.add(target); + + // For Kahn's algorithm — build in-degree map from the target-based composedPath + const inDegree = new Map(), + nextTargetsMap = new Map(); + for(let i=0, countI = composedPathForTarget.length; (i0) { + inDegreeSet.add(composedPathForTarget[i-1]) + } + + if(!iNextTargetSet) { + iNextTargetSet = new Set(); + nextTargetsMap.set(composedPathForTarget[i], iNextTargetSet); + } + iNextTargetSet.add(composedPathForTarget[i+1]); + + //Add to mergedComposedPaths + mergedComposedPaths.add(composedPathForTarget[i]); + } + + + //Now loop on all readExpressions + for(let i=0, countI = readExpressions.length; (i 0) { + inDegreeSet.add(iDescriptorComposedPath[ii-1]) + } + inDegree.set(node, inDegreeSet); + + } else if(ii > 0) { + inDegreeSet.add(iDescriptorComposedPath[ii-1]) + } + + if(!iiNextTargetSet) { + iiNextTargetSet = new Set(); + nextTargetsMap.set(node, iiNextTargetSet); + } + iiNextTargetSet.add(nodeNextTarget); + + //descriptorsTraversingReadExpressions is a Set, so we let it do the uniquing + mergedComposedPaths.add(node); + } + } + + //Now descriptorsTraversingReadExpressions should contain all the targets we need to consider for this operation. Now we just have to sort them... + //descriptorTraversingExpression + let topologicallySortedComposedPathForTarget = this.topologicallySortMergedComposedPaths(mergedComposedPaths, nextTargetsMap, inDegree); + console.log("topologicallySortedComposedPathForTarget: ", topologicallySortedComposedPathForTarget); + composedPathForTarget = topologicallySortedComposedPathForTarget; + } + + return composedPathForTarget; + } + }, + + setComposedPath: { + value: function(value) { + if(value !== this._composedPath) { + this._composedPath = value; + } + } + }, + + serializeSelf: { value:function (serializer) { serializer.setProperty("id", this.id); diff --git a/data/service/mux/synchronization-data-service.js b/data/service/mux/synchronization-data-service.js index f79730964..f3dea12b2 100644 --- a/data/service/mux/synchronization-data-service.js +++ b/data/service/mux/synchronization-data-service.js @@ -528,7 +528,8 @@ exports.SynchronizationDataService = class SynchronizationDataService extends Mu We're going to create a new operation and adapt the criteria to look into the originDataSnapshot instead */ - readOperation.composedPath().delete(this.destinationDataService); + //readOperation.composedPath().delete(this.destinationDataService); + readOperation.setComposedPath([].concat(readOperation.composedPath()).delete(this.destinationDataService)); return new Promise((resolve, reject) => {