Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 3.3.0

* Feature: Add `open` and `upgrade` methods to allow a sequence of upgrades
which can support promises returned by `addCallback` callbacks (and a
`flushIncomplete` method for flushing storage pertaining to incomplete
upgrades)
* Feature: Add `addEarlyCallback` to allow use of `idb-schema` methods
within these synchronous callbacks

## 3.2.1 / 2015-11-29

* remove `component-clone` as deps,
Expand Down
106 changes: 105 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,76 @@ req.onsuccess = (e) => {
}
```

Note that this callback will not support `addCallback` callbacks if they rely
on promises and run transactions (since `upgradeneeded`'s transaction will
expire). You can instead use `schema.open` or `schema.upgrade`.

### schema.open(dbName, [version])

With `schema.open`, in addition to getting upgrades applied (including
callbacks added by `addCallback` and even including promise-based callbacks
which utilize transactions), you can use the `db` result opened at the
latest version:

```js
schema.open('myDb', 3).then((db) => {
// Use db
})
```

However, unlike `callback()`, when `schema.open` is used, the callbacks
added by `addCallback` cannot handle operations such as adding stores
or indexes (though these operations can be executed with the other
methods of idb-schema anyways) though those added by `addEarlyCallback`
can be.

Besides conducting an upgrade, `schema.open` uses the `open` of
[idb-factory](https://github.com/treojs/idb-factory) behind the scenes, so
one can also catch errors and benefit from its fixing of browser quirks.

If a version is not supplied, the latest version available within the schema
will be used.

If you only wish to upgrade and do not wish to keep a connection open, use
`schema.upgrade`. If you wish to manage opening a connection yourself (and are
not using promises within `addCallback` callbacks), you can use
`schema.callback`.

Despite allowing for promise-based callbacks utilizing transactions, due to
[current limitations in IndexedDB](https://github.com/w3c/IndexedDB/issues/42),
we cannot get a transaction which encompasses both the store/index changes
and the store content changes, so it will not be possible to rollback the
entire version upgrade if the store/index changes transaction succeeds while
the store content change transaction fails. However, upon such a condition
idb-schema will set a `localStorage` property to disallow subsequent attempts
on `schema.open` or `schema.upgrade` to succeed until either the storage
property is manually flushed by the `flushIncomplete()` method or if the
`retry` method on the error object is invoked to return a Promise which will
reattempt to execute the failed callback and the rest of the upgrades and
which will resolve according to whether this next attempt was successful or
not.

### schema.upgrade(dbName, [version], [keepOpen=false])

Equivalent to `schema.open` but without keeping a connection open
(unless `keepOpen` is set to `true`):

```js
schema.upgrade('myDb', 3).then(() => {
// No database result is available for upgrades. Use `schema.open` if you
// wish to keep a connection to the latest version open
// for non-upgrade related transactions
})
```

### schema.flushIncomplete(dbName)

If there was an incomplete upgrade, this method will flush the local storage
used to signal to `schema.open`/`schema.upgrade` that they should not yet allow
opening until the upgrade is complete. This method should normally not be used
as it is important to ensure an upgrade occurs like a complete transaction, and
flushing will interfere with this.

### schema.stores()

Get JSON representation of database schema.
Expand Down Expand Up @@ -154,9 +224,33 @@ Options:

Delete index by `name` from current store.

### schema.addEarlyCallback(cb)

Adds a `cb` to be executed at the beginning of the `upgradeneeded` event
and passed the event object. This will, out of necessity, run synchronously,
so promises cannot safely be used therein (whether used in `schema.callback`
or `schema.open`/`schema.upgrade`).

However, due to their early execution, such callbacks are, unlike
`addCallback` callbacks used with `schema.open`/`schema.upgrade`,
able to use methods such as `addStore`.

```js
const schema = new Schema()
.addStore('users', { increment: true, keyPath: 'id' })
.addIndex('byName', 'name')
.addEarlyCallback((e) => {
schema.addIndex('byId', 'id')
})
```

### schema.addCallback(cb)

Add `cb` to be executed at the end of the `upgradeneeded` event.
Adds a `cb` to be executed at the end of the `upgradeneeded` event
(if `schema.callback()` is used) or, at the beginning of the `success`
event (if `schema.open` and `schema.upgrade` are used). If `callback`
is used, the callback will be passed the `upgradeneeded` event. If
the other two methods are used, the db result will be passed instead.

```js
new Schema()
Expand All @@ -169,6 +263,16 @@ new Schema()
})
```

Note that if you wish to use promises within such callbacks and make
transactions within them, your `addCallback` callback should return
a promise chain and then use `schema.open` or `schema.upgrade` because
these methods, unlike `schema.callback`, will cause the callbacks
to be executed safely within the more persistent `onsuccess` event (and the
callback will be passed the database result instead of the `upgradeneeded`
event). If you do not need promises, you will have the option of using
`schema.callback` in addition to `schema.open` or `schema.upgrade` (or
you can use `addEarlyCallback`).

### schema.clone()

Return a deep clone of current schema.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
],
"scripts": {
"prepublish": "babel src --out-dir lib",
"test": "eslint src/ test/ && browserify-test -t babelify && SAUCE_USERNAME=idb-schema zuul --tunnel-host http://treojs.com --no-coverage -- test/index.js",
"test:local": "eslint src/ test/ && browserify-test -t babelify",
"test": "npm run test:local && SAUCE_USERNAME=idb-schema zuul --tunnel-host http://treojs.com --no-coverage -- test/index.js",
"development": "browserify-test -t babelify --watch"
},
"dependencies": {
Expand All @@ -30,14 +31,14 @@
"babel-core": "6.1.18",
"babel-eslint": "^5.0.0-beta4",
"babel-plugin-add-module-exports": "^0.1.1",
"babel-polyfill": "^6.7.4",
"babel-preset-es2015": "^6.1.18",
"babelify": "^7.2.0",
"browserify-test": "^2.1.2",
"chai": "^3.4.1",
"es6-promise": "^3.0.2",
"eslint": "^1.10.2",
"eslint-config-airbnb": "^1.0.2",
"idb-factory": "^1.0.0",
"idb-request": "^3.0.0",
"indexeddbshim": "^2.2.1",
"lodash": "^3.10.1",
Expand Down
156 changes: 156 additions & 0 deletions src/idb-factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

/**
* Open IndexedDB database with `name`.
* Retry logic allows to avoid issues in tests env,
* when db with the same name delete/open repeatedly and can be blocked.
*
* @param {String} dbName
* @param {Number} [version]
* @param {Function} [upgradeCallback]
* @return {Promise}
*/

export function open(dbName, version, upgradeCallback) {
return new Promise((resolve, reject) => {
if (typeof version === 'function') {
upgradeCallback = version
version = undefined
}
// don't call open with 2 arguments, when version is not set
const req = version ? idb().open(dbName, version) : idb().open(dbName)
req.onblocked = (e) => {
const resume = new Promise((res, rej) => {
// We overwrite handlers rather than make a new
// open() since the original request is still
// open and its onsuccess will still fire if
// the user unblocks by closing the blocking
// connection
req.onsuccess = ev => res(ev.target.result)
req.onerror = ev => {
ev.preventDefault()
rej(ev)
}
})
e.resume = resume
reject(e)
}
if (typeof upgradeCallback === 'function') {
req.onupgradeneeded = e => {
upgradeCallback(e)
}
}
req.onerror = (e) => {
e.preventDefault()
reject(e)
}
req.onsuccess = (e) => {
resolve(e.target.result)
}
})
}

/**
* Delete `db` properly:
* - close it and wait 100ms to disk flush (Safari, older Chrome, Firefox)
* - if database is locked, due to inconsistent exectution of `versionchange`,
* try again in 100ms
*
* @param {IDBDatabase|String} db
* @return {Promise}
*/

export function del(db) {
const dbName = typeof db !== 'string' ? db.name : db

return new Promise((resolve, reject) => {
const delDb = () => {
const req = idb().deleteDatabase(dbName)
req.onblocked = (e) => {
// The following addresses part of https://bugzilla.mozilla.org/show_bug.cgi?id=1220279
e = e.newVersion === null || typeof Proxy === 'undefined' ? e : new Proxy(e, { get: (target, name) => {
return name === 'newVersion' ? null : target[name]
} })
const resume = new Promise((res, rej) => {
// We overwrite handlers rather than make a new
// delete() since the original request is still
// open and its onsuccess will still fire if
// the user unblocks by closing the blocking
// connection
req.onsuccess = ev => {
// The following are needed currently by PhantomJS: https://github.com/ariya/phantomjs/issues/14141
if (!('newVersion' in ev)) {
ev.newVersion = e.newVersion
}

if (!('oldVersion' in ev)) {
ev.oldVersion = e.oldVersion
}

res(ev)
}
req.onerror = ev => {
ev.preventDefault()
rej(ev)
}
})
e.resume = resume
reject(e)
}
req.onerror = (e) => {
e.preventDefault()
reject(e)
}
req.onsuccess = (e) => {
// The following is needed currently by PhantomJS (though we cannot polyfill `oldVersion`): https://github.com/ariya/phantomjs/issues/14141
if (!('newVersion' in e)) {
e.newVersion = null
}

resolve(e)
}
}

if (typeof db !== 'string') {
db.close()
setTimeout(delDb, 100)
} else {
delDb()
}
})
}

/**
* Compare `first` and `second`.
* Added for consistency with official API.
*
* @param {Any} first
* @param {Any} second
* @return {Number} -1|0|1
*/

export function cmp(first, second) {
return idb().cmp(first, second)
}

/**
* Get globally available IDBFactory instance.
* - it uses `global`, so it can work in any env.
* - it tries to use `global.forceIndexedDB` first,
* so you can rewrite `global.indexedDB` with polyfill
* https://bugs.webkit.org/show_bug.cgi?id=137034
* - it fallbacks to all possibly available implementations
* https://github.com/axemclion/IndexedDBShim#ios
* - function allows to have dynamic link,
* which can be changed after module's initial exectution
*
* @return {IDBFactory}
*/

function idb() {
return global.forceIndexedDB
|| global.indexedDB
|| global.webkitIndexedDB
|| global.mozIndexedDB
|| global.msIndexedDB
|| global.shimIndexedDB
}
Loading