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


  >  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 on a GSF server. The GSF JavaScript Client SDK uses this endpoint behind the scenes to provide access to server-sent events through a Server object. This tutorial demonstrates how to handle events using the SDK.

If you prefer, you could handle server events by connecting directly to the /events endpoint yourself using an EventSource object. The API for using the /events endpoint can be found here.

Currently, the server sends the following events:


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

This tutorial uses uses jQuery and the GSF JavaScript Client SDK. For convenience, a version of the SDK is provided with the GSF installation. The gsf-sdk-request-handler provides access to the SDK, and is enabled by default. If you are not using the default GSF configuration please ensure the gsf-sdk-request-handler is enabled.

The SDK included with GSF is a static version. Be sure to visit the github page to get the latest version to the SDK.

In order to follow along with this tutorial, it is recommended that you complete the Custom Request Handler Tutorial first, so a folder already exists for the files built in this tutorial. With the custom request handler configured and 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>
  <script src="../sdk/js/GSF.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 and configure a GSF.server object to connect to GSF.

// Use the GSF server where this example is hosted
var protocol = location.protocol.substring(0,location.protocol.length - 1);
var hostname = location.hostname;
var port = location.port;
// Create a server object
const server = GSF.server({
    protocol: protocol,
    address: hostname,
    port: port
});

Next, create a Service object for the ENVI service and an ISODataClassification task which we will use to submit processing jobs. Also specify a URL for the image we will use.

// Create an ENVI service object
const service = server.service('ENVI');
// Create a task object for the task we will use
const isoDataClassificationTask = service.task('ISODataClassification');
// URL of our input raster for processing
var inputRasterUrl = protocol + "://" + hostname + ":" + port + "/ese/data/qb_boulder_msi";

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." 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.

When an event is emitted, the handler function is called and passed a JSON data object containing the data payload for the event.

// 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
server.on('JobAccepted', function(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. 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.
server.on('JobStarted', function(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 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
server.on('JobProgress', function(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 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
server.on('JobCompleted', function(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 = {
            parameters: {
                INPUT_RASTER: {
                    URL    : inputRasterUrl,
                    FACTORY: 'URLRaster'
                }
            }
        };
        isoDataClassificationTask.submit(params);
        // 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>
  <script src="../sdk/js/GSF.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>
// Use the GSF server where this example is hosted
var protocol = location.protocol.substring(0,location.protocol.length - 1);
var hostname = location.hostname;
var port = location.port;
// Create a server object
const server = GSF.server({
    protocol: protocol,
    address: hostname,
    port: port
});
// Create an ENVI service object
const service = server.service('ENVI');
// Create a task object for the task we will use
const isoDataClassificationTask = service.task('ISODataClassification');
// URL of our input raster for processing
var inputRasterUrl = protocol + "://" + hostname + ":" + port + "/ese/data/qb_boulder_msi";
// 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.
server.on('JobStarted', function(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
server.on('JobProgress', function(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
server.on('JobCompleted', function(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');
});
// 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
server.on('JobAccepted', function(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>');
});
// A simple function to submit jobs to exercise our event-driven job console
$('#submitJobButton').click(
    function() {
        // Task parameters
        var params = {
            parameters: {
                INPUT_RASTER: {
                    URL    : inputRasterUrl,
                    FACTORY: 'URLRaster'
                }
            }
        };
        isoDataClassificationTask.submit(params);
        // 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.



© 2018 Harris Geospatial Solutions, Inc. |  Legal
My Account    |    Store    |    Contact Us