My diary of software development

Archive for the ‘TypeScript Development’ Category

Async.js with TypeScript

I became an instant fan of the Async JavaScript Library the first time I used it. I am currently working on an HTML5 project at work which contains a lot of asynchronous code along to access IndexedDB as well as multiple web service calls. I was talking to another developer at my company about the ‘ugliness’ and maintenance nightmare of all this async code when he introduced me to the Async library.

The code below is an example of the asynchronous code in our application. As you can tell we’re using the async.waterfall() control flow:

   1:              var deferredResult = $.Deferred<String>();
   2:              var self = this;
   3:              async.waterfall([
   4:                  (callback: any) =>
   5:                  {
   6:                      self.dbContext.ReadWorkUnitSummary(groupId)
   7:                          .then((summary: Interfaces.Models.IWorkUnitSummary) =>
   8:                          {
   9:                              self.WrapUnits(summary);
  10:                              callback(null, summary);
  11:                          })
  12:                          .fail(error => callback(error));
  13:                  },
  14:                  (summary: Interfaces.Models.IWorkUnitSummary, callback: any) =>
  15:                  {
  16:                      self.dbContext.CollapseDataRecords(summary.DataRecords)
  17:                          .then((dataRecord: Interfaces.Models.IDataRecord) => callback(null, dataRecord))
  18:                          .fail((error) => callback(error));
  19:                  },
  20:                  (dataRecord: Interfaces.Models.IDataRecord, callback: any) =>
  21:                  {
  22:                      self.dataWebService.GetSiteRule(dataRecord.DataRecordId)
  23:                          .then((siteRule: Interfaces.Models.IDataSiteRule) =>
  24:                          {
  25:                              self.ProcessSiteRule(siteRule);
  26:                              callback(null, siteRule);
  27:                          })
  28:                          .fail(error => callback(error));
  29:                  },
  30:                  (siteRule: Interfaces.Models.IDataSiteRule, callback: any) =>
  31:                  {
  32:                      self.dataWebService.CreateDataSite(siteRule)
  33:                          .then((url: string) =>
  34:                          {
  35:                              deferredResult.resolve(url);
  36:                              callback();
  37:                          })
  38:                          .fail(error => callback(error));
  39:                  }
  40:              ],
  41:              (possibleError: any) =>
  42:              {
  43:                  if (possibleError)
  44:                  {
  45:                      deferredResult.reject(Error);
  46:                  }
  47:              });
  48:   
  49:              return deferredResult;

Readable and approachable

I believe the TypeScript  language and the Async library makes the above code both readable and approachable. It is readable because a developer can parse it in her head without needing outside documentation or the author standing there explaining it. The code is approachable because there is an obvious simple recurring pattern in it so the reader feels comfortable and doesn’t run away screaming “we’ve got to rewrite this! I have no idea how it works!”.

I feel that when I write readable and approachable code it becomes easier for another developer to pick up the code and modify it than it is if I wrote the code to be ‘amazing’ and understandable by only myself (and actually I’ll forget most of it within 6 months anyway). So kudos to TypeScript and Async for helping me to deliver maintainable code to my customer.

BECOMING Less readable and approachable

As I continued working with asynchronous code I began to realize that when debugging it wasn’t easy to tell how I got to where I was in the debugger. Often blocks such as the above would call another block and then it would be difficult at best to sift through the call stack to find block of calling asynchronous code.

For example here is what the call stack looks like in the Chrome debugger when stopped inside the ProcessSiteRule() method called on line 20 above:

image

Another problem I was running into was what happened when an exception was thrown from one of the anonymous methods in the Async call chain. If an exception was thrown from within an anonymous method, the rest of the Async call chain would not be called, the promise would never be resolved, and the caller would be left hanging forever. This type of bug is really difficult to track down.

I could fix some of that by placing a try/catch into each anonymous task but then what text would I give the rejection? For example if I added lines 5and 6 to the async step below, I would get a runtime exception. I could then catch the exception and reject the promise on line 17. The information I use to reject the promise would make it difficult to track down this block of code when reading the logs.

   1:  (dataRecord: Interfaces.Models.IDataRecord, callback: any) =>
   2:  {
   3:      try
   4:      {
   5:          var cow: any = {};
   6:          cow.bark();
   7:          self.dataWebService.GetSiteRule(dataRecord.DataRecordId)
   8:              .then((siteRule: Interfaces.Models.IDataSiteRule) =>
   9:              {
  10:                  self.ProcessSiteRule(siteRule);
  11:                  callback(null, siteRule);
  12:              })
  13:              .fail(error => callback(error));
  14:      }
  15:      catch (e)
  16:      {
  17:          callback("Exception when getting the site rule for DataRecord. -\n" + JSON.stringify(e, null, 4));
  18:      } 
  19:  },

 

