UNPKG

ng-request-cache

Version:

AngularJS HTTP request cache for IndexeDB and $cacheFactory

584 lines (482 loc) 16.6 kB
###* @license $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 ###* @ngdoc function @name $indexedDBProvider.connection @function @description sets the name of the database to use @param {string} databaseName database name. @returns {object} this ### @connection = (databaseName) -> dbName = databaseName this ###* @ngdoc function @name $indexedDBProvider.upgradeDatabase @function @description provides version number and steps to upgrade the database wrapped in a callback function @param {number} newVersion new version number for the database. @param {function} callback the callback which proceeds the upgrade @returns {object} this ### @upgradeDatabase = (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) -> @transaction = db.transaction(storeNames, mode) @defer = $q.defer() @promise = @defer.promise @setupCallbacks() setupCallbacks: -> @transaction.oncomplete = => $rootScope.$apply => @defer.resolve("Transaction Completed") @transaction.onabort = (error) => $rootScope.$apply => @defer.reject("Transaction Aborted", error) @transaction.onerror = (error) => $rootScope.$apply => @defer.reject("Transaction Error", error) addTransaction(this) objectStore: (storeName) -> @transaction.objectStore(storeName) abort: -> @transaction.abort() class DbQ constructor: -> @q = $q.defer() @promise = @q.promise reject: (args...) -> $rootScope.$apply => @q.reject(args...) rejectWith: (req) -> req.onerror = req.onblocked = (e) => @reject(errorMessageFor(e)) resolve: (args...) -> $rootScope.$apply => @q.resolve(args...) notify: (args...) -> $rootScope.$apply => @q.notify(args...) dbErrorFunction: -> (error) => $rootScope.$apply => @q.reject(errorMessageFor(error)) resolveWith: (req) -> @rejectWith(req) req.onsuccess = (e) => @resolve(e.target.result) class ObjectStore constructor: (storeName, transaction) -> @storeName = storeName @store = transaction.objectStore(storeName) @transaction = transaction defer: -> new DbQ() _mapCursor: (defer, mapFunc, req = @store.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 = @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 ###* @ngdoc function @name $indexedDBProvider.store.getAllKeys @function @description gets all the keys @returns {Q} A promise which will result with all the keys ### getAllKeys: -> defer = @defer() if @store.getAllKeys req = @store.getAllKeys() defer.resolveWith(req) else @_mapCursor defer, (cursor) -> cursor.key return defer.promise ###* @ngdoc function @name $indexedDBProvider.store.clear @function @description clears all objects from this store @returns {Q} A promise that this can be done successfully. ### clear: -> defer = @defer() req = @store.clear() defer.resolveWith(req) defer.promise ###* @ngdoc function @name $indexedDBProvider.store.delete @function @description Deletes the item at the key. The operation is ignored if the item does not exist. @param {key} The key of the object to delete. @returns {Q} A promise that this can be done successfully. ### delete: (key) -> defer = @defer() defer.resolveWith(@store.delete(key)) defer.promise ###* @ngdoc function @name $indexedDBProvider.store.upsert @function @description Updates the given item @param {data} Details of the item or items to update or insert @returns {Q} A promise that this can be done successfully. ### upsert: (data) -> @_arrayOperation data, (item) => @store.put(item) ###* @ngdoc function @name $indexedDBProvider.store.insert @function @description Updates the given item @param {data} Details of the item or items to insert @returns {Q} A promise that this can be done successfully. ### insert: (data) -> @_arrayOperation data, (item) => @store.add(item) ###* @ngdoc function @name $indexedDBProvider.store.getAll @function @description Fetches all items from the store @returns {Q} A promise which resolves with copies of all items in the store ### getAll: -> defer = @defer() if @store.getAll defer.resolveWith(@store.getAll()) else @_mapCursor defer, (cursor) -> cursor.value defer.promise eachWhere: (query) -> defer = @defer() indexName = query.indexName keyRange = query.keyRange direction = query.direction req = if indexName @store.index(indexName).openCursor(keyRange, direction) else @store.openCursor(keyRange, direction) @_mapCursor(defer, ((cursor) -> cursor.value), req) defer.promise findWhere: (query) -> @eachWhere(query) ###* @ngdoc function @name $indexedDBProvider.store.each @function @description Iterates through the items in the store @param {options.beginKey} the key to start iterating from @param {options.endKey} the key to stop iterating at @param {options.direction} Direction to iterate in @returns {Q} A promise which notifies with each individual item and resolves with all of them. ### each: (options = {}) -> @eachBy(undefined, options) ###* @ngdoc function @name $indexedDBProvider.store.eachBy @function @description Iterates through the items in the store using an index @param {indexName} name of the index to use instead of the primary @param {options.beginKey} the key to start iterating from @param {options.endKey} the key to stop iterating at @param {options.direction} Direction to iterate in @returns {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 @eachWhere(q) ###* @ngdoc function @name $indexedDBProvider.store.count @function @description Returns a count of the items in the store @returns {Q} A promise which resolves with the count of all the items in the store. ### count: -> defer = @defer() defer.resolveWith(@store.count()) defer.promise ###* @ngdoc function @name $indexedDBProvider.store.find @function @description Fetches an item from the store @returns {Q} A promise which resolves with the item from the store ### find: (key) -> defer = @defer() req = @store.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 ###* @ngdoc function @name $indexedDBProvider.store.findBy @function @description Fetches an item from the store using a named index. @returns {Q} A promise which resolves with the item from the store. ### findBy: (index, key) -> defer = @defer() defer.resolveWith(@store.index(index).get(key)) defer.promise query: -> new Query() class Query constructor : -> @indexName = undefined @keyRange = undefined @direction = cursorDirection.next $lt: (value) -> @keyRange = IDBKeyRange.upperBound(value, true) this $gt: (value) -> @keyRange = IDBKeyRange.lowerBound(value, true) this $lte: (value) -> @keyRange = IDBKeyRange.upperBound(value) this $gte: (value) -> @keyRange = IDBKeyRange.lowerBound(value) this $eq: (value) -> @keyRange = IDBKeyRange.only(value) this $between: (low, hi, exLow = false, exHi = false) -> @keyRange = IDBKeyRange.bound(low, hi, exLow, exHi) this $desc: (unique) -> @direction = if unique then cursorDirection.prevunique else cursorDirection.prev this $asc: (unique) -> @direction = if unique then cursorDirection.nextunique else cursorDirection.next this $index: (indexName) -> @indexName = indexName this ###* @ngdoc method @name $indexedDB.objectStore @function @description an IDBObjectStore to use @params {string} storeName the name of the objectstore to use @returns {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) ###* @ngdoc method @name $indexedDB.closeDatabase @function @description Closes the database for use and completes all transactions. ### closeDatabase: -> closeDatabase() ###* @ngdoc method @name $indexedDB.deleteDatabase @function @description 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([]) ###* @ngdoc method @name $indexedDB.databaseInfo @function @description 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