Skip to content

Commit

Permalink
Feature: Add open and upgrade methods (and flushIncomplete) to …
Browse files Browse the repository at this point in the history
…allow a sequence of upgrades which can support promises returned by `addCallback` callbacks (and add docs and tests for multiple promise-based callbacks on different versions);

Commenting: Indicate specific Safari bug within code;
Refactoring: Use ES6 `Number.isInteger` and ES7 `Object.values()` in place of npm modules (requires babel-polyfill);
Testing: Split off local testing from Sauce; prevent blocking; increase timeout for Firefox; add test to throw with previous version
  • Loading branch information
brettz9 committed Apr 22, 2016
1 parent 5d8a7fe commit f161675
Show file tree
Hide file tree
Showing 6 changed files with 778 additions and 39 deletions.
7 changes: 7 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 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)

## 3.2.1 / 2015-11-29

* remove `component-clone` as deps,
Expand Down
84 changes: 83 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,75 @@ 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).

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 @@ -156,7 +225,11 @@ Delete index by `name` from current store.

### 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 +242,15 @@ 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`.

### 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

0 comments on commit f161675

Please sign in to comment.