This week's widget shows how to make "instantaneous" web apps with queued AJAX requests using can.Model and the can/model/queue
plugin. The plugin puts an end to spinners, progress-bars, and "loading" text. This article covers how to make apps more responsive with queued requests and recover from service errors.
Instantaneous Web Apps
Traditional, "blocking" web apps make users wait for their changes to be persisted with a:
- spinner,
- progress bar,
- disabled submit button, or
- loading text
Queued requests enable users to make changes to data without blocking, with the speed and responsiveness of a desktop app. To illustrate the difference, we built two versions of a widget that allows a user to move a character around on the screen.
Checkout the difference between the blocking and non-blocking versions:
Blocking
The character can't move until its position is persisted to the server.
Instantaneous
The player is allowed to move freely. This is achieved with the Queued requests plugin. Lets see how it works and after that, how it is used in this widget.
Queued requests plugin
The can/model/queue
plugin provides following features on top of can.Model:
Requests are queued and run sequentially
Calling .save()
adds a request to the queue. The request will be made only after previous requests have completed. For example, consider creating a Task
, saving it to the server, and before the response comes back, changing it and saving it two more times:
var task = new Task({name: "Wash"});
task.save(); // 1
task.attr('name',"Wash car");
task.save(); // 2
task.attr('name','Wash clothes');
task.save(); // 3
The requests with their responses are made in the following correct order:
1. POST /tasks
name=Wash
=> {id: 3, name: "Wash", updated: 1363623400000}
2. PUT /tasks/3
name=Wash car
=> {id: 3, name: "Wash car", updated: 1363623401000}
3. PUT /tasks/3
name=Wash clothes
=> {id: 3, name: "Wash clothes", updated: 1363623402000}
Non-conflicting property changes sent back by the server are set on the model instance.
When the first request returns, the task's .attr()
data is set to:
{ id: 3, name: "Wash clothes", updated: 1363623400000 }
The task
has the id
and updated
properties returned from the initial request, but name
is not overwritten. Instead it remains the last value set by .attr()
.
Requests are sent with a snapshot of the model instance's data
When the second request is made task.attr('name')
is "Wash clothes"
. However, name=Wash car
is sent. This is because requests are sent with a snapshot of model instance taken at the moment .save()
was called.
A backup of the last successful state is kept to allow graceful error recovery.
Call .restore()
to set the model instance's properties back to their last successful state. For example, if .save()
should fail, the following restores task's name
property to its last valid value:
task.attr('name',"Something evil");
var def = task.save();
def.fail(function(){
task.restore();
})
The Widget
The widget is implemented as the RPG
can.Control. The control:
- Creates a player model instance.
- Creates an
isMoving
compute that indicates if the player is moving. - Renders the map and live-bound player instance.
-
Saves the initial position of the player.
var RPG = can.Control({ init: function() { this.player = new Player({ position: { top : 0, left : 0, direction: “down” } }); this.isMoving = can.compute(false); this.element.html(can.view(‘avatar’, { player : this.player, isMoving : this.isMoving })); this.savePosition(); },
When an arrow key is pressed, the control updates the character's position. With live binding, the character moves as the position attributes are changed:
"{document} keydown": function(el, ev) {
this.move(directions[ev.which]);
},
move: function(direction) {
var position, direction, newPosition;
if(direction) {
this.isMoving(true);
this.player.attr(
'position.direction',
direction.toLowerCase()
);
if(direction === 'UP') {
this.player.attr(
'position.top',
min(this.player.attr('position.top') - 5, 0)
);
}
...
this.savePosition();
}
},
...
The control saves the current position of the character when its position is updated:
savePosition: function() {
this.player.save(undefined, $.proxy(this.loadBackup, this));
}
If the request fails, loadBackup
restores the character's position to the last valid state:
loadBackup: function() {
alert('You walked in to the fire.' +
'Resurrecting you to the last good position');
this.player.restore(true);
}
The widget without the queue plugin seems much slower because character's movements are blocked while the state is persisted.
With the queue plugin, you can just fire the save requests after each move and the plugin will take care of the requests ordering without blocking - moving around and replays are smooth and in the right order.
Queue state
Queue plugin exposes the hasQueuedRequests
(can.Model.prototype.hasQueuedRequests
) live-bindable function which allows you to monitor the queue state. In this demo, it is used to show the "Saving!" message while the requests are running:
<span>{{#hasQueuedRequests}}Saving!{{/hasQueuedRequests}}</span>
Conclusion
Queued requests allow you to save models early and often, without caring about pending requests. The can/model/queue
plugin enables "instantaneous" web apps. You can use it in your projects without any changes to the code, it just works.
What will you build with it?