UNPKG

angular-cached-resource

Version:

An AngularJS module to interact with RESTful resources, even when browser is offline

230 lines (189 loc) 8.82 kB
DEFAULT_ACTIONS = get: { method: 'GET', } query: { method: 'GET', isArray: yes } save: { method: 'POST', } remove: { method: 'DELETE', } delete: { method: 'DELETE', } ResourceCacheEntry = require './resource_cache_entry' ResourceCacheArrayEntry = require './resource_cache_array_entry' CachedResourceManager = require './cached_resource_manager' resourceManagerListener = null app = angular.module 'ngCachedResource', ['ngResource'] app.factory '$cachedResource', ['$resource', '$timeout', '$q', '$log', ($resource, $timeout, $q, $log) -> resourceManager = new CachedResourceManager($timeout) document.removeEventListener 'online', resourceManagerListener if resourceManagerListener resourceManagerListener = (event) -> resourceManager.flushQueues() document.addEventListener 'online', resourceManagerListener processReadArgs = (args) -> # according to the ngResource documentation: # Resource.action([parameters], [success], [error]) args = Array::slice.call args params = if angular.isObject(args[0]) then args.shift() else {} [success, error] = args deferred = $q.defer() deferred.promise.then success if angular.isFunction success deferred.promise.catch error if angular.isFunction error {params, deferred} # this is kind of like angular.extend(), except that if an attribute # on newObject (or any of its children) is equivalent to the same # attribute on oldObject, we won't overwrite it. This is useful if # you are trying to keep track of deeply nested references to a # resource's attributes from different scopes, for example. modifyObjectInPlace = (oldObject, newObject) -> # the `when` clauses below are horrible hacks that needs to be fixed for key in Object.keys(oldObject) when key[0] isnt '$' delete oldObject[key] unless newObject[key]? for key in Object.keys(newObject) when key[0] isnt '$' if angular.isObject(oldObject[key]) and angular.isObject(newObject[key]) modifyObjectInPlace(oldObject[key], newObject[key]) else if not angular.equals(oldObject[key], newObject[key]) oldObject[key] = newObject[key] readArrayCache = (name, CachedResource) -> -> {params, deferred: cacheDeferred} = processReadArgs(arguments) httpDeferred = $q.defer() arrayInstance = new Array() arrayInstance.$promise = cacheDeferred.promise arrayInstance.$httpPromise = httpDeferred.promise cacheArrayEntry = new ResourceCacheArrayEntry(CachedResource.$key, params) resource = CachedResource.$resource[name].apply(CachedResource.$resource, arguments) resource.$promise.then -> cachedResourceInstances = resource.map (resourceInstance) -> new CachedResource resourceInstance arrayInstance.splice(0, arrayInstance.length, cachedResourceInstances...) cacheDeferred.resolve arrayInstance unless cacheArrayEntry.value httpDeferred.resolve arrayInstance resource.$promise.catch (error) -> cacheDeferred.reject error unless cacheArrayEntry.value httpDeferred.reject error arrayInstance.$httpPromise.then (response) -> cacheArrayReferences = [] for instance in response cacheInstanceParams = instance.$params() if Object.keys(cacheInstanceParams).length is 0 $log.error """ instance #{instance} doesn't have any boundParams. Please, make sure you specified them in your resource's initialization, f.e. `{id: "@id"}`, or it won't be cached. """ else cacheArrayReferences.push cacheInstanceParams cacheInstanceEntry = new ResourceCacheEntry(CachedResource.$key, cacheInstanceParams) cacheInstanceEntry.set instance, false cacheArrayEntry.set cacheArrayReferences if cacheArrayEntry.value for cacheInstanceParams in cacheArrayEntry.value cacheInstanceEntry = new ResourceCacheEntry(CachedResource.$key, cacheInstanceParams) arrayInstance.push new CachedResource cacheInstanceEntry.value # Resolve the promise as the cache is ready cacheDeferred.resolve arrayInstance arrayInstance readCache = (name, CachedResource) -> -> {params, deferred: cacheDeferred} = processReadArgs(arguments) httpDeferred = $q.defer() instance = new CachedResource $promise: cacheDeferred.promise $httpPromise: httpDeferred.promise cacheEntry = new ResourceCacheEntry(CachedResource.$key, params) readHttp = -> resource = CachedResource.$resource[name].call(CachedResource.$resource, params) resource.$promise.then (response) -> angular.extend(instance, response) cacheDeferred.resolve instance unless cacheEntry.value httpDeferred.resolve instance cacheEntry.set response, false resource.$promise.catch (error) -> cacheDeferred.reject error unless cacheEntry.value httpDeferred.reject error if cacheEntry.dirty resourceManager.getQueue(CachedResource).processResource params, readHttp else readHttp() if cacheEntry.value angular.extend(instance, cacheEntry.value) cacheDeferred.resolve instance instance writeCache = (action, CachedResource) -> -> # according to the ngResource documentation: # Resource.action([parameters], postData, [success], [error]) # or # resourceInstance.action([parameters], [success], [error]) instanceMethod = @ instanceof CachedResource args = Array::slice.call arguments params = if not instanceMethod and angular.isObject(args[1]) args.shift() else if instanceMethod and angular.isObject(args[0]) args.shift() else {} resource = if instanceMethod @ else new CachedResource(args.shift()) [success, error] = args resource.$resolved = false params[param] = value for param, value of resource.$params() deferred = $q.defer() resource.$promise = deferred.promise deferred.promise.then success if angular.isFunction(success) deferred.promise.catch error if angular.isFunction(error) cacheEntry = new ResourceCacheEntry(CachedResource.$key, params) cacheEntry.set(resource, true) unless angular.equals(cacheEntry.data, resource) queueDeferred = $q.defer() queueDeferred.promise.then (httpResource) -> modifyObjectInPlace(resource, httpResource) resource.$resolved = true deferred.resolve(resource) queueDeferred.promise.catch deferred.reject queue = resourceManager.getQueue(CachedResource) queue.enqueue(params, action, queueDeferred) queue.flush() resource return -> # we are mimicking the API of $resource, which is: # $resource(url, [paramDefaults], [actions]) # ...but adding an additional cacheKey param in the beginning, so we have: # # $cachedResource(resourceKey, url, [paramDefaults], [actions]) args = Array::slice.call arguments $key = args.shift() url = args.shift() while args.length arg = args.pop() if angular.isObject(arg[Object.keys(arg)[0]]) actions = arg else paramDefaults = arg actions = angular.extend({}, DEFAULT_ACTIONS, actions) paramDefaults ?= {} boundParams = {} for param, paramDefault of paramDefaults when paramDefault[0] is '@' boundParams[paramDefault.substr(1)] = param Resource = $resource.call(null, url, paramDefaults, actions) isPermissibleBoundValue = (value) -> angular.isDate(value) or angular.isNumber(value) or angular.isString(value) class CachedResource $cache: true # right now this is just a flag, eventually it could be useful for cache introspection (see https://github.com/goodeggs/angular-cached-resource/issues/8) constructor: (attrs) -> angular.extend @, attrs $params: -> params = {} for attribute, param of boundParams when isPermissibleBoundValue @[attribute] params[param] = @[attribute] params @$resource: Resource @$key: $key for name, params of actions handler = if params.method is 'GET' and params.isArray readArrayCache(name, CachedResource) else if params.method is 'GET' readCache(name, CachedResource) else if params.method in ['POST', 'PUT', 'DELETE'] writeCache(name, CachedResource) CachedResource[name] = handler CachedResource::["$#{name}"] = handler unless params.method is 'GET' resourceManager.add(CachedResource) resourceManager.flushQueues() CachedResource ] module?.exports = app