In addition placing these draconian try/catch blocks in each anonymous Async task would make them a simple stack of asynchronous tasks much heavier and less readable than before.

Asynchronous job

I wanted to find a way to do the following with our asynchronous code:

  1. Provide specific information on which task in the anonymous Async call chain that an exception was thrown.
  2. Do not cause an unresolved promise hang when an exception was thrown in the asynchronous task.
  3. Be readable and approachable.

As I sat back and studied the code I realized that most of my code was using the async.waterfall() or async.series() control flows. I started thinking of our async code such as the above block as an Async Job. Those multiple asynchronous tasks handed to the Async control flow would be steps in an AsyncJob.

Here is the class I came up with which does the same thing as the async.waterfall at the beginning of this article. I called the job an AsyncJob and my specific class is named GetDataSiteUrlJob. Here is the basic structure without implementation of my class:

   1:  export class GetDataSiteUrlJob
   2:  extends Helpers.AsyncJob.BaseAsyncJob
   3:  implements Interfaces.Helpers.IAsyncJob
   4:  {
   5:  constructor(groupId: number,
   6:      dataWebService: Interfaces.WebService.IDataWebService,
   7:      dbContext: Interfaces.Db.IDbContext)
   8:   
   9:   
  10:  public Run(): JQueryPromise<string>
  11:            
  12:  private Initialize()
  13:   
  14:  private ReadWorkUnitSummary(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  15:   
  16:  private WrapUnits(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  17:   
  18:  private CollapseDataRecords(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  19:        
  20:  private ProcessSummarySiteRule(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  21:   
  22:  private CreateDatasite(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  23:  }

I’ll explain each element of this class, its base class, its interface, and the delegates in more detail later but first I want to show the the information that can be provided when an exception is thrown in one of the Async tasks.

I added this breaking code to the GetDataSiteUrlJob.ProcessSummarySiteRule() method:

   1:  private ProcessSummarySiteRule(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
   2:  {
   3:      var cow: any = {};
   4:      cow.bark();
   5:   
   6:      callback();
   7:  }
 
When I ran the GetDataSiteUrlJob the information below is what it was able to provide when the cow could not bark in ProcessSummarySiteRule():
 

image

interface explanation

   1:  export interface IAsyncJob
   2:  {
   3:      Run(): JQueryPromise<any>;
   4:  }

 

The IAsyncJob interface is quite simple. There is one method to call: Run() which returns a promise.

 

child Class explanation

Here is the relevant code of the GetDataSiteUrlJob class:

   1:  export class GetDataSiteUrlJob
   2:  extends Helpers.AsyncJob.BaseAsyncJob
   3:  implements Interfaces.Helpers.IAsyncJob
   4:  {
   5:  private workUnitSummary: Interfaces.Models.IWorkUnitSummary;
   6:  private groupId: number;
   7:  private dataWebService: Interfaces.WebService.IDataWebService;
   8:  private dbContext: Interfaces.Db.IDbContext;
   9:  private siteRule: Interfaces.Models.IDataSiteRule;
  10:  private collapsedDataRecord: Interfaces.Models.IDataRecord;
  11:   
  12:  constructor(groupId: number,
  13:  dataWebService: Interfaces.WebService.IDataWebService,
  14:  dbContext: Interfaces.Db.IDbContext)
  15:  {
  16:      super($.Deferred<any>());
  17:   
  18:      this.groupId = groupId;
  19:      this.dataWebService = dataWebService;
  20:      this.dbContext = dbContext;
  21:   
  22:      this.Initialize();
  23:  }
  24:   
  25:  public Run(): JQueryPromise<string>
  26:  {
  27:      return this.PerformRun();
  28:  }
  29:            
  30:  private Initialize()
  31:  {
  32:      this.AppendJobStep(this.ReadWorkUnitSummary);
  33:      this.AppendJobStep(this.WrapUnits);
  34:      this.AppendJobStep(this.CollapseDataRecords);
  35:      this.AppendJobStep(this.ProcessSummarySiteRule);
  36:      this.AppendJobStep(this.CreateDatasite);
  37:  }
  38:   
  39:  private ReadWorkUnitSummary(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  40:   
  41:  private WrapUnits(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  42:   
  43:  private CollapseDataRecords(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  44:          
  45:  private ProcessSummarySiteRule(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  46:   
  47:  private CreateDatasite(callback: Helpers.AsyncJob.Delegates.CallbackDelegate): void
  48:  {
  49:      var self = this;
  50:      this.dataWebService.CreateDataSite(this.siteRule)
  51:          .then((url: string) =>
  52:          {
  53:              self.jobResult = url;
  54:              callback();
  55:          })
  56:          .fail((error) => callback(error));
  57:  }
  58:  }

 

On line 16 in the constructor, we pass in the deferred object which we want to use.

Line 25 is the implementation of IAsyncJob.Run(). It calls the base class method PerformRun() which executes each of our job steps.

Line 30 is the Initialize() method which was called from the constructor. This is where we append each job step to the job. These job steps are akin to the asynchronous methods passed to Async.

On line 53 the code is setting self.jobResult to a URL. The self.jobResult is defined in the base class and represents what will be passed into the deferred.resolve() method at the end of this job. Remember that the IAsyncJob.Run() returns a promise.

Each job step must be defined to take a CallbackDelegate parameter. This parameter is akin to the callback argument to each of the anonymous methods in the Async call:

   1:  (callback: any) =>
   2:  {
   3:      self.dbContext.ReadWorkUnitSummary(groupId)
   4:          .then((summary: Interfaces.Models.IWorkUnitSummary) =>
   5:          {
   6:              self.WrapUnits(summary);
   7:              callback(null, summary);
   8:          })
   9:          .fail(error => callback(error));
  10:  },

 

base class

   1:  export class BaseAsyncJob
   2:  {
   3:  public jobResult: any;
   4:  public deferredResult: JQueryDeferred<any>;
   5:  public jobSteps = new Array<Delegates.JobStepDelegate>();
   6:  private currentStepName: string;
   7:  private jobCallStack = new Array<String>();
   8:   
   9:   
  10:  constructor(deferredResult: JQueryDeferred<any>)
  11:  {
  12:  this.deferredResult = deferredResult;
  13:  }
  14:   
  15:  public PerformRun(): JQueryPromise<any>
  16:  {
  17:  try
  18:  {
  19:      var proxySteps = this.CreateProxySteps();
  20:      async.series(proxySteps, this.CompleteRun.bind(this));
  21:  }
  22:  catch (e)
  23:  {
  24:      this.deferredResult.reject(e);
  25:  }
  26:   
  27:  return this.deferredResult.promise();
  28:  }
  29:   
  30:  public HandleExceptionDuringJobStep(exception: any, callback: any): void
  31:  {
  32:  try
  33:  {
  34:      var exceptionDescription =
  35:          {
  36:              ExceptionInMethod: this.currentStepName,
  37:              Exception: exception,
  38:              JobCallStack: this.jobCallStack,
  39:          };
  40:   
  41:      callback(exceptionDescription);
  42:  }
  43:  catch (ex)
  44:  {
  45:      callback(ex);
  46:  }
  47:  }
  48:   
  49:  public CompleteRun(possibleError: any): void
  50:  {
  51:  if (possibleError)
  52:  {
  53:      this.deferredResult.reject(possibleError);
  54:  }
  55:  else
  56:  {
  57:      this.deferredResult.resolve(this.jobResult);
  58:  }
  59:  }
  60:   
  61:  private GetClassNameFromConstructor(constructorText: string): string
  62:  {
  63:  var childClassName = "?";
  64:  try
  65:  {
  66:      var funcNameRegex = /function (.{1,})\(/;
  67:      var results = (funcNameRegex).exec(constructorText);
  68:      childClassName = (results && results.length > 1) ? results[1] : "?";
  69:  }
  70:  catch (ex)
  71:  {
  72:  }
  73:   
  74:  return childClassName;
  75:  }
  76:   
  77:  private GetClassAndMethodName(method: Function): string
  78:  {
  79:  var classAndMethodName = "";
  80:   
  81:  try
  82:  {
  83:      var calleeMethodText = method.toString();
  84:      var methodsOnThis = this['__proto__'];
  85:      var methodName: string;
  86:   
  87:      for (var methodOnThis in methodsOnThis)
  88:      {
  89:          if (calleeMethodText == methodsOnThis[methodOnThis])
  90:          {
  91:              methodName = methodOnThis;
  92:              break;
  93:          }
  94:      }
  95:      methodName = (methodName == "?" ? "anonymous" : methodName);
  96:   
  97:      var constructorText = methodsOnThis['constructor'];
  98:      var className = this.GetClassNameFromConstructor(constructorText);
  99:      classAndMethodName = $.format("{0}.{1}()", className, methodName);
 100:  }
 101:  catch (ex)
 102:  {
 103:  }
 104:   
 105:  return classAndMethodName;
 106:  }
 107:   
 108:  private ProxyStep(actualStep: Delegates.JobStepDelegate,
 109:  callback: AsyncJob.Delegates.CallbackDelegate)
 110:  : void
 111:  {
 112:  this.currentStepName = this.GetClassAndMethodName(actualStep);
 113:  try
 114:  {
 115:      this.jobCallStack.push(this.currentStepName);
 116:      if (this.deferredResult.state() != Enums.JQueryDeferredState.Pending)
 117:      {
 118:          callback();
 119:          return;
 120:      }
 121:   
 122:      actualStep.bind(this)(callback);
 123:  }
 124:  catch (ex)
 125:  {
 126:      this.HandleExceptionDuringJobStep(ex, callback);
 127:  }
 128:  }
 129:   
 130:  public AppendJobStep(jobStep: Delegates.JobStepDelegate): void
 131:  {
 132:  this.jobSteps.push(jobStep);
 133:  }
 134:   
 135:  private CreateProxySteps(): AsyncJob.Delegates.ProxyStepDelegate[]
 136:  {
 137:  var proxySteps = new Array<AsyncJob.Delegates.ProxyStepDelegate>();
 138:  this.jobSteps.forEach((jobStep: AsyncJob.Delegates.JobStepDelegate)=>
 139:  {
 140:      var proxyStep = this.ProxyStep.bind(this, jobStep);
 141:      proxySteps.push(proxyStep);
 142:  });
 143:   
 144:  return proxySteps;
 145:  }
 146:  }

 

Line 130 is the AppendJobStep() method. This was called from the Initialize() method in our child class and all it does is push the passed job step into an array.

Line 15 is the PerformRun() method which is called from our child class method Run(). This method first creates an array of proxy steps and then uses the Async.series() method to iterate these steps in an async fashion.

Line 20 executes the CompleteRun() method when the Async.series() is completed in PerformRun().

Line 137 is the CreateProxySteps() method called from PerformRun(). It takes the array of job steps created by the calls to AppendJobStep() and wraps each inside a call to ProxyStep(). It then takes the wrapped calls and appends it to an array which is finally returned from the method.

Line 108 is the ProxyStep() method. The first thing it does is retrieve the Class.MethodName() for the actual job step.

On line 116 the method checks to see if our deferred is still pending which means no previous step has failed or resolved the deferred. If it is still pending the actual step will be executed on line 122.

Line 126 is within the catch block which wraps the call to the job step. This calls the method HandleExceptionDuringJobStep().

Line 30 is the method HandleExceptionDuringJobStep(). This method creates a POJO object containing the exception method’s name, the actual exception object, and the job callstack array. Next it executes the callback and passes it the constructed POJO object.

Line 49 is the method CompleteRun() which was called inside PerformRun() when the Async.series() was completed. This method will either reject or resolve the deferredResult object. Remember the deferredResult object was created in the constructor of our child class and passed to us in the BaseAsyncJob constructor. It is what our child class Run() method returns to its caller.

delegates

There are 3 delegates:

  1. CallbackDelegate
  2. JobStepDelegate
  3. ProxyStepDelegate
   1:  export interface CallbackDelegate
   2:  {
   3:  (possibleError?: any): void;
   4:  }
   5:   
   6:   
   7:  export interface JobStepDelegate
   8:  {
   9:      (callback: CallbackDelegate): void;
  10:  }
  11:   
  12:   
  13:  export interface ProxyStepDelegate
  14:  {
  15:      (actualStep: JobStepDelegate, callback: CallbackDelegate): void
  16:  }