You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: 6-data-storage/03-indexeddb/article.md
+27-27Lines changed: 27 additions & 27 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,7 @@ That power is usually excessive for traditional client-server apps. IndexedDB is
16
16
17
17
The native interface to IndexedDB, described in the specification <https://www.w3.org/TR/IndexedDB>, is event-based.
18
18
19
-
We can also use `async/await` with the help of a promise-based wrapper, like <https://github.com/jakearchibald/idb>. That's pretty convenient, but the wrapper is not perfect, it can't replace events for all cases. So we'll start with events, and then, after we gain understanding of IndexedDb, we'll use the wrapper.
19
+
We can also use `async/await` with the help of a promise-based wrapper, like <https://github.com/jakearchibald/idb>. That's pretty convenient, but the wrapper is not perfect, it can't replace events for all cases. So we'll start with events, and then, after we gain an understanding of IndexedDb, we'll use the wrapper.
20
20
21
21
## Open database
22
22
@@ -31,7 +31,7 @@ let openRequest = indexedDB.open(name, version);
31
31
-`name` -- a string, the database name.
32
32
-`version` -- a positive integer version, by default `1` (explained below).
33
33
34
-
We can have many databases with different names, but all of them exist within the current origin (domain/protocol/port). Different websites can't access databases of each other.
34
+
We can have many databases with different names, but all of them exist within the current origin (domain/protocol/port). Different websites can't access each other's databases.
35
35
36
36
The call returns `openRequest` object, we should listen to events on it:
37
37
-`success`: database is ready, there's the "database object" in `openRequest.result`, that we should use it for further calls.
@@ -40,15 +40,15 @@ The call returns `openRequest` object, we should listen to events on it:
40
40
41
41
**IndexedDB has a built-in mechanism of "schema versioning", absent in server-side databases.**
42
42
43
-
Unlike server-side databases, IndexedDB is client-side, the data is stored in the browser, so we, developers, don't have "any time" access to it. So, when we published a new version of our app, and the user visits our webpage, 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 full-time access to it. So, when we have published a new version of our app, and the user visits our webpage, we may need to update the database.
44
44
45
45
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.
46
46
47
-
The `upgradeneeded` event also triggers when the database did not exist yet (technically, it's version is `0`), so we can perform initialization.
47
+
The `upgradeneeded` event also triggers when the database doesn't yet exist (technically, it's version is `0`), so we can perform the initialization.
48
48
49
49
Let's say we published the first version of our app.
50
50
51
-
Then we can open the database with version `1` and perform the initialization in `upgradeneeded` handler like this:
51
+
Then we can open the database with version `1` and perform the initialization in an `upgradeneeded` handler like this:
52
52
53
53
```js
54
54
let openRequest =indexedDB.open("store", *!*1*/!*);
Please note: as our current version is `2`, `onupgradeneeded` handler has a code branch for version `0`, suitable for users that come for the first time and have no database, and also for version `1`, for upgrades.
92
+
Please note: as our current version is `2`, `onupgradeneeded` handler has a code branch for version `0`, suitable for users that are accessing for the first time and have no database, and also for version `1`, for upgrades.
93
93
94
94
And then, only if `onupgradeneeded` handler finishes without errors, `openRequest.onsuccess` triggers, and the database is considered successfully opened.
95
95
@@ -103,9 +103,9 @@ let deleteRequest = indexedDB.deleteDatabase(name)
103
103
```warn header="We can't open an older version of the database"
104
104
If the current user database has a higher version than in the `open` call, e.g. the existing DB version is `3`, and we try to `open(...2)`, then that's an error, `openRequest.onerror` triggers.
105
105
106
-
That's odd, but such thing may happen when a visitor loaded an outdated JavaScript code, e.g. from a proxy cache. So the code is old, but his database is new.
106
+
That's rare, but such a thing may happen when a visitor loads outdated JavaScript code, e.g. from a proxy cache. So the code is old, but his database is new.
107
107
108
-
To protect from errors, we should check `db.version` and suggest him to reload the page. Use proper HTTP caching headers to avoid loading the old code, so that you'll never have such problem.
108
+
To protect from errors, we should check `db.version` and suggest a page reload. Use proper HTTP caching headers to avoid loading the old code, so that you'll never have such problems.
109
109
```
110
110
111
111
### Parallel update problem
@@ -121,13 +121,13 @@ So there's a tab with an open connection to DB version `1`, while the second tab
121
121
122
122
The problem is that a database is shared between two tabs, as it'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, including the one in the first tab.
123
123
124
-
In order to organize that, the `versionchange` event triggers in such case on the "outdated" database object. We should listen to it and close the old database connection (and probably suggest the visitor to reload the page, to load the updated code).
124
+
In order to organize that, the `versionchange` event triggers on the "outdated" database object. We should listen for it and close the old database connection (and probably suggest a page reload, to load the updated code).
125
125
126
-
If we don't listen to`versionchange` event and don't close the old connection, then the second, new connection won't be made. The `openRequest` object will emit the `blocked` event instead of `success`. So the second tab won't work.
126
+
If we don't listen for the`versionchange` event and don't close the old connection, then the second, new connection won't be made. The `openRequest` object will emit the `blocked` event instead of `success`. So the second tab won't work.
127
127
128
128
Here's the code to correctly handle the parallel upgrade.
129
129
130
-
It installs `onversionchange` handler after the database is opened, that closes the old connection:
130
+
It installs an `onversionchange` handler after the database is opened, that closes the old connection:
131
131
132
132
```js
133
133
let openRequest =indexedDB.open("store", 2);
@@ -163,9 +163,9 @@ Here we do two things:
163
163
1. Add `db.onversionchange` listener after a successful opening, to be informed about a parallel update attempt.
164
164
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`.
165
165
166
-
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.
166
+
There are other variants. For example, we can take the time to close things gracefully in `db.onversionchange`, and prompt the visitor to save the data before the connection is closed. The new updating connection will be blocked immediately after `db.onversionchange` has finished without closing, and we can ask the visitor in the new tab to close other tabs for the update.
167
167
168
-
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.
168
+
These update collisions happen rarely, but we should at least have some handling for them, e.g. `onblocked` handler, so that our script doesn't surprise the user by dying silently.
169
169
170
170
## Object store
171
171
@@ -179,16 +179,16 @@ Despite being named an "object store", primitives can be stored too.
179
179
180
180
IndexedDB uses the [standard serialization algorithm](https://www.w3.org/TR/html53/infrastructure.html#section-structuredserializeforstorage) to clone-and-store an object. It's like `JSON.stringify`, but more powerful, capable of storing much more datatypes.
181
181
182
-
An example of object that can't be stored: an object with circular references. Such objects are not serializable. `JSON.stringify` also fails for such objects.
182
+
An example of an object that can't be stored: an object with circular references. Such objects are not serializable. `JSON.stringify` also fails for such objects.
183
183
184
184
**There must be a unique `key` for every value in the store.**
185
185
186
-
A key must have a type one of: number, date, string, binary, or array. It's an unique identifier: we can search/remove/update values by the key.
186
+
A key must be one of the these types - number, date, string, binary, or array. It's a unique identifier, so we can search/remove/update values by the key.
187
187
188
188

189
189
190
190
191
-
As we'll see very soon, we can provide a key when we add a value to the store, similar to `localStorage`. But when we store objects, IndexedDB allows to setup an object property as the key, that's much more convenient. Or we can auto-generate keys.
191
+
As we'll see very soon, we can provide a key when we add a value to the store, similar to `localStorage`. But when we store objects, IndexedDB allows setting up an object property as the key, which is much more convenient. Or we can auto-generate keys.
**An object store can only be created/modified while updating the DB version, in `upgradeneeded` handler.**
216
216
217
-
That's a technical limitation. Outside of the handler we'll be able to add/remove/update the data, but object stores can be created/removed/altered only during version update.
217
+
That's a technical limitation. Outside of the handler we'll be able to add/remove/update the data, but object stores can only be created/removed/altered during a version update.
218
218
219
-
To perform database version upgrade, there are two main approaches:
219
+
To perform a database version upgrade, there are two main approaches:
220
220
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).
221
221
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.
222
222
@@ -249,7 +249,7 @@ The term "transaction" is generic, used in many kinds of databases.
249
249
250
250
A transaction is a group operations, that should either all succeed or all fail.
251
251
252
-
For instance, when a person buys something, we need:
252
+
For instance, when a person buys something, we need to:
253
253
1. Subtract the money from their account.
254
254
2. Add the item to their inventory.
255
255
@@ -261,7 +261,7 @@ Transactions can guarantee that.
There's also `versionchange` transaction type: such transactions can do everything, but we can't create them manually. IndexedDB automatically creates a `versionchange` transaction when opening the database, for `updateneeded` handler. That's why it's a single place where we can update the database structure, create/remove object stores.
274
274
275
-
```smart header="Why there exist different types of transactions?"
275
+
```smart header="Why are there different types of transactions?"
276
276
Performance is the reason why transactions need to be labeled either `readonly` and `readwrite`.
277
277
278
-
Many `readonly` transactions are able to access concurrently the same store, but `readwrite` transactions can't. A `readwrite` transaction "locks" the store for writing. The next transaction must wait before the previous one finishes before accessing the same store.
278
+
Many `readonly` transactions are able to access the same store concurrently, but `readwrite` transactions can't. A `readwrite` transaction "locks" the store for writing. The next transaction must wait before the previous one finishes before accessing the same store.
279
279
```
280
280
281
281
After the transaction is created, we can add an item to the store, like this:
1. Create a transaction, mention all stores it's going to access, at `(1)`.
312
+
1. Create a transaction, mentioning all the stores it's going to access, at `(1)`.
313
313
2. Get the store object using `transaction.objectStore(name)`, at `(2)`.
314
314
3. Perform the request to the object store `books.add(book)`, at `(3)`.
315
315
4. ...Handle request success/error `(4)`, then we can make other requests if needed, etc.
316
316
317
317
Object stores support two methods to store a value:
318
318
319
319
-**put(value, [key])**
320
-
Add the `value` to the store. The `key` is supplied only if the object store did not have `keyPath` or `autoIncrement` option. If there's already a value with same key, it will be replaced.
320
+
Add the `value` to the store. The `key` is supplied only if the object store did not have `keyPath` or `autoIncrement` option. If there's already a value with the same key, it will be replaced.
321
321
322
322
-**add(value, [key])**
323
323
Same as `put`, but if there's already a value with the same key, then the request fails, and an error with the name `"ConstraintError"` is generated.
@@ -329,7 +329,7 @@ Similar to opening a database, we can send a request: `books.add(book)`, and the
329
329
330
330
## Transactions' autocommit
331
331
332
-
In the example above we started the transaction and made `add` request. But as we stated previously, a transaction may have multiple associated requests, that must either all success or all fail. How do we mark the transaction as finished, no more requests to come?
332
+
In the example above we started the transaction and made `add` request. But as we stated previously, a transaction may have multiple associated requests, that must either all succeed or all fail. How do we mark the transaction as finished, with no more requests to come?
333
333
334
334
The short answer is: we don't.
335
335
@@ -343,7 +343,7 @@ So, in the example above no special call is needed to finish the transaction.
343
343
344
344
Transactions auto-commit principle has an important side effect. We can't insert an async operation like `fetch`, `setTimeout` in the middle of transaction. IndexedDB will not keep the transaction waiting till these are done.
345
345
346
-
In the code below `request2` in line `(*)` fails, because the transaction is already committed, can't make any request in it:
346
+
In the code below,`request2` in line `(*)` fails, because the transaction is already committed, and can't make any request in it:
0 commit comments