UNPKG

15.4 kBMarkdownView Raw
1# IndexedDB with usability.
2
3This is a tiny (~1.19kB brotli'd) library that mostly mirrors the IndexedDB API, but with small improvements that make a big difference to usability.
4
51. [Installation](#installation)
61. [Changes](#changes)
71. [Browser support](#browser-support)
81. [API](#api)
9 1. [`openDB`](#opendb)
10 1. [`deleteDB`](#deletedb)
11 1. [`unwrap`](#unwrap)
12 1. [`wrap`](#wrap)
13 1. [General enhancements](#general-enhancements)
14 1. [`IDBDatabase` enhancements](#idbdatabase-enhancements)
15 1. [`IDBTransaction` enhancements](#idbtransaction-enhancements)
16 1. [`IDBCursor` enhancements](#idbcursor-enhancements)
17 1. [Async iterators](#async-iterators)
181. [Examples](#examples)
191. [TypeScript](#typescript)
20
21# Installation
22
23## Using npm
24
25```sh
26npm install idb
27```
28
29Then, assuming you're using a module-compatible system (like webpack, Rollup etc):
30
31```js
32import { openDB, deleteDB, wrap, unwrap } from 'idb';
33
34async function doDatabaseStuff() {
35 const db = await openDB(…);
36}
37```
38
39## Directly in a browser
40
41### Using the modules method directly via jsdelivr:
42
43```html
44<script type="module">
45 import { openDB, deleteDB, wrap, unwrap } from 'https://cdn.jsdelivr.net/npm/idb@8/+esm';
46
47 async function doDatabaseStuff() {
48 const db = await openDB(…);
49 }
50</script>
51```
52
53### Using external script reference
54
55```html
56<script src="https://cdn.jsdelivr.net/npm/idb@8/build/umd.js"></script>
57<script>
58 async function doDatabaseStuff() {
59 const db = await idb.openDB(…);
60 }
61</script>
62```
63
64A global, `idb`, will be created, containing all exports of the module version.
65
66# Changes
67
68[See details of (potentially) breaking changes](CHANGELOG.md).
69
70# Browser support
71
72This library targets modern browsers, as in Chrome, Firefox, Safari, and other browsers that use those engines, such as Edge. IE is not supported.
73
74# API
75
76## `openDB`
77
78This method opens a database, and returns a promise for an enhanced [`IDBDatabase`](https://w3c.github.io/IndexedDB/#database-interface).
79
80```js
81const db = await openDB(name, version, {
82 upgrade(db, oldVersion, newVersion, transaction, event) {
83 // …
84 },
85 blocked(currentVersion, blockedVersion, event) {
86 // …
87 },
88 blocking(currentVersion, blockedVersion, event) {
89 // …
90 },
91 terminated() {
92 // …
93 },
94});
95```
96
97- `name`: Name of the database.
98- `version` (optional): Schema version, or `undefined` to open the current version.
99- `upgrade` (optional): Called if this version of the database has never been opened before. Use it to specify the schema for the database. This is similar to the [`upgradeneeded` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/upgradeneeded_event) in plain IndexedDB.
100 - `db`: An enhanced `IDBDatabase`.
101 - `oldVersion`: Last version of the database opened by the user.
102 - `newVersion`: Whatever new version you provided.
103 - `transaction`: An enhanced transaction for this upgrade. This is useful if you need to get data from other stores as part of a migration.
104 - `event`: The event object for the associated `upgradeneeded` event.
105- `blocked` (optional): Called if there are older versions of the database open on the origin, so this version cannot open. This is similar to the [`blocked` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBOpenDBRequest/blocked_event) in plain IndexedDB.
106 - `currentVersion`: Version of the database that's blocking this one.
107 - `blockedVersion`: The version of the database being blocked (whatever version you provided to `openDB`).
108 - `event`: The event object for the associated `blocked` event.
109- `blocking` (optional): Called if this connection is blocking a future version of the database from opening. This is similar to the [`versionchange` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/versionchange_event) in plain IndexedDB.
110 - `currentVersion`: Version of the open database (whatever version you provided to `openDB`).
111 - `blockedVersion`: The version of the database that's being blocked.
112 - `event`: The event object for the associated `versionchange` event.
113- `terminated` (optional): Called if the browser abnormally terminates the connection, but not on regular closures like calling `db.close()`. This is similar to the [`close` event](https://developer.mozilla.org/en-US/docs/Web/API/IDBDatabase/close_event) in plain IndexedDB.
114
115## `deleteDB`
116
117Deletes a database.
118
119```js
120await deleteDB(name, {
121 blocked() {
122 // …
123 },
124});
125```
126
127- `name`: Name of the database.
128- `blocked` (optional): Called if the database already exists and there are open connections that don’t close in response to a versionchange event, the request will be blocked until they all close.
129 - `currentVersion`: Version of the database that's blocking the delete operation.
130 - `event`: The event object for the associated 'versionchange' event.
131
132## `unwrap`
133
134Takes an enhanced IndexedDB object and returns the plain unmodified one.
135
136```js
137const unwrapped = unwrap(wrapped);
138```
139
140This is useful if, for some reason, you want to drop back into plain IndexedDB. Promises will also be converted back into `IDBRequest` objects.
141
142## `wrap`
143
144Takes an IDB object and returns a version enhanced by this library.
145
146```js
147const wrapped = wrap(unwrapped);
148```
149
150This is useful if some third party code gives you an `IDBDatabase` object and you want it to have the features of this library.
151
152## General enhancements
153
154Once you've opened the database the API is the same as IndexedDB, except for a few changes to make things easier.
155
156Firstly, any method that usually returns an `IDBRequest` object will now return a promise for the result.
157
158```js
159const store = db.transaction(storeName).objectStore(storeName);
160const value = await store.get(key);
161```
162
163### Promises & throwing
164
165The library turns all `IDBRequest` objects into promises, but it doesn't know in advance which methods may return promises.
166
167As a result, methods such as `store.put` may throw instead of returning a promise.
168
169If you're using async functions, there's no observable difference.
170
171### Transaction lifetime
172
173TL;DR: **Do not `await` other things between the start and end of your transaction**, otherwise the transaction will close before you're done.
174
175An IDB transaction auto-closes if it doesn't have anything left do once microtasks have been processed. As a result, this works fine:
176
177```js
178const tx = db.transaction('keyval', 'readwrite');
179const store = tx.objectStore('keyval');
180const val = (await store.get('counter')) || 0;
181await store.put(val + 1, 'counter');
182await tx.done;
183```
184
185But this doesn't:
186
187```js
188const tx = db.transaction('keyval', 'readwrite');
189const store = tx.objectStore('keyval');
190const val = (await store.get('counter')) || 0;
191// This is where things go wrong:
192const newVal = await fetch('/increment?val=' + val);
193// And this throws an error:
194await store.put(newVal, 'counter');
195await tx.done;
196```
197
198In this case, the transaction closes while the browser is fetching, so `store.put` fails.
199
200## `IDBDatabase` enhancements
201
202### Shortcuts to get/set from an object store
203
204It's common to create a transaction for a single action, so helper methods are included for this:
205
206```js
207// Get a value from a store:
208const value = await db.get(storeName, key);
209// Set a value in a store:
210await db.put(storeName, value, key);
211```
212
213The shortcuts are: `get`, `getKey`, `getAll`, `getAllKeys`, `count`, `put`, `add`, `delete`, and `clear`. Each method takes a `storeName` argument, the name of the object store, and the rest of the arguments are the same as the equivalent `IDBObjectStore` method.
214
215### Shortcuts to get from an index
216
217The shortcuts are: `getFromIndex`, `getKeyFromIndex`, `getAllFromIndex`, `getAllKeysFromIndex`, and `countFromIndex`.
218
219```js
220// Get a value from an index:
221const value = await db.getFromIndex(storeName, indexName, key);
222```
223
224Each method takes `storeName` and `indexName` arguments, followed by the rest of the arguments from the equivalent `IDBIndex` method.
225
226## `IDBTransaction` enhancements
227
228### `tx.store`
229
230If a transaction involves a single store, the `store` property will reference that store.
231
232```js
233const tx = db.transaction('whatever');
234const store = tx.store;
235```
236
237If a transaction involves multiple stores, `tx.store` is undefined, you need to use `tx.objectStore(storeName)` to get the stores.
238
239### `tx.done`
240
241Transactions have a `.done` promise which resolves when the transaction completes successfully, and otherwise rejects with the [transaction error](https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/error).
242
243```js
244const tx = db.transaction(storeName, 'readwrite');
245await Promise.all([
246 tx.store.put('bar', 'foo'),
247 tx.store.put('world', 'hello'),
248 tx.done,
249]);
250```
251
252If you're writing to the database, `tx.done` is the signal that everything was successfully committed to the database. However, it's still beneficial to await the individual operations, as you'll see the error that caused the transaction to fail.
253
254## `IDBCursor` enhancements
255
256Cursor advance methods (`advance`, `continue`, `continuePrimaryKey`) return a promise for the cursor, or null if there are no further values to provide.
257
258```js
259let cursor = await db.transaction(storeName).store.openCursor();
260
261while (cursor) {
262 console.log(cursor.key, cursor.value);
263 cursor = await cursor.continue();
264}
265```
266
267## Async iterators
268
269You can iterate over stores, indexes, and cursors:
270
271```js
272const tx = db.transaction(storeName);
273
274for await (const cursor of tx.store) {
275 // …
276}
277```
278
279Each yielded object is an `IDBCursor`. You can optionally use the advance methods to skip items (within an async iterator they return void):
280
281```js
282const tx = db.transaction(storeName);
283
284for await (const cursor of tx.store) {
285 console.log(cursor.value);
286 // Skip the next item
287 cursor.advance(2);
288}
289```
290
291If you don't manually advance the cursor, `cursor.continue()` is called for you.
292
293Stores and indexes also have an `iterate` method which has the same signature as `openCursor`, but returns an async iterator:
294
295```js
296const index = db.transaction('books').store.index('author');
297
298for await (const cursor of index.iterate('Douglas Adams')) {
299 console.log(cursor.value);
300}
301```
302
303# Examples
304
305## Keyval store
306
307This is very similar to `localStorage`, but async. If this is _all_ you need, you may be interested in [idb-keyval](https://www.npmjs.com/package/idb-keyval). You can always upgrade to this library later.
308
309```js
310import { openDB } from 'idb';
311
312const dbPromise = openDB('keyval-store', 1, {
313 upgrade(db) {
314 db.createObjectStore('keyval');
315 },
316});
317
318export async function get(key) {
319 return (await dbPromise).get('keyval', key);
320}
321export async function set(key, val) {
322 return (await dbPromise).put('keyval', val, key);
323}
324export async function del(key) {
325 return (await dbPromise).delete('keyval', key);
326}
327export async function clear() {
328 return (await dbPromise).clear('keyval');
329}
330export async function keys() {
331 return (await dbPromise).getAllKeys('keyval');
332}
333```
334
335## Article store
336
337```js
338import { openDB } from 'idb/with-async-ittr.js';
339
340async function demo() {
341 const db = await openDB('Articles', 1, {
342 upgrade(db) {
343 // Create a store of objects
344 const store = db.createObjectStore('articles', {
345 // The 'id' property of the object will be the key.
346 keyPath: 'id',
347 // If it isn't explicitly set, create a value by auto incrementing.
348 autoIncrement: true,
349 });
350 // Create an index on the 'date' property of the objects.
351 store.createIndex('date', 'date');
352 },
353 });
354
355 // Add an article:
356 await db.add('articles', {
357 title: 'Article 1',
358 date: new Date('2019-01-01'),
359 body: '…',
360 });
361
362 // Add multiple articles in one transaction:
363 {
364 const tx = db.transaction('articles', 'readwrite');
365 await Promise.all([
366 tx.store.add({
367 title: 'Article 2',
368 date: new Date('2019-01-01'),
369 body: '…',
370 }),
371 tx.store.add({
372 title: 'Article 3',
373 date: new Date('2019-01-02'),
374 body: '…',
375 }),
376 tx.done,
377 ]);
378 }
379
380 // Get all the articles in date order:
381 console.log(await db.getAllFromIndex('articles', 'date'));
382
383 // Add 'And, happy new year!' to all articles on 2019-01-01:
384 {
385 const tx = db.transaction('articles', 'readwrite');
386 const index = tx.store.index('date');
387
388 for await (const cursor of index.iterate(new Date('2019-01-01'))) {
389 const article = { ...cursor.value };
390 article.body += ' And, happy new year!';
391 cursor.update(article);
392 }
393
394 await tx.done;
395 }
396}
397```
398
399# TypeScript
400
401This library is fully typed, and you can improve things by providing types for your database:
402
403```ts
404import { openDB, DBSchema } from 'idb';
405
406interface MyDB extends DBSchema {
407 'favourite-number': {
408 key: string;
409 value: number;
410 };
411 products: {
412 value: {
413 name: string;
414 price: number;
415 productCode: string;
416 };
417 key: string;
418 indexes: { 'by-price': number };
419 };
420}
421
422async function demo() {
423 const db = await openDB<MyDB>('my-db', 1, {
424 upgrade(db) {
425 db.createObjectStore('favourite-number');
426
427 const productStore = db.createObjectStore('products', {
428 keyPath: 'productCode',
429 });
430 productStore.createIndex('by-price', 'price');
431 },
432 });
433
434 // This works
435 await db.put('favourite-number', 7, 'Jen');
436 // This fails at compile time, as the 'favourite-number' store expects a number.
437 await db.put('favourite-number', 'Twelve', 'Jake');
438}
439```
440
441To define types for your database, extend `DBSchema` with an interface where the keys are the names of your object stores.
442
443For each value, provide an object where `value` is the type of values within the store, and `key` is the type of keys within the store.
444
445Optionally, `indexes` can contain a map of index names, to the type of key within that index.
446
447Provide this interface when calling `openDB`, and from then on your database will be strongly typed. This also allows your IDE to autocomplete the names of stores and indexes.
448
449## Opting out of types
450
451If you call `openDB` without providing types, your database will use basic types. However, sometimes you'll need to interact with stores that aren't in your schema, perhaps during upgrades. In that case you can cast.
452
453Let's say we were renaming the 'favourite-number' store to 'fave-nums':
454
455```ts
456import { openDB, DBSchema, IDBPDatabase } from 'idb';
457
458interface MyDBV1 extends DBSchema {
459 'favourite-number': { key: string; value: number };
460}
461
462interface MyDBV2 extends DBSchema {
463 'fave-num': { key: string; value: number };
464}
465
466const db = await openDB<MyDBV2>('my-db', 2, {
467 async upgrade(db, oldVersion) {
468 // Cast a reference of the database to the old schema.
469 const v1Db = db as unknown as IDBPDatabase<MyDBV1>;
470
471 if (oldVersion < 1) {
472 v1Db.createObjectStore('favourite-number');
473 }
474 if (oldVersion < 2) {
475 const store = v1Db.createObjectStore('favourite-number');
476 store.name = 'fave-num';
477 }
478 },
479});
480```
481
482You can also cast to a typeless database by omitting the type, eg `db as IDBPDatabase`.
483
484Note: Types like `IDBPDatabase` are used by TypeScript only. The implementation uses proxies under the hood.
485
486# Developing
487
488```sh
489npm run dev
490```
491
492This will also perform type testing.
493
494To test, navigate to `build/test/` in a browser. You'll need to set up a [basic web server](https://www.npmjs.com/package/serve) for this.