📜 ⬆️ ⬇️

PouchDB or what to do when the “internet is stable”

Nowadays, the speed of the WEB-application greatly affects the loyalty of users. Often you have to transfer some business logic to client code running in the user's browser.

In such a situation, it becomes necessary to quickly obtain data related to tasks. But sometimes the time for their request from the server database is too large, and sometimes the connection to the network is completely unstable or absent. In this situation, you can resort to storing data locally at the user and synchronize as needed.

Acquaintance


PouchDB is a database that works on the basis of existing solutions for storing information in the user's browser. In fact, it acts as a facade and provides a universal API regardless of the conditions under which the application is launched.
')
image

Browser versions in which PouchDB works:


Since this is a NoSQL database, the terminology is slightly different from the usual relational solutions. You can see the difference in the correspondence table:

image

Base API


To create a database, just run:

const db = new PouchDB('databaseName');

and you can already work with it. Creating, deleting and reading data from PouchDB is just as easy:

db.put(data)
.then(response => { /* handling result */ })
.catch(err => { /* errors handling */ });
db.remove(data)
.then(response => { /* handling result */ })
.catch(err => { /* errors handling */ });
db.get(data)
.then(doc => { /* do something with document */ })
.catch(err => { /* errors handling */ });

Where to use?


One of the main areas of application of such functionality is continuous work from the user's point of view. For example, the user started filling out a form with some data, until he finished, you can save them locally. Thus, it is possible not only to enable the user to interrupt, but also to protect against the loss of the entered data in case of errors while sending them to the server.

Local databases can also be used for caching. For example, a user is viewing ads from his or her smartphone. You can save the list locally and provide information much faster.

Another direction is the work offline. For example, a user can manage his notes without access to the Internet, while they are synchronized as soon as access appears. Anyway, you can build full-fledged offline or desktop applications without limiting the functionality and without the additional costs of supporting or renting servers.

Replication and conflicts


The main feature of PouchDB is a flexible synchronization system. This database can be synchronized with other local and remote systems. Such functionality is available out of the box and does not require additional settings or writing a large amount of code.

The only condition is that you can only synchronize with the CouchDB-like protocols. This can be CouchDB, Cloudant, Couchbase Sync Gateway or PouchDB Server. The latter, in turn, can be a layer between other services, like Redis, Riak, or MySQL, through the LevelUP ecosystem .

Simple sync example:

const localDB = new PouchDB('databaseName');
const remoteDbUrl = 'http://domain:5984/dbName';
localDB.sync(remoteDbUrl, {live: true, retry: true});

During synchronization, a conflict may arise when the record has been changed on both sides since the last update. PouchDB uses a record revision system to properly resolve such conflicts.

Each entity has its own history of changes. Each change, including “creation”, generates a unique identifier of the entity version:

{
"_id": "test-doc-1",
"_rev": "1-A6157A5EA545C99B00FF904EEF05FD9F",
"content": "Some content"
}

For any operations with entities, it is necessary to specify the revision number. In the simplest cases, this helps to solve the so-called “immediate conflicts” using optimistic blocking.

In real life there is a more serious problem - “possible conflicts”. They may occur during the synchronization of databases, and then to understand which version of the record should remain ultimately becomes not so easy. In PouchDB, to resolve such situations, an entity change history is recorded, and all versions are saved during synchronization.

This allows you to resolve the conflict without blocking the synchronization process, both using the algorithm and manually.

Synchronization options can be different: unidirectional replication to either of two sides, bi-directional replication.
Customization occurs with a few lines of code:

localDB.replicate.to(remoteDB).on('complete', () => {}).on('error', (err) => {});

or in the other direction:

localDB.replicate.from(remoteDB).on('complete', () => {}).on('error', (err) => {});

or both at once:

localDB.sync(remoteDB);
view raw pouchDB-sync.js hosted with ❤ by GitHub

As expected, you can handle the events generated during synchronization:

localDB
.sync(remoteDB, { live: true, retry: true })
.on('change', (info) => { /* */ })
.on('paused', (err) => { /* */ })
.on('active', () => { /* */ })
.on('denied', (err) => { /* */ })
.on('complete', (info) => { /* */ })
.on('error', (err) => { /* */ });

You can use this functionality for different types of applications:



Map / Reduce functions


To simplify and speed up content searching, PouchDB uses Map / Reduce functions. A close analogy to them is building an index in a relational database.

For example, to “index” the name field in a document, you can use the following construction:

const ddoc = {
_id: '_design/my_index',
views: {
by_name: {
map: function (doc) {
emit(doc.name);
}.toString()
}
}
};
pouch
.put(ddoc)
.then(() => { /* success */ })
.catch((err) => { /* errors handling */ });
view raw pouchDB-index.js hosted with ❤ by GitHub

Using the created function is as follows:

localDB
.query('my_index/by_name', {key: 'foo'})
.then((res) => { /* handle result */ })
.catch((err) => { /* handle error */ });

You can use a large number of different parameters affecting the sample, for example, the maximum number of records, the display of complete information about the document or only its identifier and others:

pouch
.query(mapFunction, {
startkey: 'C',
endkey: 'C\uffff',
limit: 5,
include_docs: true
})
.then((result) => { /* handle result */ })
.catch((err) => { /* handle errors */ });

First you need to create a map function, for example:

function mapFunction(doc) {
emit(doc.name);
}

It will "index" the specified fields. You can control the processing of each value. In the example below, only documents with the type brick are processed, and only the color field. In addition, the blue color will be processed and saved as the string “Wow! Blue brick! ”

pouch.query(mapReduceFun, {
key: 'White',
reduce: true,
group: true
})
.then((result) => { /* handle result */ })
.catch((err) => { /* handle errors */ });

Then, by created indexes, you can search or aggregate data by any parameter. To do this, use reduce-functions, they combine all the data obtained after the work of the map-function and process them as needed. Using the written map function you can implement:

const mapReduceFun = {
map: mapFunction,
reduce: '_count'
};

Thus, at first we will select records by the specified parameters and ultimately calculate the number of documents found:

pouch.query(mapReduceFun, {
key: 'White',
reduce: true,
group: true
})
.then((result) => { /* handle result */ })
.catch((err) => { /* handle errors */ });

Conclusion


At this functionality PouchDB is not limited.

There are many more features recommended for use by advanced users. And if the possibilities out of the box are not enough, you can connect one or more plug-ins. Their choice is large enough and constantly expanding.

Faced with the problems of fast data access, organization of serverless architecture and unstable Internet connection, you can pay attention to PouchDB as one of the solutions.

Source: https://habr.com/ru/post/342318/


All Articles