diff --git a/platform/features/layout/src/LayoutController.js b/platform/features/layout/src/LayoutController.js index b30de80ba3..017793d2e6 100644 --- a/platform/features/layout/src/LayoutController.js +++ b/platform/features/layout/src/LayoutController.js @@ -45,7 +45,8 @@ define( * @param {Scope} $scope the controller's Angular scope */ function LayoutController($scope) { - var self = this; + var self = this, + callbackCount = 0; // Update grid size when it changed function updateGridSize(layoutGrid) { @@ -91,23 +92,26 @@ define( e.preventDefault(); } - function getComposition(domainObject){ - return domainObject.useCapability('composition'); - } - - function composeView (composition){ - $scope.composition = composition; - return composition.map(function (object) { - return object.getId(); - }) || []; - } - //Will fetch fully contextualized composed objects, and populate // scope with them. - function refreshComposition(ids) { - return getComposition($scope.domainObject) - .then(composeView) - .then(function(ids){self.layoutPanels(ids);}); + function refreshComposition() { + //Keep a track of how many composition callbacks have been made + var thisCount = ++callbackCount; + + $scope.domainObject.useCapability('composition').then(function(composition){ + var ids; + + //Is this callback for the most recent composition + // request? If not, discard it. Prevents race condition + if (thisCount === callbackCount){ + ids = composition.map(function (object) { + return object.getId(); + }) || []; + + $scope.composition = composition; + self.layoutPanels(ids); + } + }); } // End drag; we don't want to put $scope into this diff --git a/platform/features/layout/test/LayoutControllerSpec.js b/platform/features/layout/test/LayoutControllerSpec.js index dcafefc718..bbed271d2c 100644 --- a/platform/features/layout/test/LayoutControllerSpec.js +++ b/platform/features/layout/test/LayoutControllerSpec.js @@ -33,7 +33,8 @@ define( testConfiguration, controller, mockCompositionCapability, - mockComposition; + mockComposition, + mockCompositionObjects; function mockPromise(value){ return { @@ -67,6 +68,7 @@ define( testModel = {}; mockComposition = ["a", "b", "c"]; + mockCompositionObjects = mockComposition.map(mockDomainObject); testConfiguration = { panels: { @@ -77,7 +79,7 @@ define( } }; - mockCompositionCapability = mockPromise(mockComposition.map(mockDomainObject)); + mockCompositionCapability = mockPromise(mockCompositionObjects); mockScope.domainObject = mockDomainObject("mockDomainObject"); mockScope.model = testModel; @@ -107,6 +109,30 @@ define( ); }); + it("Is robust to concurrent changes to composition", function () { + var secondMockComposition = ["a", "b", "c", "d"], + secondMockCompositionObjects = secondMockComposition.map(mockDomainObject), + firstCompositionCB, + secondCompositionCB; + + spyOn(mockCompositionCapability, "then"); + mockScope.$watchCollection.mostRecentCall.args[1](); + mockScope.$watchCollection.mostRecentCall.args[1](); + + firstCompositionCB = mockCompositionCapability.then.calls[0].args[0]; + secondCompositionCB = mockCompositionCapability.then.calls[1].args[0]; + + //Resolve promises in reverse order + secondCompositionCB(secondMockCompositionObjects); + firstCompositionCB(mockCompositionObjects); + + //Expect the promise call that was initiated most recently to + // be the one used to populate scope, irrespective of order that + // it was eventually resolved + expect(mockScope.composition).toBe(secondMockCompositionObjects); + }); + + it("provides styles for frames, from configuration", function () { mockScope.$watchCollection.mostRecentCall.args[1](); expect(controller.getFrameStyle("a")).toEqual({