<commit_before>// tempart templating library

;(function(tempartCompiler) {
	////-----------------------------------------------------------------------------------------
	// you need to overwrite this function, to have working partial support
	tempartCompiler.partial = function(block, context, currentValues, dirties, path ){
		// @TODO do a better api for that..
		console.error('Overwrite the function tempartCompiler.partial to have partials');
		return '';
	};
	////-----------------------------------------------------------------------------------------
	// returns the html and adds context to currentValues
	// path is not used, it only is passed to partial
	tempartCompiler.compile = function( blocks, content, currentValues, dirties, path, prefix ){
		if(!currentValues) currentValues = {};
		if(!dirties) dirties = '*';
		if(!path) path = '/';
		if(!prefix) prefix = '';
		var local = {};
		return this._handleBlocks(blocks, content, local, currentValues, this._batchDirties( dirties ), path, prefix);
	};
	////-----------------------------------------------------------------------------------------
	// batches the dirties
	// @FIXME shouldn't be inside lib, should be given as this in the parameter
	tempartCompiler._batchDirties = function( dirties ){
		if( dirties === '*' ) return dirties;
		var result = {};
		var batchMap = {push: 'append', unshift: 'prepend', set: 'update'};
		// @TODO update / remove
		for( var key in dirties ){
			if( dirties.hasOwnProperty( key )){
				var dirty = dirties[ key ];
				result[key] = {append: [], prepend: [], update: []};
				for( var i = 0; i < dirty.length; i++ ){
					var entity = dirty[ i ];
					var type = batchMap[ entity.type ];
					if( type == 'update' ){
						result[ key ][ type ] = entity.value;
						break; // Update is alway a reference, prepend update are included
					} else {
						result[ key ][ type ].push( entity.value );
					}
				}
			}
		}
		return result;
	};

	////-----------------------------------------------------------------------------------------
	// iterates threw the block on one level and calls the type-handler
	tempartCompiler._handleBlocks = function( blocks, content, local, currentValues, dirties, path, prefix ){
		var result = "";
		for( var i = 0; i < blocks.length; i++ ){
			result += this._handleBlock( blocks[i], content, local, currentValues, dirties, path, prefix );
		}
		return result;
	};
	////-----------------------------------------------------------------------------------------
	// returns the compiled block, depending on the inserted data
	tempartCompiler._handleBlock = function( block, content, local, currentValues, dirties, path, prefix ){
		var detailPrefix = prefix + this.types.executes.options.prefixDelimiter + block.id;
		if(block.type === 'log' || block.type === 'bindAttr' ){ // No need for pre and suffix at log/attribute-bindings
			if( dirties === '*' ){
				return this.types[block.type]( block, content, local, currentValues, dirties, path, prefix );
			} else {
				return this.types.dirties[block.type]( block, content, local, currentValues, dirties, path, prefix );
			}
		} else {
			if( dirties === '*' ){
				return this.types.executes.prefix( detailPrefix ) + this.types[ block.type ]( block, content, local, currentValues, dirties, path, prefix ) + this.types.executes.suffix( detailPrefix );
			} else {
				return this.types.dirties[ block.type ]( block, content, local, currentValues, dirties, path, prefix );
			}
		}
	};

	////-----------------------------------------------------------------------------------------
	// Contains the possible handlers
	tempartCompiler.types = {
		////-----------------------------------------------------------------------------------------
		// checks if a variable changed and update its attribute
		bindAttr: function( block, content, local, currentValues, dirties, path, prefix ){
			var result = block.content;
			result += this.executes.options.attrStart + '="' +prefix + this.executes.options.prefixDelimiter + block.id + '"';
			// @TODO is it a good idea to add attrEnd?
			// result += this.executes.options.attrEnd   + '="' +prefix + this.executes.options.prefixDelimiter + block.id + '"';
			currentValues[ block.id ] = {};
			for( var i = 0; i < block.order.length; i++ ){
				result += block.order[ i ] + '="';
				var depending = block.contains[ i ];
				result += this[depending.type]( depending, content, local, currentValues[ block.id ], dirties, path, prefix ) + '" ';
			}
			result += '>';
			return result;
		},
		////-----------------------------------------------------------------------------------------
		// returns a variable
		variable: function( block, content, local, currentValues, dirties ){
			var value = this.executes.get(block.depending[ 0 ], content, local );
			currentValues[ block.id ] = value;
			return value;
		},
		////-----------------------------------------------------------------------------------------
		// returns html
		echo: function( block, content, local, currentValues, dirties ){
			return block.content;
		},
		////-----------------------------------------------------------------------------------------
		// calls handleBlocks depending on the data contributed
		if: function( block, content, local, currentValues, dirties, path, prefix ) {
			var type = this.executes.condition( block.depending[ 0 ], content, local );
			currentValues[ block.id ] = {type: type, contains: {}};
			prefix += this.executes.options.prefixDelimiter + block.id;

			return tempartCompiler._handleBlocks(block[type], content, local, currentValues[block.id].contains, dirties, path, prefix);
		},
		////-----------------------------------------------------------------------------------------
		// sets a variable of an array and calls handleBlocks in the containing level
		each: function( block, content, local, currentValues, dirties, path, prefix ) {
			var value = this.executes.get(block.depending[ 0 ], content, local );
			currentValues[block.id] = {values: {}, order: []};

			if( value && value.length ){
				var result = "";
				for(var i = 0; i < value.length; i++) {
					var rand = this.executes.random();
					currentValues[ block.id ].values[ rand ] = {};
					currentValues[block.id].order.push( rand );
					var detailPrefix = prefix + this.executes.options.prefixDelimiter + block.id + ':' + rand;
					this.executes.set( block.depending[2], value[i], local );
					result += tempartCompiler._handleBlocks( block.contains, content, local, currentValues[ block.id ].values[ rand ], dirties, path, detailPrefix );
				}
				return result;
			} else {
				var elsePrefix = prefix + this.executes.options.prefixDelimiter + block.id;
				return tempartCompiler._handleBlocks( block.elseContains, content, local, currentValues, dirties, path, elsePrefix);
			}
		},
		////-----------------------------------------------------------------------------------------
		// puts values in the console
		log: function(block, content, local) {
			console.log( this.executes.get(block.depending[ 0 ], content, local ));
			return "";
		},
		////-----------------------------------------------------------------------------------------
		// patial handler, for rewriting contexts and stuff
		partial: function( block, content, local, currentValues, dirties, path ) {
			if(block.path !== '/') {
				path = path + '/' + block.path;
			}
			return tempartCompiler.partial(block, content, currentValues, dirties, path);
		},
		executes: {
			////-----------------------------------------------------------------------------------------
			// Executables are in need of options too!
			options: {
				domNode:   'script',
				attrStart: 'tempartStart',
				attrEnd: 'tempartEnd',
				prefixDelimiter: '-'
			},
			////-----------------------------------------------------------------------------------------
			// Checks if something is in the local space
			get: function( key, global, local ){
				if( local.hasOwnProperty( key )){
					return local[ key ];
				} else {
					return this._get( key.split( '.' ), global );
				}
			},
			////-----------------------------------------------------------------------------------------
			// handles dotnotation for getting values
			_get: function( keyParts, global) {
				var firstKey = keyParts.shift();
				if(!keyParts.length) {
					return global[ firstKey ];
				} else {
					return this._get( keyParts, global[ firstKey ] );
				}
			},
			////-----------------------------------------------------------------------------------------
			// sets things in the local space, we dont want to manipulate the original object
			set: function( key, value, scope ){
				scope[ key ] = value;
			},
			////-----------------------------------------------------------------------------------------
			// Removes some values from the local place
			unset: function( key, scope ){
				delete scope[key];
			},
			////-----------------------------------------------------------------------------------------
			// Generates start-element of an block
			prefix: function( prefix ){
				return '<' +this.options.domNode+ ' ' +this.options.attrStart+ '="' +prefix + '"></' +this.options.domNode+ '>';
			},
			////-----------------------------------------------------------------------------------------
			// Generates end-element of an block
			suffix: function( suffix ){
				return '<' +this.options.domNode+ ' ' +this.options.attrEnd+ '="' +suffix + '"></' +this.options.domNode+ '>';
			},
			////-----------------------------------------------------------------------------------------
			// Generates a random string (needed for length changing #each)
			random: function() {
				return Math.random().toString(36).substring(7);
			},
			////-----------------------------------------------------------------------------------------
			// decides what type will be used
			condition: function( key, global, local ){
				if(this.get( key, global, local )) {
					return 'contains';
				} else {
					return 'elseContains';
				}
			}
		},
		////-----------------------------------------------------------------------------------------
		// Special implementation of each lazy-block
		dirties: {
			////-----------------------------------------------------------------------------------------
			// only changed values should be iterated, or when in contains dependings something changed
			each: function( block, content, local, currentValues, dirties, path, prefix ){
				var previous = currentValues[ block.id ];
				var key = block.depending[ 0 ];
				if( dirties[ key ] !== undefined ){
					var types = ['update', 'prepend', 'append'];
					for( var i = 0; i < types.length; i++ ){
						var type  = types[ i ];
						var value = dirties[ key ][ type ];
						if( value.length ){
							tempartCompiler.types.executes.set( key, value, local );
							var tmpCurrentValues = {};
							// @FIXME overwriting result is not possible in server side
							var result = tempartCompiler.types[ block.type ]( block, content, local, tmpCurrentValues, '*', path, prefix );
							if( type === 'update' ){
								currentValues[ block.id ] = tmpCurrentValues[ block.id ];
							} else {
								for( var orderIndex = 0; orderIndex < tmpCurrentValues[ block.id ].order.length; orderIndex++ ) {
									var rand = null;
									if( type === 'append' ){
										rand =  tmpCurrentValues[ block.id ].order[ orderIndex ];
										currentValues[ block.id ].order.push( rand );
									} else if( type === 'prepend' ){
										var order = tmpCurrentValues[ block.id ].order;
										rand =  order[ order.length - orderIndex - 1];
										currentValues[ block.id ].order.unshift( rand );
									} else {
										throw 'Unknown type!';
									}
									currentValues[ block.id ].values[ rand ] = tmpCurrentValues[ block.id ].values[ rand ];
								}
							}
							tempartCompiler.dom[ type ]( prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, result );
							tempartCompiler.types.executes.unset( key, local ); // @FIXME is this actually needed?
						}
					}
				} else {
					// @TODO searchblocks
					throw "Not yet implemented";
				}

			},
			////-----------------------------------------------------------------------------------------
			// checks if a variable changed and update its attribute
			bindAttr: function( block, content, local, currentValues, dirties, path, prefix ){
				// throw 'Not yet implemented';
			},
			////-----------------------------------------------------------------------------------------
			// checks if a variable changed
			variable: function( block, content, local, currentValues, dirties, path, prefix ){
				var previous = currentValues[ block.id ];
				var now      = tempartCompiler.types[ block.type ]( block, content, local, currentValues, dirties );
				if( previous != now ){
					tempartCompiler.dom.update( prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, now );
				}
			},
			////-----------------------------------------------------------------------------------------
			// an echo of a constant never changes
			echo: function() {},
			////-----------------------------------------------------------------------------------------
			// only updates when something is different, or an contains depending changed
			// @TODO
			if: function( block, content, local, currentValues, dirties, path, prefix ) {
				var type = tempartCompiler.types.executes.condition( block.depending[ 0 ], content, local );

				if( currentValues[ block.id ].type == type ){
					currentValues[ block.id ] = {type: type, contains: {}};
					prefix += tempartCompiler.types.executes.options.prefixDelimiter + block.id;
					return tempartCompiler._handleBlocks( block[ type ], content, local, currentValues[ block.id ].contains, dirties, path, prefix);
				} else {
					currentValues[ block.id ].type = type;
					var now = tempartCompiler._handleBlocks( block[ type ], content, local, currentValues[ block.id ].contains, '*', path, prefix);
					tempartCompiler.dom.update( prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, now );
				}

			},
			////-----------------------------------------------------------------------------------------
			// I actually don't know what exactly will happen here, some dependencieneeds to be done...
			// @TODO
			partial: function() {

			}
		},
	};

	tempartCompiler.dom = {
		////-----------------------------------------------------------------------------------------
		// Puts the things where they belong
		prepend: function( id, html ){
			this.obj( id, tempartCompiler.types.executes.options.attrStart).insertAdjacentHTML( 'afterend', html );
		},
		////-----------------------------------------------------------------------------------------
		// Puts the things where they belong
		append: function( id, html ){
			this.obj( id, tempartCompiler.types.executes.options.attrEnd).insertAdjacentHTML( 'beforebegin', html );
		},
		////-----------------------------------------------------------------------------------------
		// Realises nextUntil and its replacement
		update: function( id, html ) {
			var first = this.obj( id, tempartCompiler.types.executes.options.attrStart );
			var end   = this.obj( id, tempartCompiler.types.executes.options.attrEnd );
			while( first.nextSibling != end ) {
				first.nextSibling.remove(); // @FIXME I want to batch that, but how?
			}
			first.insertAdjacentHTML( 'beforebegin', html );
		},
		////-----------------------------------------------------------------------------------------
		// Searches for the right objects
		obj: function( id, attr ){
			var objs = document.querySelectorAll( '[' + attr + ']' );
			for(var i = 0; i < objs.length; i++) {
				var obj = objs[ i ];
				if( obj.getAttribute( attr ) === id ){
					return obj;
				}
			}
		}
	};
}(typeof module == 'object' ? module.exports : window.tempartCompiler= {}));
<commit_msg>[bindAttr] it freaking works with dirties<commit_after>// tempart templating library

