ng-request-cache
Version:
AngularJS HTTP request cache for IndexeDB and $cacheFactory
584 lines (482 loc) • 16.6 kB
text/coffeescript
###*
$indexedDBProvider
(c) 2014 Bram Whillock (bramski)
Forked from original work by clements Capitan (webcss)
License: MIT
###
'use strict'
angular.module('indexedDB', []).provider '$indexedDB', ->
indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB || window.shimIndexedDB
IDBKeyRange = window.IDBKeyRange || window.mozIDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
dbMode =
readonly: "readonly"
readwrite: "readwrite"
readyState =
pending: "pending"
cursorDirection =
next: "next"
nextunique: "nextunique"
prev: "prev"
prevunique: "prevunique"
apiDirection =
ascending: cursorDirection.next
descending: cursorDirection.prev
dbName = ''
dbVersion = 1
db = null
upgradesByVersion = {}
dbPromise = null
allTransactions = []
defaultQueryOptions =
useIndex: undefined
keyRange: null
direction: cursorDirection.next
applyNeededUpgrades = (oldVersion, event, db, tx, $log) ->
for version of upgradesByVersion
if not upgradesByVersion.hasOwnProperty(version) or version <= oldVersion
continue
$log.log "$indexedDB: Running upgrade : " + version + " from " + oldVersion
upgradesByVersion[version] event, db, tx
return
errorMessageFor = (e) ->
if e.target.readyState is readyState.pending
"Error: Operation pending"
else
(e.target.webkitErrorMessage || e.target.error.message || e.target.errorCode)
appendResultsToPromise = (promise, results) ->
if results isnt undefined
promise.then ->
results
else
promise
###*
function
$indexedDBProvider.connection
sets the name of the database to use
{string} databaseName database name.
{object} this
###
= (databaseName) ->
dbName = databaseName
this
###*
function
$indexedDBProvider.upgradeDatabase
provides version number and steps to upgrade the database wrapped in a
callback function
{number} newVersion new version number for the database.
{function} callback the callback which proceeds the upgrade
{object} this
###
= (newVersion, callback) ->
upgradesByVersion[newVersion] = callback
dbVersion = Math.max.apply(null, Object.keys(upgradesByVersion))
this
@$get = ['$q', '$rootScope', '$log', ($q, $rootScope, $log) ->
rejectWithError = (deferred) ->
(error) ->
$rootScope.$apply ->
deferred.reject(errorMessageFor(error))
createDatabaseConnection = ->
deferred = $q.defer()
dbReq = indexedDB.open(dbName, parseInt(dbVersion) || 1)
dbReq.onsuccess = ->
db = dbReq.result
$rootScope.$apply ->
deferred.resolve db
return
return
dbReq.onblocked = dbReq.onerror = rejectWithError(deferred)
dbReq.onupgradeneeded = (event) ->
db = event.target.result
tx = event.target.transaction
$log.log "$indexedDB: Upgrading database '#{db.name}' from version #{event.oldVersion} to version #{event.newVersion} ..."
applyNeededUpgrades event.oldVersion, event, db, tx, $log
return
deferred.promise
openDatabase = ->
dbPromise ||= createDatabaseConnection()
closeDatabase = ->
openDatabase().then ->
db.close()
db = null
dbPromise = null
validateStoreNames = (storeNames) ->
found = true
for storeName in storeNames
found = found & db.objectStoreNames.contains(storeName)
found
openTransaction = (storeNames, mode = dbMode.readonly) ->
openDatabase().then ->
unless validateStoreNames(storeNames)
return $q.reject("Object stores " + storeNames + " do not exist.")
new Transaction(storeNames, mode)
keyRangeForOptions = (options) ->
IDBKeyRange.bound(options.beginKey, options.endKey) if options.beginKey and options.endKey
addTransaction = (transaction) ->
allTransactions.push(transaction.promise)
transaction.promise.finally ->
index = allTransactions.indexOf(transaction.promise)
allTransactions.splice(index,1) if index > -1
class Transaction
constructor: (storeNames, mode = dbMode.readonly) ->
= db.transaction(storeNames, mode)
= $q.defer()
= .promise
setupCallbacks: ->
.oncomplete = =>
$rootScope.$apply =>
.resolve("Transaction Completed")
.onabort = (error) =>
$rootScope.$apply =>
.reject("Transaction Aborted", error)
.onerror = (error) =>
$rootScope.$apply =>
.reject("Transaction Error", error)
addTransaction(this)
objectStore: (storeName) ->
.objectStore(storeName)
abort: ->
.abort()
class DbQ
constructor: ->
= $q.defer()
= .promise
reject: (args...) ->
$rootScope.$apply =>
.reject(args...)
rejectWith: (req) ->
req.onerror = req.onblocked = (e) =>
resolve: (args...) ->
$rootScope.$apply =>
.resolve(args...)
notify: (args...) ->
$rootScope.$apply =>
.notify(args...)
dbErrorFunction: ->
(error) =>
$rootScope.$apply =>
.reject(errorMessageFor(error))
resolveWith: (req) ->
req.onsuccess = (e) =>
class ObjectStore
constructor: (storeName, transaction) ->
= storeName
= transaction.objectStore(storeName)
= transaction
defer: ->
new DbQ()
_mapCursor: (defer, mapFunc, req = .openCursor()) ->
results = []
defer.rejectWith(req)
req.onsuccess = (e) ->
if cursor = e.target.result
results.push(mapFunc(cursor))
defer.notify(mapFunc(cursor))
cursor.continue()
else
defer.resolve(results)
_arrayOperation: (data, mapFunc) ->
defer =
data = [data] unless angular.isArray(data)
for item in data
req = mapFunc(item)
results = []
defer.rejectWith(req)
req.onsuccess = (e) ->
results.push(e.target.result)
defer.notify(e.target.result)
defer.resolve(results) if results.length >= data.length
if data.length == 0
return $q.when([])
defer.promise
###*
function
$indexedDBProvider.store.getAllKeys
gets all the keys
{Q} A promise which will result with all the keys
###
getAllKeys: ->
defer =
if .getAllKeys
req = .getAllKeys()
defer.resolveWith(req)
else
defer, (cursor) ->
cursor.key
return defer.promise
###*
function
$indexedDBProvider.store.clear
clears all objects from this store
{Q} A promise that this can be done successfully.
###
clear: ->
defer =
req = .clear()
defer.resolveWith(req)
defer.promise
###*
function
$indexedDBProvider.store.delete
Deletes the item at the key. The operation is ignored if the item does not exist.
{key} The key of the object to delete.
{Q} A promise that this can be done successfully.
###
delete: (key) ->
defer =
defer.resolveWith(.delete(key))
defer.promise
###*
function
$indexedDBProvider.store.upsert
Updates the given item
{data} Details of the item or items to update or insert
{Q} A promise that this can be done successfully.
###
upsert: (data) ->
data, (item) =>
.put(item)
###*
function
$indexedDBProvider.store.insert
Updates the given item
{data} Details of the item or items to insert
{Q} A promise that this can be done successfully.
###
insert: (data) ->
data, (item) =>
.add(item)
###*
function
$indexedDBProvider.store.getAll
Fetches all items from the store
{Q} A promise which resolves with copies of all items in the store
###
getAll: ->
defer =
if .getAll
defer.resolveWith(.getAll())
else
defer, (cursor) ->
cursor.value
defer.promise
eachWhere: (query) ->
defer =
indexName = query.indexName
keyRange = query.keyRange
direction = query.direction
req = if indexName
.index(indexName).openCursor(keyRange, direction)
else
.openCursor(keyRange, direction)
defer.promise
findWhere: (query) ->
###*
function
$indexedDBProvider.store.each
Iterates through the items in the store
{options.beginKey} the key to start iterating from
{options.endKey} the key to stop iterating at
{options.direction} Direction to iterate in
{Q} A promise which notifies with each individual item and resolves with all of them.
###
each: (options = {}) ->
###*
function
$indexedDBProvider.store.eachBy
Iterates through the items in the store using an index
{indexName} name of the index to use instead of the primary
{options.beginKey} the key to start iterating from
{options.endKey} the key to stop iterating at
{options.direction} Direction to iterate in
{Q} A promise which notifies with each individual item and resolves with all of them.
###
eachBy: (indexName = undefined, options = {}) ->
q = new Query()
q.indexName = indexName
q.keyRange = keyRangeForOptions options
q.direction = options.direction || defaultQueryOptions.direction
###*
function
$indexedDBProvider.store.count
Returns a count of the items in the store
{Q} A promise which resolves with the count of all the items in the store.
###
count: ->
defer =
defer.resolveWith(.count())
defer.promise
###*
function
$indexedDBProvider.store.find
Fetches an item from the store
{Q} A promise which resolves with the item from the store
###
find: (key) ->
defer =
req = .get(key)
defer.rejectWith(req)
req.onsuccess = (e) =>
if e.target.result
defer.resolve(e.target.result)
else
defer.reject("#{@storeName}:#{key} not found.")
defer.promise
###*
function
$indexedDBProvider.store.findBy
Fetches an item from the store using a named index.
{Q} A promise which resolves with the item from the store.
###
findBy: (index, key) ->
defer =
defer.resolveWith(.index(index).get(key))
defer.promise
query: ->
new Query()
class Query
constructor : ->
= undefined
= undefined
= cursorDirection.next
$lt: (value) ->
= IDBKeyRange.upperBound(value, true)
this
$gt: (value) ->
= IDBKeyRange.lowerBound(value, true)
this
$lte: (value) ->
= IDBKeyRange.upperBound(value)
this
$gte: (value) ->
= IDBKeyRange.lowerBound(value)
this
$eq: (value) ->
= IDBKeyRange.only(value)
this
$between: (low, hi, exLow = false, exHi = false) ->
= IDBKeyRange.bound(low, hi, exLow, exHi)
this
$desc: (unique) ->
= if unique then cursorDirection.prevunique else cursorDirection.prev
this
$asc: (unique) ->
= if unique then cursorDirection.nextunique else cursorDirection.next
this
$index: (indexName) ->
= indexName
this
###*
method
$indexedDB.objectStore
an IDBObjectStore to use
{string} storeName the name of the objectstore to use
{object} ObjectStore
###
openStore: (storeName, callBack, mode = dbMode.readwrite) ->
openTransaction([storeName], mode).then (transaction) ->
results = callBack(new ObjectStore(storeName, transaction))
appendResultsToPromise(transaction.promise, results)
openStores: (storeNames, callback, mode = dbMode.readwrite) ->
openTransaction(storeNames, mode).then (transaction) ->
objectStores = for storeName in storeNames
new ObjectStore(storeName, transaction)
results = callback.apply(null, objectStores)
appendResultsToPromise(transaction.promise, results)
openAllStores: (callback, mode = dbMode.readwrite) ->
openDatabase().then =>
storeNames = Array.prototype.slice.apply(db.objectStoreNames)
transaction = new Transaction(storeNames, mode)
objectStores = for storeName in storeNames
new ObjectStore(storeName, transaction)
results = callback.apply(null, objectStores)
appendResultsToPromise(transaction.promise, results)
###*
method
$indexedDB.closeDatabase
Closes the database for use and completes all transactions.
###
closeDatabase: ->
closeDatabase()
###*
method
$indexedDB.deleteDatabase
Closes and then destroys the current database. Returns a promise that resolves when this is persisted.
###
deleteDatabase: ->
closeDatabase().then ->
defer = new DbQ()
defer.resolveWith(indexedDB.deleteDatabase(dbName))
defer.promise
.finally ->
$log.log "$indexedDB: #{dbName} database deleted."
queryDirection: apiDirection
flush: ->
if allTransactions.length > 0
$q.all(allTransactions)
else
$q.when([])
###*
method
$indexedDB.databaseInfo
Returns information about this database.
###
databaseInfo: ->
openDatabase().then ->
transaction = null
storeNames = Array.prototype.slice.apply(db.objectStoreNames)
openTransaction(storeNames, dbMode.readonly).then (transaction) ->
stores = for storeName in storeNames
store = transaction.objectStore(storeName)
{
name: storeName
keyPath: store.keyPath
autoIncrement: store.autoIncrement
indices: Array.prototype.slice.apply(store.indexNames)
}
transaction.promise.then ->
return {
name: db.name
version: db.version
objectStores: stores
}
]
return