Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: James Allen
Ben Nadel at CFCamp 2023 (Freising, Germany) with: James Allen

Creating A PouchDB Playground In The Browser With JavaScript

By
Published in

Yesterday, after I put up my post about PouchDB data modeling in my offline first application, a few people asked me how I was going about experimenting with the PouchDB API. Doing so is actually quite easy. Since PouchDB is written in JavaScript, playing with it is as simple as loading it in the Browser and sending it commands. Of course, the asynchronous nature of PouchDB makes the "console" less than ideal (for experimenting). As such, I like to run my experiments directly in the page itself.

UPDATE (2016-12-15): In the video I talk about needed a Promises shim. This was incorrect. Nolan Lawson corrected me, pointing out that PouchDB will actually use the "lie" library to shim Promises at this time. So, there is no need to include an additional shim.

Run this demo in my JavaScript Demos project on GitHub.

Each PouchDB operation both accepts a callback and returns a Promise so that you can use whichever asynchronous workflow dovetails most naturally with your style. But, this means that PouchDB expects the Promise object to exist, regardless of whether or not you use it. And, while most modern browsers support Promises, they are not yet universal. As such, PouchDB currently uses the "lie" Promise library the shim Promises (though this may change in the future).

When I start experimenting with PouchDB, I want each page-load to result in a pristine database. This way, I don't run into "coincidental" conflicts. Meaning, I want all conflicts to be explicitly laid out in the experiment so that I can code against them. To accomplish this, my experiments always start with a dropping of the database followed by a recreation of it:

var dbName = "javascript-demos-pouchdb-playground";

// Creating the PouchDB database instance is a synchronous operation. This means that we
// can immediately start to interact with the "db" object. The data will be persisted to
// either IndexedDB or WebSQL (favoring IndexedDB), with plugins available for other
// persistence engines (ex, localStorage).
var db = new PouchDB( dbName );

// When I am playing around with PouchDB, I like to destroy and recreate the database on
// each test run. This way, any conflicts with existing data are explicitly coded into
// the experiment and not a byproduct of dirty data.
var promise = db.destroy().then(
	function() {

		// Once we destroy the database, we have to create a new one otherwise we'll get
		// an error, "Error: database is destroyed".
		db = new PouchDB( dbName );

	}
);

As you can see here, I start by creating and then removing the database. And, while the PouchDB database abstraction is created synchronously, operations against it are all performed asynchronously. As such, I have to "wait" for the .remove() operation to complete before I recreate the pristine database.

Once I have the pristine database, running the experiment is just a giant Promise chain in which each asynchronous "thenable" result in chained to the next operation.

// Drop and re-create the database.
db.destroy().then(
	function() {

		db = new PouchDB( dbName );

	}
).then(
	function() {

		// Perform and return some async operation against PouchDB.

	}
).then(
	function() {

		// Perform and return some async operation against PouchDB.

	}
).then(
	function() {

		// Perform and return some async operation against PouchDB.

	}
).catch(
	function( error ) {

		console.warn( "An error occurred:" );
		console.error( error );

	}
);

To bring it all together into a concrete example, here's one of the experiments that I was running when I was trying to build out my understanding of secondary indices (aka, Views) in PouchDB. In the following code, I insert a collection of Friends with "Interests". Then, I try to query the collection of friends based on various interest combinations. Since this post is more about the "experiment workflow" itself and less about the PouchDB operations, I won't offer up any explanation beyond the embedded comments.

<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>
		Creating A PouchDB Playground In The Browser With JavaScript
	</title>