;(function(tempartCompiler) {
	////-----------------------------------------------------------------------------------------
	// you need to overwrite this function, to have working partial support
	tempartCompiler.partial = function(block, context, currentValues, dirties, path ){
		// @TODO do a better api for that..
		console.error('Overwrite the function tempartCompiler.partial to have partials');
		return '';
	};
	////-----------------------------------------------------------------------------------------
	// returns the html and adds context to currentValues
	// path is not used, it only is passed to partial
	tempartCompiler.compile = function( blocks, content, currentValues, dirties, path, prefix ){
		if(!currentValues) currentValues = {};
		if(!dirties) dirties = '*';
		if(!path) path = '/';
		if(!prefix) prefix = '';
		var local = {};
		return this._handleBlocks(blocks, content, local, currentValues, this._batchDirties( dirties ), path, prefix);
	};
	////-----------------------------------------------------------------------------------------
	// batches the dirties
	// @FIXME shouldn't be inside lib, should be given as this in the parameter
	tempartCompiler._batchDirties = function( dirties ){
		if( dirties === '*' ) return dirties;
		var result = {};
		var batchMap = {push: 'append', unshift: 'prepend', set: 'update'};
		// @TODO update / remove
		for( var key in dirties ){
			if( dirties.hasOwnProperty( key )){
				var dirty = dirties[ key ];
				result[key] = {append: [], prepend: [], update: []};
				for( var i = 0; i < dirty.length; i++ ){
					var entity = dirty[ i ];
					var type = batchMap[ entity.type ];
					if( type == 'update' ){
						result[ key ][ type ] = entity.value;
						break; // Update is alway a reference, prepend update are included
					} else {
						result[ key ][ type ].push( entity.value );
					}
				}
			}
		}
		return result;
	};

	////-----------------------------------------------------------------------------------------
	// iterates threw the block on one level and calls the type-handler
	tempartCompiler._handleBlocks = function( blocks, content, local, currentValues, dirties, path, prefix ){
		var result = "";
		for( var i = 0; i < blocks.length; i++ ){
			result += this._handleBlock( blocks[i], content, local, currentValues, dirties, path, prefix );
		}
		return result;
	};
	////-----------------------------------------------------------------------------------------
	// returns the compiled block, depending on the inserted data
	tempartCompiler._handleBlock = function( block, content, local, currentValues, dirties, path, prefix ){
		var detailPrefix = prefix + this.types.executes.options.prefixDelimiter + block.id;
		if(block.type === 'log' || block.type === 'bindAttr' ){ // No need for pre and suffix at log/attribute-bindings
			if( dirties === '*' ){
				return this.types[block.type]( block, content, local, currentValues, dirties, path, prefix );
			} else {
				return this.types.dirties[block.type]( block, content, local, currentValues, dirties, path, prefix );
			}
		} else {
			if( dirties === '*' ){
				return this.types.executes.prefix( detailPrefix ) + this.types[ block.type ]( block, content, local, currentValues, dirties, path, prefix ) + this.types.executes.suffix( detailPrefix );
			} else {
				return this.types.dirties[ block.type ]( block, content, local, currentValues, dirties, path, prefix );
			}
		}
	};

	////-----------------------------------------------------------------------------------------
	// Contains the possible handlers
	tempartCompiler.types = {
		////-----------------------------------------------------------------------------------------
		// checks if a variable changed and update its attribute
		bindAttr: function( block, content, local, currentValues, dirties, path, prefix ){
			var result = block.content;
			result += this.executes.options.attrStart + '="' +prefix + this.executes.options.prefixDelimiter + block.id + '"';
			// @TODO is it a good idea to add attrEnd?
			// result += this.executes.options.attrEnd   + '="' +prefix + this.executes.options.prefixDelimiter + block.id + '"';
			currentValues[ block.id ] = {};
			for( var i = 0; i < block.order.length; i++ ){
				result += block.order[ i ] + '="';
				var dependingBlock = block.contains[ i ];
				result += this[dependingBlock.type]( dependingBlock, content, local, currentValues[ block.id ], dirties, path, prefix ) + '" ';
			}
			result += '>';
			return result;
		},
		////-----------------------------------------------------------------------------------------
		// returns a variable
		variable: function( block, content, local, currentValues, dirties ){
			var value = this.executes.get(block.depending[ 0 ], content, local );
			currentValues[ block.id ] = value;
			return value;
		},
		////-----------------------------------------------------------------------------------------
		// returns html
		echo: function( block, content, local, currentValues, dirties ){
			return block.content;
		},
		////-----------------------------------------------------------------------------------------
		// calls handleBlocks depending on the data contributed
		if: function( block, content, local, currentValues, dirties, path, prefix ) {
			var type = this.executes.condition( block.depending[ 0 ], content, local );
			currentValues[ block.id ] = {type: type, contains: {}};
			prefix += this.executes.options.prefixDelimiter + block.id;

			return tempartCompiler._handleBlocks(block[type], content, local, currentValues[block.id].contains, dirties, path, prefix);
		},
		////-----------------------------------------------------------------------------------------
		// sets a variable of an array and calls handleBlocks in the containing level
		each: function( block, content, local, currentValues, dirties, path, prefix ) {
			var value = this.executes.get(block.depending[ 0 ], content, local );
			currentValues[block.id] = {values: {}, order: []};

			if( value && value.length ){
				var result = "";
				for(var i = 0; i < value.length; i++) {
					var rand = this.executes.random();
					currentValues[ block.id ].values[ rand ] = {};
					currentValues[block.id].order.push( rand );
					var detailPrefix = prefix + this.executes.options.prefixDelimiter + block.id + ':' + rand;
					this.executes.set( block.depending[2], value[i], local );
					result += tempartCompiler._handleBlocks( block.contains, content, local, currentValues[ block.id ].values[ rand ], dirties, path, detailPrefix );
				}
				return result;
			} else {
				var elsePrefix = prefix + this.executes.options.prefixDelimiter + block.id;
				return tempartCompiler._handleBlocks( block.elseContains, content, local, currentValues, dirties, path, elsePrefix);
			}
		},
		////-----------------------------------------------------------------------------------------
		// puts values in the console
		log: function(block, content, local) {
			console.log( this.executes.get(block.depending[ 0 ], content, local ));
			return "";
		},
		////-----------------------------------------------------------------------------------------
		// patial handler, for rewriting contexts and stuff
		partial: function( block, content, local, currentValues, dirties, path ) {
			if(block.path !== '/') {
				path = path + '/' + block.path;
			}
			return tempartCompiler.partial(block, content, currentValues, dirties, path);
		},
		executes: {
			////-----------------------------------------------------------------------------------------
			// Executables are in need of options too!
			options: {
				domNode:   'script',
				attrStart: 'tempartStart',
				attrEnd: 'tempartEnd',
				prefixDelimiter: '-'
			},
			////-----------------------------------------------------------------------------------------
			// Checks if something is in the local space
			get: function( key, global, local ){
				if( local.hasOwnProperty( key )){
					return local[ key ];
				} else {
					return this._get( key.split( '.' ), global );
				}
			},
			////-----------------------------------------------------------------------------------------
			// handles dotnotation for getting values
			_get: function( keyParts, global) {
				var firstKey = keyParts.shift();
				if(!keyParts.length) {
					return global[ firstKey ];
				} else {
					return this._get( keyParts, global[ firstKey ] );
				}
			},
			////-----------------------------------------------------------------------------------------
			// sets things in the local space, we dont want to manipulate the original object
			set: function( key, value, scope ){
				scope[ key ] = value;
			},
			////-----------------------------------------------------------------------------------------
			// Removes some values from the local place
			unset: function( key, scope ){
				delete scope[key];
			},
			////-----------------------------------------------------------------------------------------
			// Generates start-element of an block
			prefix: function( prefix ){
				return '<' +this.options.domNode+ ' ' +this.options.attrStart+ '="' +prefix + '"></' +this.options.domNode+ '>';
			},
			////-----------------------------------------------------------------------------------------
			// Generates end-element of an block
			suffix: function( suffix ){
				return '<' +this.options.domNode+ ' ' +this.options.attrEnd+ '="' +suffix + '"></' +this.options.domNode+ '>';
			},
			////-----------------------------------------------------------------------------------------
			// Generates a random string (needed for length changing #each)
			random: function() {
				return Math.random().toString(36).substring(7);
			},
			////-----------------------------------------------------------------------------------------
			// decides what type will be used
			condition: function( key, global, local ){
				if(this.get( key, global, local )) {
					return 'contains';
				} else {
					return 'elseContains';
				}
			}
		},
		////-----------------------------------------------------------------------------------------
		// Special implementation of each lazy-block
		dirties: {
			////-----------------------------------------------------------------------------------------
			// only changed values should be iterated, or when in contains dependings something changed
			each: function( block, content, local, currentValues, dirties, path, prefix ){
				var previous = currentValues[ block.id ];
				var key = block.depending[ 0 ];
				if( dirties[ key ] !== undefined ){
					var types = ['update', 'prepend', 'append'];
					for( var i = 0; i < types.length; i++ ){
						var type  = types[ i ];
						var value = dirties[ key ][ type ];
						if( value.length ){
							tempartCompiler.types.executes.set( key, value, local );
							var tmpCurrentValues = {};
							// @FIXME overwriting result is not possible in server side
							var result = tempartCompiler.types[ block.type ]( block, content, local, tmpCurrentValues, '*', path, prefix );
							if( type === 'update' ){
								currentValues[ block.id ] = tmpCurrentValues[ block.id ];
							} else {
								for( var orderIndex = 0; orderIndex < tmpCurrentValues[ block.id ].order.length; orderIndex++ ) {
									var rand = null;
									if( type === 'append' ){
										rand =  tmpCurrentValues[ block.id ].order[ orderIndex ];
										currentValues[ block.id ].order.push( rand );
									} else if( type === 'prepend' ){
										var order = tmpCurrentValues[ block.id ].order;
										rand =  order[ order.length - orderIndex - 1];
										currentValues[ block.id ].order.unshift( rand );
									} else {
										throw 'Unknown type!';
									}
									currentValues[ block.id ].values[ rand ] = tmpCurrentValues[ block.id ].values[ rand ];
								}
							}
							tempartCompiler.dom[ type ]( prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, result );
							tempartCompiler.types.executes.unset( key, local ); // @FIXME is this actually needed?
						}
					}
				} else {
					// @TODO searchblocks
					throw "Not yet implemented";
				}

			},
			////-----------------------------------------------------------------------------------------
			// checks if a variable changed and update its attribute
			bindAttr: function( block, content, local, currentValues, dirties, path, prefix ){
				// throw 'Not yet implemented';
				for(var i = 0; i < block.order.length; i++) {
					var attribute = block.order[ i ];
					var dependingBlock = block.contains[ i ];
					for( var dependingIndex = 0; dependingIndex < dependingBlock.depending.length; dependingIndex++ ){
						var depending = dependingBlock.depending[dependingIndex];
						if(dirties[depending]) {
							var oldValue = currentValues[ block.id ][ dependingBlock.id ];
							var newValue = tempartCompiler.types[dependingBlock.type]( dependingBlock, content, local, currentValues[ block.id ], dirties, path, prefix );
							if(oldValue != newValue) {
								tempartCompiler.dom.updateAttribute(prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, attribute, newValue);
							}
						}
					}
				}
			},
			////-----------------------------------------------------------------------------------------
			// checks if a variable changed
			variable: function( block, content, local, currentValues, dirties, path, prefix ){
				var previous = currentValues[ block.id ];
				var now      = tempartCompiler.types[ block.type ]( block, content, local, currentValues, dirties );
				if( previous != now ){
					tempartCompiler.dom.update( prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, now );
				}
			},
			////-----------------------------------------------------------------------------------------
			// an echo of a constant never changes
			echo: function() {},
			////-----------------------------------------------------------------------------------------
			// only updates when something is different, or an contains depending changed
			// @TODO
			if: function( block, content, local, currentValues, dirties, path, prefix ) {
				var type = tempartCompiler.types.executes.condition( block.depending[ 0 ], content, local );

				if( currentValues[ block.id ].type == type ){
					currentValues[ block.id ] = {type: type, contains: {}};
					prefix += tempartCompiler.types.executes.options.prefixDelimiter + block.id;
					return tempartCompiler._handleBlocks( block[ type ], content, local, currentValues[ block.id ].contains, dirties, path, prefix);
				} else {
					currentValues[ block.id ].type = type;
					var now = tempartCompiler._handleBlocks( block[ type ], content, local, currentValues[ block.id ].contains, '*', path, prefix);
					tempartCompiler.dom.update( prefix + tempartCompiler.types.executes.options.prefixDelimiter + block.id, now );
				}

			},
			////-----------------------------------------------------------------------------------------
			// I actually don't know what exactly will happen here, some dependencieneeds to be done...
			// @TODO
			partial: function() {

			}
		},
	};

	tempartCompiler.dom = {
		////-----------------------------------------------------------------------------------------
		// Puts the things where they belong
		prepend: function( id, html ){
			this.obj( id, tempartCompiler.types.executes.options.attrStart).insertAdjacentHTML( 'afterend', html );
		},
		////-----------------------------------------------------------------------------------------
		// Puts the things where they belong
		append: function( id, html ){
			this.obj( id, tempartCompiler.types.executes.options.attrEnd).insertAdjacentHTML( 'beforebegin', html );
		},
		////-----------------------------------------------------------------------------------------
		// Realises nextUntil and its replacement
		update: function( id, html ) {
			var first = this.obj( id, tempartCompiler.types.executes.options.attrStart );
			var end   = this.obj( id, tempartCompiler.types.executes.options.attrEnd );
			while( first.nextSibling != end ) {
				first.nextSibling.remove(); // @FIXME I want to batch that, but how?
			}
			first.insertAdjacentHTML( 'beforebegin', html );
		},
		////-----------------------------------------------------------------------------------------
		// Updates only the attributes of an dom element
		updateAttribute: function( id, attribute, value ){
			var obj = this.obj( id, tempartCompiler.types.executes.options.attrStart);
			if( attribute === 'value' ){
				if( obj[attribute] != value ){
					obj[attribute] = value;
				}
			} else {
				if( obj.getAttribute(attribute) != value ){
					obj.setAttribute(attribute, value);
				}
			}
		},
		////-----------------------------------------------------------------------------------------
		// Searches for the right objects
		obj: function( id, attr ){
			var objs = document.querySelectorAll( '[' + attr + ']' );
			for(var i = 0; i < objs.length; i++) {
				var obj = objs[ i ];
				if( obj.getAttribute( attr ) === id ){
					return obj;
				}
			}
		}
	};
}(typeof module == 'object' ? module.exports : window.tempartCompiler= {}));
<|endoftext|>


