Welcome to the Harris Geospatial product documentation center. Here you will find reference guides, help documents, and product libraries.


Harris Geospatial / Docs Center / Geospatial Services Framework / GSF Tutorial: Server-Sent Events

Geospatial Services Framework

GSF Tutorial: Server-Sent Events

GSF - Tutorial - Server-Sent Events

Server-Sent Events are a way for clients to get updates on job progress and status changes without having to poll the server. Instead, the server pushes data to the client asynchronously.

The gsf-events-request-handler implements the W3C Server-Sent Events Recommendation by providing an event-stream on the /events endpoint.

A client that wants to listen for these events adds listeners to the JavaScript EventSource object for named events that they are interested in handling.

Currently, the server sends the following events and associated data:

Event   Data
JobAccepted   jobId
JobStarted   jobId
JobProgress   jobId, progress, message
JobCompleted   jobId, success

Server-Sent Events are used here to build a basic job console that provides progress and status updates without polling. Specifically, this tutorial employs the EventSource object and jQuery. Note: As in other tutorials, the full example code block is provided at the bottom of the page.

In order to follow along this tutorial, it is recommended to complete the Custom Request Handler Tutorial first, so a folder already exists for the html file built in this tutorial. With the custom request handler working, create a new file in the custom-request-handler/html folder called console.html and stub out its contents as follows.

<html>
<head>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
  <!-- TODO fill me in -->
  <script>
    <!-- TODO fill me in -->
  </script>
</body>
</html>

First, create a five-column table to which rows can be dynamically added for displaying job information as Server-Sent Events are emitted. Define the columns using <th> tags and give them each a unique ID so jQuery can easily update the cell values. Put the following HTML code in the <body> section of console.html above the <script> section.

<table id="jobTable" border="1" cellpadding="1" cellspacing="1">
  <thead>
    <tr>
      <th>Job ID</th>
      <th id="status">Status</th>
      <th id="progress">Progress</th>
      <th id="progressMessage" width="240">Progress Message</th>
      <th id="result">Result</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

The next step is to write event-driven JavaScript code to update the table as events are emitted. Put all JavaScript code in the <script> section in the <body>. First, create an EventSource object that listens on the /events endpoint.

// The EventSource object
var events = new EventSource('/events');

Next, add listeners for the various events that are of interest, starting with the JobAccepted event.

When a job is accepted, the code should add a new row to the table and set the ID of the new row to the jobId that was accepted. The jobId should go in the first column, and the Status column should be set to "Accepted."

When an event is emitted, the handler function is called and passed the event object. The event object has a data property, which is a JSON string containing the data payload for the event. The function can JSON.parse() the string into an object to easily get the jobId that was accepted.

The third column will contain the progress percentage, the fourth column the progress message, and the fifth column the job result, either succeeded or failed.

// On the JobAccepted event, add a new table row with id = jobId
// We will update cells in this row as subsequent events are emitted for the job
events.addEventListener('JobAccepted', function(event) {
  // Parse the event.data JSON string into an object
  var data = JSON.parse(event.data);
  // Add a new row for the jobId that was accepted
  $('#jobTable').append(
    '<tr id="' + data.jobId + '">' +   // New table row
      '<td>' + data.jobId + '</td>' +  // Job ID
      '<td>Accepted</td>' +            // Status
      '<td/>' +                        // Progress percentage, empty for now
      '<td/>' +                        // Progress message, empty for now
      '<td/>' +                        // Result, Succeeded/Failed, empty for now
    '</tr>');
});

Next, update the various cells in the row as subsequent events are emitted for the job. Now use a simple jQuery utility function to get a cell in the table based on its row and column ID.

// Utility function to get the table cell for a particular row and column
function getCell(rowId, columnId) {
  var row = $('#' + rowId);
  var col = $('#' + columnId).index();
  return row.find('td').eq(col);
}

After a job is accepted, at some point later it will be started. It may be started right away if a worker is available, or it may remain queued for a while if all workers are busy. When the handler receives the JobStarted event, set the status column to "Started" and the job progress to 0%.

// On the JobStarted event, set status to started and progress to zero.
events.addEventListener('JobStarted', function(event) {
  // Parse event data into json object
  var data = JSON.parse(event.data);
  // Set status to Started
  getCell(data.jobId, 'status').html('Started');
  // Set progress percent to 0%
  getCell(data.jobId, 'progress').html('0%');
});

While the job is running, the task writer may chose to emit progress updates. If the handler recieves a JobProgress event, the event.data object will have three fields: jobId, progress, and message. Update the progress percentage and message for the job appropriately.

// On the JobProgress event, update the progress and message for the job
events.addEventListener('JobProgress', function(event) {
  // Parse event data into json object
  var data = JSON.parse(event.data);
  // Update the progress percentage
  getCell(data.jobId, 'progress').html(data.progress + '%');
  // Update the progress message
  getCell(data.jobId, 'progressMessage').html(data.message);
});

Finally, when a job is complete, the handler gets a JobCompleted event. This event has two fields on the event.data object: jobId and success (boolean). Set the status to "Completed," progress percentage to 100%, and the result to either "Succeeded" or "Failed."

// On the JobCompleted event, set progress to 100% and update the result
events.addEventListener('JobCompleted', function(event) {
  // Parse event data into json object
  var data = JSON.parse(event.data);
  // Set the job status to Completed
  getCell(data.jobId, 'status').html('Completed');
  // Set the progress to 100%
  getCell(data.jobId, 'progress').html('100%');
  // Set the result to Succeeded or Failed
  getCell(data.jobId, 'result').html(data.success ? 'Succeeded' : 'Failed');
});

