Limitless Chaining of Continuation object in Salesforce

How to create a Continuation Server in Salesforce with the help of JavaScript remoting and Continuation Object

Chaining of Continuation Object in Salesforce
Chaining of Continuation Object in Salesforce

Around two years back, I posted an article on Continuation Object in Salesforce. As we know because of governor limits like concurrent Apex limit or CPU time limit errors, its always recommended to perform long running Apex process asynchronously. Processes specially like calling external API.

We can come up with multiple designs if number of APIs are less. However, you would risk scalability of Salesforce if number of APIs to be called is 10+. We cannot use chaining of continuation object directly as currently its limited to 3. If we call these API’s from Javascript (like using JavaScript remoting) then still we are using synchronous Apex which would be trade off for scalability.

What happens when we try to chain more than 3 Continuation Object call
What happens when we try to chain more than 3 Continuation Object in Salesforce

In Short, below are challenges in front of us

  • We need to call 10+ API from Visualforce
  • Cannot use chaining of Continuation Object as current chaining limit is 3
  • Cannot invoke each API from JavaScript remoting because it will be considered as Synchronous Apex

Lots of talk, now get into action. We would discuss how to create a Continuation Server in Salesforce with the help of Continuation Object and JavaScript remoting. In this approach, we would be able to call unlimited APIs asynchronously from Salesforce.

continuationDemoJSRemoting.vfp

 <apex:page controller="ContinuationDemoJSRemoting" standardStylesheets="false" sidebar="false" showHeader="false" title="Continuation Server Demo">
 <!-- <apex:slds /> Images (loading image) are not imported using this tag -->
 <apex:stylesheet value="{!URLFOR($Resource.SLDS2_3_1, 'assets/styles/salesforce-lightning-design-system.min.css')}" /> 
 
 <div aria-hidden="true" style="width:100%;height:100%"> 
 <div class="slds slds-box slds-scope"> 
 <div class=" slds-m-top_xx-large slds-m-bottom_small"> 
 Total API calls to make : <input type="text" value="3" placeholder="Total API calls to make, Default 3" id="txtAPICallsToMake" />
 <input value="Start Request" type="button" onclick="startRequest()"/> 
 </div>
 <br /> <br />
 
 <div> 
 Note : With increase in each API count, Response time would also be increased. <br />
 <b>Responses :</b> <span id="resultContainer"></span>
 </div> 
 
 <div id="modalWindowMsg" style="height:640px;" class="hide">
 <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-describedby="modal-content" class="slds-modal slds-fade-in-open">
 <div class="slds-modal__container">
 <header class="slds-modal__header"> 
 <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">API Callout status</h2>
 </header>
 <div class="slds-modal__content slds-p-around_medium" id="modal-content" >
 <p>
 <span id="modal-content-status"></span>
 </p> 
 <p>
 <div class="demo-only" style="height:3rem;">
 <div role="status" class="slds-spinner slds-spinner_medium">
 <span class="slds-assistive-text">Loading</span>
 <div class="slds-spinner__dot-a"></div>
 <div class="slds-spinner__dot-b"></div>
 </div>
 </div>
 </p> 
 </div>
 <footer class="slds-modal__footer">
 <button class="slds-button slds-button_neutral" onclick="cancelModal()">Cancel</button> 
 </footer>
 </div>
 </section>
 <div id="modalWindowOverlay" class="slds-backdrop slds-backdrop_open"></div>
 </div>
 </div>
</div>
 <style>
 .modal{
 visibility: visible !important;
 opacity: 1 !important;
 transition: opacity 0.4s linear;
 }
 .hide{
 display:none;
 }
 #modal-content{
 padding: 1rem;
 }
 </style>
 <script type="text/javascript">
 Visualforce.remoting.timeout = 120000; 
 var apiNumber = 1;
 var totalAPICallsToMake = 3;
 var resultContainer = document.getElementById("resultContainer") ;
 var modalcontentstatus = document.getElementById("modal-content-status") ;
 var startTime,endTime,elapsedSecond; 

 function startRequest(){ 
 var callnumbers = document.getElementById("txtAPICallsToMake").value;
 if(callnumbers){
 totalAPICallsToMake = callnumbers;
 } 
 document.getElementById("modalWindowOverlay").classList.add('modal');
 document.getElementById("modalWindowMsg").classList.remove('hide'); 
 submitCalls(apiNumber);
 }
 
 function submitCalls(count){
 modalcontentstatus.innerHTML='Callback initiated for API number - '+count; 
 startTime = new Date().getTime();
 Visualforce.remoting.Manager.invokeAction(
 '{!$RemoteAction.ContinuationDemoJSRemoting.callService}',
 count, 
 handleResult
 );
 }
 
 function handleResult(result, event){
 endTime = new Date().getTime();
 elapsedSecond = (endTime - startTime) / 1000;
 modalcontentstatus.innerHTML='Callback completed'; 
 if (event.status) {
 resultContainer.innerHTML=resultContainer.innerHTML+'<br /> Time Taken for API '+ apiNumber +' : '+elapsedSecond+' sec , Response : '+result; 
 }
 
 if(apiNumber < totalAPICallsToMake){
 apiNumber++; 
 submitCalls(apiNumber);
 }else{
 cancelModal();
 }
 } 
 
 function cancelModal(){ 
 document.getElementById("modalWindowOverlay").classList.remove('modal');
 document.getElementById("modalWindowMsg").classList.add('hide');
 }
 </script>
</apex:page>

ContinuationDemoJSRemoting.class

/**
 * @Author		:		Jitendra Zaa
 * @Date		:		30 May 2017
 * @Desc		:		Demo of Continuation Server using Javascript remoting
 * 
 * */
public class ContinuationDemoJSRemoting {
    
    private static final String CALLOUT_URL = 'https://node-count.herokuapp.com/'; 
    private static Map<Integer,String> errors = null;
        
    private static void initializeErrorcodes(){
        if(errors == null){
            errors = new Map<Integer,String>();
            errors.put(2000,'The timeout was reached, and the server did not get a chance to respond.');
            errors.put(2001,'There was a connection failure.');
            errors.put(2002,'Exceptions occurred.');
            errors.put(2003,'The response hasn’t arrived (which also means that the Apex asynchronous callout framework hasn’t resumed).');
            errors.put(2004,'The response size is too large (greater than 1 MB).');
        } 
    }
    
    @RemoteAction
    public static Object callService(Integer count){  
        // Return it to the system for processing
        return constructCallout('callback1Respone',count);
    }
    
    public static Object callback1Respone(Object state) {
        HttpResponse response = Continuation.getResponse((String)state);  
        Integer statusCode = response.getStatusCode();
        if (statusCode >= 2000) {
            initializeErrorcodes();
            return 'Continuation error: ' + errors.get(statusCode);
        } 
        return response.getBody();
    }
    
    private static Object constructCallout(String callbackMethodName,Integer count){
        Continuation chainedContinuation = null;  
        chainedContinuation = new Continuation(60); 
        chainedContinuation.continuationMethod=callbackMethodName; 
        HttpRequest req = new HttpRequest();
        req.setMethod('GET');
        req.setEndpoint(CALLOUT_URL+count); 
        chainedContinuation.state = chainedContinuation.addHttpRequest(req); 
        return chainedContinuation;  
    } 
	
}

Related posts