<commit_before>var should    = require('should')
  , maxminded = require('../maxminded.js')
  , fs        = require('fs')
  , geofile   = '/tmp/GeoLiteCity.dat'
;

if (fs.existsSync(geofile)) {
    fs.unlinkSync(geofile);
}

describe('CloudFlare', function() {
    describe('successful USA lookup', function() {

        maxminded.init();
        it("latitude should be 38", function(){
            var header = {'cf-ipcountry':'US'};
            var geo = maxminded.getLocation('66.6.44.4', header);
            should.exist(geo);
            geo.latitude.should.equal(38);
        });

    });

});<commit_msg>clean up<commit_after>var should    = require('should')
  , maxminded = require('../maxminded.js')
  , fs        = require('fs')
  , geofile   = '/tmp/GeoLiteCity.dat'
;

if (fs.existsSync(geofile)) {
    fs.unlinkSync(geofile);
}

describe('CloudFlare', function() {
    describe('successful USA lookup', function() {

        maxminded.init();
        it("latitude should be 38", function(){
            var header = {'cf-ipcountry':'US'};
            var geo = maxminded.getLocation('66.6.44.4', header);

            should.exist(geo);
            should.exist(geo.latitude);
            geo['latitude'].should.equal(38);
        });

    });

});<|endoftext|>


<commit_before>var async = require('async');
var assert = require('assert');
var sbuff = require('simple-bufferstream');
var S3rver = require('s3rver');
var fs = require('fs');
var shortid = require('shortid');
var through = require('through2');
var metricDebug = require('debug')('metrics');
var S3Storage = require('../lib/s3');

require('dash-assert');