</head>
<body>

	<h1>
		Creating A PouchDB Playground In The Browser With JavaScript
	</h1>

	<p>
		<em>Look at console &mdash; things being logged, yo!</em>
	</p>

	<script type="text/javascript" src="../../vendor/pouchdb/6.0.7/pouchdb-6.0.7.min.js"></script>
	<script type="text/javascript">

		var dbName = "javascript-demos-pouchdb-playground";

		// Creating the PouchDB database instance is a synchronous operation. This means
		// that we can immediately start to interact with the "db" object. The data will
		// be persisted to either IndexedDB or WebSQL (favoring IndexedDB), with plugins
		// available for other persistence engines (ex, localStorage).
		var db = new PouchDB( dbName );

		// When I am playing around with PouchDB, I like to destroy and recreate the
		// database on each test run. This way, any conflicts with existing data are
		// explicitly coded into the experiment and not a byproduct of dirty data.
		db.destroy().then(
			function() {

				// Once we destroy the database, we have to create a new one otherwise
				// we'll get an error, "Error: database is destroyed".
				db = new PouchDB( dbName );

			}
		)

		// At this point, we have a pristine PouchDB instance to experiment with. Every
		// PouchDB operation returns a Promise (though you could use a Callback if you
		// wanted to for some reason). So, to start experimenting, we can just chain the
		// "thenable" operations together.
		.then(
			function() {

				// Let's insert some Friend data.
				var promise = db.bulkDocs([
					{
						_id: "friend:kim",
						name: "Kim",
						interests: [ "Movies", "Computers", "Cooking" ]
					},
					{
						_id: "friend:sarah",
						name: "Sarah",
						interests: [ "Museums", "Working Out", "Movies" ]
					},
					{
						_id: "friend:joanna",
						name: "Joanna",
						interests: [ "Working Out", "Poetry", "Dancing" ]
					}
				]);

				return( promise );

			}
		).then(
			function() {

				// Each Friend has a collection of interests. In order to look up a
				// Friend by one of their interests, we have to index the documents by
				// the collection of interests.
				// --
				// From the documentation:
				// https://pouchdb.com/2014/05/01/secondary-indexes-have-landed-in-pouchdb.html
				//
				// Technically, a design doc can contain multiple views, but there's
				// really no advantage to this. Plus, it can even cause performance
				// problems in CouchDB, since all the indexes are written to a single
				// file. So we recommend that you create one view per design doc, and
				// use the same name for both, in order to make things simpler.
				var promise = db.put({
					_id: "_design/friends-by-interest",
					views: {
						"friends-by-interest": {
							map: function( doc ) {

								// Only add to the index, Friends that have interests.
								if ( /^friend:/i.test( doc._id ) && doc.interests ) {

									// Index each ( Friend x Interest ) combination
									// separately. This way, we can search by any interest.
									doc.interests.forEach(
										function( interest, i ) {

											emit( interest );

										}
									);

								}

							}.toString() // The map function goes into the database as a String.
						}
					}
				});

				// Views are populated lazily in PouchDB / CouchDB. As such, we need to
				// run a query on the view before PouchDB will bother building the
				// internal B+Tree index on it. This will make the next "real query"
				// faster since it doesn't have to do any pre-query work.
				promise = promise.then(
					function() {

						return( db.query( "friends-by-interest", { limit: 0 } ) );

					}
				);

				return( promise );

			}
		).then(
			function() {

				// Now that we have our index, we can find all of the friends that like
				// "Movies". The include_docs flag will pull in the documents associated
				// with each index key.
				var promise = db.query(
					"friends-by-interest",
					{
						key: "Movies",
						include_docs: true
					}
				);

				promise = promise.then(
					function( results ) {

						console.group( "Found %s friends that like Movies.", results.rows.length );
						console.info( "{ key: \"Movies\" }" );
						results.rows.forEach(
							function( row ) {

								console.log( row.doc.name );

							}
						);
						console.groupEnd();

					}
				);

				return( promise );

			}
		).then(
			function() {

				// Looking up Friends by a single interest is easy (above); but, looking
				// Friends by multiple interests is not quite so easy. We can certainly
				// query the index for multiple keys (ie, multiple interests):
				var promise = db.query(
					"friends-by-interest",
					{
						keys: [ "Movies", "Working Out" ],
						include_docs: true
					}
				);

				// .... but, that won't give us quite what we want. It's not like a
				// SQL "WHERE" clause; its more like a "UNION" operation that returns
				// records for Friends that have ANY of the given interests.
				promise = promise.then(
					function( results ) {

						// CAUTION: Returns more records that we might "expect".
						console.group( "Found %s friends that like Movies & Working Out.", results.rows.length );
						console.info( "{ keys: [ \"Movies\", \"Working Out\" ] }" );
						results.rows.forEach(
							function( row ) {

								console.log( row.doc.name );

							}
						);
						console.groupEnd();

					}
				);

				return( promise );

			}
		).then(
			function() {

				// To fix the "too many records" problem, we could use the suggestion
				// that I learned from Jesse Hallett, which is to search for one matching
				// interest and then do a client-side filter for "all" matching interests:
				// --
				// http://sitr.us/2009/06/30/database-queries-the-couchdb-way.html
				var promise = db.query(
					"friends-by-interest",
					{
						key: "Movies",
						include_docs: true
					}
				);

				promise = promise.then(
					function( results ) {

						var rows = results.rows.filter(
							function( row ) {

								// We already know that all the recipients like "Movies";
								// so, we now have to check to see which ones ALSO like
								// "Working Out".
								return( row.doc.interests.includes( "Working Out" ) );

							}
						);

						console.group( "Found %s friends that like Movies & Working Out.", rows.length );
						console.info( "{ key: \"Movies\" } + client-side filtering" );
						rows.forEach(
							function( row ) {

								console.log( row.doc.name );

							}
						);
						console.groupEnd();

					}
				);

				return( promise );

			}
		).then(
			function() {

				// Now, if we don't want to do client-side filtering based on multiple
				// interests, we can try to come up with a more "semantic" index.
				// Meaning, we can create an index that abstracts various combinations
				// of interests behind an application-specific rating.
				var promise = db.put({
					_id: "_design/friends-by-rating",
					views: {
						"friends-by-rating": {
							map: function( doc ) {

								// Only add to the index, Friends that have interests.
								if ( /^friend:/i.test( doc._id ) && doc.interests ) {

									// Now, instead of indexing the cross-product of
									// Friends and Interests, we'll build various
									// combinations into the index itself. So, Friends
									// that like movies AND working are "awesome"!
									if (
										doc.interests.includes( "Movies" ) &&
										doc.interests.includes( "Working Out" )
										) {

										emit( "awesome" );

									}

									// ... and, Friends that like movies AND working out
									// AND computers are "amazing"!
									if (
										doc.interests.includes( "Movies" ) &&
										doc.interests.includes( "Working Out" ) &&
										doc.interests.includes( "Computers" )
										) {

										emit( "amazing" );

									}

								}

							}.toString() // The map function goes into the database as a String.
						}
					}
				});

				// Populate the secondary index ahead of time.
				promise = promise.then(
					function() {

						return( db.query( "friends-by-rating", { limit: 0 } ) );

					}
				);

				return( promise );

			}
		).then(
			function() {

				// Now that we have an index that abstracts the combinations of interests
				// behind more general "rating", we can query that index for "awesome"
				// friends, ie. those who like Movies and Working Out.
				var promise = db.query(
					"friends-by-rating",
					{
						key: "awesome",
						include_docs: true
					}
				);

				promise = promise.then(
					function( results ) {

						console.group( "Found %s friends that are Awesome (ie, Movies & Working Out).", results.rows.length );
						console.info( "{ key: \"awesome\" }" );
						results.rows.forEach(
							function( row ) {

								console.log( row.doc.name );

							}
						);
						console.groupEnd();

					}
				);

				return( promise );

			}
		).catch(
			function( error ) {

				console.warn( "An error occurred:" );
				console.error( error );

			}
		);

	</script>

</body>
</html>

When we run this code in the browser, we get a fresh copy of the database each time. As such, every time we refresh the browser, we get the exact same output:

Creating a PouchDB playground in the browser using JavaScript.

For me, the predictable nature of this workflow makes the learning process easier to understand. Since I don't have to worry about the side-effects of each page execution, I know that every result I receive can be attributed to the PouchDB operations in the current page. This is how I like to experiment with PouchDB in the browser using vanilla JavaScript (which eventually leads to experimenting with PouchDB in Angular 2).

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel