Skip to content

Commit 078ddb0

Browse files
authored
Update 6-data-storage-03-indexeddb-article.md
1 parent 77373b2 commit 078ddb0

1 file changed

Lines changed: 82 additions & 20 deletions

File tree

6-data-storage/03-indexeddb/article.md

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,18 @@ We can have many databases with different names, but all of them exist within th
3535

3636
After the call, we need to listen to events on `openRequest` object:
3737
- `success`: database is ready, there's the "database object" in `openRequest.result`, that we should use it for further calls.
38-
- `error`: open failed.
39-
- `upgradeneeded`: database version is outdated (see below).
38+
- `error`: opening failed.
39+
- `upgradeneeded`: database is ready, but its version is outdated (see below).
4040

4141
**IndexedDB has a built-in mechanism of "schema versioning", absent in server-side databases.**
4242

43-
Unlike server-side databases, IndexedDB is client-side, in the browser, so we don't have the data at hands. But when we publish a new version of our app, we may need to update the database.
43+
Unlike server-side databases, IndexedDB is client-side, the data is stored in the browser, so we, developers, don't have direct access to it. But when we publish a new version of our app, we may need to update the database.
4444

4545
If the local database version is less than specified in `open`, then a special event `upgradeneeded` is triggered, and we can compare versions and upgrade data structures as needed.
4646

4747
The event also triggers when the database did not exist yet, so we can perform initialization.
4848

49-
For instance, when we first publish our app, we open it with version `1` and perform the initialization in `upgradeneeded` handler:
49+
When we first publish our app, we open it with version `1` and perform the initialization in `upgradeneeded` handler:
5050

5151
```js
5252
let openRequest = indexedDB.open("store", *!*1*/!*);
@@ -71,10 +71,10 @@ When we publish the 2nd version:
7171
```js
7272
let openRequest = indexedDB.open("store", *!*2*/!*);
7373

74-
// check the existing database version, do the updates if needed:
7574
openRequest.onupgradeneeded = function() {
75+
// the existing database version is less than 2 (or it doesn't exist)
7676
let db = openRequest.result;
77-
switch(db.version) { // existing (old) db version
77+
switch(db.version) { // existing db version
7878
case 0:
7979
// version 0 means that the client had no database
8080
// perform initialization
@@ -85,6 +85,8 @@ openRequest.onupgradeneeded = function() {
8585
};
8686
```
8787

88+
So, in `openRequest.onupgradeneeded` we update the database. Soon we'll see how it's done. And then, only if its handler finishes without errors, `openRequest.onsuccess` triggers.
89+
8890
After `openRequest.onsuccess` we have the database object in `openRequest.result`, that we'll use for further operations.
8991

9092
To delete a database:
@@ -94,9 +96,70 @@ let deleteRequest = indexedDB.deleteDatabase(name)
9496
// deleteRequest.onsuccess/onerror tracks the result
9597
```
9698

99+
```warn header="Can we open an old version?"
100+
Now what if we try to open a database with a lower version than the current one? E.g. the existing DB version is 3, and we try to `open(...2)`.
101+
102+
That's an error, `openRequest.onerror` triggers.
103+
104+
Such thing may happen if the visitor loaded an outdated code, e.g. from a proxy cache. We should check `db.version`, suggest him to reload the page. And also re-check our caching headers to ensure that the visitor never gets old code.
105+
```
106+
107+
### Parallel update problem
108+
109+
As we're talking about versioning, let's tackle a small related problem.
110+
111+
Let's say, a visitor opened our site in a browser tab, with database version 1.
112+
113+
Then we rolled out an update, and the same visitor opens our site in another tab. So there are two tabs, both with our site, but one has an open connection with DB version 1, while the other one attempts to update it in `upgradeneeded` handler.
114+
115+
The problem is that a database is shared between two tabs, as that's the same site, same origin. And it can't be both version 1 and 2. To perform the update to version 2, all connections to version 1 must be closed.
116+
117+
In order to organize that, the `versionchange` event triggers an open database object when a parallel upgrade is attempted. We should listen to it, so that we should close the database (and probably suggest the visitor to reload the page, to load the updated code).
118+
119+
If we don't close it, then the second, new connection will be blocked with `blocked` event instead of `success`.
120+
121+
Here's the code to do that:
122+
123+
```js
124+
let openRequest = indexedDB.open("store", 2);
125+
126+
openRequest.onupgradeneeded = ...;
127+
openRequest.onerror = ...;
128+
129+
openRequest.onsuccess = function() {
130+
let db = openRequest.result;
131+
132+
*!*
133+
db.onversionchange = function() {
134+
db.close();
135+
alert("Database is outdated, please reload the page.")
136+
};
137+
*/!*
138+
139+
// ...the db is ready, use it...
140+
};
141+
142+
*!*
143+
openRequest.onblocked = function() {
144+
// there's another open connection to same database
145+
// and it wasn't closed after db.onversionchange triggered for them
146+
};
147+
*/!*
148+
```
149+
150+
Here we do two things:
151+
152+
1. Add `db.onversionchange` listener after a successful opening, to be informed about a parallel update attempt.
153+
2. Add `openRequest.onblocked` listener to handle the case when an old connection wasn't closed. This doesn't happen if we close it in `db.onversionchange`.
154+
155+
There are other variants. For example, we can take time to close things gracefully in `db.onversionchange`, prompt the visitor to save the data before the connection is closed. The new updating connection will be blocked immediatelly after `db.onversionchange` finished without closing, and we can ask the visitor in the new tab to close other tabs for the update.
156+
157+
Such update collision happens rarely, but we should at least have some handling for it, e.g. `onblocked` handler, so that our script doesn't surprise the user by dying silently.
97158

98159
## Object store
99160

161+
To store something in IndexedDB, we need an *object store*.
162+
100163
An object store is a core concept of IndexedDB. Counterparts in other databases are called "tables" or "collections". It's where the data is stored. A database may have multiple stores: one for users, another one for goods, etc.
101164

102165
Despite being named an "object store", primitives can be stored too.
@@ -146,12 +209,12 @@ To perform database version upgrade, there are two main approaches:
146209
1. We can implement per-version upgrade functions: from 1 to 2, from 2 to 3, from 3 to 4 etc. Then, in `upgradeneeded` we can compare versions (e.g. old 2, now 4) and run per-version upgrades step by step, for every intermediate version (2 to 3, then 3 to 4).
147210
2. Or we can just examine the database: get a list of existing object stores as `db.objectStoreNames`. That object is a [DOMStringList](https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#domstringlist) that provides `contains(name)` method to check for existance. And then we can do updates depending on what exists and what doesn't.
148211

149-
For small databases the second path may be simpler.
212+
For small databases the second variant may be simpler.
150213

151214
Here's the demo of the second approach:
152215

153216
```js
154-
let openRequest = indexedDB.open("db", 1);
217+
let openRequest = indexedDB.open("db", 2);
155218

156219
// create/upgrade the database without version checks
157220
openRequest.onupgradeneeded = function() {
@@ -407,9 +470,9 @@ Methods that involve searching support either exact keys or so-called "range que
407470

408471
Ranges are created using following calls:
409472

410-
- `IDBKeyRange.lowerBound(lower, [open])` means: `>lower` (or `lower` if `open` is true)
411-
- `IDBKeyRange.upperBound(upper, [open])` means: `<upper` (or `upper` if `open` is true)
412-
- `IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen])` means: between `lower` and `upper`, with optional equality if the corresponding `open` is true.
473+
- `IDBKeyRange.lowerBound(lower, [open])` means: `lower` (or `>lower` if `open` is true)
474+
- `IDBKeyRange.upperBound(upper, [open])` means: `upper` (or `<upper` if `open` is true)
475+
- `IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen])` means: between `lower` and `upper`. If the open flags is true, the corresponding key is not included in the range.
413476
- `IDBKeyRange.only(key)` -- a range that consists of only one `key`, rarely used.
414477

415478
All searching methods accept a `query` argument that can be either an exact key or a key range:
@@ -428,16 +491,16 @@ Request examples:
428491
// get one book
429492
books.get('js')
430493

431-
// get books with 'css' < id < 'html'
494+
// get books with 'css' <= id <= 'html'
432495
books.getAll(IDBKeyRange.bound('css', 'html'))
433496

434-
// get books with 'html' <= id
435-
books.getAll(IDBKeyRange.lowerBound('html', true))
497+
// get books with id < 'html'
498+
books.getAll(IDBKeyRange.upperBound('html', true))
436499

437500
// get all books
438501
books.getAll()
439502

440-
// get all keys: id >= 'js'
503+
// get all keys: id > 'js'
441504
books.getAllKeys(IDBKeyRange.lowerBound('js', true))
442505
```
443506

@@ -517,7 +580,7 @@ request.onsuccess = function() {
517580
We can also use `IDBKeyRange` to create ranges and looks for cheap/expensive books:
518581

519582
```js
520-
// find books where price < 5
583+
// find books where price <= 5
521584
let request = priceIndex.getAll(IDBKeyRange.upperBound(5));
522585
```
523586

@@ -615,7 +678,7 @@ In the example above the cursor was made for the object store.
615678

616679
But we also can make a cursor over an index. As we remember, indexes allow to search by an object field. Cursors over indexes to precisely the same as over object stores -- they save memory by returning one value at a time.
617680

618-
For cursors over indexes, `cursor.key` is the index key (e.g. price), and we should use `cursor.primaryKey` property the object key:
681+
For cursors over indexes, `cursor.key` is the index key (e.g. price), and we should use `cursor.primaryKey` property for the object key:
619682

620683
```js
621684
let request = priceIdx.openCursor(IDBKeyRange.upperBound(5));
@@ -737,12 +800,11 @@ IndexedDB can be thought of as a "localStorage on steroids". It's a simple key-v
737800

738801
The best manual is the specification, [the current one](https://w3c.github.io/IndexedDB) is 2.0, but few methods from [3.0](https://w3c.github.io/IndexedDB/) (it's not much different) are partially supported.
739802

740-
The usage can be described with a few phrases:
803+
The basic usage can be described with a few phrases:
741804

742805
1. Get a promise wrapper like [idb](https://github.com/jakearchibald/idb).
743806
2. Open a database: `idb.openDb(name, version, onupgradeneeded)`
744-
- Create object storages in indexes in `onupgradeneeded` handlers.
745-
- Update version if needed - either by comparing numbers or just checking what exists.
807+
- Create object storages and indexes in `onupgradeneeded` handler or perform version update if needed.
746808
3. For requests:
747809
- Create transaction `db.transaction('books')` (readwrite if needed).
748810
- Get the object store `transaction.objectStore('books')`.

0 commit comments

Comments
 (0)