describe('S3Storage', function() {
  var self;

  var port = 9991;
  var bucket = '4front-deployments';

  before(function(done) {
    this.timeout(5000);
    self = this;
    var s3rver = new S3rver();

    var fakeS3Dir = '/tmp/s3rver';
    async.series([
      function(cb) {
        // Make sure the fakeS3Dir exists
        fs.mkdir(fakeS3Dir, function(err) {
          if (err && err.code !== 'EEXIST') return cb(err);

          cb();
        });
      },
      function(cb) {
        // Start the fake S3 server
        s3rver.setHostname('localhost')
          .setPort(port)
          .setDirectory(fakeS3Dir)
          .setSilent(true)
          .run(cb);
      }
    ], done);
  });

  beforeEach(function(done) {
    this.options = {
      bucket: bucket,
      accessKeyId: '123',
      secretAccessKey: 'abc',
      endpoint: 'localhost:' + port,
      sslEnabled: false,
      s3ForcePathStyle: true,
      metrics: {
        timing: function(key, ms) {
          metricDebug('%s - %s ms', key, Math.round(ms * 100) / 100);
        },
        increment: function(key) {
          metricDebug('increment %s', key);
        }
      }
    };

    this.s3Storage = new S3Storage(this.options);

    // Ensure the bucket exists
    var bucketParams = {ACL: 'public-read', Bucket: this.options.bucket};
    this.s3Storage._s3.createBucket(bucketParams, function(err) {
      if (err && err.code !== 'BucketAlreadyExists') return done(err);

      done();
    });
  });

  it('writeFile', function(done) {
    var fileContents = 'text file contents';

    var fileInfo = {
      path: 'appid/versionid/files/plain.txt',
      contents: sbuff(fileContents),
      size: fileContents.length
    };

    async.series([
      function(cb) {
        self.s3Storage.writeFile(fileInfo, cb);
      },
      function(cb) {
        self.s3Storage.fileExists(fileInfo.path, function(err, exists) {
          if (err) return cb(err);

          assert.isTrue(exists);
          cb();
        });
      },
      function(cb) {
        var output = '';
        self.s3Storage.readFileStream(fileInfo.path)
          .on('data', function(chunk) {
            output += chunk.toString();
          })
          .on('error', function(err) {
            return cb(err);
          })
          .on('end', function() {
            assert.equal(output, fileContents);
            cb();
          });
      }
    ], done);
  });

  describe('readFile', function() {
    it('existing file', function(done) {
      var fileContents = 'text file contents';

      var fileInfo = {
        path: 'appid/versionid/files/' + shortid.generate() + '.txt',
        contents: sbuff(fileContents),
        size: fileContents.length
      };

      async.series([
        function(cb) {
          self.s3Storage.writeFile(fileInfo, cb);
        },
        function(cb) {
          self.s3Storage.readFile(fileInfo.path, function(err, data) {
            if (err) return cb(err);

            assert.equal(data, fileContents);
            cb();
          });
        }
      ], done);
    });

    it('missing file', function(done) {
      self.s3Storage.readFile('directory/missingfile.txt', function(err, data) {
        if (err) return done(err);

        assert.isNull(data);
        done();
      });
    });
  });

  // The S3 mock doesn't support the upload function
  // it('writeStream', function(done) {
  //   var fileContents = "text file contents";
  //
  //   var fileInfo = {
  //     path: "appid/versionid/files/plain.txt",
  //     contents: sbuff(fileContents)
  //   };
  //
  //   async.series([
  //     function(cb) {
  //       self.s3Storage.writeStream(fileInfo, cb);
  //     },
  //     function(cb) {
  //       self.s3Storage.readFile(fileInfo.path, function(err, data) {
  //         assert.equal(data, fileContents);
  //       });
  //     }
  //   ], done);
  // });

  it('fileExists returns false for missing file', function(done) {
    self.s3Storage.fileExists('dir/missingfile.txt', function(err, exists) {
      if (err) return done(err);

      assert.isFalse(exists);
      done();
    });
  });

  describe('readFileStream', function() {
    beforeEach(function(done) {
      self = this;
      this.fileContents = 'asasfasdfasdf';
      // this.filePath =
      this.fileInfo = {
        path: 'appid/versionid/files/' + shortid.generate() + '.txt',
        contents: sbuff(this.fileContents),
        size: this.fileContents.length,
        gzipEncoded: true,
        maxAge: 10000
      };

      this.s3Storage.writeFile(this.fileInfo, done);
    });

    it('metadata event emitted', function(done) {
      var output = '';
      this.s3Storage.readFileStream(this.fileInfo.path)
        .on('metadata', function(metadata) {
          assert.equal(metadata.contentType, 'text/plain; charset=utf-8');
          // assert.equal(metadata['cache-control'], 'max-age=' + self.fileInfo.maxAge);
        })
        .on('error', done)
        .pipe(through(function(chunk, enc, cb) {
          output += chunk.toString();
          cb();
        }, function() {
          assert.equal(output, self.fileContents);
          done();
        }));
    });

    it('read missing file', function(done) {
      self.s3Storage.readFileStream('directory/missingfile.txt')
        .on('missing', function(err) {
          assert.equal(err.code, 'fileNotFound');
          done();
        })
        .on('end', function() {
          assert.fail('error event should have fired');
        });
    });
  });

  it('listFiles()', function(done) {
    var prefix = 'dir1/dir2';

    var files = [prefix + '/js/main.js', prefix + '/css/styles.css', prefix + '/index.html'];
    async.series([
      function(cb) {
        deployTestFiles(self.s3Storage, files, cb);
      },
      function(cb) {
        self.s3Storage.listFiles(prefix, function(err, data) {
          if (err) return cb(err);

          assert.noDifferences(files, data);
          cb();
        });
      }
    ], done);
  });

  it('deleteFiles()', function(done) {
    var prefix = 'dir1';
    var files = [prefix + 'js/main.js', prefix + 'css/styles.css'];

    async.series([
      function(cb) {
        // Upload some files
        deployTestFiles(self.s3Storage, files, cb);
      },
      function(cb) {
        // Delete the version
        self.s3Storage.deleteFiles(prefix, cb);
      },
      function(cb) {
        // Verify that the files are gone.
        async.each(files, function(filePath, cb1) {
          self.s3Storage.fileExists(filePath, function(err, exists) {
            if (err) return cb1(err);

            assert.isFalse(exists);
            cb1();
          });
        }, cb);
      }
    ], done);
  });

  describe('getMetadata()', function() {
    it('existing file', function(done) {
      var contents = 'text file contents';
      var fileInfo = {
        path: 'appid/versionid/files/plain.txt',
        contents: sbuff(contents),
        size: contents.length
      };

      self.s3Storage.writeFile(fileInfo, function() {
        self.s3Storage.getMetadata(fileInfo.path, function(err, metadata) {
          assert.equal(metadata.contentType, 'text/plain; charset=utf-8');
          done();
        });
      });
    });

    it('non-existant file', function(done) {
      self.s3Storage.getMetadata('missingfile.txt', function(err, metadata) {
        assert.isNull(metadata);
        done();
      });
    });
  });

  describe('key prefix', function() {
    it('writes with prefix', function(done) {
      this.options.keyPrefix = 'prefix/';

      var contents = 'text file contents';
      var fileInfo = {
        path: 'pathname/plain.txt',
        contents: sbuff(contents),
        size: contents.length
      };

      self.s3Storage.writeFile(fileInfo, function() {
        self.s3Storage._listKeys('pathname', function(err, keys) {
          assert.equal(keys[0], 'prefix/pathname/plain.txt');
          done();
        });
      });
    });
  });

  describe('fallback bucket', function() {
    beforeEach(function(done) {
      self = this;
      // Ensure the bucket exists
      this.options.fallback = {
        bucket: 'fallback-bucket'
      };

      this.s3Storage = new S3Storage(this.options);

      var bucketParams = {ACL: 'public-read', Bucket: this.options.fallback.bucket};
      this.s3Storage._s3Fallback.createBucket(bucketParams, function(err) {
        if (err && err.code !== 'BucketAlreadyExists') return done(err);
        done();
      });
    });

    it('file exists in fallback but not primary', function(done) {
      var filename = shortid.generate() + '.txt';
      var s3Options = {
        Bucket: this.options.fallback.bucket,
        Key: filename,
        Body: '123',
        ContentType: 'text/plain'
      };

      async.series([
        function(cb) {
          self.s3Storage._s3Fallback.upload(s3Options, cb);
        },
        function(cb) {
          self.s3Storage.fileExists(filename, function(err, exists) {
            if (err) return cb(err);
            assert.equal(exists, 'fallback');
            cb();
          });
        }
      ], done);
    });

    it('file does not exist in primary or fallback', function(done) {
      var filename = shortid.generate() + '.txt';

      self.s3Storage.fileExists(filename, function(err, exists) {
        if (err) return done(err);
        assert.equal(exists, false);
        done();
      });
    });

    it('reads object from fallback bucket if specified', function(done) {
      var filename = shortid.generate() + '.txt';
      var s3Options = {
        Bucket: this.options.fallback.bucket,
        Key: filename,
        Body: '123',
        ContentType: 'text/plain'
      };

      self.s3Storage._s3Fallback.upload(s3Options, function() {
        var output = '';
        self.s3Storage.readFileStream(filename, true)
          .pipe(through(function(chunk, enc, cb) {
            output += chunk.toString();
            cb();
          }, function() {
            assert.equal(output, '123');
            done();
          }));
      });
    });
  });

  function deployTestFiles(s3Storage, files, callback) {
    async.each(files, function(path, cb) {
      var fileInfo = {
        path: path,
        contents: sbuff(path),
        size: path.length
      };
      s3Storage.writeFile(fileInfo, cb);
    }, callback);
  }
});
<commit_msg>Fixing unit tests to use filestreams<commit_after>var os = require('os');
var path = require('path');
var async = require('async');
var assert = require('assert');
var sbuff = require('simple-bufferstream');
var S3rver = require('s3rver');
var fs = require('fs');
var shortid = require('shortid');
var through = require('through2');
var metricDebug = require('debug')('metrics');
var S3Storage = require('../lib/s3');

require('dash-assert');

