At the beginning of 2010, Mark Miller started a discussion in the DAP WG about whether some of DAP's APIs couldn't be exposed as RESTful web services that would be somehow localised.
While he didn't list which APIs this approach would apply to, one can presume that it would apply better to those APIs on DAP's charter that provide information services (Contacts, Calendar, Tasks, Messaging, System Information, Communications Log, Gallery) than to those that may be characterised as "closer to the metal" (File System, Application Launcher, Capture) or directly integrated with the system (User Interaction).
This document intends to analyse the pros and cons of such an approach, taking into account the gearing towards information services described above.
There is a large body of experience in designing both Javascript and RESTful APIs, but without concrete examples we will likely nevertheless be left counting angels. As a result, to get a feel for the issues that may crop up in developing a local-REST approach (henceforth, LREST) and how it compares with straight JS APIs we'll start from a related and well-known JS API — Geolocation — and try to adapt it to LREST.
The examples we use are stolen from the Geolocation API specification. Note that in order to show of the code only what is fully relevant, we assume that we have access to the common jQuery library in order to perform requests (though naturally any other library will do, and the equivalent XMLHttpRequest code can easily be inferred).
Original example:
function showMap (position) { // Show a map centred at (position.coords.latitude, position.coords.longitude). } // One-shot position request. navigator.geolocation.getCurrentPosition(showMap);
LREST version:
function showMap (position) { // Show a map centred at (position.coords.latitude, position.coords.longitude). } // One-shot position request. $.getJSON("service://w3c/geo/get-location", showMap);
The port is fairly straightforward (assuming the availability of a library — it would be longer with
bare-metal XMLHttpRequest). Things to note are the fact that we expose a URL (an important detail
which will be discussed further) and that for better or for worse we've made it possible to make
synchronous requests. The data that showMap
gets can be exactly the same in both cases.
Original example:
function scrollMap(position) { // Scrolls the map so that it is centred at (position.coords.latitude, position.coords.longitude). } function handleError(error) { // Update a div element with error.message. } // Request repeated updates. var watchId = navigator.geolocation.watchPosition(scrollMap, handleError); function buttonClickHandler() { // Cancel the updates when the user clicks a button. navigator.geolocation.clearWatch(watchId); }
LREST version (requiring the Comet plugin):
function scrollMap (position) { // Scrolls the map so that it is centred at (position.coords.latitude, position.coords.longitude). } function handleError (error) { // Update a div element with error.message. } $.cometd.configure("service://w3c/geo"); var errH = $.cometd.addListener("/meta/unsuccessful", handleError); var sucH = $.cometd.subscribe("/watch-position", scrollMap); $.cometd.handshake(); function buttonClickHandler () { // Cancel the updates when the user clicks a button. $.cometd.removeListener(errH); $.cometd.unsubscribe(sucH); }
This provides for a richer and more problematic example. Handling errors is not what adds real overhead here
(the first example could easily handle them as well by using $.ajax
instead of $.getJSON
)
but rather the fact that HTTP wasn't designed with pub-sub in mind.
I therefore chose to use the Bayeux Comet protocol because it is well-known and has existing library support. That being said, this approach is hardly optimal, and isn't all that well documented and supported in the wild (a Google search shows that it is largely the knowledge of a limited group rather than commonly used).
There may be better ways of handling subscriptions (event-source for instance) — feedback is welcome. If subscriptions do indeed prove to be too much of a problem at this point in time, the LREST approach may not necessarily be disqualified, but might have to be limited to APIs that don't need this functionality (which probably maps to Contacts, Calendar, Tasks, Messaging, Gallery but excludes System Information and Communications Log).
Original example:
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, {maximumAge:600000}); function successCallback(position) { // By using the 'maximumAge' option above, the position // object is guaranteed to be at most 10 minutes old. } function errorCallback(error) { // Update a div element with error.message. }
LREST version:
function successCallback(position) { // By using the 'maximumAge' option above, the position // object is guaranteed to be at most 10 minutes old. } function errorCallback(error) { // Update a div element with error.message. } $.ajax({ url: "service://w3c/geo/get-location", dataType: "json", beforeSend: function (xhr) { xhr.setRequestHeader("If-Modified-Since", formatHTTPDate(-600000)); }, success: successCallback, error: errorCallback, });
This example is functional, albeit more convoluted. Weasel alert: the formatHTTPDate()
method is not described in the code, and while not too difficult to implement it does represent
extra work that any author would have to do in order to support the caching functionality here.
As discussed above, we can see that adding error handling is trivial.
Original example:
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, {maximumAge:600000, timeout:0}); function successCallback (position) { // By using the 'maximumAge' option above, the position // object is guaranteed to be at most 10 minutes old. // By using a 'timeout' of 0 milliseconds, if there is // no suitable cached position available, the User Agent // will immediately invoke the error callback with code // TIMEOUT and will not initiate a new position // acquisition process. } function errorCallback (error) { switch(error.code) { case error.TIMEOUT: // Quick fallback when no suitable cached position exists. doFallback(); // Acquire a new position object. navigator.geolocation.getCurrentPosition(successCallback, errorCallback); break; case ... // treat the other error cases. }; } function doFallback() { // No fresh enough cached position available. // Fallback to a default position. }
LREST version:
Unknown
Supporting a timeout is not hard, in fact $.ajax
does have a timeout parameter
which could be used for any value other than the one used here: zero. The special semantics of
timeout zero here taken to mean "give me a value from cache or don't give me anything" have no
mapping to HTTP.
Again, while this must be noted as a limitation — which prevents the implementation of Geolocation as a LREST service atop the existing XHR stack — it may not preclude other APIs that have no such requirement from being supported.
Several points have been outlined above that require further discussion. On top of this, some additional, less concrete items have been brought up on the mailing list and need be addressed.
All the examples used above make use of a mythical service:
URI scheme. This should be seen
as a placeholder rather than as a recommendation for the ideal solution. There are several considerations
to look at before making a decision.
The first option is to mint a service:
URI scheme that we could use to access LREST
services. This would involve defining what the authority is (we could use the domain name of the
defining body), followed by an identifying path for the given API, and possibly a query string
for further identification (for instance a JSONQuery).
There are several downsides to this approach, one of which is the number of hoops that one has to jump through in order to mint a new scheme (and we'd get a lot of flak if we didn't register it — not that we'd necessarily get any less flak for doing the "right thing"). The main problem is that we are defining this endpoint as acting like an HTTP endpoint, accessed through HTTP APIs, with HTTP return codes and HTTP semantics, HTTP headers... you get the idea: it quacks like HTTP, in fact it is HTTP. If it's HTTP, then why mint a new scheme instead of using HTTP?
And that's the second option: just using HTTP. In fact, one of the arguments in favour of LREST is that websites could offer their own endpoints with the same API as the local one; for instance, Facebook could expose the Contacts API in the very same way that the LREST contacts are exposed. But that opens up the question of how one would point to an actual LREST service, knowing that it cannot be mixed up with something on the network (if only for security reasons). None of the options I can think of (highjacking localhost, using a magic URI that couldn't otherwise exist or depends on a specific TLD, using a magic port) is good, in fact they all seem fairly horrible.
If using HTTP URIs doesn't work and defining a new scheme is fraught with peril and inelegance, are we damned if we do and done in damnation? Not necessarily, for there is no issue that cannot be addressed by being stuffed far, far away under the carpet. We could therefore use completely opaque identifiers:
$.getJSON(navigator.geolocation.getPosition, showMap); $.getJSON(navigator.services.contacts.get, listContacts); // etc.
Since of course those opaque strings could be exposed, we would still need something defined but it
could be as simple as a uuid:
URI, or a given namespace ID inside of a URN with an
arbitrary, implementation-dependent value. Similar discussions are ongoing in WebApps concerning
the File Reader API.
One issue being discussed and not addressed in this document is that of access control. Indeed, most APIs that we are concerned with should not be exposed without user consent.
In effect, it is unlikely that solutions that have been listed so far would function for LREST. Both CORS and UMP are concerned with access to remote services being expressed by said remote service (as opposed to interactively by a user). While at the implementation level they could be used (in a rather naïve implementation that would actually use a local web server) to authorise the request they don't address the problem of getting the user's consent in the first place. Likewise, OAuth could be used to provide the script with a form of token that it would use to access the API, but that doesn't solve consent either (and potentially leads to rather clumsy UI).
I would expect all the APIs concerned by a potential LREST model to have similar access control mechanisms, and such mechanisms to be used irrespective of whether they are exposed through LREST or through vanilla JS APIs (e.g. infobar decision).
Issues outlined in this section only need to be addressed if we do select a LREST approach, they are listed mostly for purposes of discussion.
An LREST approach does not prevent us from being formal. In fact, specifications will need to define what the JSON objects that are exchanged over LREST look like (note that I am assuming JSON to the protocol's syntax rather than XML, as it is much easier to process from JS and fits the requirements).
Two formalisms could be used: WebIDL or JSON Schema. Neither has been finalised. JSON Schema is more powerful but WebIDL is better known and more supported in tooling. We would also need to define how unknown properties are expected to be treated and how implementers are supposed to make extensions. Common serialisations for some frequently needed types (such as Dates) may be needed.
Some API calls require filtering, for instance to find a given contact by its full name. This could be defined using JSON Query, with the caveat that it is not currently well specified.
These could be out of scope (as described above), or a comet protocol could be used.
One aspect that isn't covered in the Geolocation examples is creation of a new object for storage. This could be done rather naturally using POST and PUT requests.
The level of indirection involved in using LREST raises performance and integrability concerns. While translation between native representation and Javascript objects has a non-negligible cost, it will be lower than that involved in resolving a URI (even if opaque) to a service, and producing the JSON representation (which will then have to be parsed...) for the same object.
This issue could be partly alleviated by having a responseJSON
field available on
XMLHttpRequest objects under certain conditions, and only serialising if responseText
is requested. Protocol overhead should not be a major issue as most of it can be emulated away (i.e.
skipped) though one would have to look at each specific header supported by XMLHttpRequest to
make sure that it would not cause particular issues (notably semantic clashes when ignored).
Nevertheless integration into existing runtimes needs to be investigated, notably the feasibility for approaches such as PhoneGap to intercept LREST requests and to process them with sufficient efficiency on mobile devices.
Ideally, if the debate continues we would want to benefit from closer evaluation of the feasibility and a rough idea of the performance impact.
John Kemp of the TAG has suggested that the WG merely use REST as a model from which to create conventional JS APIs rather than going all the way to defining a full LREST approach. This idea has issues similar to those in Steve Lewontin's interesting suggestion to model at least some of the APIs (probably the same set as selected here) as databases in that it doesn't cleanly account for subscriptions, but it does have the advantage of a more readily defined querying system (given the pains of agreeing on some form of SQL).
This may indeed be an option, though how it will be exposed in specifications or how it is to be evaluated against proposed API designs would still need to be decided.