Building Calliope – Sproutcore data sources (Part III)

« Go to Part II

Game (aka Local) data source (..continues from Part II)

Before going further, let’s recap what data the application has to deal with during its operations:

  1. The last turn of the selected game, along with its letters’ configuration (service */turns/{number});
  2. The list of pivotal actions belonging to the game and to its last turn (service */actions?pivotOnly&turn={turnNumber});
  3. Periodically, the list of new actions to perform (service */actions?afterAction={actionId}).

where * stands for /me/games/{id}.

The first item – related to the current turn and letters – has been pointed out in Part II. In this part I’m going to dig deeper into the last two points.

The second and third point of the list are threatened in a uniform way, with the usage of a remote query.

Remote queries differ from local ones in many ways, here are the ones I was able to understand:

  • everytime a remote query is issued (with a call to find on the data store), the fetch method is invoked in the data source – in the case of a local query, instead, fetch is invoked only the first time the query is issued;
  • the results of a remote query are explicitly set by the code handling the fetch – in the case of a local query, the code handling the fetch has only the responsibility of loading records into the store, the retrieval part is done internally by Sproutcore.

Basically, local queries are handled by Sproutcore internally, they’re sort of filters on the data already contained in the store, whereas remote queries are handled by user code, and are a way to retrieve new data from the server, retaining full control over what is retrieved as a result.

Given the information above, remote queries where the choice to go for action retrieval. I started by declaring two static methods in Calliope.Action to act as helpers in creating the queries themself; each method supporting one of the points above:

Calliope.Action.query_PIVOT = function(turnNumber) {
  return SC.Query.remote(Calliope.Action, {pivotOnly: YES, turn: turnNumber});
};
Calliope.Action.query_LIVE = function(lastActionId) {
  return SC.Query.remote(Calliope.Action, {afterAction: lastActionId});
};

The creation of queries is really straightforward, you can fill an object with all the information you need in the fetching phase and pass it as the second argument of SC.Query.remote. In the first example I’m passing in the flag pivotOnly and the number of the turn to retrieve (case 1 above); in the other example I’m passing just the id of the last seen action as afterAction (case 2 above).

Let’s now have a look at how these queries are used inside the application. The usage is really simple:

(case 1)
var actions = store.find(Calliope.Action.query_PIVOT(currentGame.get("turnCount")))

(case 2)
var actions = store.find(Calliope.Action.query_LIVE(lastSeenActionId))

When the find method is issued, as stated above, Sproutcore invokes the fetch method in the data source, passing along the query that was issued. The implementation of that method must provide an explicit result for the query. Here is the implementation part relative to the queries on Action:

  fetch: function(store, query) {
    // check whether the query is a remote query on Calliope.Action
    if (query.get("isRemote") && query.get("recordType") === Calliope.Action) {
      // build the url with parameters afterAction, pivotOnly and afterAction
      var url = "/actions";
      var reqQuery = [];
      if (!SC.none(query.afterAction)) {
        reqQuery.pushObject("afterAction=%@".fmt(query.afterAction));
      }
      if (!SC.none(query.turn)) {
        reqQuery.pushObject("turn=%@".fmt(query.turn));
      }
      if (query.pivotOnly) {
        reqQuery.pushObject("pivotOnly");
      }
      if (reqQuery.length > 0) {
        url += "?" + reqQuery.join("&");
      }

      // issue the remote request
      Calliope.session.createRequest({
        address: this.getUrl(url)
      }).notify(this, this._didFetchActions, store, query)
      .send();
      return YES;
    }

    return NO;
  }

The method checks the query to see if it is remote and is relative to Calliope.Action, and then uses the parameters passed to the query (which, as you can notice, are accessible directly in query) in order to build the full url of the remote request. In the first case the url would look like this:

/actions?turn={query.turnNumber}&pivotOnly

whilst in the second case it would look like this:

/actions?afterAction={query.afterAction}

As seen in Part II, the url is then completed by calling getUrl (line 22), which prepends /me/games/{gameId} to it.

Each of the service calls above return a list of actions similar to the following one (more details here):

[
  {
    "type":"letter_drag_end",
    "attributes":"{ ... }",
    "time":"2011-04-08 09:05:00",
    "t":"1302246300040",
    "actionId":24456,
    "pivot":true,
    "player":1
  },
  ...
]

This data is then passed to the notify method _didFetchActions which is in charge of loading it into the store as a series of Calliope.Actions, and of explicitly setting the results of the query. Here is the implementation:

  _didFetchActions: function(response, store, query) {
    var body = this._getResponseBody(response, store, query);
    if (!SC.none(body)) {
      // load the actions into the store to retrieve the store keys
      var storeKeys = store.loadRecords(Calliope.Action, body);
      // load query results
      store.loadQueryResults(query, storeKeys);
    }
  }

As you can see there is nothing complicated here. The two steps described above are handled literally by two lines of code. The first call to loadRecords loads the server data into the store as Calliope.Action instances and receives, as a result, the list of the associated store keys; the second call to loadQueryResults does nothing but explicitly setting the results for the query, which finally gets accessible to the caller of find.

As already pointed out in Part II, the call to the server happens asynchronously, so the list of actions retrieved that way isn’t already available at the end of the call to find. What is returned is an instance of SC.RecordArray with an empty storeKeys variable. By observing the array for changes you can be notified when data is ready to be used; here is an example on how to do it:

Calliope.SomeObject = SC.Object.extend({
  _actionsArray: null,
  loadActions: function() {
    this._actionsArray = store.find(Calliope.Action.query_LINE(15));
  },
  _actionsArrayDidChange() {
    if (!SC.none(this._actionsArray.storeKeys)) {
      // DO STUFF
    }
  }.observes("*_actionsArray.[]")
});

Last note

This is the end of my first article about the basics of Sproutcore data sources. Hope you liked it and if you did – or didn’t – please drop a line.

There is more about data sources I would have liked to share, in particular the usage of chain to create nested data stores, and the handling of data saving; I’m planning a series of articles about those subjects as well, if you are interested just let me know.

There is also one open point I have not faced yet in the application but I will eventually have to: what’s the right way of refreshing the data store for “global” data like games and players? (for example in order to know that a new game has been created, or a new player has registered).

« Go to Part II
« Go back to the index

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s