describe('S3Storage', function() {
  var self;

  var port = 9991;
  var bucket = '4front-deployments';

  before(function(done) {
    this.timeout(5000);
    self = this;
    var s3rver = new S3rver();

    var fakeS3Dir = '/tmp/s3rver';
    async.series([
      function(cb) {
        // Make sure the fakeS3Dir exists
        fs.mkdir(fakeS3Dir, function(err) {
          if (err && err.code !== 'EEXIST') return cb(err);

          cb();
        });
      },
      function(cb) {
        // Start the fake S3 server
        s3rver.setHostname('localhost')
          .setPort(port)
          .setDirectory(fakeS3Dir)
          .setSilent(true)
          .run(cb);
      }
    ], done);
  });

  beforeEach(function(done) {
    this.options = {
      bucket: bucket,
      accessKeyId: '123',
      secretAccessKey: 'abc',
      endpoint: 'localhost:' + port,
      sslEnabled: false,
      s3ForcePathStyle: true,
      metrics: {
        timing: function(key, ms) {
          metricDebug('%s - %s ms', key, Math.round(ms * 100) / 100);
        },
        increment: function(key) {
          metricDebug('increment %s', key);
        }
      }
    };

    this.s3Storage = new S3Storage(this.options);

    // Ensure the bucket exists
    var bucketParams = {ACL: 'public-read', Bucket: this.options.bucket};
    this.s3Storage._s3.createBucket(bucketParams, function(err) {
      if (err && err.code !== 'BucketAlreadyExists') return done(err);

      done();
    });
  });

  it('writeFile', function(done) {
    var fileContents = 'text file contents';
    var testFile = path.join(os.tmpdir(), shortid.generate() + '.txt');
    fs.writeFileSync(testFile, fileContents);

    var fileInfo = {
      path: 'appid/versionid/files/plain.txt',
      contents: fs.createReadStream(testFile),
      size: fileContents.length
    };

    async.series([
      function(cb) {
        self.s3Storage.writeFile(fileInfo, cb);
      },
      function(cb) {
        self.s3Storage.fileExists(fileInfo.path, function(err, exists) {
          if (err) return cb(err);

          assert.isTrue(exists);
          cb();
        });
      },
      function(cb) {
        var output = '';
        self.s3Storage.readFileStream(fileInfo.path)
          .on('data', function(chunk) {
            output += chunk.toString();
          })
          .on('error', function(err) {
            return cb(err);
          })
          .on('end', function() {
            assert.equal(output, fileContents);
            cb();
          });
      }
    ], done);
  });

  describe('readFile', function() {
    it('existing file', function(done) {
      var fileContents = 'text file contents';
      var fileName = shortid.generate() + '.txt';
      var testFile = path.join(os.tmpdir(), fileName);
      fs.writeFileSync(testFile, fileContents);

      var fileInfo = {
        path: 'appid/versionid/files/' + fileName,
        contents: fs.createReadStream(testFile),
        size: fileContents.length
      };

      async.series([
        function(cb) {
          self.s3Storage.writeFile(fileInfo, cb);
        },
        function(cb) {
          self.s3Storage.readFile(fileInfo.path, function(err, data) {
            if (err) return cb(err);

            assert.equal(data, fileContents);
            cb();
          });
        }
      ], done);
    });

    it('missing file', function(done) {
      self.s3Storage.readFile('directory/missingfile.txt', function(err, data) {
        if (err) return done(err);

        assert.isNull(data);
        done();
      });
    });
  });

  // The S3 mock doesn't support the upload function
  // it('writeStream', function(done) {
  //   var fileContents = "text file contents";
  //
  //   var fileInfo = {
  //     path: "appid/versionid/files/plain.txt",
  //     contents: sbuff(fileContents)
  //   };
  //
  //   async.series([
  //     function(cb) {
  //       self.s3Storage.writeStream(fileInfo, cb);
  //     },
  //     function(cb) {
  //       self.s3Storage.readFile(fileInfo.path, function(err, data) {
  //         assert.equal(data, fileContents);
  //       });
  //     }
  //   ], done);
  // });

  it('fileExists returns false for missing file', function(done) {
    self.s3Storage.fileExists('dir/missingfile.txt', function(err, exists) {
      if (err) return done(err);

      assert.isFalse(exists);
      done();
    });
  });

  describe('readFileStream', function() {
    beforeEach(function(done) {
      self = this;
      this.fileContents = 'asasfasdfasdf';
      this.fileName = shortid.generate() + '.txt';
      this.filePath = path.join(os.tmpdir(), this.fileName);
      fs.writeFileSync(this.filePath, this.fileContents);

      this.fileInfo = {
        path: 'appid/versionid/files/' + this.fileName,
        contents: fs.createReadStream(this.filePath),
        size: this.fileContents.length,
        gzipEncoded: true,
        maxAge: 10000
      };

      this.s3Storage.writeFile(this.fileInfo, done);
    });

    it('metadata event emitted', function(done) {
      var output = '';
      this.s3Storage.readFileStream(this.fileInfo.path)
        .on('metadata', function(metadata) {
          assert.equal(metadata.contentType, 'text/plain; charset=utf-8');
          // assert.equal(metadata['cache-control'], 'max-age=' + self.fileInfo.maxAge);
        })
        .on('error', done)
        .pipe(through(function(chunk, enc, cb) {
          output += chunk.toString();
          cb();
        }, function() {
          assert.equal(output, self.fileContents);
          done();
        }));
    });

    it('read missing file', function(done) {
      self.s3Storage.readFileStream('directory/missingfile.txt')
        .on('missing', function(err) {
          assert.equal(err.code, 'fileNotFound');
          done();
        })
        .on('end', function() {
          assert.fail('error event should have fired');
        });
    });
  });

  it('listFiles()', function(done) {
    var prefix = 'dir1/dir2';

    var files = [prefix + '/js/main.js', prefix + '/css/styles.css', prefix + '/index.html'];
    async.series([
      function(cb) {
        deployTestFiles(self.s3Storage, files, cb);
      },
      function(cb) {
        self.s3Storage.listFiles(prefix, function(err, data) {
          if (err) return cb(err);

          assert.noDifferences(files, data);
          cb();
        });
      }
    ], done);
  });

  it('deleteFiles()', function(done) {
    var prefix = 'dir1';
    var files = [prefix + 'js/main.js', prefix + 'css/styles.css'];

    async.series([
      function(cb) {
        // Upload some files
        deployTestFiles(self.s3Storage, files, cb);
      },
      function(cb) {
        // Delete the version
        self.s3Storage.deleteFiles(prefix, cb);
      },
      function(cb) {
        // Verify that the files are gone.
        async.each(files, function(filePath, cb1) {
          self.s3Storage.fileExists(filePath, function(err, exists) {
            if (err) return cb1(err);

            assert.isFalse(exists);
            cb1();
          });
        }, cb);
      }
    ], done);
  });

  describe('getMetadata()', function() {
    it('existing file', function(done) {
      var contents = 'text file contents';
      var fileInfo = {
        path: 'appid/versionid/files/plain.txt',
        contents: sbuff(contents),
        size: contents.length
      };

      self.s3Storage.writeFile(fileInfo, function() {
        self.s3Storage.getMetadata(fileInfo.path, function(err, metadata) {
          assert.equal(metadata.contentType, 'text/plain; charset=utf-8');
          done();
        });
      });
    });

    it('non-existant file', function(done) {
      self.s3Storage.getMetadata('missingfile.txt', function(err, metadata) {
        assert.isNull(metadata);
        done();
      });
    });
  });

  describe('key prefix', function() {
    it('writes with prefix', function(done) {
      this.options.keyPrefix = 'prefix/';

      var contents = 'text file contents';
      var fileInfo = {
        path: 'pathname/plain.txt',
        contents: sbuff(contents),
        size: contents.length
      };

      self.s3Storage.writeFile(fileInfo, function() {
        self.s3Storage._listKeys('pathname', function(err, keys) {
          assert.equal(keys[0], 'prefix/pathname/plain.txt');
          done();
        });
      });
    });
  });

  describe('fallback bucket', function() {
    beforeEach(function(done) {
      self = this;
      // Ensure the bucket exists
      this.options.fallback = {
        bucket: 'fallback-bucket'
      };

      this.s3Storage = new S3Storage(this.options);

      var bucketParams = {ACL: 'public-read', Bucket: this.options.fallback.bucket};
      this.s3Storage._s3Fallback.createBucket(bucketParams, function(err) {
        if (err && err.code !== 'BucketAlreadyExists') return done(err);
        done();
      });
    });

    it('file exists in fallback but not primary', function(done) {
      var filename = shortid.generate() + '.txt';
      var s3Options = {
        Bucket: this.options.fallback.bucket,
        Key: filename,
        Body: '123',
        ContentType: 'text/plain'
      };

      async.series([
        function(cb) {
          self.s3Storage._s3Fallback.upload(s3Options, cb);
        },
        function(cb) {
          self.s3Storage.fileExists(filename, function(err, exists) {
            if (err) return cb(err);
            assert.equal(exists, 'fallback');
            cb();
          });
        }
      ], done);
    });

    it('file does not exist in primary or fallback', function(done) {
      var filename = shortid.generate() + '.txt';

      self.s3Storage.fileExists(filename, function(err, exists) {
        if (err) return done(err);
        assert.equal(exists, false);
        done();
      });
    });

    it('reads object from fallback bucket if specified', function(done) {
      var filename = shortid.generate() + '.txt';
      var s3Options = {
        Bucket: this.options.fallback.bucket,
        Key: filename,
        Body: '123',
        ContentType: 'text/plain'
      };

      self.s3Storage._s3Fallback.upload(s3Options, function() {
        var output = '';
        self.s3Storage.readFileStream(filename, true)
          .pipe(through(function(chunk, enc, cb) {
            output += chunk.toString();
            cb();
          }, function() {
            assert.equal(output, '123');
            done();
          }));
      });
    });
  });

  function deployTestFiles(s3Storage, files, callback) {
    async.each(files, function(filename, cb) {
      var filePath = path.join(os.tmpdir(), shortid.generate());
      fs.writeFileSync(filePath, filename);
      var fileInfo = {
        path: filename,
        contents: fs.createReadStream(filePath),
        size: filename.length
      };
      s3Storage.writeFile(fileInfo, cb);
    }, callback);
  }
});
<|endoftext|>


<commit_before>'use strict';

const Nodgine = require('./src/nodgine');
const libHttp = require('http');

let instance = new Nodgine();

instance.addMiddleWare((aRequest, aResponse) => {
    aResponse
        .setStatusCode(200)
        .write('PRE');
});

instance.addMiddleWare((aRequest, aResponse) => {
    aResponse
        .write('POST');
});

instance.addController('/', (aRequest, aResponse, aParamsHash) => {
    aResponse
        .write('Controller1')
        .write(JSON.stringify(aParamsHash));
});

instance.addController('/:id', (aRequest, aResponse, aParamsHash) => {
    aResponse
        .write('Controller2')
        .write(JSON.stringify(aParamsHash));
});

libHttp.createServer(instance.getRouter()).listen(8888);
console.log('Lisntening at 8888');<commit_msg>Removed testrun.js<commit_after><|endoftext|>


<commit_before>'use strict'

const C = require('../constants/constants')
const DistributedStateRegistry = require('../cluster/distributed-state-registry')
const SocketWrapper = require('../message/socket-wrapper')
const utils = require('./utils')

class SubscriptionRegistry {

  /**
   * A generic mechanism to handle subscriptions from sockets to topics.
   * A bit like an event-hub, only that it registers SocketWrappers rather
   * than functions
   *
   * @constructor
   *
   * @param {Object} options deepstream options
   * @param {String} topic one of C.TOPIC
   * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS
   */
  constructor(options, topic, clusterTopic) {
    this._delayedBroadcasts = new Map()
    this._delay = -1
    if (options.broadcastTimeout !== undefined) {
      this._delay = options.broadcastTimeout
    }
    this._subscriptions = new Map()
    this._sockets = new Map()
    this._options = options
    this._topic = topic
    this._subscriptionListener = null
    this._constants = {
      MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS,
      SUBSCRIBE: C.ACTIONS.SUBSCRIBE,
      UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE,
      NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED
    }
    this._onBroadcastTimeout = this._onBroadcastTimeout.bind(this)

    this._setupRemoteComponents(clusterTopic)
  }

  /**
   * Setup all the remote components and actions required to deal with the subscription
   * via the cluster.
   */
  _setupRemoteComponents(clusterTopic) {
    this._clusterSubscriptions = new DistributedStateRegistry(clusterTopic || `${this._topic}_${C.TOPIC.SUBSCRIPTIONS}`, this._options)
    this._clusterSubscriptions.on('add', this._onClusterSubscriptionAdded.bind(this))
    this._clusterSubscriptions.on('remove', this._onClusterSubscriptionRemoved.bind(this))
  }

  /**
   * Return all the servers that have this subscription.
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllServers(subscriptionName) {
    return this._clusterSubscriptions.getAllServers(subscriptionName)
  }

  /**
   * Return all the servers that have this subscription excluding the current
   * server name
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllRemoteServers(subscriptionName) {
    const serverNames = this._clusterSubscriptions.getAllServers(subscriptionName)
    const localServerIndex = serverNames.indexOf(this._options.serverName)
    if (localServerIndex > -1) {
      serverNames.splice(serverNames.indexOf(this._options.serverName), 1)
    }
    return serverNames
  }

  /**
   * Returns a list of all the topic this registry
   * currently has subscribers for
   *
   * @public
   * @returns {Array} names
   */
  getNames() {
    return this._clusterSubscriptions.getAll()
  }

  /**
   * Returns true if the subscription exists somewhere
   * in the cluster
   *
   * @public
   * @returns {Array} names
   */
  hasName(subscriptionName) {
    return this._clusterSubscriptions.has(subscriptionName)
  }

  /**
  * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back.
  * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
  *
  * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED
  * @param {string} value The value to override with.
  *
  * @public
  * @returns {void}
  */
  setAction(name, value) {
    this._constants[name.toUpperCase()] = value
  }

