Comparing version 2.0.0 to 2.2.0
(function() { | ||
"use strict"; | ||
/* globals angular */ | ||
angular.module( "absync", [] ); | ||
}()); |
(function() { | ||
"use strict"; | ||
/* globals angular */ | ||
angular.module( "absync", [] ); | ||
}());;(function() { | ||
"use strict"; | ||
/* globals angular, io */ | ||
/** | ||
@@ -56,2 +60,7 @@ * Please make note of the following conventions: | ||
self.__collections = {}; | ||
// The entities that absync provides. | ||
// The keys are the names of the entities, the value contains the constructor of | ||
// the respective cache service. | ||
self.__entities = {}; | ||
} | ||
@@ -108,3 +117,3 @@ | ||
// Collection names (and, thus service names) have to be unique. | ||
// Collection/entity names (and, thus service names) have to be unique. | ||
// We can't create multiple services with the same name. | ||
@@ -114,2 +123,5 @@ if( self.__collections[ name ] ) { | ||
} | ||
if( self.__entities[ name ] ) { | ||
throw new Error( "An entity with the name '" + name + "' was already requested. Names for collections must be unique and can't be shared with entities." ); | ||
} | ||
@@ -126,2 +138,29 @@ // Register the service configuration. | ||
/** | ||
* Request a new synchronized entity. | ||
* This only registers the intent to use that entity. It will be constructed when it is first used. | ||
* @param {String} name The name of the entity and service name. | ||
* @param {AbsyncServiceConfiguration|Object} configuration The configuration for this entity. | ||
*/ | ||
AbsyncProvider.prototype.entity = function AbsyncProvider$entity( name, configuration ) { | ||
var self = this; | ||
// Collection/entity names (and, thus service names) have to be unique. | ||
// We can't create multiple services with the same name. | ||
if( self.__entities[ name ] ) { | ||
throw new Error( "An entity with the name '" + name + "' was already requested. Names for entities must be unique." ); | ||
} | ||
if( self.__collections[ name ] ) { | ||
throw new Error( "A collection with the name '" + name + "' was already requested. Names for entities must be unique and can't be shared with collections." ); | ||
} | ||
// Register the service configuration. | ||
// __absyncCache will return a constructor for a service with the given configuration. | ||
self.__entities[ name ] = self.__absyncCache( name, configuration ); | ||
// Register the new service. | ||
// Yes, we want an Angular "service" here, because we want it constructed with "new". | ||
self.__provide.service( name, self.__entities[ name ] ); | ||
}; | ||
/** | ||
* Register the service factory. | ||
@@ -167,3 +206,3 @@ * @returns {AbsyncService} | ||
var _absyncProvider = this.__absyncProvider; | ||
var self = this; | ||
var self = this; | ||
@@ -229,2 +268,4 @@ // If we have no configured socket.io connection yet, remember to register it later. | ||
"use strict"; | ||
/* globals angular */ | ||
/** | ||
@@ -285,10 +326,15 @@ * Please make note of the following conventions: | ||
// The entity cache must be constructed as an empty array, to allow the user to place watchers on it. | ||
// We must never replace the cache with a new array, we must always manipulate the existing one. | ||
// The entity cache must be constructed as an empty array or object, to allow the user to place watchers on it. | ||
// We must never replace the cache with a new array or object, we must always manipulate the existing one. | ||
// Otherwise watchers will not behave as the user expects them to. | ||
/* @type {Array<configuration.model>} */ | ||
self.entityCache = []; | ||
/* @type {Array<configuration.model>|configuration.model} */ | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
// The raw cache is data that hasn't been deserialized and is used internally. | ||
self.__entityCacheRaw = null; | ||
// Should request caching be used at all? | ||
self.enableRequestCache = true; | ||
// Cache requests made to the backend to avoid multiple, simultaneous requests for the same resource. | ||
self.__requestCache = {}; | ||
// TODO: Using deferreds is an anti-pattern and probably provides no value here. | ||
@@ -366,3 +412,3 @@ self.__dataAvailableDeferred = $q.defer(); | ||
* Event handler for when the initial badge of raw data becomes available. | ||
* @param {Array<Object>} rawData | ||
* @param {Array<Object>|Object} rawData | ||
* @private | ||
@@ -373,18 +419,35 @@ */ | ||
// The symbol self.entityCache is expected to be an empty array. | ||
// We initialize it in the constructor to an empty array and we don't expect any writes to have | ||
// happened to it. In case writes *did* happen, we assume that whoever wrote to it knows what | ||
// they're doing. | ||
rawData[ configuration.collectionName ].forEach( deserializeCollectionEntry ); | ||
if( Array.isArray( self.entityCache ) ) { | ||
// The symbol self.entityCache is expected to be an empty array. | ||
// We initialize it in the constructor to an empty array and we don't expect any writes to have | ||
// happened to it. In case writes *did* happen, we assume that whoever wrote to it knows what | ||
// they're doing. | ||
rawData[ configuration.collectionName ].forEach( deserializeCollectionEntry ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Notify the rest of the application about a fresh collection. | ||
self.scope.$broadcast( "collectionNew", { | ||
service : self, | ||
cache : self.entityCache | ||
} ); | ||
// Notify the rest of the application about a fresh collection. | ||
self.scope.$broadcast( "collectionNew", { | ||
service : self, | ||
cache : self.entityCache | ||
} ); | ||
} else { | ||
var deserialized = self.deserializer( rawData[ configuration.entityName ] ); | ||
self.__updateCacheWithEntity( deserialized ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Notify the rest of the application about a fresh entity. | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : deserialized | ||
} ); | ||
} | ||
function deserializeCollectionEntry( rawEntity ) { | ||
@@ -402,3 +465,3 @@ self.entityCache.push( self.deserializer( rawEntity ) ); | ||
CacheService.prototype.__onEntityReceived = function CacheService$onEntityReceived( event, args ) { | ||
var self = this; | ||
var self = this; | ||
var _entityReceived = args; | ||
@@ -425,3 +488,3 @@ | ||
CacheService.prototype.__onCollectionReceived = function CacheService$onCollectionReceived( event, args ) { | ||
var self = this; | ||
var self = this; | ||
var _collectionReceived = args; | ||
@@ -457,12 +520,21 @@ | ||
// If the user did not provide information necessary to work with a collection, immediately return | ||
// a promise for an empty collection. The user could still use read() to grab individual entities. | ||
if( !configuration.collectionName || !configuration.collectionUri ) { | ||
return self.q.when( [] ); | ||
if( configuration.entityName && configuration.entityUri ) { | ||
self.__entityCacheRaw = {}; | ||
self.httpInterface | ||
.get( configuration.entityUri ) | ||
.then( onSingleEntityReceived, onSingleEntityRetrievalFailure ); | ||
} else { | ||
// If the user did not provide information necessary to work with a collection, immediately return | ||
// a promise for an empty collection. The user could still use read() to grab individual entities. | ||
return self.q.when( [] ); | ||
} | ||
} else { | ||
self.logInterface.info( self.logPrefix + "Retrieving '" + configuration.collectionName + "' collection…" ); | ||
self.httpInterface | ||
.get( configuration.collectionUri ) | ||
.then( onCollectionReceived, onCollectionRetrievalFailure ); | ||
} | ||
self.logInterface.info( self.logPrefix + "Retrieving '" + configuration.collectionName + "' collection…" ); | ||
self.httpInterface | ||
.get( configuration.collectionUri ) | ||
.then( onCollectionReceived, onCollectionRetrievalFailure ); | ||
} | ||
@@ -504,2 +576,26 @@ | ||
} | ||
/** | ||
* Invoked when the entity was received from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onSingleEntityReceived( serverResponse ) { | ||
if( !serverResponse.data[ configuration.entityName ] ) { | ||
throw new Error( "The response from the server was not in the expected format. It should have a member named '" + configuration.entityName + "'." ); | ||
} | ||
self.__entityCacheRaw = serverResponse.data; | ||
self.__dataAvailableDeferred.resolve( serverResponse.data ); | ||
} | ||
/** | ||
* Invoked when there was an error while trying to retrieve the entity from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onSingleEntityRetrievalFailure( serverResponse ) { | ||
self.logInterface.error( self.logPrefix + "Unable to retrieve the entity from the server.", | ||
serverResponse ); | ||
self.__entityCacheRaw = null; | ||
self.scope.$emit( "absyncError", serverResponse ); | ||
} | ||
}; | ||
@@ -519,2 +615,4 @@ | ||
self.logInterface.debug( self.logPrefix + "Requesting entity '" + id + "' (forceReload:" + forceReload + ")…" ); | ||
if( !forceReload ) { | ||
@@ -526,2 +624,3 @@ // Check if the entity is in the cache and return instantly if found. | ||
if( entity.id === id ) { | ||
self.logInterface.debug( self.logPrefix + "Requested entity '" + id + "' is served from cache." ); | ||
return self.q.when( entity ); | ||
@@ -532,5 +631,5 @@ } | ||
// Grab the entity from the backend. | ||
return self.httpInterface | ||
.get( configuration.entityUri + "/" + id ) | ||
self.logInterface.debug( self.logPrefix + "Requested entity '" + id + "' is fetched from backend." ); | ||
return self.__requestEntity( id ) | ||
.then( onEntityRetrieved, onEntityRetrievalFailure ); | ||
@@ -568,2 +667,35 @@ | ||
/** | ||
* Request an entity from the backend. | ||
* @param {String} id The ID of the entity. | ||
* @returns {Promise<configuration.model>|IPromise<TResult>|IPromise<void>} | ||
* @private | ||
*/ | ||
CacheService.prototype.__requestEntity = function CacheService$requestEntity( id ) { | ||
var self = this; | ||
if( self.enableRequestCache && self.__requestCache && self.__requestCache[ id ] ) { | ||
self.logInterface.debug( self.logPrefix + "Entity request '" + id + "' served from request cache." ); | ||
return self.__requestCache[ id ]; | ||
} | ||
var requestUri = configuration.entityUri + ( id ? ( "/" + id ) : "" ); | ||
// Grab the entity from the backend. | ||
var request = self.httpInterface | ||
.get( requestUri ) | ||
.then( remoteRequestFromCache.bind( self, id ) ); | ||
if( self.enableRequestCache && self.__requestCache ) { | ||
self.__requestCache[ id ] = request; | ||
} | ||
return request; | ||
function remoteRequestFromCache( id, serverResponse ) { | ||
delete self.__requestCache[ id ]; | ||
return serverResponse; | ||
} | ||
}; | ||
/** | ||
* Updates an entity and persists it to the backend and the cache. | ||
@@ -674,4 +806,32 @@ * @param {configuration.model} entity | ||
self.logInterface.info( self.logPrefix + "Updating entity in cache…" ); | ||
self.logInterface.info( self.logPrefix + "Updating entity '" + entityToCache.id || self.name + "' in cache…", | ||
entityToCache ); | ||
if( !Array.isArray( self.entityCache ) ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache, | ||
updated : entityToCache | ||
} ); | ||
if( typeof self.entityCache.copyFrom === "function" ) { | ||
self.entityCache.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( self.entityCache, entityToCache ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache | ||
} ); | ||
return; | ||
} | ||
var found = false; | ||
@@ -884,2 +1044,4 @@ for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
"use strict"; | ||
/* globals angular */ | ||
angular | ||
@@ -886,0 +1048,0 @@ .module( "absync" ) |
(function() { | ||
"use strict"; | ||
/* globals angular, io */ | ||
/** | ||
@@ -53,2 +55,7 @@ * Please make note of the following conventions: | ||
self.__collections = {}; | ||
// The entities that absync provides. | ||
// The keys are the names of the entities, the value contains the constructor of | ||
// the respective cache service. | ||
self.__entities = {}; | ||
} | ||
@@ -105,3 +112,3 @@ | ||
// Collection names (and, thus service names) have to be unique. | ||
// Collection/entity names (and, thus service names) have to be unique. | ||
// We can't create multiple services with the same name. | ||
@@ -111,2 +118,5 @@ if( self.__collections[ name ] ) { | ||
} | ||
if( self.__entities[ name ] ) { | ||
throw new Error( "An entity with the name '" + name + "' was already requested. Names for collections must be unique and can't be shared with entities." ); | ||
} | ||
@@ -123,2 +133,29 @@ // Register the service configuration. | ||
/** | ||
* Request a new synchronized entity. | ||
* This only registers the intent to use that entity. It will be constructed when it is first used. | ||
* @param {String} name The name of the entity and service name. | ||
* @param {AbsyncServiceConfiguration|Object} configuration The configuration for this entity. | ||
*/ | ||
AbsyncProvider.prototype.entity = function AbsyncProvider$entity( name, configuration ) { | ||
var self = this; | ||
// Collection/entity names (and, thus service names) have to be unique. | ||
// We can't create multiple services with the same name. | ||
if( self.__entities[ name ] ) { | ||
throw new Error( "An entity with the name '" + name + "' was already requested. Names for entities must be unique." ); | ||
} | ||
if( self.__collections[ name ] ) { | ||
throw new Error( "A collection with the name '" + name + "' was already requested. Names for entities must be unique and can't be shared with collections." ); | ||
} | ||
// Register the service configuration. | ||
// __absyncCache will return a constructor for a service with the given configuration. | ||
self.__entities[ name ] = self.__absyncCache( name, configuration ); | ||
// Register the new service. | ||
// Yes, we want an Angular "service" here, because we want it constructed with "new". | ||
self.__provide.service( name, self.__entities[ name ] ); | ||
}; | ||
/** | ||
* Register the service factory. | ||
@@ -164,3 +201,3 @@ * @returns {AbsyncService} | ||
var _absyncProvider = this.__absyncProvider; | ||
var self = this; | ||
var self = this; | ||
@@ -167,0 +204,0 @@ // If we have no configured socket.io connection yet, remember to register it later. |
(function() { | ||
"use strict"; | ||
/* globals angular */ | ||
/** | ||
@@ -58,10 +60,15 @@ * Please make note of the following conventions: | ||
// The entity cache must be constructed as an empty array, to allow the user to place watchers on it. | ||
// We must never replace the cache with a new array, we must always manipulate the existing one. | ||
// The entity cache must be constructed as an empty array or object, to allow the user to place watchers on it. | ||
// We must never replace the cache with a new array or object, we must always manipulate the existing one. | ||
// Otherwise watchers will not behave as the user expects them to. | ||
/* @type {Array<configuration.model>} */ | ||
self.entityCache = []; | ||
/* @type {Array<configuration.model>|configuration.model} */ | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
// The raw cache is data that hasn't been deserialized and is used internally. | ||
self.__entityCacheRaw = null; | ||
// Should request caching be used at all? | ||
self.enableRequestCache = true; | ||
// Cache requests made to the backend to avoid multiple, simultaneous requests for the same resource. | ||
self.__requestCache = {}; | ||
// TODO: Using deferreds is an anti-pattern and probably provides no value here. | ||
@@ -139,3 +146,3 @@ self.__dataAvailableDeferred = $q.defer(); | ||
* Event handler for when the initial badge of raw data becomes available. | ||
* @param {Array<Object>} rawData | ||
* @param {Array<Object>|Object} rawData | ||
* @private | ||
@@ -146,18 +153,35 @@ */ | ||
// The symbol self.entityCache is expected to be an empty array. | ||
// We initialize it in the constructor to an empty array and we don't expect any writes to have | ||
// happened to it. In case writes *did* happen, we assume that whoever wrote to it knows what | ||
// they're doing. | ||
rawData[ configuration.collectionName ].forEach( deserializeCollectionEntry ); | ||
if( Array.isArray( self.entityCache ) ) { | ||
// The symbol self.entityCache is expected to be an empty array. | ||
// We initialize it in the constructor to an empty array and we don't expect any writes to have | ||
// happened to it. In case writes *did* happen, we assume that whoever wrote to it knows what | ||
// they're doing. | ||
rawData[ configuration.collectionName ].forEach( deserializeCollectionEntry ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Notify the rest of the application about a fresh collection. | ||
self.scope.$broadcast( "collectionNew", { | ||
service : self, | ||
cache : self.entityCache | ||
} ); | ||
// Notify the rest of the application about a fresh collection. | ||
self.scope.$broadcast( "collectionNew", { | ||
service : self, | ||
cache : self.entityCache | ||
} ); | ||
} else { | ||
var deserialized = self.deserializer( rawData[ configuration.entityName ] ); | ||
self.__updateCacheWithEntity( deserialized ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Notify the rest of the application about a fresh entity. | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : deserialized | ||
} ); | ||
} | ||
function deserializeCollectionEntry( rawEntity ) { | ||
@@ -175,3 +199,3 @@ self.entityCache.push( self.deserializer( rawEntity ) ); | ||
CacheService.prototype.__onEntityReceived = function CacheService$onEntityReceived( event, args ) { | ||
var self = this; | ||
var self = this; | ||
var _entityReceived = args; | ||
@@ -198,3 +222,3 @@ | ||
CacheService.prototype.__onCollectionReceived = function CacheService$onCollectionReceived( event, args ) { | ||
var self = this; | ||
var self = this; | ||
var _collectionReceived = args; | ||
@@ -230,12 +254,21 @@ | ||
// If the user did not provide information necessary to work with a collection, immediately return | ||
// a promise for an empty collection. The user could still use read() to grab individual entities. | ||
if( !configuration.collectionName || !configuration.collectionUri ) { | ||
return self.q.when( [] ); | ||
if( configuration.entityName && configuration.entityUri ) { | ||
self.__entityCacheRaw = {}; | ||
self.httpInterface | ||
.get( configuration.entityUri ) | ||
.then( onSingleEntityReceived, onSingleEntityRetrievalFailure ); | ||
} else { | ||
// If the user did not provide information necessary to work with a collection, immediately return | ||
// a promise for an empty collection. The user could still use read() to grab individual entities. | ||
return self.q.when( [] ); | ||
} | ||
} else { | ||
self.logInterface.info( self.logPrefix + "Retrieving '" + configuration.collectionName + "' collection…" ); | ||
self.httpInterface | ||
.get( configuration.collectionUri ) | ||
.then( onCollectionReceived, onCollectionRetrievalFailure ); | ||
} | ||
self.logInterface.info( self.logPrefix + "Retrieving '" + configuration.collectionName + "' collection…" ); | ||
self.httpInterface | ||
.get( configuration.collectionUri ) | ||
.then( onCollectionReceived, onCollectionRetrievalFailure ); | ||
} | ||
@@ -277,2 +310,26 @@ | ||
} | ||
/** | ||
* Invoked when the entity was received from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onSingleEntityReceived( serverResponse ) { | ||
if( !serverResponse.data[ configuration.entityName ] ) { | ||
throw new Error( "The response from the server was not in the expected format. It should have a member named '" + configuration.entityName + "'." ); | ||
} | ||
self.__entityCacheRaw = serverResponse.data; | ||
self.__dataAvailableDeferred.resolve( serverResponse.data ); | ||
} | ||
/** | ||
* Invoked when there was an error while trying to retrieve the entity from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onSingleEntityRetrievalFailure( serverResponse ) { | ||
self.logInterface.error( self.logPrefix + "Unable to retrieve the entity from the server.", | ||
serverResponse ); | ||
self.__entityCacheRaw = null; | ||
self.scope.$emit( "absyncError", serverResponse ); | ||
} | ||
}; | ||
@@ -292,2 +349,4 @@ | ||
self.logInterface.debug( self.logPrefix + "Requesting entity '" + id + "' (forceReload:" + forceReload + ")…" ); | ||
if( !forceReload ) { | ||
@@ -299,2 +358,3 @@ // Check if the entity is in the cache and return instantly if found. | ||
if( entity.id === id ) { | ||
self.logInterface.debug( self.logPrefix + "Requested entity '" + id + "' is served from cache." ); | ||
return self.q.when( entity ); | ||
@@ -305,5 +365,5 @@ } | ||
// Grab the entity from the backend. | ||
return self.httpInterface | ||
.get( configuration.entityUri + "/" + id ) | ||
self.logInterface.debug( self.logPrefix + "Requested entity '" + id + "' is fetched from backend." ); | ||
return self.__requestEntity( id ) | ||
.then( onEntityRetrieved, onEntityRetrievalFailure ); | ||
@@ -341,2 +401,35 @@ | ||
/** | ||
* Request an entity from the backend. | ||
* @param {String} id The ID of the entity. | ||
* @returns {Promise<configuration.model>|IPromise<TResult>|IPromise<void>} | ||
* @private | ||
*/ | ||
CacheService.prototype.__requestEntity = function CacheService$requestEntity( id ) { | ||
var self = this; | ||
if( self.enableRequestCache && self.__requestCache && self.__requestCache[ id ] ) { | ||
self.logInterface.debug( self.logPrefix + "Entity request '" + id + "' served from request cache." ); | ||
return self.__requestCache[ id ]; | ||
} | ||
var requestUri = configuration.entityUri + ( id ? ( "/" + id ) : "" ); | ||
// Grab the entity from the backend. | ||
var request = self.httpInterface | ||
.get( requestUri ) | ||
.then( remoteRequestFromCache.bind( self, id ) ); | ||
if( self.enableRequestCache && self.__requestCache ) { | ||
self.__requestCache[ id ] = request; | ||
} | ||
return request; | ||
function remoteRequestFromCache( id, serverResponse ) { | ||
delete self.__requestCache[ id ]; | ||
return serverResponse; | ||
} | ||
}; | ||
/** | ||
* Updates an entity and persists it to the backend and the cache. | ||
@@ -447,4 +540,32 @@ * @param {configuration.model} entity | ||
self.logInterface.info( self.logPrefix + "Updating entity in cache…" ); | ||
self.logInterface.info( self.logPrefix + "Updating entity '" + entityToCache.id || self.name + "' in cache…", | ||
entityToCache ); | ||
if( !Array.isArray( self.entityCache ) ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache, | ||
updated : entityToCache | ||
} ); | ||
if( typeof self.entityCache.copyFrom === "function" ) { | ||
self.entityCache.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( self.entityCache, entityToCache ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache | ||
} ); | ||
return; | ||
} | ||
var found = false; | ||
@@ -451,0 +572,0 @@ for( var entityIndex = 0, entity = self.entityCache[ 0 ]; |
(function() { | ||
"use strict"; | ||
/* globals angular */ | ||
angular | ||
@@ -4,0 +6,0 @@ .module( "absync" ) |
@@ -1,2 +0,2 @@ | ||
!function(){"use strict";angular.module("absync",[])}(),function(){"use strict";function e(e,n){return new t(e,n)}function t(e,t){var n=this;n.__provide=e,n.__absyncCache=t,n.__ioSocket=null,n.__registerLater=[],n.__collections={}}function n(e){this.__absyncProvider=e}angular.module("absync").provider("absync",e),e.$inject=["$provide","absyncCache"],t.prototype.configure=function(e){var t=this,n=e.socket||e,i=io&&io.Socket&&n instanceof io.Socket;if("function"==typeof n)t.__ioSocket=n();else{if(!i)throw new Error("configure() expects input to be a function or a socket.io Socket instance.");t.__ioSocket=n}t.__registerLater.length&&(t.__registerLater.forEach(t.__registerListener.bind(t)),t.__registerLater=[])},t.prototype.__registerListener=function(e){var t=this;t.$get().__handleEntityEvent(e.eventName,e.callback)},t.prototype.collection=function(e,t){var n=this;if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for collections must be unique.");n.__collections[e]=n.__absyncCache(e,t),n.__provide.service(e,n.__collections[e])},t.prototype.$get=function(){return new n(this)},n.prototype.configure=function(e){var t=this.__absyncProvider;t.configure(e)},n.prototype.on=function(e,t){var n=this.__absyncProvider,i=this;return n.__ioSocket?i.__handleEntityEvent(e,t):n.__registerLater.length>8192?null:(n.__registerLater.push({eventName:e,callback:t}),null)},n.prototype.__handleEntityEvent=function(e,t){var n=this.__absyncProvider;return n.__ioSocket.on(e,t),function(){n.__ioSocket.removeListener(e,t)}},n.prototype.emit=function(e,t,n){var i=this.__absyncProvider;if(!i.__ioSocket)throw new Error("socket.io is not initialized.");i.__ioSocket.emit(e,t,function(){n&&n.apply(i.__ioSocket,arguments)})}}(),function(){"use strict";function e(e,n){function i(i,o,r,a,c,s){var l=this,h=n.injector||o,f=h.has(n.model);if(!f)throw new Error("Unable to construct the '"+e+"' service, because the referenced model '"+n.model+"' is not available for injection.");var y="string"==typeof n.model?h.get(n.model):n.model,d=y.serialize||n.serialize||t,u=y.deserialize||n.deserialize||t;l.name=e,l.configuration=n,l.entityCache=[],l.__entityCacheRaw=null,l.__dataAvailableDeferred=a.defer(),l.__objectsAvailableDeferred=a.defer(),l.dataAvailable=l.__dataAvailableDeferred.promise,l.objectsAvailable=l.__objectsAvailableDeferred.promise,l.httpInterface=i,l.logInterface=r,l.scope=c,l.q=a,l.logPrefix="absync:"+e.toLocaleUpperCase()+" ",l.forceEarlyCacheUpdate=!1,l.serializer=d,l.deserializer=u,s.on(n.entityName,l.__onEntityOnWebsocket.bind(l)),s.on(n.collectionName,l.__onCollectionOnWebsocket.bind(l)),c.$on(n.entityName,l.__onEntityReceived.bind(l)),c.$on(n.collectionName,l.__onCollectionReceived.bind(l)),l.dataAvailable.then(l.__onDataAvailable.bind(l)),l.logInterface.info(l.logPrefix+"service was instantiated.")}return i.$inject=["$http","$injector","$log","$q","$rootScope","absync"],i.prototype.__onEntityOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.entityName,e[n.entityName])},i.prototype.__onCollectionOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.collectionName,e[n.collectionName])},i.prototype.__onDataAvailable=function(e){function t(e){i.entityCache.push(i.deserializer(e))}var i=this;e[n.collectionName].forEach(t),i.__objectsAvailableDeferred.resolve(i.entityCache),i.scope.$broadcast("collectionNew",{service:i,cache:i.entityCache})},i.prototype.__onEntityReceived=function(e,t){var n=this,i=t;1===Object.keys(i).length&&i.hasOwnProperty("id")?(n.logInterface.info(n.logPrefix+"Entity was deleted from the server. Updating cache…"),n.__removeEntityFromCache(i.id)):(n.logInterface.debug(n.logPrefix+"Entity was updated on the server. Updating cache…"),n.__updateCacheWithEntity(n.deserializer(i)))},i.prototype.__onCollectionReceived=function(e,t){function n(e){var t=i.deserializer(e);i.__updateCacheWithEntity(t)}var i=this,o=t;i.entityCache.length=0,o.forEach(n)},i.prototype.ensureLoaded=function(e){function t(e){if(!e.data[n.collectionName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.collectionName+"'.");o.__entityCacheRaw=e.data,o.__dataAvailableDeferred.resolve(e.data)}function i(e){o.logInterface.error(o.logPrefix+"Unable to retrieve the collection from the server.",e),o.__entityCacheRaw=null,o.scope.$emit("absyncError",e)}var o=this;if(e=e===!0,null===o.__entityCacheRaw||e){if(o.__entityCacheRaw=[],!n.collectionName||!n.collectionUri)return o.q.when([]);o.logInterface.info(o.logPrefix+"Retrieving '"+n.collectionName+"' collection…"),o.httpInterface.get(n.collectionUri).then(t,i)}return o.q.all([o.dataAvailable,o.objectsAvailable]).then(function(){return o.entityCache})},i.prototype.read=function(e,t){function i(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");var t=r.deserializer(e.data[n.entityName]);return r.__updateCacheWithEntity(t),t}function o(t){r.logInterface.error(r.logPrefix+"Unable to retrieve entity with ID '"+e+"' from the server.",t),r.scope.$emit("absyncError",t)}var r=this;if(t=t===!0,!t)for(var a=0,c=r.entityCache[0];a<r.entityCache.length;++a,c=r.entityCache[a])if(c.id===e)return r.q.when(c);return r.httpInterface.get(n.entityUri+"/"+e).then(i,o)},i.prototype.update=function(e){function t(e){if(e.data[n.entityName]){var t=o.deserializer(e.data[n.entityName]);return o.forceEarlyCacheUpdate&&o.__updateCacheWithEntity(t),t}throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.")}function i(e){o.logInterface.error(o.logPrefix+"Unable to store entity on the server.",e),o.logInterface.error(e)}var o=this,r=o.reduceComplex(e),a=o.serializer(r),c={};return c[n.entityName]=a,"undefined"!=typeof e.id?o.httpInterface.put(n.entityUri+"/"+e.id,c).then(t,i):o.httpInterface.post(n.collectionUri,c).then(t,i)},i.prototype.create=i.prototype.update,i.prototype["delete"]=function(e){function t(e){return o.__removeEntityFromCache(r)}function i(e){throw o.logInterface.error(e.data),new Error("Unable to delete entity.")}var o=this,r=e.id;return o.httpInterface["delete"](n.entityUri+"/"+r).then(t)["catch"](i)},i.prototype.__updateCacheWithEntity=function(e){var t=this;t.logInterface.info(t.logPrefix+"Updating entity in cache…");for(var n=!1,i=0,o=t.entityCache[0];i<t.entityCache.length;++i,o=t.entityCache[i])if(o.id==e.id){t.scope.$broadcast("beforeEntityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache[i],updated:e});var r=t.entityCache[i];"function"==typeof r.copyFrom?r.copyFrom(e):angular.extend(r,e),n=!0,t.scope.$broadcast("entityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache[i]});break}n||(t.entityCache.push(e),t.scope.$broadcast("entityNew",{service:t,cache:t.entityCache,entity:e}))},i.prototype.__removeEntityFromCache=function(e){for(var t=this,n=0,i=t.entityCache[0];n<t.entityCache.length;++n,i=t.entityCache[n])if(i.id==e){t.scope.$broadcast("beforeEntityRemoved",{service:t,cache:t.entityCache,entity:i}),t.entityCache.splice(n,1),t.scope.$broadcast("entityRemoved",{service:t,cache:t.entityCache,entity:i});break}},i.prototype.lookupTableById=function(){for(var e=this,t=[],n=0;n<e.entityCache.length;++n)t[e.entityCache[n].id]=e.entityCache[n];return t},i.prototype.reduceComplex=function(e,t){var n=this,i=t?[]:{};for(var o in e)e.hasOwnProperty(o)&&(Array.isArray(e[o])?i[o]=n.reduceComplex(e[o],!0):e[o]&&e[o].id?i[o]=e[o].id:i[o]=e[o]);return i},i.prototype.populateComplex=function(e,t,n,i){function o(o,r){function c(n){e[t][r]=n}if("string"!=typeof e[t][r]){if(!i||"object"!=typeof e[t][r]||"string"!=typeof e[t][r].id)return a.q.when(!1);e[t][r]=e[t][r].id}return n.read(e[t][r]).then(c)}function r(n){e[t]=n}var a=this;if(Array.isArray(e[t])){var c=e[t].map(o);return a.q.all(c)}if("string"!=typeof e[t]){if(!i||"object"!=typeof e[t]||"string"!=typeof e[t].id)return a.q.when(!1);e[t]=e[t].id}return n.read(e[t]).then(r)},i}function t(e){return e}angular.module("absync").constant("absyncCache",e)}(),function(){"use strict";function e(){return t}function t(e,t,n,i,o,r,a,c){this.model=e,this.collectionUri=t,this.entityUri=n;var s=e.prototype.constructor.name.toLowerCase();this.collectionName=i||s+"s",this.entityName=o||s,this.deserialize=r||void 0,this.serialize=a||void 0,this.injector=c||void 0}angular.module("absync").service("AbsyncServiceConfiguration",e)}(); | ||
!function(){"use strict";angular.module("absync",[])}(),function(){"use strict";function e(e,n){return new t(e,n)}function t(e,t){var n=this;n.__provide=e,n.__absyncCache=t,n.__ioSocket=null,n.__registerLater=[],n.__collections={},n.__entities={}}function n(e){this.__absyncProvider=e}angular.module("absync").provider("absync",e),e.$inject=["$provide","absyncCache"],t.prototype.configure=function(e){var t=this,n=e.socket||e,r=io&&io.Socket&&n instanceof io.Socket;if("function"==typeof n)t.__ioSocket=n();else{if(!r)throw new Error("configure() expects input to be a function or a socket.io Socket instance.");t.__ioSocket=n}t.__registerLater.length&&(t.__registerLater.forEach(t.__registerListener.bind(t)),t.__registerLater=[])},t.prototype.__registerListener=function(e){var t=this;t.$get().__handleEntityEvent(e.eventName,e.callback)},t.prototype.collection=function(e,t){var n=this;if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for collections must be unique.");if(n.__entities[e])throw new Error("An entity with the name '"+e+"' was already requested. Names for collections must be unique and can't be shared with entities.");n.__collections[e]=n.__absyncCache(e,t),n.__provide.service(e,n.__collections[e])},t.prototype.entity=function(e,t){var n=this;if(n.__entities[e])throw new Error("An entity with the name '"+e+"' was already requested. Names for entities must be unique.");if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for entities must be unique and can't be shared with collections.");n.__entities[e]=n.__absyncCache(e,t),n.__provide.service(e,n.__entities[e])},t.prototype.$get=function(){return new n(this)},n.prototype.configure=function(e){var t=this.__absyncProvider;t.configure(e)},n.prototype.on=function(e,t){var n=this.__absyncProvider,r=this;return n.__ioSocket?r.__handleEntityEvent(e,t):n.__registerLater.length>8192?null:(n.__registerLater.push({eventName:e,callback:t}),null)},n.prototype.__handleEntityEvent=function(e,t){var n=this.__absyncProvider;return n.__ioSocket.on(e,t),function(){n.__ioSocket.removeListener(e,t)}},n.prototype.emit=function(e,t,n){var r=this.__absyncProvider;if(!r.__ioSocket)throw new Error("socket.io is not initialized.");r.__ioSocket.emit(e,t,function(){n&&n.apply(r.__ioSocket,arguments)})}}(),function(){"use strict";function e(e,n){function r(r,i,o,a,c,s){var l=this,h=n.injector||i,y=h.has(n.model);if(!y)throw new Error("Unable to construct the '"+e+"' service, because the referenced model '"+n.model+"' is not available for injection.");var d="string"==typeof n.model?h.get(n.model):n.model,f=d.serialize||n.serialize||t,u=d.deserialize||n.deserialize||t;l.name=e,l.configuration=n,l.entityCache=n.collectionName?[]:{},l.__entityCacheRaw=null,l.enableRequestCache=!0,l.__requestCache={},l.__dataAvailableDeferred=a.defer(),l.__objectsAvailableDeferred=a.defer(),l.dataAvailable=l.__dataAvailableDeferred.promise,l.objectsAvailable=l.__objectsAvailableDeferred.promise,l.httpInterface=r,l.logInterface=o,l.scope=c,l.q=a,l.logPrefix="absync:"+e.toLocaleUpperCase()+" ",l.forceEarlyCacheUpdate=!1,l.serializer=f,l.deserializer=u,s.on(n.entityName,l.__onEntityOnWebsocket.bind(l)),s.on(n.collectionName,l.__onCollectionOnWebsocket.bind(l)),c.$on(n.entityName,l.__onEntityReceived.bind(l)),c.$on(n.collectionName,l.__onCollectionReceived.bind(l)),l.dataAvailable.then(l.__onDataAvailable.bind(l)),l.logInterface.info(l.logPrefix+"service was instantiated.")}return r.$inject=["$http","$injector","$log","$q","$rootScope","absync"],r.prototype.__onEntityOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.entityName,e[n.entityName])},r.prototype.__onCollectionOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.collectionName,e[n.collectionName])},r.prototype.__onDataAvailable=function(e){function t(e){r.entityCache.push(r.deserializer(e))}var r=this;if(Array.isArray(r.entityCache))e[n.collectionName].forEach(t),r.__objectsAvailableDeferred.resolve(r.entityCache),r.scope.$broadcast("collectionNew",{service:r,cache:r.entityCache});else{var i=r.deserializer(e[n.entityName]);r.__updateCacheWithEntity(i),r.__objectsAvailableDeferred.resolve(r.entityCache),r.scope.$broadcast("entityNew",{service:r,cache:r.entityCache,entity:i})}},r.prototype.__onEntityReceived=function(e,t){var n=this,r=t;1===Object.keys(r).length&&r.hasOwnProperty("id")?(n.logInterface.info(n.logPrefix+"Entity was deleted from the server. Updating cache…"),n.__removeEntityFromCache(r.id)):(n.logInterface.debug(n.logPrefix+"Entity was updated on the server. Updating cache…"),n.__updateCacheWithEntity(n.deserializer(r)))},r.prototype.__onCollectionReceived=function(e,t){function n(e){var t=r.deserializer(e);r.__updateCacheWithEntity(t)}var r=this,i=t;r.entityCache.length=0,i.forEach(n)},r.prototype.ensureLoaded=function(e){function t(e){if(!e.data[n.collectionName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.collectionName+"'.");a.__entityCacheRaw=e.data,a.__dataAvailableDeferred.resolve(e.data)}function r(e){a.logInterface.error(a.logPrefix+"Unable to retrieve the collection from the server.",e),a.__entityCacheRaw=null,a.scope.$emit("absyncError",e)}function i(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");a.__entityCacheRaw=e.data,a.__dataAvailableDeferred.resolve(e.data)}function o(e){a.logInterface.error(a.logPrefix+"Unable to retrieve the entity from the server.",e),a.__entityCacheRaw=null,a.scope.$emit("absyncError",e)}var a=this;if(e=e===!0,null===a.__entityCacheRaw||e)if(a.__entityCacheRaw=[],n.collectionName&&n.collectionUri)a.logInterface.info(a.logPrefix+"Retrieving '"+n.collectionName+"' collection…"),a.httpInterface.get(n.collectionUri).then(t,r);else{if(!n.entityName||!n.entityUri)return a.q.when([]);a.__entityCacheRaw={},a.httpInterface.get(n.entityUri).then(i,o)}return a.q.all([a.dataAvailable,a.objectsAvailable]).then(function(){return a.entityCache})},r.prototype.read=function(e,t){function r(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");var t=o.deserializer(e.data[n.entityName]);return o.__updateCacheWithEntity(t),t}function i(t){o.logInterface.error(o.logPrefix+"Unable to retrieve entity with ID '"+e+"' from the server.",t),o.scope.$emit("absyncError",t)}var o=this;if(t=t===!0,o.logInterface.debug(o.logPrefix+"Requesting entity '"+e+"' (forceReload:"+t+")…"),!t)for(var a=0,c=o.entityCache[0];a<o.entityCache.length;++a,c=o.entityCache[a])if(c.id===e)return o.logInterface.debug(o.logPrefix+"Requested entity '"+e+"' is served from cache."),o.q.when(c);return o.logInterface.debug(o.logPrefix+"Requested entity '"+e+"' is fetched from backend."),o.__requestEntity(e).then(r,i)},r.prototype.__requestEntity=function(e){function t(e,t){return delete r.__requestCache[e],t}var r=this;if(r.enableRequestCache&&r.__requestCache&&r.__requestCache[e])return r.logInterface.debug(r.logPrefix+"Entity request '"+e+"' served from request cache."),r.__requestCache[e];var i=n.entityUri+(e?"/"+e:""),o=r.httpInterface.get(i).then(t.bind(r,e));return r.enableRequestCache&&r.__requestCache&&(r.__requestCache[e]=o),o},r.prototype.update=function(e){function t(e){if(e.data[n.entityName]){var t=i.deserializer(e.data[n.entityName]);return i.forceEarlyCacheUpdate&&i.__updateCacheWithEntity(t),t}throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.")}function r(e){i.logInterface.error(i.logPrefix+"Unable to store entity on the server.",e),i.logInterface.error(e)}var i=this,o=i.reduceComplex(e),a=i.serializer(o),c={};return c[n.entityName]=a,"undefined"!=typeof e.id?i.httpInterface.put(n.entityUri+"/"+e.id,c).then(t,r):i.httpInterface.post(n.collectionUri,c).then(t,r)},r.prototype.create=r.prototype.update,r.prototype["delete"]=function(e){function t(e){return i.__removeEntityFromCache(o)}function r(e){throw i.logInterface.error(e.data),new Error("Unable to delete entity.")}var i=this,o=e.id;return i.httpInterface["delete"](n.entityUri+"/"+o).then(t)["catch"](r)},r.prototype.__updateCacheWithEntity=function(e){var t=this;if(t.logInterface.info(t.logPrefix+"Updating entity '"+e.id||t.name+"' in cache…",e),!Array.isArray(t.entityCache))return t.scope.$broadcast("beforeEntityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache,updated:e}),"function"==typeof t.entityCache.copyFrom?t.entityCache.copyFrom(e):angular.extend(t.entityCache,e),void t.scope.$broadcast("entityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache});for(var n=!1,r=0,i=t.entityCache[0];r<t.entityCache.length;++r,i=t.entityCache[r])if(i.id==e.id){t.scope.$broadcast("beforeEntityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache[r],updated:e});var o=t.entityCache[r];"function"==typeof o.copyFrom?o.copyFrom(e):angular.extend(o,e),n=!0,t.scope.$broadcast("entityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache[r]});break}n||(t.entityCache.push(e),t.scope.$broadcast("entityNew",{service:t,cache:t.entityCache,entity:e}))},r.prototype.__removeEntityFromCache=function(e){for(var t=this,n=0,r=t.entityCache[0];n<t.entityCache.length;++n,r=t.entityCache[n])if(r.id==e){t.scope.$broadcast("beforeEntityRemoved",{service:t,cache:t.entityCache,entity:r}),t.entityCache.splice(n,1),t.scope.$broadcast("entityRemoved",{service:t,cache:t.entityCache,entity:r});break}},r.prototype.lookupTableById=function(){for(var e=this,t=[],n=0;n<e.entityCache.length;++n)t[e.entityCache[n].id]=e.entityCache[n];return t},r.prototype.reduceComplex=function(e,t){var n=this,r=t?[]:{};for(var i in e)e.hasOwnProperty(i)&&(Array.isArray(e[i])?r[i]=n.reduceComplex(e[i],!0):e[i]&&e[i].id?r[i]=e[i].id:r[i]=e[i]);return r},r.prototype.populateComplex=function(e,t,n,r){function i(i,o){function c(n){e[t][o]=n}if("string"!=typeof e[t][o]){if(!r||"object"!=typeof e[t][o]||"string"!=typeof e[t][o].id)return a.q.when(!1);e[t][o]=e[t][o].id}return n.read(e[t][o]).then(c)}function o(n){e[t]=n}var a=this;if(Array.isArray(e[t])){var c=e[t].map(i);return a.q.all(c)}if("string"!=typeof e[t]){if(!r||"object"!=typeof e[t]||"string"!=typeof e[t].id)return a.q.when(!1);e[t]=e[t].id}return n.read(e[t]).then(o)},r}function t(e){return e}angular.module("absync").constant("absyncCache",e)}(),function(){"use strict";function e(){return t}function t(e,t,n,r,i,o,a,c){this.model=e,this.collectionUri=t,this.entityUri=n;var s=e.prototype.constructor.name.toLowerCase();this.collectionName=r||s+"s",this.entityName=i||s,this.deserialize=o||void 0,this.serialize=a||void 0,this.injector=c||void 0}angular.module("absync").service("AbsyncServiceConfiguration",e)}(); | ||
//# sourceMappingURL=maps/absync.concat.min.js.map |
@@ -8,2 +8,4 @@ var application = require( "./package.json" ); | ||
var jscs = require( "gulp-jscs" ); | ||
var jshint = require( "gulp-jshint" ); | ||
var jsValidate = require( "gulp-jsvalidate" ); | ||
var ngAnnotate = require( "gulp-ng-annotate" ); | ||
@@ -16,2 +18,3 @@ var order = require( "gulp-order" ); | ||
var sourcemaps = require( "gulp-sourcemaps" ); | ||
var stylish = require( "jshint-stylish" ); | ||
var uglify = require( "gulp-uglify" ); | ||
@@ -46,2 +49,3 @@ var vinylPaths = require( "vinyl-paths" ); | ||
.pipe( jscs.reporter() ) | ||
.pipe( jsValidate() ) | ||
.pipe( order() ) | ||
@@ -56,2 +60,5 @@ // Only pass through files that have changed since the last build iteration (relevant during "watch"). | ||
} ) ) | ||
.pipe( jsValidate() ) | ||
.pipe( jshint() ) | ||
.pipe( jshint.reporter( stylish ) ) | ||
// Put Angular dependency injection annotation where needed. | ||
@@ -58,0 +65,0 @@ .pipe( ngAnnotate() ) |
@@ -18,2 +18,3 @@ // Karma configuration | ||
files : [ | ||
"node_modules/phantomjs-polyfill/bind-polyfill.js", | ||
"node_modules/angular/angular.js", | ||
@@ -20,0 +21,0 @@ "node_modules/angular-mocks/angular-mocks.js", |
{ | ||
"name": "absync", | ||
"version": "2.0.0", | ||
"version": "2.2.0", | ||
"description": "absync", | ||
@@ -28,2 +28,4 @@ "main": "dist/development/absync.concat.js", | ||
"gulp-jscs": "~3.0.2", | ||
"gulp-jshint": "~1.11.2", | ||
"gulp-jsvalidate": "~2.0.0", | ||
"gulp-ng-annotate": "~1.1.0", | ||
@@ -37,2 +39,3 @@ "gulp-order": "~1.1.1", | ||
"jscs": "~2.5.1", | ||
"jshint-stylish": "~2.0.1", | ||
"karma": "~0.13.15", | ||
@@ -42,5 +45,6 @@ "karma-chai": "~0.1.0", | ||
"karma-mocha": "~0.2.1", | ||
"karma-phantomjs2-launcher": "~0.3.2", | ||
"karma-phantomjs-launcher": "~0.2.1", | ||
"mocha": "~2.3.4", | ||
"phantomjs2": "~2.0.1", | ||
"phantomjs": "~1.9.19", | ||
"phantomjs-polyfill": "~0.0.1", | ||
"slug": "~0.9.1", | ||
@@ -47,0 +51,0 @@ "vinyl-paths": "~2.1.0" |
![](doc/logo.png) | ||
[![Build Status](https://travis-ci.org/oliversalzburg/absync.svg?branch=master)](https://travis-ci.org/oliversalzburg/absync) | ||
## 2.0 Notice | ||
@@ -4,0 +6,0 @@ 2.0 no longer includes the backend data sanitation part that 1.0 provided. |
@@ -0,1 +1,3 @@ | ||
/* globals angular */ | ||
angular.module( "absync", [] ); |
@@ -0,1 +1,3 @@ | ||
/* globals angular, io */ | ||
/** | ||
@@ -50,2 +52,7 @@ * Please make note of the following conventions: | ||
self.__collections = {}; | ||
// The entities that absync provides. | ||
// The keys are the names of the entities, the value contains the constructor of | ||
// the respective cache service. | ||
self.__entities = {}; | ||
} | ||
@@ -102,3 +109,3 @@ | ||
// Collection names (and, thus service names) have to be unique. | ||
// Collection/entity names (and, thus service names) have to be unique. | ||
// We can't create multiple services with the same name. | ||
@@ -108,2 +115,5 @@ if( self.__collections[ name ] ) { | ||
} | ||
if( self.__entities[ name ] ) { | ||
throw new Error( "An entity with the name '" + name + "' was already requested. Names for collections must be unique and can't be shared with entities." ); | ||
} | ||
@@ -120,2 +130,29 @@ // Register the service configuration. | ||
/** | ||
* Request a new synchronized entity. | ||
* This only registers the intent to use that entity. It will be constructed when it is first used. | ||
* @param {String} name The name of the entity and service name. | ||
* @param {AbsyncServiceConfiguration|Object} configuration The configuration for this entity. | ||
*/ | ||
AbsyncProvider.prototype.entity = function AbsyncProvider$entity( name, configuration ) { | ||
var self = this; | ||
// Collection/entity names (and, thus service names) have to be unique. | ||
// We can't create multiple services with the same name. | ||
if( self.__entities[ name ] ) { | ||
throw new Error( "An entity with the name '" + name + "' was already requested. Names for entities must be unique." ); | ||
} | ||
if( self.__collections[ name ] ) { | ||
throw new Error( "A collection with the name '" + name + "' was already requested. Names for entities must be unique and can't be shared with collections." ); | ||
} | ||
// Register the service configuration. | ||
// __absyncCache will return a constructor for a service with the given configuration. | ||
self.__entities[ name ] = self.__absyncCache( name, configuration ); | ||
// Register the new service. | ||
// Yes, we want an Angular "service" here, because we want it constructed with "new". | ||
self.__provide.service( name, self.__entities[ name ] ); | ||
}; | ||
/** | ||
* Register the service factory. | ||
@@ -161,3 +198,3 @@ * @returns {AbsyncService} | ||
var _absyncProvider = this.__absyncProvider; | ||
var self = this; | ||
var self = this; | ||
@@ -164,0 +201,0 @@ // If we have no configured socket.io connection yet, remember to register it later. |
@@ -0,1 +1,3 @@ | ||
/* globals angular */ | ||
/** | ||
@@ -56,10 +58,15 @@ * Please make note of the following conventions: | ||
// The entity cache must be constructed as an empty array, to allow the user to place watchers on it. | ||
// We must never replace the cache with a new array, we must always manipulate the existing one. | ||
// The entity cache must be constructed as an empty array or object, to allow the user to place watchers on it. | ||
// We must never replace the cache with a new array or object, we must always manipulate the existing one. | ||
// Otherwise watchers will not behave as the user expects them to. | ||
/* @type {Array<configuration.model>} */ | ||
self.entityCache = []; | ||
/* @type {Array<configuration.model>|configuration.model} */ | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
// The raw cache is data that hasn't been deserialized and is used internally. | ||
self.__entityCacheRaw = null; | ||
// Should request caching be used at all? | ||
self.enableRequestCache = true; | ||
// Cache requests made to the backend to avoid multiple, simultaneous requests for the same resource. | ||
self.__requestCache = {}; | ||
// TODO: Using deferreds is an anti-pattern and probably provides no value here. | ||
@@ -136,3 +143,3 @@ self.__dataAvailableDeferred = $q.defer(); | ||
* Event handler for when the initial badge of raw data becomes available. | ||
* @param {Array<Object>} rawData | ||
* @param {Array<Object>|Object} rawData | ||
* @private | ||
@@ -143,18 +150,35 @@ */ | ||
// The symbol self.entityCache is expected to be an empty array. | ||
// We initialize it in the constructor to an empty array and we don't expect any writes to have | ||
// happened to it. In case writes *did* happen, we assume that whoever wrote to it knows what | ||
// they're doing. | ||
rawData[ configuration.collectionName ].forEach( deserializeCollectionEntry ); | ||
if( Array.isArray( self.entityCache ) ) { | ||
// The symbol self.entityCache is expected to be an empty array. | ||
// We initialize it in the constructor to an empty array and we don't expect any writes to have | ||
// happened to it. In case writes *did* happen, we assume that whoever wrote to it knows what | ||
// they're doing. | ||
rawData[ configuration.collectionName ].forEach( deserializeCollectionEntry ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Notify the rest of the application about a fresh collection. | ||
self.scope.$broadcast( "collectionNew", { | ||
service : self, | ||
cache : self.entityCache | ||
} ); | ||
// Notify the rest of the application about a fresh collection. | ||
self.scope.$broadcast( "collectionNew", { | ||
service : self, | ||
cache : self.entityCache | ||
} ); | ||
} else { | ||
var deserialized = self.deserializer( rawData[ configuration.entityName ] ); | ||
self.__updateCacheWithEntity( deserialized ); | ||
// Resolve our "objects are available" deferred. | ||
// TODO: We could just as well initialize objectAvailable to the return value of this call block. | ||
self.__objectsAvailableDeferred.resolve( self.entityCache ); | ||
// Notify the rest of the application about a fresh entity. | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : deserialized | ||
} ); | ||
} | ||
function deserializeCollectionEntry( rawEntity ) { | ||
@@ -172,3 +196,3 @@ self.entityCache.push( self.deserializer( rawEntity ) ); | ||
CacheService.prototype.__onEntityReceived = function CacheService$onEntityReceived( event, args ) { | ||
var self = this; | ||
var self = this; | ||
var _entityReceived = args; | ||
@@ -195,3 +219,3 @@ | ||
CacheService.prototype.__onCollectionReceived = function CacheService$onCollectionReceived( event, args ) { | ||
var self = this; | ||
var self = this; | ||
var _collectionReceived = args; | ||
@@ -227,12 +251,21 @@ | ||
// If the user did not provide information necessary to work with a collection, immediately return | ||
// a promise for an empty collection. The user could still use read() to grab individual entities. | ||
if( !configuration.collectionName || !configuration.collectionUri ) { | ||
return self.q.when( [] ); | ||
if( configuration.entityName && configuration.entityUri ) { | ||
self.__entityCacheRaw = {}; | ||
self.httpInterface | ||
.get( configuration.entityUri ) | ||
.then( onSingleEntityReceived, onSingleEntityRetrievalFailure ); | ||
} else { | ||
// If the user did not provide information necessary to work with a collection, immediately return | ||
// a promise for an empty collection. The user could still use read() to grab individual entities. | ||
return self.q.when( [] ); | ||
} | ||
} else { | ||
self.logInterface.info( self.logPrefix + "Retrieving '" + configuration.collectionName + "' collection…" ); | ||
self.httpInterface | ||
.get( configuration.collectionUri ) | ||
.then( onCollectionReceived, onCollectionRetrievalFailure ); | ||
} | ||
self.logInterface.info( self.logPrefix + "Retrieving '" + configuration.collectionName + "' collection…" ); | ||
self.httpInterface | ||
.get( configuration.collectionUri ) | ||
.then( onCollectionReceived, onCollectionRetrievalFailure ); | ||
} | ||
@@ -274,2 +307,26 @@ | ||
} | ||
/** | ||
* Invoked when the entity was received from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onSingleEntityReceived( serverResponse ) { | ||
if( !serverResponse.data[ configuration.entityName ] ) { | ||
throw new Error( "The response from the server was not in the expected format. It should have a member named '" + configuration.entityName + "'." ); | ||
} | ||
self.__entityCacheRaw = serverResponse.data; | ||
self.__dataAvailableDeferred.resolve( serverResponse.data ); | ||
} | ||
/** | ||
* Invoked when there was an error while trying to retrieve the entity from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onSingleEntityRetrievalFailure( serverResponse ) { | ||
self.logInterface.error( self.logPrefix + "Unable to retrieve the entity from the server.", | ||
serverResponse ); | ||
self.__entityCacheRaw = null; | ||
self.scope.$emit( "absyncError", serverResponse ); | ||
} | ||
}; | ||
@@ -289,2 +346,4 @@ | ||
self.logInterface.debug( self.logPrefix + "Requesting entity '" + id + "' (forceReload:" + forceReload + ")…" ); | ||
if( !forceReload ) { | ||
@@ -296,2 +355,3 @@ // Check if the entity is in the cache and return instantly if found. | ||
if( entity.id === id ) { | ||
self.logInterface.debug( self.logPrefix + "Requested entity '" + id + "' is served from cache." ); | ||
return self.q.when( entity ); | ||
@@ -302,5 +362,5 @@ } | ||
// Grab the entity from the backend. | ||
return self.httpInterface | ||
.get( configuration.entityUri + "/" + id ) | ||
self.logInterface.debug( self.logPrefix + "Requested entity '" + id + "' is fetched from backend." ); | ||
return self.__requestEntity( id ) | ||
.then( onEntityRetrieved, onEntityRetrievalFailure ); | ||
@@ -338,2 +398,35 @@ | ||
/** | ||
* Request an entity from the backend. | ||
* @param {String} id The ID of the entity. | ||
* @returns {Promise<configuration.model>|IPromise<TResult>|IPromise<void>} | ||
* @private | ||
*/ | ||
CacheService.prototype.__requestEntity = function CacheService$requestEntity( id ) { | ||
var self = this; | ||
if( self.enableRequestCache && self.__requestCache && self.__requestCache[ id ] ) { | ||
self.logInterface.debug( self.logPrefix + "Entity request '" + id + "' served from request cache." ); | ||
return self.__requestCache[ id ]; | ||
} | ||
var requestUri = configuration.entityUri + ( id ? ( "/" + id ) : "" ); | ||
// Grab the entity from the backend. | ||
var request = self.httpInterface | ||
.get( requestUri ) | ||
.then( remoteRequestFromCache.bind( self, id ) ); | ||
if( self.enableRequestCache && self.__requestCache ) { | ||
self.__requestCache[ id ] = request; | ||
} | ||
return request; | ||
function remoteRequestFromCache( id, serverResponse ) { | ||
delete self.__requestCache[ id ]; | ||
return serverResponse; | ||
} | ||
}; | ||
/** | ||
* Updates an entity and persists it to the backend and the cache. | ||
@@ -444,4 +537,32 @@ * @param {configuration.model} entity | ||
self.logInterface.info( self.logPrefix + "Updating entity in cache…" ); | ||
self.logInterface.info( self.logPrefix + "Updating entity '" + entityToCache.id || self.name + "' in cache…", | ||
entityToCache ); | ||
if( !Array.isArray( self.entityCache ) ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache, | ||
updated : entityToCache | ||
} ); | ||
if( typeof self.entityCache.copyFrom === "function" ) { | ||
self.entityCache.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( self.entityCache, entityToCache ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache | ||
} ); | ||
return; | ||
} | ||
var found = false; | ||
@@ -448,0 +569,0 @@ for( var entityIndex = 0, entity = self.entityCache[ 0 ]; |
@@ -0,1 +1,3 @@ | ||
/* globals angular */ | ||
angular | ||
@@ -2,0 +4,0 @@ .module( "absync" ) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
532998
2985
71
1
29