An AJAX Auto-Save Implementation

Lenny Turetsky ·

For a recent client engagement, we were tasked with implementing auto-save on a multi-field form: any time any of the field values changed, we’d save the form to the server. This is a common scenario where users are composing longer inputs, such as emails, word processing, and spreadsheets.

While this may initially seem straightforward, there are a few gotchas to deal with. First, though, let’s have a look at the simplest thing that could possibly work:


<form method="POST" action="/our/url">
<textarea name="input" onChange="save(event)" />
</form>
<script language="javascript">
function save(event) {
var request = new XMLHttpRequest();
request.open(event.target.form.method, event.target.form.action);
request.send(new FormData(event.target.form));
}
</script>

This seems like it’ll work, but there are a couple of surprises that this implementation leads to:

  1. Too many HTTP requests to the server – one for every letter that’s typed.
  2. Sometimes – not always – the data saved on the server will be a couple of changes (letters) behind what the user entered, and it never catches up. In other words: data is lost!

The first issue can be solved pretty easily by wrapping the save() function’s implementation inside lodash’s debounce method, which causes the function to run no more than once every second or, in our implementation, 150 milliseconds.

But the second issue is perplexing. Why is old data ever in the DB? And why does it only happen sometimes? Intermittent bugs are the worst, but also the most interesting. For one thing, it happened less often once we solved the first issue by sending fewer requests. That was a clue.

It turns out that the cause of the data-loss problem is that the browser can issue multiple requests to the same server. And if the server receives (or handles) the first request after the second request, then the (old) data from the first request overwrites the (new) data from the second request.

To test this hypothesis – and verify the solution – we changed our server implementation to sleep() for a second whenever the input had an even number of characters. That pretty much guaranteed that requests with an odd number of characters would be processed before requests with an even number.  And when we did that it turns out that the even number requests would “stick” because they would run after the odd number requests had completed. Here’s an implementation of the server in Ruby on Rails:


class RecordsController < ApplicationController
def update
# TODO: remove the line below when the testing is complete
sleep(1) if 0 == params[:input].length % 2
@record = Record.find(params[:id])
@record.update(field_name: params[:input])
end
end

Versioning Updates

So, how do we solve this problem? Well, we could make our HTTP requests synchronous, but that may result in poor user experience because the browser could “freeze” if the server or network are slow.

A better solution is versioning. We need to know which version of the data is being sent to the server so we can prevent older versions from overwriting later versions. This can be done by adding a version_number field to the record, and rejecting any input where version_number is less than what’s already in the database.

We were tempted to replace version_number with the current timestamp, but that’s dangerous because a client might have the wrong time. If its clock is behind, its edits are never saved, but if its clock is ahead then no one else’s edits are saved. (Note, however, that this still isn’t a solution for letting multiple users edit the same record simultaneously. That’s out of scope here.)

Here’s a modified version of the server code:


class RecordsController < ApplicationController
def update
# TODO: remove the line below when the testing is complete
sleep(1) if 0 == params[:input].length % 2
@record = Record.find(params[:id])
@record.update(field_name: params[:input]) if params[:version_number] > @record.version_number
end
end

Now the Javascript has to increment the version_number every time it saves.


<form method="POST" action="/our/url">
<!– this should have the current value of version_number –>
<input type="hidden" name="version_number" value="1" />
<textarea name="input" onChange="save(event)" />
</form>
function save(event) {
event.target.form['version_number'].value++;
var request = new XMLHttpRequest();
request.open(event.target.form.method, event.target.form.action);
request.send(new FormData(event.target.form));
}

Voila! No more old data in the DB.