  /**
   * Broadcasts the enqueued messages for the timed out subscription room.
   *
   * @param   {Object} delayedBroadcasts holds information of what messages to send and where
   *
   * @public
   * @returns {void}
   */
  _onBroadcastTimeout() {
    this._delayedBroadcastsTimer = null
    for (const entry of this._delayedBroadcasts) {
      const name = entry[0]
      const sockets = this._subscriptions.get(name)
      const delayedBroadcasts = entry[1]
      const uniqueSenders = delayedBroadcasts.uniqueSenders
      const sharedMessages = delayedBroadcasts.sharedMessages

      if (!sockets || sockets.length === 0 || sharedMessages.length === 0) {
        this._delayedBroadcasts.delete(name)
        continue
      }

      // for all unique senders and their gaps, build their special messages
      for (const uniqueSender of uniqueSenders) {
        uniqueSender.message = sharedMessages.substring(0, uniqueSender.gaps[0].start)
        let lastStop = uniqueSender.gaps[0].stop
        for (let j = 1; j < uniqueSender.gaps.length; j++) {
          uniqueSender.message += sharedMessages.substring(lastStop, uniqueSender.gaps[j].start)
          lastStop = uniqueSender.gaps[j].stop
        }
        uniqueSender.message += sharedMessages.substring(lastStop, sharedMessages.length)
      }

      for (const uniqueSender of uniqueSenders) {
        if (uniqueSender.message) {
          uniqueSender.socket.sendNative(uniqueSender.message)
        }
      }

      // for all sockets in this subscription name, send either sharedMessage or this socket's
      // specialized message. only sockets that sent something will have a special message, all
      // other sockets are only listeners and receive the exact same (sharedMessage) message.
      const preparedMessage = SocketWrapper.prepareMessage(sharedMessages)
      let j = 0
      for (const socket of sockets) {
        if (j < uniqueSenders.length && uniqueSenders[j].uuid === socket.uuid) {
          j++
        } else {
          socket.sendPrepared(preparedMessage)
        }
      }
      SocketWrapper.finalizeMessage(preparedMessage)

      delayedBroadcasts.uniqueSenders.splice(0, delayedBroadcasts.uniqueSenders.length)
      delayedBroadcasts.sharedMessages = ''
    }
  }

  /**
   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
   * be reordered in relation to *other* subscription names, but never in relation to the same
   * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
   * broadcast.
   *
   * @param   {String} name      the name/topic the subscriber was previously registered for
   * @param   {String} msgString the message as string
   * @param   {[SocketWrapper]} sender an optional socketWrapper that shouldn't receive the message
   *
   * @public
   * @returns {void}
   */
  sendToSubscribers(name, msgString, sender) {
    if (!this._subscriptions.has(name)) {
      return
    }

    // not all messages are valid, this should be fixed elsewhere!
    if (msgString.charAt(msgString.length - 1) !== C.MESSAGE_SEPERATOR) {
      msgString += C.MESSAGE_SEPERATOR
    }

    // if not already a delayed broadcast, create it
    let delayedBroadcasts = this._delayedBroadcasts.get(name)
    if (delayedBroadcasts === undefined) {
      delayedBroadcasts = {
        uniqueSenders: [],
        sharedMessages: ''
      }
      this._delayedBroadcasts.set(name, delayedBroadcasts)
    }

    // append this message to the sharedMessage, the message that
    // is shared in the broadcast to every listener-only
    const start = delayedBroadcasts.sharedMessages.length
    delayedBroadcasts.sharedMessages += msgString
    const stop = delayedBroadcasts.sharedMessages.length

    // uniqueSendersMap maps from uuid to offset in uniqueSendersVector
    // each uniqueSender has a vector of "gaps" in relation to sharedMessage
    // senders should not receive what they sent themselves, so a gap is inserted
    // for every send from this sender
    if (sender && sender.uuid !== undefined) {
      const uniqueSenders = delayedBroadcasts.uniqueSenders
      const index = utils.sortedIndexBy(uniqueSenders, sender, 'uuid')

      if (uniqueSenders[index] !== sender) {
        uniqueSenders.splice(index, 0, {
          uuid: sender.uuid,
          message: null,
          gaps: []
        })
      }

      uniqueSenders[index].gaps.push({ start, stop })
    }

    // reuse the same timer if already started
    if (!this._delayedBroadcastsTimer) {
      if (this._delay !== -1) {
        this._delayedBroadcastsTimer = setTimeout(this._onBroadcastTimeout, this._delay)
      } else {
        this._onBroadcastTimeout()
      }
    }
  }

  /**
   * Adds a SocketWrapper as a subscriber to a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {void}
   */
  subscribe(name, socketWrapper) {
    let sockets = this._subscriptions.get(name)

    if (!sockets) {
      sockets = []
      this._subscriptions.set(name, sockets)
    }

    const index = utils.sortedIndexBy(sockets, socketWrapper, 'uuid')

    if (sockets[index] === socketWrapper) {
      const msg = `repeat supscription to "${name}" by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg)
      socketWrapper.sendError(this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name)
      return
    }

    sockets.splice(index, 0, socketWrapper)

    let names = this._sockets.get(socketWrapper)
    if (!names) {
      this._sockets.set(socketWrapper, names = [])
      socketWrapper.once('close', (socketWrapper) => {
        for (let n = names.length - 1; n >= 0; --n) {
          this.unsubscribe(names[n], socketWrapper)
        }
        this._sockets.delete(name)
      })
    }

    names.splice(utils.sortedIndex(names, name), 0, name)

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionMade(
        name,
        socketWrapper,
        sockets.length
      )
    }

    this._clusterSubscriptions.add(name)

    const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
    this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg)
    socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.SUBSCRIBE, name])
  }

  /**
   * Removes a SocketWrapper from the list of subscriptions for a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   * @param   {Boolean} silent supresses logs and unsubscribe ACK messages
   *
   * @public
   * @returns {void}
   */
  unsubscribe(name, socketWrapper, silent) {
    const sockets = this._subscriptions.get(name)

    const index = utils.sortedIndexBy(sockets, socketWrapper, 'uuid')

    if (!sockets || sockets[index] !== socketWrapper) {
      const msg = `${socketWrapper.user} is not subscribed to ${name}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg)
      socketWrapper.sendError(this._topic, this._constants.NOT_SUBSCRIBED, name)
      return
    }

    sockets.splice(index, 1)

    const names = this._sockets.get(socketWrapper)
    names.splice(utils.sortedIndex(names, name), 1)

    if (sockets.length === 0) {
      this._clusterSubscriptions.remove(name)
      this._subscriptions.delete(name)
    }

    if (this._subscriptionListener) {
      const allServerNames = this._clusterSubscriptions.getAllServers(name)
      const indexOfCurrentNode = allServerNames.indexOf(this._options.serverName)
      if (indexOfCurrentNode > -1) {
        allServerNames.splice(indexOfCurrentNode, 1)
      }
      this._subscriptionListener.onSubscriptionRemoved(
        name,
        socketWrapper,
        sockets.length,
        allServerNames.length
      )
    }

    this._clusterSubscriptions.remove(name)

    if (!silent) {
      const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg)
      socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.UNSUBSCRIBE, name])
    }
  }

  /**
   * Returns an array of SocketWrappers that are subscribed
   * to <name> or null if there are no subscribers
   *
   * @param   {String} name
   *
   * @public
   * @returns {Array} SocketWrapper[]
   */
  getLocalSubscribers(name) {
    return this._subscriptions.get(name) || []
  }

  /**
   * Returns true if there are SocketWrappers that
   * are subscribed to <name> or false if there
   * aren't any subscribers
   *
   * @param   {String}  name
   *
   * @public
   * @returns {Boolean} hasLocalSubscribers
   */
  hasLocalSubscribers(name) {
    return this._subscriptions.has(name)
  }

  /**
   * Allows to set a subscriptionListener after the class had been instantiated
   *
   * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method
   *
   * @public
   * @returns {void}
   */
  setSubscriptionListener(subscriptionListener) {
    this._subscriptionListener = subscriptionListener
  }

  /**
   * Called when a subscription has been added to the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from subscribe
   * @param  {String} name the name that was added
   */
  _onClusterSubscriptionAdded(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionMade(name, null, 1)
    }
  }

  /**
   * Called when a subscription has been removed from the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from unsubscribe
   * @param  {String} name the name that was removed
   */
  _onClusterSubscriptionRemoved(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionRemoved(name, null, 0, 0)
    }
  }

}

module.exports = SubscriptionRegistry
<commit_msg>refactor(subscription-registry): Optimize and simplify subscription cleanup-<commit_after>'use strict'

const C = require('../constants/constants')
const DistributedStateRegistry = require('../cluster/distributed-state-registry')
const SocketWrapper = require('../message/socket-wrapper')
const utils = require('./utils')

class SubscriptionRegistry {

  /**
   * A generic mechanism to handle subscriptions from sockets to topics.
   * A bit like an event-hub, only that it registers SocketWrappers rather
   * than functions
   *
   * @constructor
   *
   * @param {Object} options deepstream options
   * @param {String} topic one of C.TOPIC
   * @param {[String]} clusterTopic A unique cluster topic, if not created uses format: topic_SUBSCRIPTIONS
   */
  constructor(options, topic, clusterTopic) {
    this._delayedBroadcasts = new Map()
    this._delay = -1
    if (options.broadcastTimeout !== undefined) {
      this._delay = options.broadcastTimeout
    }
    this._subscriptions = new Map()
    this._options = options
    this._topic = topic
    this._subscriptionListener = null
    this._constants = {
      MULTIPLE_SUBSCRIPTIONS: C.EVENT.MULTIPLE_SUBSCRIPTIONS,
      SUBSCRIBE: C.ACTIONS.SUBSCRIBE,
      UNSUBSCRIBE: C.ACTIONS.UNSUBSCRIBE,
      NOT_SUBSCRIBED: C.EVENT.NOT_SUBSCRIBED
    }
    this._onBroadcastTimeout = this._onBroadcastTimeout.bind(this)
    this._onSocketClose = this._onSocketClose.bind(this)

    this._setupRemoteComponents(clusterTopic)
  }

  /**
   * Setup all the remote components and actions required to deal with the subscription
   * via the cluster.
   */
  _setupRemoteComponents(clusterTopic) {
    this._clusterSubscriptions = new DistributedStateRegistry(clusterTopic || `${this._topic}_${C.TOPIC.SUBSCRIPTIONS}`, this._options)
    this._clusterSubscriptions.on('add', this._onClusterSubscriptionAdded.bind(this))
    this._clusterSubscriptions.on('remove', this._onClusterSubscriptionRemoved.bind(this))
  }

