How to Create a PouchDB Infobot
So you know how to set up PouchDB. What’s next?
Write an app, of course!
We’re going to develop a simple infobot style app that lets us store and retrieve useful information. Thanks to the wonders of PouchDB, we can automatically sync that information to and from a remote server. This means that we could have many clients, meaning multiple users potentially collaborating; and many servers, providing load balancing.
Designing The App
To store information in the original infobot, you use a statement with the syntax “A is B”, and then infobot will silently retain this information. Example:
<user> ey is http://engineyard.com/
Later you can reclaim B by interrogating A with the syntax “A?”, like so:
<user> ey?
<infobot> somebody said ey is http://engineyard.com/
We will have to adapt our app a little for the web environment, but we’ll try to retain some of the feel of this method of input. The biggest challenge is replicating the “chat” environment in a webpage.
Since the data is just an associative array (usually called a hash table), we could have two inputs corresponding to the common operations of hash tables, put and get. Put would take two inputs, the A and B of the above, and get would take just the A. But this would require tabbing or clicking between the fields. It’s also harder to extend if we wanted to, for example, start to support other operations such as delete.
Instead, we’ll retain the conversational input and parse it using some simple regexps.
Setting Up
First we should create a checklist of things to be installed and available:
- Apache CouchDB installed
- CORS set up correctly in CouchDB
- CouchDB started
- Command line CoffeeScript compiler using Node.js
And our working directory will contain three minified JS files:
- jQuery (we’re using
jquery-2.1.1.min.js
) - CoffeeScript (we’re using version 1.7.1 of
coffee-script.js
) - PouchDB (we’re using
pouchdb-3.0.3.min.js
)
You can use whatever the latest versions of these scripts are, and this example should still work. But if you run into problems, try using versions close to those given above.
PouchDB does not require CoffeeScript or jQuery. We’re using those libraries for convenience, but you’re free to pair PouchDB with anything, or nothing.
Now we create a tiny boilerplate HTML file:
<!doctype html>
<meta charset="utf-8">
<title>PouchDB Infobot</title>
<script src="jquery-2.1.1.min.js"></script>
<script src="coffee-script.js"></script>
<script src="pouchdb-3.0.3.min.js"></script>
<script type="text/coffeescript" src="infobot.coffee"></script>
You can call this file whatever you like, e.g. infobot.html
. Notice that this links to an infobot.coffee
file which doesn’t currently exist. That’s what we’re going to write, to create our app.
Telling The Story
Reading procedural code is like reading a story. The runtime execution flow is like an unfolding plot, from the beginning of the script, the initialisation routines, onwards.
But when a program is written, the story is told in reverse! Imagine a script that gets the user’s name and then tells them how many vowels are in their name:
vowels = (name) ->
total = 0
total += name.count vowel for vowel in ["a", "e", "i", "o", "u"]
total
hello = (name) ->
say "Hello, #{ name }!"
say "Your name contains #{ vowels name } vowels"
When we run the code, execution will start with the hello function. Then the hello function uses the vowels function. But we had to put the vowels function first in the source code, because otherwise the hello function’s variable scope won’t include the vowel function. This is why programs have to be written backwards.
When we explain programs, however, we want to explain them as though telling the story—explaining them forwards. So the following code tells the story in reverse. When you create the CoffeeScript file from these functions, you’ll need to paste them in reverse order for the file to work!
We’re going to explain each part of the code in reverse order. As we do, copy and paste the code examples into your infobot.coffee
file, starting at the bottom and working your way up. The functions that we explain are written in bold monospace
in the code blocks.
Script
We start by registering an action to do when jQuery fires a document ready event:
$ script
This means that when the document has successfully loaded and is ready for us to work in, jQuery will call our script function. Here it is:
script = () ->
beautiful_style()
connect_to_couchdb("infobot")
This sets up the CSS so that the page looks nice, and then continues to connecting to CouchDB, which will start the further series of things we want to do to make our app work. We’re using infobot
for the name of the CouchDB database.
Beautiful Style
beautiful_style = () ->
$("body").css
font: "1em 'Helvetica Neue', Helvetica"
padding: "36px"
The style is set by jQuery. You can update this if you want to make it look even more beautiful, e.g. by setting a lighter weight font, or by making the inputs centre aligned.
Connect to CouchDB
connect_to_couchdb = (name) ->
db = PouchDB name
$("<p>Synchronising the remote database...</p>").appendTo "body"
db.sync("http://127.0.0.1:5984/#{ name }", {live: true})
.on("uptodate", -> synchronisation_succeeded db)
The name of the database, that we passed in script
, is infobot
. This function connects to CouchDB, creates that database if it doesn’t already exist, and then tells the user that we’re going to synchronise. We do a db.sync
call, which is PouchDB parlance for a bidirectional replication. We’re just using a local CouchDB instance running on 127.0.0.1:5984 for this sample app. We set the option {live: true}
so that it will continue to bidirectionally synchronise between the browser and the CouchDB server.
We then register an action to perform when the synchronisation is currently up to date. Actually we only want to do something the first time that the synchronisation is up to date, i.e. set up the UI. But we can handle this within the synchronisation_succeeded
function itself.
Synchronisation Succeeded
synchronisation_succeeded = (db) ->
if $("#input").size() < 1
$("<p>Successfully synchronised!</p>").appendTo "body"
input = create_input()
input.bind "enter", (e) ->
user_input db, input.val()
input.val ""
input.focus()
Since we successfully synchronised with the CouchDB server, we can now set up the UI. We only want to do this the first time that PouchDB synchronises, so how do we make sure that we haven’t already synchronised? PouchDB doesn’t provide any way to only bind to the first instance of an event firing, so we have to roll our own method.
We simply check for an existing UI component, #input
, and if it exists then we know that we’ve already created the UI. If it doesn’t exist, then we go ahead and create it. To create it, we first notify the user that the synchronisation succeeded. Then we create the input element which will be used for getting commands from the user. Next we register an action to perform when the user inputs a command. We do two things: process their input using the user_input
function, and then clear out the value just entered so that they can enter some more without the old value lingering around.
After all this, we focus the input element, so that the page is ready straight away for the user to input some text. Since we don’t do anything after this, the next action is up to the user! We’re now waiting on user input, and then we’ll call user_input
each time they ask us to do something.
Create Input
This is how the user input element was created:
create_input = () ->
$("<input>").attr(
id: "input"
type: "text"
size: "79"
).keyup((e) -> $(this).trigger "enter" if e.keyCode is 13)
.appendTo($("<p>").appendTo "body")
It’s just an <input>
element within a <p>
—there’s no <form>
. It works by listening to the user pressing the return key inside the input. The key code 13 means return. This triggers the enter
event, which is what we bound to in the sychronisation_succeeded
function. (It’s the block starting input.bind "enter", (e) ->
).
User Input
Once we have the user input, we want to process it.
There are basically three modes:
- The user gives us “A?”, so we need to get the answer
- The user gives us “A is B”, so we need to store A = B
- The user gives us something else, which is an error
To select which mode we want to use, we’ll use some regular expressions:
user_input = (db, text) ->
get = text.match /^([^ ]+)[?]$/
put = text.match /^([^ ]+) is (.*)$/
if get then user_get db, get[1]
else if put then user_put db, put[1], put[2]
else user_error text
The first regular expression matches the “A?” case, and the second matches the “A is B” case. We don’t need a regular expression for the case of user error, since anything that doesn’t match either of those two is defined as an error.
Once we’ve determined which of the three different kinds of input the user has entered, we then turn to the function corresponding to that kind of input.
User GET
If the user wants us to get an item from the infobot, then we need to do a GET
action on the CouchDB server using PouchDB. This is a db.get
call:
user_get = (db, A) ->
db.get A , (err, doc) ->
if not err
display "Someone told me that #{ strong A } is #{ strong doc.B }"
else
display "Sorry, I don't know about #{ strong A }"
There are two possibilities when we do a GET
. Either it succeeds, or it doesn’t! If it succeeds, then we can just tell the user what value was stored. If it doesn’t succeed, that usually means that no value has been stored yet. Even if it was some different error, the basic fact is that the bot can’t give a reply. So we just tell the use that infobot doesn’t know about A. We could extend infobot here to give us more specific errors if we get them.
User PUT
If the user wants us to put an item into infobot, then we need to do a PUT
action on the CouchDB server using PouchDB. This is a db.put
call, but unlike with the db.get
version, we can’t just go ahead and do that. The problem is that if a value has already been stored for our A, then we have to update that document instead of storing it. These are two different concepts in CouchDB, because when you update a document, you must specify which version you’re trying to update. This is how CouchDB implements Multiversion Concurrency Control.
To figure out whether we need to store or update, we first do a db.get
. If we get an error from that db.get
call, we know that there’s no existing document and we just need to store. If we got a successful response, on the other hand, then we need to update the document.
This is the most complex part of this code, so don’t worry too much if you don’t understand this entirely on first read. We take the approach of deleting the document before then storing a new version, to avoid having to deal with revisions.
user_put = (db, A, B) ->
doc = {_id: A, B: B}
db.get A, (err, old) ->
if not err
db.remove old, (err, resp) ->
if not err
put_doc db, doc, A
else
console.log "Remove error: #{ err }"
display "Sorry, was unable to store #{ strong A }!"
else
put_doc db, doc, A
Once we’ve made sure that there’s no existing document in there, i.e. deleting an existing one if necessary, we can just put a the new version using the put_doc
function.
Note that we’re storing user input directly as a document ID. For our simple app, this is fine. But in a real app, you’d want to namespace the document ID so that there was no possible conflict with CouchDB reserved IDs. Alternately, you could switch to automatically generated UUIDs and use a view, which is a type of query. But that is beyond the scope of the present post.
PUT doc
To put the information to the CouchDB server, we use db.put
:
put_doc = (db, doc, A) ->
db.put doc, (err, resp) ->
if not err
display "Stored #{ strong A } successfully"
else
console.log "Put error: #{ err }"
display "Sorry, was unable to store #{ strong A }!"
This is quite straightforward compared to the logic of the user_put
function that calls this simpler piece of machinery. We simply store the document, and then if PouchDB tells us that everything worked correctly, we tell the user. Otherwise, we tell them that we were unable to store it, having logged the error to the JavaScript console for good measure.
User Error
If the user gave us some input that we don’t understand, we can tell them that we didn’t understand it, having also logged it to the JavaScript console for easier debugging:
user_error = (text) ->
console.log "User entered: #{ text }"
display "Sorry, I didn't understand that input!"
Display and Strong
You may have noticed that to tell the user what’s happening, we’ve been using the display
function, and that the display command argument often uses the strong
function in order to provide formatting. These are quite straightforward functions:
display = (text) ->
$("<p>#{ text }</p>").insertBefore "#input"
strong = (text) ->
"<strong>#{ text }</strong>"
And that’s it, the code should now be functional!
Results
When you save these code snippets, in reverse order, into your infobot.coffee
, and have everything else in the checklist up and running, then you should be able to interact with your HTML like this.
If you’d like to just copy and paste the full file contents in one go, here’s one I made earlier.
Because this app makes cross origin requests (i.e. speaks to a CouchDB database on a different domain) you’ll need serve it up over HTTP. You can do this by making use of a neat feature of the Python standard library.
In your code directory, run:
$ python -m SimpleHTTPServer
Then visit http://0.0.0.0:8000/infobot.html
in your browser:
Replicate this screenshot by typing:
noah is cool
Then type:
noah?
Conclusion
We built a very simple app with CouchDB and PouchDB. This combination gives us a lot of flexibility. For example, we could use our simple test app from multiple browsers without having to rely on the local state of a single browser to save all of our data. And with a small change—setting a remote instead of a local server—we can enable multiple users to work in a single system.
Better yet, this app works offline. Try it. Stop CouchDB. You can continue to use the app. You can set values and you can retrieve values. If you start CouchDB again and reload the page, PouchDB takes advantage of CouchDB’s powerful sync protocol to bring the two databases (one in your browser, and one managed by CouchDB) back to a state of consistency.
The next step would be to set up continuous replication so that no reload was necessary. This would keep the two databases in sync whenever there was a connection present. And all of this, completely for free for your app.
If you enjoyed this tutorial, or if you found anything confusing or had any problems, please let us know in the comments. If you have any general comments about using PouchDB, we’d be interested to hear those too! Throw us a comment using the form below.
Share your thoughts with @engineyard on Twitter