The only thing remaining is a way to exercise the event-driven job console. Add a button and implement the click() function to submit a job each time it is pressed. As a test, use the ISODataClassification task since it reports progress updates and provides good feedback while it is running. Place the following code above the <table> in the <body> section.

<form action="">
  <input id="submitJobButton" value="Submit Job" type="submit">
</form>

Finally add the following JavaScript code to the <script> section in the <body>.

// A simple function to submit jobs to exercise our event-driven job console
$('#submitJobButton').click(
  function() {
    // Task parameters
    var params = {
      INPUT_RASTER: {
        URL    : 'http://localhost:9191/ese/data/qb_boulder_msi',
        FACTORY: 'URLRaster'
      }
    };
    $.ajax({
      type       : 'POST',
      url        : '/ese/services/ENVI/ISODataClassification/submitJob',
      data       : JSON.stringify(params),
      contentType: 'application/json; charset=utf-8',
      dataType   : 'json',
      cache      : false, // important to work on IE11
      error      : function(error) {
        alert(JSON.stringify(error));
      },
      success    : function(response) {
        // job submit succeeded, carry on
      }
    });
    // return false from click() to suppress the default action
    // since we already handled it
    return false;
  }
);

Now launch a browser (Chrome or Firefox) and go to http://localhost:9191/custom/console.html to try it out.

For another example based on these concepts visit the Server-Sent Events Example page.

Full Example Code Block

Copy the following code into a console.html file within the custom-request-handler/html folder. This folder and other supporting files are created during the Custom Request Handler Tutorial.

<html>
<head>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
</head>
<body>
  <form action="">
    <input id="submitJobButton" value="Submit Job" type="submit">
  </form>    
  <table id="jobTable" border="1" cellpadding="1" cellspacing="1">
    <thead>
      <tr>
        <th>Job ID</th>
        <th id="status">Status</th>
        <th id="progress">Progress</th>
        <th id="progressMessage" width="240">Progress Message</th>
        <th id="result">Result</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
  <script>
    // The EventSource object
    var events = new EventSource('/events');
    // On the JobAccepted event, add a new table row with id = jobId
    // We will update cells in this row as subsequent events are emitted for the job
    events.addEventListener('JobAccepted', function(event) {
      // Parse the event.data JSON string into an object
      var data = JSON.parse(event.data);
      // Add a new row for the jobId that was accepted
      $('#jobTable').append(
        '<tr id="' + data.jobId + '">' +   // New table row
          '<td>' + data.jobId + '</td>' +  // Job ID
          '<td>Accepted</td>' +    // Status
          '<td/>' +      // Progress percentage, empty for now
          '<td/>' +      // Progress message, empty for now
          '<td/>' +      // Result, Succeeded/Failed, empty for now
        '</tr>');
    });
    // Utility function to get the table cell for a particular row and column
    function getCell(rowId, columnId) {
      var row = $('#' + rowId);
      var col = $('#' + columnId).index();
      return row.find('td').eq(col);
    }
    // On the JobStarted event, set status to started and progress to zero.
    events.addEventListener('JobStarted', function(event) {
      // Parse event data into json object
      var data = JSON.parse(event.data);
      // Set status to Started
      getCell(data.jobId, 'status').html('Started');
      // Set progress percent to 0%
      getCell(data.jobId, 'progress').html('0%');
    });
    // On the JobProgress event, update the progress and message for the job
    events.addEventListener('JobProgress', function(event) {
       // Parse event data into json object
        var data = JSON.parse(event.data);
        // Update the progress percentage
        getCell(data.jobId, 'progress').html(data.progress + '%');
        // Update the progress message
        getCell(data.jobId, 'progressMessage').html(data.message);
    });
    // On the JobCompleted event, set progress to 100% and update the result
    events.addEventListener('JobCompleted', function(event) {
      // Parse event data into json object
      var data = JSON.parse(event.data);
      // Set the job status to Completed
      getCell(data.jobId, 'status').html('Completed');
      // Set the progress to 100%
      getCell(data.jobId, 'progress').html('100%');
      // Set the result to Succeeded or Failed
      getCell(data.jobId, 'result').html(data.success ? 'Succeeded' : 'Failed');
    });
    // A simple function to submit jobs to exercise our event-driven job console
    $('#submitJobButton').click(
      function() {
        // Task parameters
        var params = {
          INPUT_RASTER: {
          URL  : 'http://localhost:9191/ese/data/qb_boulder_msi',
          FACTORY: 'URLRaster'
        }
      };
      $.ajax({
        type   : 'POST',
        url  : '/ese/services/ENVI/ISODataClassification/submitJob',
        data   : JSON.stringify(params),
        contentType: 'application/json; charset=utf-8',
        dataType   : 'json',
        cache  : false, // important to work on IE11
        error  : function(error) {
        alert(JSON.stringify(error));
        },
        success  : function(response) {
          // job submit succeeded, carry on
        }
      });
      // return false from click() to suppress the default action
      // since we already handled it
      return false;
    }
  );
  </script>
</body>
</html>

Note: The JavaScript EventSource object is not currently supported by some browsers, particularly IE and Edge. (See: CanIUse.com) Work around this issue by using an EventSource "Polyfill", which is JavaScript code that "fakes out" the EventSource API for these problematic browsers by using AJAX polling under the hood.

If using this tutorial in IE, download an EventSource polyfill, for example this one. eventsource.min.js is the only required file. Download the file and place it in the custom-request-handler/html folder. Then add the following line to the <head> section of console.html.

    <script src="eventsource.min.js"></script>

Now the example should work in IE and Edge.



© 2017 Exelis Visual Information Solutions, Inc. |  Legal
My Account    |    Buy    |    Contact Us