  /**
   * Return all the servers that have this subscription.
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllServers(subscriptionName) {
    return this._clusterSubscriptions.getAllServers(subscriptionName)
  }

  /**
   * Return all the servers that have this subscription excluding the current
   * server name
   *
   * @param  {String} subscriptionName the subscriptionName to look for
   *
   * @public
   * @return {Array}  An array of all the servernames with this subscription
   */
  getAllRemoteServers(subscriptionName) {
    const serverNames = this._clusterSubscriptions.getAllServers(subscriptionName)
    const localServerIndex = serverNames.indexOf(this._options.serverName)
    if (localServerIndex > -1) {
      serverNames.splice(serverNames.indexOf(this._options.serverName), 1)
    }
    return serverNames
  }

  /**
   * Returns a list of all the topic this registry
   * currently has subscribers for
   *
   * @public
   * @returns {Array} names
   */
  getNames() {
    return this._clusterSubscriptions.getAll()
  }

  /**
   * Returns true if the subscription exists somewhere
   * in the cluster
   *
   * @public
   * @returns {Array} names
   */
  hasName(subscriptionName) {
    return this._clusterSubscriptions.has(subscriptionName)
  }

  /**
  * This method allows you to customise the SubscriptionRegistry so that it can send custom events and ack messages back.
  * For example, when using the C.ACTIONS.LISTEN, you would override SUBSCRIBE with C.ACTIONS.SUBSCRIBE and UNSUBSCRIBE with UNSUBSCRIBE
  *
  * @param {string} name The name of the the variable to override. This can be either MULTIPLE_SUBSCRIPTIONS, SUBSCRIBE, UNSUBSCRIBE, NOT_SUBSCRIBED
  * @param {string} value The value to override with.
  *
  * @public
  * @returns {void}
  */
  setAction(name, value) {
    this._constants[name.toUpperCase()] = value
  }

  _onSocketClose (socketWrapper) {
    for (const entry of this._subscriptions) {
      if (entry[1][utils.sortedIndexBy(entry[1], socketWrapper, 'uuid')] === socketWrapper) {
        this.unsubscribe(entry[0], socketWrapper)
      }
    }
  }

  /**
   * Broadcasts the enqueued messages for the timed out subscription room.
   *
   * @param   {Object} delayedBroadcasts holds information of what messages to send and where
   *
   * @public
   * @returns {void}
   */
  _onBroadcastTimeout() {
    this._delayedBroadcastsTimer = null
    for (const entry of this._delayedBroadcasts) {
      const name = entry[0]
      const sockets = this._subscriptions.get(name)
      const delayedBroadcasts = entry[1]
      const uniqueSenders = delayedBroadcasts.uniqueSenders
      const sharedMessages = delayedBroadcasts.sharedMessages

      if (!sockets || sockets.length === 0 || sharedMessages.length === 0) {
        this._delayedBroadcasts.delete(name)
        continue
      }

      // for all unique senders and their gaps, build their special messages
      for (const uniqueSender of uniqueSenders) {
        uniqueSender.message = sharedMessages.substring(0, uniqueSender.gaps[0].start)
        let lastStop = uniqueSender.gaps[0].stop
        for (let j = 1; j < uniqueSender.gaps.length; j++) {
          uniqueSender.message += sharedMessages.substring(lastStop, uniqueSender.gaps[j].start)
          lastStop = uniqueSender.gaps[j].stop
        }
        uniqueSender.message += sharedMessages.substring(lastStop, sharedMessages.length)
      }

      for (const uniqueSender of uniqueSenders) {
        if (uniqueSender.message) {
          uniqueSender.socket.sendNative(uniqueSender.message)
        }
      }

      // for all sockets in this subscription name, send either sharedMessage or this socket's
      // specialized message. only sockets that sent something will have a special message, all
      // other sockets are only listeners and receive the exact same (sharedMessage) message.
      const preparedMessage = SocketWrapper.prepareMessage(sharedMessages)
      let j = 0
      for (const socket of sockets) {
        if (j < uniqueSenders.length && uniqueSenders[j].uuid === socket.uuid) {
          j++
        } else {
          socket.sendPrepared(preparedMessage)
        }
      }
      SocketWrapper.finalizeMessage(preparedMessage)

      delayedBroadcasts.uniqueSenders.splice(0, delayedBroadcasts.uniqueSenders.length)
      delayedBroadcasts.sharedMessages = ''
    }
  }

  /**
   * Enqueues a message string to be broadcast to all subscribers. Broadcasts will potentially
   * be reordered in relation to *other* subscription names, but never in relation to the same
   * subscription name. Each broadcast is given 'broadcastTimeout' ms to coalesce into one big
   * broadcast.
   *
   * @param   {String} name      the name/topic the subscriber was previously registered for
   * @param   {String} msgString the message as string
   * @param   {[SocketWrapper]} sender an optional socketWrapper that shouldn't receive the message
   *
   * @public
   * @returns {void}
   */
  sendToSubscribers(name, msgString, sender) {
    if (!this._subscriptions.has(name)) {
      return
    }

    // not all messages are valid, this should be fixed elsewhere!
    if (msgString.charAt(msgString.length - 1) !== C.MESSAGE_SEPERATOR) {
      msgString += C.MESSAGE_SEPERATOR
    }

    // if not already a delayed broadcast, create it
    let delayedBroadcasts = this._delayedBroadcasts.get(name)
    if (delayedBroadcasts === undefined) {
      delayedBroadcasts = {
        uniqueSenders: [],
        sharedMessages: ''
      }
      this._delayedBroadcasts.set(name, delayedBroadcasts)
    }

    // append this message to the sharedMessage, the message that
    // is shared in the broadcast to every listener-only
    const start = delayedBroadcasts.sharedMessages.length
    delayedBroadcasts.sharedMessages += msgString
    const stop = delayedBroadcasts.sharedMessages.length

    // uniqueSendersMap maps from uuid to offset in uniqueSendersVector
    // each uniqueSender has a vector of "gaps" in relation to sharedMessage
    // senders should not receive what they sent themselves, so a gap is inserted
    // for every send from this sender
    if (sender && sender.uuid !== undefined) {
      const uniqueSenders = delayedBroadcasts.uniqueSenders
      const index = utils.sortedIndexBy(uniqueSenders, sender, 'uuid')

      if (uniqueSenders[index] !== sender) {
        uniqueSenders.splice(index, 0, {
          uuid: sender.uuid,
          message: null,
          gaps: []
        })
      }

      uniqueSenders[index].gaps.push({ start, stop })
    }

    // reuse the same timer if already started
    if (!this._delayedBroadcastsTimer) {
      if (this._delay !== -1) {
        this._delayedBroadcastsTimer = setTimeout(this._onBroadcastTimeout, this._delay)
      } else {
        this._onBroadcastTimeout()
      }
    }
  }

