Welcome to the third and final (for now) entry into my series looking at wrapper libraries for working with IndexedDB. I began this series earlier this month demonstrating a simple Contacts database implemented with IndexedDB. In the second entry, I demonstrated how the Dexie library made working with IndexedDB much simpler. Today I'm going to look at my last "planned" entry (I may revisit this again if I find more) in this series - using DPP, or Deep Persistent Proxy Objects for JavaScript.

DPP makes use of the JavaScript Proxy Object feature. This is a low-level feature that lets you control how access to an object is provided. I'd like to say this is 'new' but it's been discussed for some time. It's got great support so it's safe to use, but honestly, it feels like something more tailored for library/framework developers versus "day to day" coding. Of course, this is exactly the kind of case that DPP covers - a library on top of IndexedDB.

How does DPP work? You begin with one asynchronous call to initialize your object. The simplest example would look like so:

let dpp = await createDPP({ storeName:'MyDataStore' });

This will create a store, MyDataStore, under a default IndexedDB database named DPP. You can also specify a custom name:

let dpp = await createDPP({ storeName:'MyDataStore', idb_name:'MyDB' });

Once you've initialized DPP, you then ask for the data like so:

let myObj = await dpp.start();

Just to be clear, here's a "full" example:

let dpp = await createDPP({ storeName:'MyDataStore' });
let myObj = await dpp.start();

At this point, myObj is persisted. What does that mean? I can do:

myObj.purpose = "to store crap";
myObj.lastStore = new Date();

And... I'm done. That's it. The object with two keys will be persisted and the next time I work with it, I can just use it. For example:

if(myObj.purpose) {
    console.log(`My purpose is ${myObj.purpose}`);
} else myObj.purpose = "to store crap";

Notice this is all done with regular synchronous calls. The library handles everything behind the scenes using web workers. (You can get more details on the project's readme docs.)

So how does this impact our example application? As before, I'm going to skip over discussing DOM stuff and just focus on the bits related to persistence.

Include DPP

Including DPP requires using an import statement. I'll be honest and say this still kinda confuses me in client-side JavaScript. I need to learn more about it.

const {createDPP} = await import('https://cdn.jsdelivr.net/gh/robtweed/DPP/src/dpp_browser.min.js');

Initialize the Database

Initializing the database just uses the two lines I showed above. The result is an object, not a database, so I changed from initDb to initContacts. So in my init function, I've got:

contactsOb = await initContacts();

And initContacts is:

async function initContacts() {

    const {createDPP} = await import('https://cdn.jsdelivr.net/gh/robtweed/DPP/src/dpp_browser.min.js');
    let dpp = await createDPP({
        storeName: 'contacts_dpp'
    });
    
    let contactsOb = await dpp.start();
    if(!contactsOb.contacts) contactsOb.contacts = [];
    
    
    return contactsOb;
}

Notice I check for contacts under the main object. I'm working with an array of data so I'm going to store it as contactsOb.contacts.

Working with Data

Now for the fun part. In my previous two blog posts, I had one function for each use of working with DOM stuff, like rendering contacts, editing, etc, and one function each for persistence. That made it easier to switch from "pure IndexedDB" to Dexie. However, all of that is gone now. I'm not doing the persistence, DPP is.

So for example, I don't need a function to get contacts. I already have it. And when I want to render them, I just do:

contactsOb.contacts.forEach((c,i) => {

If I want to save a contact, I either update an existing contact by its index or add it to the end of the array:

if($key.value) {
    let idx = parseInt($key.value,10);
    contactsOb.contacts[idx] = contact;
} else contactsOb.contacts.push(contact);

Deleting a contact is just a splice call:

let key = parseInt(e.target.dataset.key,10);
contactsOb.contacts.splice(key, 1);

And... I'm done. Honestly, it's shocking how cool DPP is. My original version had 181 lines of JavaScript. My Dexie version brought that down to 106. The DPP version goes down even more, to 97. While not as dramatic of a drop, there's not one line of IndexedDB code in there as it's all handled by the library. Here's the complete version for you to see:

See the Pen IDB DPP by Raymond Camden (@cfjedimaster) on CodePen.

Again, please pardon the kind of ugly formatting of the display in the embed, but I'm blown away by how simple DPP makes the application. If you've used it, I'd love to hear about it so please drop me a line. Also, if you've got suggestions for other IndexedDB libraries, please share them with me.