Intermediate Tutorial

Table of Contents

Goal

This tutorial is designed to show how to use multiple Gears APIs in conjunction. It shows how to create an offline application that performs operations in the background with WorkerPool and utilizes the client-side database. The actual application that is created is a simple stock monitor that allows the user to specify stock tickers (e.g. "goog") to be viewed in realtime or offline.

This tutorial will cover these Gears APIs:

You will need:

  • Ability to edit basic JavaScript
  • Ability to publish files to the HTTP server that hosts your files
  • PHP on your HTTP server or a server-side proxy of your choice
  • For a list of supported browsers and platforms, see gears.google.com.
  • (optional) Firebug for Firefox to see debug messages

Files for this tutorial:

  • Code for Parts 1, 2 & 3

This section will cover how to cache application files offline. After this step is complete you will have a page whose HTML, JavaScript & CSS can be accessed offline.

Include gears_init.js, Set onLoad

The first step is including gears_init.js in stock_tutorial.html. This initializes the google.gears.factory object into the browser's DOM and enables Gears functionality (if the user has Gears installed). Next, we set the body onLoad event to call init() which is in stock_ticker.js.

Check for Gears, Initialize LocalServer

We will put our JavaScript code in stock_ticker.js. The first step is to create an init function that will check if the user has Gears installed on their browser. If they don't, then the code will redirect the user to the gears.google.com page to ask them if they would like to install Gears. Notice that in the URL there are CGI arguments which enable the developer to have a custom message appear on the install page as well as a redirect-upon-install URL. These arguments are message and return. If the user does have Gears installed then the code will create an instance of the LocalServer class -- the container of ManagedResourceStore and ResourceStore. You can learn more about the CGI args here.

function init() {
  if(!window.google || !google.gears) {
    location.href = "http://gears.google.com/?action=install&message=" +
                    "Stock ticker requires Google Gears to be installed." +
                    "&return=" + location.href;
    return;
  } else {
    localServer = google.gears.factory.create("beta.localserver");
  }

  createManagedStore();
}

Initialize ManagedResourceStore, Capture Resources

This function creates an instance of ManagedResourceStore, specifies a manifest file whose contents will be cached, sets a callback function that will refresh the page when the contents are captured, and then initiates the capture.

function createManagedStore() {
  managedStore = localServer.createManagedStore(MANAGED_STORE_NAME);
  managedStore.manifestUrl = MANIFEST_FILENAME;

  managedStore.oncomplete = function(details) {
    if (details.newVersion != '') {
      location.href = location.href;
    }
  }

  managedStore.checkForUpdate();
}
If you want to go a step further and be clued in on the status of the capture, try using the ManagedResourceStore callback methods. They allow you to set functions to be called when certain events fire during the capture. Try adding this code before checkForUpdate():
managedStore.onerror = function(error) {
  alert("Error: " + error.message);
}

managedStore.onprogress = function(details) {
  alert('Have downloaded ' + details.filesComplete + '/' + details.filesTotal + ' files.');
}

Setup Manifest File

The ManagedResourceStore will download all files we specify in the manifest file. Take a look at stock_ticker_manifest.json and you will see the JSON (JavaScript Object Notation) that specifies the version and list of files that should be cached on users' machines. For more information on creating a manifest file please read this reference page.

{
  "betaManifestVersion": 1,
  "version": "version 1",
  "entries": [
      { "url": ".", "src": "stock_ticker.html"},
      { "url": "stock_ticker.html"},
      { "url": "stock_ticker.css"},
      { "url": "stock_ticker.js"},
      { "url": "gears_init.js"}
    ]
}

Run Code

Now try running the code (you can find step1 code here). After visiting stock_ticker.html once you should have the resources stored (and if you used the callbacks you will know when they are done downloading). Try putting your browser in 'offline' mode or disabling your internet connection, clear you browser cache and then reload stock_ticker.html. It should now work offline.

In this section we will use Gears' HTTPRequest & Database APIs to make the application grab stock quotes and store them in the database to be viewable offline.

Create Database

The first step is to create a database where we can store our information for offline access. The steps are simple - create an instance of the Database class, open a database with database.open(name) (Gears creates one if it doesn't exist) and then execute a table creation on the database. There are only 3 fields per entry in the database - symbol, price and change. To make things straightforward they are all TEXT types, though any type that SQLite allows can be used. Now we are ready to insert/update/select/delete.

function createDatabase() {
  database = google.gears.factory.create("beta.database");
  database.open(DATABASE_NAME);
  database.execute('create table if not exists ' + DB_TABLE_NAME +
                   ' (symbol TEXT, price TEXT, change TEXT)');
}

Draw Stock Table From Database

To display the stocks when the application is loaded we will select all entries from the database. That code is located in init(). It uses a function selectAllStocks which runs a database query to grab all of the entries in the database:

database.execute('SELECT * FROM ' + DB_TABLE_NAME);
Then it loops through the results and adds each entry to the HTML.
var rs = selectAllStocks();
while(rs.isValidRow()) {
  var stock = {
    symbol : rs.field(0),
    price : rs.field(1),
    change : rs.field(2)
  }
  updateHTMLTable(stock);
  rs.next();
}
rs.close()

Add Stocks

Create an HTML input element in stock_ticker.html that has an onkeydown listener. Whenever a user hits "enter" in the input box getStock will be called. The getStock function will create a Gears HTTPRequest and send a query to a Yahoo service for stock quotes. When the results are received they will be inserted into the database and the HTML table.

There are a few things to note about the request made in getStock:

  1. Due to security restrictions, browsers do not allow JavaScript to make a request to a cross-domain resource. However, PHP and other server-side scripts are not bound by this restriction. This is the reason why the request is sent to yahoo_ticker_proxy.php. If you aren't familiar with a proxy like this, the basic idea of it is that we are going to make an XMLHttpRequest to the PHP script which will make a request to Yahoo for the stock ticker.
  2. In the request URL there is an extra CGI argument, random. It is set to a random number between 0 and 1,000,000. This is done because browsers cache requests that are made but the stock ticker needs fresh stock quotes. By adding in a random number (using (new Date()).getTime() is also common) the browser will never serve requests from cache.
  3. The onreadystatechange method is the callback method that is triggered when the request changes states. One of these state changes, state 4, signifies that the request has completed.
Take a look at getStock:
function getStock(ticker) {
  var request = google.gears.factory.create('beta.httprequest');

  // This will keep the browser from cacheing our requests to Yahoo Finance
  var dontCacheRequest = '&random=' + Math.floor(Math.random()*1000000);
  request.open('GET', 'yahoo_ticker_proxy.php?q=http://download.finance' +
               '.yahoo.com/d/quotes.csv?s=' + ticker +
               '&f=sl1d1t1c1ohgv&e=.csv' + dontCacheRequest);

  request.onreadystatechange = function() {
    if (request.readyState == 4) {
      var stockArray = request.responseText.split(',');
      var stock = {
        symbol : ticker,
        value : stockArray[1],
        change : stockArray[4]
      }
      insertIntoDatabase(stock);
      updateHTMLTable(stock);
    }
  };
  request.send();
}

Update Manifest File, Run

Upload the new code and update the version string in the manifest file so that the new files will be cached by Gears.

This section will cover how to use the Gears WorkerPool in order to complete tasks in the background.

Currently the application only grabs stock values upon loading. With WorkerPool we can have the quotes updated frequently in the background. Think of WorkerPool as threading for JavaScript. Gears' Timer module will be needed because workers do not have access to the window object which has the traditional methods for timers, setTimeout() and setInterval().

The strategy will be to spawn a single WorkerPool worker that will monitor all stock quotes for us. This will also serve as our database manager process. It is good practice to use a single WorkerPool worker to be the only interaction point with a database -- it is more performant and it eliminates database access concurrency issues.

Create a WorkerPool

Think of a WorkerPool instance as the parent thread and all created workers as the child threads. The WorkerPool will tell the workers what they need to do, the workers will complete the tasks and then report back the result. To communicate, the WorkerPool and workers send messages to each other. Sending messages is handled by sendMessage() and received messages are handled by the onmessage() callback.

In this code, the createWorkerPool() function handles the creation of the application's WorkerPool. It also sets the onmessage() handler to be a function that will receive a stock ticker, price and %change which will write those values to the HTML. As you can see, the onmessage() handler will be passed a messageObject. This object consists of a message and a sender ID. The message can be a JavaScript object, a string, etc. Check out the reference for the messageObject.

function createWorkerPool() {
  workerPool = google.gears.factory.create('beta.workerpool');
  workerPool.onmessage = function(a, b, messageObject) {

    if (messageObject.body.operation == 'removedTicker') {
      removeFromHTMLTable(messageObject.body.symbol);
    }

    if (messageObject.body.operation == 'addedTicker') {
      var stock = {
        'symbol' : messageObject.body.symbol,
        'price' : messageObject.body.price,
        'change' : messageObject.body.change
      };
      updateHTMLTable(stock);
    }
  };
}

Create the Worker to Monitor Stocks

The application will now create a single worker to monitor all stock tickers efficiently.

In this application, the createWorker function handles creating a new worker. This function calls the Gears method WorkerPool.createWorkerFromUrl() to initialize a worker. However, the worker won't do anything until a message is sent to it. We call addStocks to send a message to the worker to start monitoring new stocks. In this function workerPool.sendMessage is used to send that message. The first parameter sent is the JSON message, the second parameter is the child worker's ID which is how the message is sent to the correct worker. This message gets received by worker.js in the onmessage callback function.

function createWorker(symbols) {
  workerId = workerPool.createWorkerFromUrl('worker.js');
  addStocks(symbols);
}

function addStocks(symbols) {
  var message = {
    'operation' : 'addTickers',
    'symbols' : symbols
  }
  workerPool.sendMessage(message, workerId);
}

Worker Message Handling

When the worker is initialized it will run until the application is exited. It has an array that keeps track of all stocks it should be monitoring and it checks every 3 seconds to see if there are stocks to update. If there are it will do a request for their values and return the results to the parent WorkerPool.

The stocks that the worker monitors can be changed by the WorkerPool in stock_ticker.js. The worker receives a message in the onmessage handler and it switches on the operation property to decide whether to add or remove a stock from its monitoring list.

There are three scenarios:

  1. The application just loaded and the worker needs to initialize the database and begin the monitoring process
  2. The user has entered a new stock
  3. The user is removing a stock
In situations 2 and 3 all that needs to happen is a simple push/splice (remove) from the global tickers array since there is a timer that automatically checks the tickers array every 3 seconds.

wp.onmessage = function(a, b, messageObject) {
  var operation = messageObject.body.operation;
  if (operation == 'addTickers') {
    var stockSymbols = messageObject.body.symbols;
    parent = messageObject.sender;
    // If the worker was just created for the first time
    if (database == null) {
      createDatabase();
      tickers = stockSymbols;
      timer.setTimeout(getStocks, 10);
    } else {
      // If the worker exists and we are adding new symbols
      for (var i=0; i < stockSymbols.length; i++) {
        tickers.push(stockSymbols[i]);
      }
    }
  }

  if (operation == 'removeTicker') {
    var symbolToRemove = messageObject.body.symbol;
    removeStock(symbolToRemove);
    for (var j=0; j < tickers.length; j++) {
      var curTicker = tickers[j];
      if (symbolToRemove == curTicker) {
        // When we cut it out of the array it won't get polled next time the
        // HTTPRequest is made for stocks
        tickers.splice(j, 1);
      }
    }
  }
};

Now take a peek at the code that monitors the stocks in the background -- getStocks. It is fairly straightforward since it is only a slightly modified version of Part 2's getStock function. The only difference is that this one uses wp.sendMessage(stock, parent) to report stock values back to the parent WorkerPool and then uses timer.setTimeout(getStocks, 3000) (a 3 second timer) to schedule the next stock quote grab.

function getStocks() {
 // Gotta have stocks to monitor!
 if (tickers.length > 0) {
   var request = google.gears.factory.create('beta.httprequest');

   // Let's keep the requests returning fresh results.
   var randomNum = Math.floor(Math.random()*1000000);
   var tickersReq = tickers.join('%7C'); // %7C is the up and down | symbol

   // One url request for multiple stocks
   var url = 'yahoo_ticker_proxy.php?q=http://download.finance.yahoo.com/d/quotes.csv?s='+tickersReq+'&f=sl1d1t1c1ohgv&e=.csv&random=' + randomNum;
   request.open('GET', url);
   request.onreadystatechange = function() {
     if (request.readyState == 4) {
       // Split the stocks
       var stockResults = request.responseText.split('\n');
       // Forget the last newline, it doesn't have a stock in it
       stockResults.splice(stockResults.length - 1, 1);
       for (var i=0; i < stockResults.length; i++) {
         // Split up the details of this stock
         var stockDetails = stockResults[i].split(',');
         var stock = {
           'operation' : 'addedTicker',
           'symbol' : stockDetails[0].replace(/"/g, ''),
           'price' : stockDetails[1],
           'change' : stockDetails[4]
         };
         insertIntoDatabase(stock);
         if (typeof stock.price != 'undefined' && typeof stock.change != 'undefined') {
           wp.sendMessage(stock, parent);
         }
       }
     }
   };
   request.send();
 }

 timer.setTimeout(getStocks, 3000);
}

Run it

Congratulations, you now have the know-how to implement 5 of the Gears modules! Upload your code, increment the manifest file's version number and go play around with it!