  /**
   * Adds a SocketWrapper as a subscriber to a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   *
   * @public
   * @returns {void}
   */
  subscribe(name, socketWrapper) {
    let sockets = this._subscriptions.get(name)

    if (!sockets) {
      sockets = []
      this._subscriptions.set(name, sockets)
    }

    const index = utils.sortedIndexBy(sockets, socketWrapper, 'uuid')

    if (sockets[index] === socketWrapper) {
      const msg = `repeat supscription to "${name}" by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.MULTIPLE_SUBSCRIPTIONS, msg)
      socketWrapper.sendError(this._topic, this._constants.MULTIPLE_SUBSCRIPTIONS, name)
      return
    }

    sockets.splice(index, 0, socketWrapper)

    if (socketWrapper.listeners('close').indexOf(this._onSocketClose) === -1) {
      socketWrapper.once('close', this._onSocketClose)
    }

    if (this._subscriptionListener) {
      this._subscriptionListener.onSubscriptionMade(
        name,
        socketWrapper,
        sockets.length
      )
    }

    this._clusterSubscriptions.add(name)

    const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
    this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.SUBSCRIBE, logMsg)
    socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.SUBSCRIBE, name])
  }

  /**
   * Removes a SocketWrapper from the list of subscriptions for a topic
   *
   * @param   {String} name
   * @param   {SocketWrapper} socketWrapper
   * @param   {Boolean} silent supresses logs and unsubscribe ACK messages
   *
   * @public
   * @returns {void}
   */
  unsubscribe(name, socketWrapper, silent) {
    const sockets = this._subscriptions.get(name)

    const index = utils.sortedIndexBy(sockets, socketWrapper, 'uuid')

    if (!sockets || sockets[index] !== socketWrapper) {
      const msg = `${socketWrapper.user} is not subscribed to ${name}`
      this._options.logger.log(C.LOG_LEVEL.WARN, this._constants.NOT_SUBSCRIBED, msg)
      socketWrapper.sendError(this._topic, this._constants.NOT_SUBSCRIBED, name)
      return
    }

    sockets.splice(index, 1)

    if (sockets.length === 0) {
      this._clusterSubscriptions.remove(name)
      this._subscriptions.delete(name)
    }

    if (this._subscriptionListener) {
      const allServerNames = this._clusterSubscriptions.getAllServers(name)
      const indexOfCurrentNode = allServerNames.indexOf(this._options.serverName)
      if (indexOfCurrentNode > -1) {
        allServerNames.splice(indexOfCurrentNode, 1)
      }
      this._subscriptionListener.onSubscriptionRemoved(
        name,
        socketWrapper,
        sockets.length,
        allServerNames.length
      )
    }

    this._clusterSubscriptions.remove(name)

    if (!silent) {
      const logMsg = `for ${this._topic}:${name} by ${socketWrapper.user}`
      this._options.logger.log(C.LOG_LEVEL.DEBUG, this._constants.UNSUBSCRIBE, logMsg)
      socketWrapper.sendMessage(this._topic, C.ACTIONS.ACK, [this._constants.UNSUBSCRIBE, name])
    }
  }

  /**
   * Returns an array of SocketWrappers that are subscribed
   * to <name> or null if there are no subscribers
   *
   * @param   {String} name
   *
   * @public
   * @returns {Array} SocketWrapper[]
   */
  getLocalSubscribers(name) {
    return this._subscriptions.get(name) || []
  }

  /**
   * Returns true if there are SocketWrappers that
   * are subscribed to <name> or false if there
   * aren't any subscribers
   *
   * @param   {String}  name
   *
   * @public
   * @returns {Boolean} hasLocalSubscribers
   */
  hasLocalSubscribers(name) {
    return this._subscriptions.has(name)
  }

  /**
   * Allows to set a subscriptionListener after the class had been instantiated
   *
   * @param {SubscriptionListener} subscriptionListener - a class exposing a onSubscriptionMade and onSubscriptionRemoved method
   *
   * @public
   * @returns {void}
   */
  setSubscriptionListener(subscriptionListener) {
    this._subscriptionListener = subscriptionListener
  }

  /**
   * Called when a subscription has been added to the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from subscribe
   * @param  {String} name the name that was added
   */
  _onClusterSubscriptionAdded(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionMade(name, null, 1)
    }
  }

  /**
   * Called when a subscription has been removed from the cluster
   * This can be invoked locally or remotely, so we check if it
   * is a local invocation and ignore it if so in favour of the
   * call done from unsubscribe
   * @param  {String} name the name that was removed
   */
  _onClusterSubscriptionRemoved(name) {
    if (this._subscriptionListener && !this.hasLocalSubscribers(name)) {
      this._subscriptionListener.onSubscriptionRemoved(name, null, 0, 0)
    }
  }

}

module.exports = SubscriptionRegistry
<|endoftext|>


<commit_before>import {ValidationGroupBuilder} from '../validation/validation-group-builder';
import {ValidationResult} from '../validation/validation-result';

export class ValidationGroup {
  constructor(subject, observerLocator) {
    this.result = new ValidationResult();
    this.subject = subject;
    this.validationProperties = [];
    this.builder = new ValidationGroupBuilder(observerLocator, this);
  }

  checkAll() {
    for (let i = this.validationProperties.length - 1; i >= 0; i--) {
      var validatorProperty = this.validationProperties[i];
      validatorProperty.validateCurrentValue(true);
    }
    return this.result.isValid;
  }

  ensure(propertyName) {
    return this.builder.ensure(propertyName);
  }

  notEmpty() {
    return this.builder.notEmpty();
  }

  minimum(minimumValue) {
    return this.builder.minimum(minimumValue);
  }

  between(minimumValue, maximumValue) {
    return this.builder.between(minimumValue, maximumValue);
  }

  maximum(maximumValue) {
    return this.builder.maximum(maximumValue);
  }

  equals(otherValue, otherValueLabel) {
    return this.builder.equals(otherValue, otherValueLabel);
  }

  notEquals(otherValue, otherValueLabel) {
    return this.builder.notEquals(otherValue, otherValueLabel);
  }

  email() {
    return this.builder.email();
  }

  in(collection) {
    return this.builder.in(collection);
  }

  minLength(minimumValue) {
    return this.builder.minLength(minimumValue);
  }

  maxLength(maximumValue) {
    return this.builder.maxLength(maximumValue);
  }

  betweenLength(minimumValue, maximumValue) {
    return this.builder.betweenLength(minimumValue, maximumValue);
  }

  isNumeric() {
    return this.builder.isNumeric();
  }

  isDigit() {
    return this.builder.isDigit();
  }

  isAlphanumeric() {
    return this.builder.isAlphaNumeric();
  }

  isAlphanumericOrWhitespace() {
    return this.builder.isAlphanumericOrWhitespace();
  }

  isStrongPassword(minimumComplexityLevel) {
    return this.builder.isStrongPassword(minimumComplexityLevel);
  }

  matchesRegex(regexString) {
    return this.builder.matchesRegex(regexString);
  }

  matches(regex) {
    return this.builder.matches(regex);
  }

  passes(customFunction, threshold) {
    return this.builder.passes(customFunction, threshold);
  }

  passesRule(validationRule) {
    return this.builder.passesRule(validationRule);
  }

  if(conditionExpression, threshold) {
    return this.builder.if(conditionExpression, threshold);
  }

  else() {
    return this.builder.else();
  }

  endIf() {
    return this.builder.endIf();
  }

  switch(conditionExpression) {
    return this.builder.switch(conditionExpression);
  }

  case(caseLabel) {
    return this.builder.case(caseLabel);
  }

  default() {
    return this.builder.default();
  }

  endSwitch() {
    return this.builder.endSwitch();
  }
}
<commit_msg>doc(validation-group): add JSDoc to public methods<commit_after>import {ValidationGroupBuilder} from '../validation/validation-group-builder';
import {ValidationResult} from '../validation/validation-result';

/**
 * Encapsulates validation rules and their current validation state for a given subject
 * @class ValidationGroup
 * @constructor
 */
export class ValidationGroup {
  /**
   * Instantiates a new {ValidationGroup}
   * @param subject The subject to evaluate
   * @param observerLocator The observerLocator used to monitor changes on the subject
   */
  constructor(subject, observerLocator) {
    this.result = new ValidationResult();
    this.subject = subject;
    this.validationProperties = [];
    this.builder = new ValidationGroupBuilder(observerLocator, this);
  }

  /**
   * Causes each property to re-evaluate: gets the latest value, marks the property as 'dirty', runs validation rules on latest value, updates this.result
   * @returns {bool} True/false indicating if every property is valid
   */
  checkAll() {
    for (let i = this.validationProperties.length - 1; i >= 0; i--) {
      var validatorProperty = this.validationProperties[i];
      validatorProperty.validateCurrentValue(true);
    }
    return this.result.isValid;
  }

  /**
   * Adds a validation property for the specified path
   * @param {String} propertyPath the path of the property/field, for example 'firstName' or 'address.muncipality.zipCode'
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  ensure(propertyPath) {
    return this.builder.ensure(propertyPath);
  }

  /**
   * Adds a validation rule that checks a value for being 'notEmpty', 'required'
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  notEmpty() {
    return this.builder.notEmpty();
  }

  /**
   * Adds a validation rule that checks a value for being greater than or equal to a threshold
   * @param minimumValue the threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  minimum(minimumValue) {
    return this.builder.minimum(minimumValue);
  }

  /**
   * Adds a validation rule that checks a value for being greater than or equal to a threshold, and less than another threshold
   * @param minimumValue The minimum threshold
   * @param maximumValue The maximum threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  between(minimumValue, maximumValue) {
    return this.builder.between(minimumValue, maximumValue);
  }

  /**
   * Adds a validation rule that checks a value for being less than a threshold
   * @param maximumValue The threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  maximum(maximumValue) {
    return this.builder.maximum(maximumValue);
  }

  /**
   * Adds a validation rule that checks a value for being equal to a threshold
   * @param otherValue The threshold
   * @param otherValueLabel Optional: a label to use in the validation message
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  equals(otherValue, otherValueLabel) {
    return this.builder.equals(otherValue, otherValueLabel);
  }

  /**
   * Adds a validation rule that checks a value for not being equal to a threshold
   * @param otherValue The threshold
   * @param otherValueLabel Optional: a label to use in the validation message
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  notEquals(otherValue, otherValueLabel) {
    return this.builder.notEquals(otherValue, otherValueLabel);
  }

  /**
   * Adds a validation rule that checks a value for being a valid email address
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  email() {
    return this.builder.email();
  }

  /**
   * Adds a validation rule that checks a value for being equal to at least one other value in a particular collection
   * @param collection The threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  in(collection) {
    return this.builder.in(collection);
  }

  /**
   * Adds a validation rule that checks a value for having a length greater than or equal to a specified threshold
   * @param minimumValue The threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  minLength(minimumValue) {
    return this.builder.minLength(minimumValue);
  }

  /**
   * Adds a validation rule that checks a value for having a length less than a specified threshold
   * @param maximumValue The threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  maxLength(maximumValue) {
    return this.builder.maxLength(maximumValue);
  }
  /**
   * Adds a validation rule that checks a value for having a length greater than or equal to a specified threshold and less than another threshold
   * @param minimumValue The minimum threshold
   * @param maximumValue The maximum threshold
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  betweenLength(minimumValue, maximumValue) {
    return this.builder.betweenLength(minimumValue, maximumValue);
  }

  /**
   * Adds a validation rule that checks a value for being numeric, this includes formatted numbers like '-3,600.25'
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  isNumeric() {
    return this.builder.isNumeric();
  }

  /**
   * Adds a validation rule that checks a value for being strictly numeric, this excludes formatted numbers like '-3,600.25'
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  isDigit() {
    return this.builder.isDigit();
  }

  /**
   * Adds a validation rule that checks a value for only containing alphanumerical characters
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  isAlphanumeric() {
    return this.builder.isAlphaNumeric();
  }

  /**
   * Adds a validation rule that checks a value for only containing alphanumerical characters or whitespace
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  isAlphanumericOrWhitespace() {
    return this.builder.isAlphanumericOrWhitespace();
  }

  /**
   * Adds a validation rule that checks a value for being a strong password. A strong password contains at least the specified of the following groups: lowercase characters, uppercase characters, digits and special characters.
   * @param minimumComplexityLevel {Number} Optionally, specifiy the number of groups to match. Default is 4.
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  isStrongPassword(minimumComplexityLevel) {
    return this.builder.isStrongPassword(minimumComplexityLevel);
  }

  /**
   * Adds a validation rule that checks a value for matching a particular regex
   * @param regexString {String} the regex to match
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  matchesRegex(regexString) {
    return this.builder.matchesRegex(regexString);
  }

  /**
   * Adds a validation rule that checks a value for matching a particular regex
   * @param regexString {Regex} the regex to match
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  matches(regex) {
    return this.builder.matches(regex);
  }

  /**
   * Adds a validation rule that checks a value for passing a custom function
   * @param customFunction {Function} The custom function that needs to pass, that takes two arguments: newValue (the value currently being evaluated) and optionally: threshold, and returns true/false.
   * @param threshold {Object} An optional threshold that will be passed to the customFunction
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  passes(customFunction, threshold) {
    return this.builder.passes(customFunction, threshold);
  }

  /**
   * Adds the {ValidationRule}
   * @param validationRule {ValudationRule} The rule that needs to pass
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  passesRule(validationRule) {
    return this.builder.passesRule(validationRule);
  }

  /**
   * Specifies that the next validation rules only need to be evaluated when the specified conditionExpression is true
   * @param conditionExpression {Function} a function that returns true of false.
   * @param threshold {Object} an optional treshold object that is passed to the conditionExpression
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  if(conditionExpression, threshold) {
    return this.builder.if(conditionExpression, threshold);
  }

  /**
   * Specifies that the next validation rules only need to be evaluated when the previously specified conditionExpression is false.
   * See: if(conditionExpression, threshold)
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  else() {
    return this.builder.else();
  }

  /**
   * Specifies that the execution of next validation rules no longer depend on the the previously specified conditionExpression.
   * See: if(conditionExpression, threshold)
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  endIf() {
    return this.builder.endIf();
  }

  /**
   * Specifies that the next validation rules only need to be evaluated when they are preceded by a case that matches the conditionExpression
   * @param conditionExpression {Function} a function that returns a case label to execute. This is optional, when omitted the case label will be matched using the underlying property's value
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  switch(conditionExpression) {
    return this.builder.switch(conditionExpression);
  }

  /**
   * Specifies that the next validation rules only need to be evaluated when the caseLabel matches the value returned by a preceding switch statement
   * See: switch(conditionExpression)
   * @param caseLabel {Object} the case label
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  case(caseLabel) {
    return this.builder.case(caseLabel);
  }
  /**
   * Specifies that the next validation rules only need to be evaluated when not other caseLabel matches the value returned by a preceding switch statement
   * See: switch(conditionExpression)
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  default() {
    return this.builder.default();
  }
  /**
   * Specifies that the execution of next validation rules no longer depend on the the previously specified conditionExpression.
   * See: switch(conditionExpression)
   * @returns {ValidationGroup} returns this ValidationGroup, to enable fluent API
   */
  endSwitch() {
    return this.builder.endSwitch();
  }
}
<|endoftext|>