Source: suid.js

Source: suid.js

/**! 
 * Distributed Service-Unique IDs that are short and sweet.
 * 
 * http://download.github.io/suid
 * 
 * @Author Stijn de Witt (http://StijnDeWitt.com)
 * @Copyright (c) 2015. Some rights reserved. 
 * @License CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/) 
 */
/** @namespace ws.suid */
(function(u,m,d) {var p='picolog',l; if(this.log&&this.log.INFO){l=this.log;}
	if(typeof define=='function'&&define.amd){define(u,['require'],function(r){return d(r.defined(p)?r(p):l);});}
	else if(typeof exports=='object'){try{require.resolve(p);l=require(p);}catch(e){}module.exports=d(l);}
	else{this[m] = d(l);}
}('suid','Suid',function(log){'use strict';
var
SHARDSIZE = 2,
IDSIZE = 64,
THROTTLE = 5000,
POOL = 'suidpool', 
DETECT = 'suiddetect',
ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz',
localStorageSupported = (function(ls){try{ls.setItem(DETECT, DETECT);ls.removeItem(DETECT);return true;}catch(e){return false;}})(localStorage),
currentBlock,
currentId,
readyListeners = [],
win = typeof window != 'undefined' ? window : typeof global != 'undefined' ? global : {},
config = getConfig(win.Suid, {server:'/suid/suid.json', min:3, max:4});

/**
 * Distributed Service-Unique IDs that are short and sweet.
 *  
 * <p>When called without arguments, defaults to <tt>Suid(0)</tt>.</p> 
 * 
 * <p>When called with an argument, constructs a new Suid based
 * on the given value, which may be either a:</p>
 * 
 * <ul>
 *   <li>Number</li>
 *   <li>Base-36 String</li>
 *   <li>Other Suid</li>
 * </ul>
 *
 * <p>This constructor function may be called without the <tt>new</tt> keyword.</p>
 * 
 * <p><b>Examples</b></p>
 * 
 * <big><pre>
 * // Call without arguments
 * var NOP = new Suid();
 *  
 * // call with a Number argument
 * var ZERO = new Suid(0);
 * var ONE = new Suid(1);
 * NOP.equals(ZERO); // true
 *
 * // New-less invocation
 * var TWO = Suid(2);
 * 
 * // call with a base-36 string argument
 * var suid1 = Suid('14she');
 * 
 * // call with another suid as argument
 * var suid2 = Suid(suid1);
 * </pre></big>
 * 
 * @param value The value for the new Suid.
 * 
 * @class Suid
 * @memberof! ws.suid
 */
var Suid = (function() {
	function Suid(value) {
		if (! (this instanceof Suid)) {return new Suid(value);}
		if (value === undefined) {value = 0;}
		if (typeof value === 'string') {value = parseInt(value, 36);}
		this.value = value instanceof Suid ? value.value : value;
		Number.call(this, this.value);
	}

	Suid.prototype = Object.create(Number.prototype);
	Suid.prototype.constructor = Suid;

	/** 
	 * Converts this suid to a string.
	 * 
	 * The returned String will be in base-36 format. 
	 * For example: <tt>'14she'</tt>.
	 * 
	 * @return The base-36 string.
	 * 
	 * @memberof! ws.suid.Suid#
	 */
	Suid.prototype.toString = function Suid_toString() {
		return this.value.toString(36); 
	};

	/** 
	 * Converts this suid to a JSON string.
	 * 
	 * The returned String will be in base-36 format. 
	 * For example: <tt>'14she'</tt>. This method
	 * is called by <tt>JSON.stringify</tt>.
	 * 
	 * @return The JSON string.
	 * 
	 * @memberof! ws.suid.Suid#
	 * 
	 * @see {@link ws.suid.Suid.revive}
	 */
	Suid.prototype.toJSON = function Suid_toJSON() {
		return this.toString();
	};

	/**
	 * Returns the underlying value of this suid.
	 * 
	 * @return The underlying primitive Number value.
	 * 
	 * @memberof! ws.suid.Suid#
	 */
	Suid.prototype.valueOf = function Suid_valueOf() {
		return this.value;
	};
	
	/**
	 * Compares this suid with <tt>that</tt>.
	 * 
	 * @return <tt>-1</tt> when this suid is less than, 
	 *         <tt>0</tt> when it is equal to and 
	 *         <tt>+1</tt> when it is greater than <tt>that</tt>.
	 * 
	 * @memberof! ws.suid.Suid#
	 */
	Suid.prototype.compare = function Suid_compare(that) {
		that = Suid(that);
		return this.value < that.value ? -1 : this.value > that.value ? 1 : 0;
	};
	
	/**
	 * Indicates whether this suid and <tt>that</tt> are equal.
	 * 
	 * @return <tt>true</tt> when the values are equal, <tt>false</tt> otherwise.
	 * 
	 * @memberof! ws.suid.Suid#
	 */
	Suid.prototype.equals = function Suid_equals(that) {
		return this.value === Suid(that).value;
	};

	/**
	 * Indicates whether the given string value looks like a valid suid.
	 * 
	 * If this method returns true, this only indicates that it's 
	 * *probably* valid. There are no guarantees.
	 * 
	 * @param str The string to test.
	 * @return <tt>true</tt> if it looks valid, <tt>false</tt> otherwise.
	 * 
	 * @memberof! ws.suid.Suid
	 */
	Suid.looksValid = function Suid_looksValid(value) {
		if (!value) {return false;}
		var len = value.length;
		if ((!len) || (len > 11)) {return false;}
		if ((len === 11) && (ALPHABET.indexOf(value.charAt(0)) > 2)) {return false;}
		for (var i=0; i<len; i++) {
			if (ALPHABET.indexOf(value.charAt(i)) === -1) {return false;}
		}
		return true;
	};

	/**
	 * Creates a reviver function to be used i.c.w. JSON.parse.
	 * 
	 * Example:
	 * 
	 * <big><pre>
	 * var object = {
	 *   id: Suid(),
	 *   name: 'Example'
	 * };
	 * var obj, json = JSON.stringify(object); 
	 * // json === '{"id":"19b","name":"Example"}'
	 * obj = JSON.parse(json, Suid.revive('id'));   // OR
	 * obj = JSON.parse(json, Suid.revive(['id'])); // OR
	 * obj = JSON.parse(json, Suid.revive(function(key,val){return key === 'id';}));
	 * // obj.id instanceof Suid === true
	 * </pre></big>
	 * 
	 * @param prop The (array of) name(s) of properties to be revived, or an evaluator function.
	 * @returns A reviver function
	 * 
	 * @memberof! ws.suid.Suid
	 */
	Suid.revive = function Suid_revive(prop) {
		var mayRevive = typeof prop === undefined ? function(){return true;} : function(key){return prop.indexOf(key) !== -1;};
		if (typeof prop == 'string') {prop = [prop];}
		if (typeof prop == 'function') {mayRevive = prop;}
		return function reviver(key, value) {
			return mayRevive(key, value) && Suid.looksValid(value) ? new Suid(value) : value;
		};
	};

	/**
	 * Generates the next suid.
	 * 
	 * @return The next new suid.
	 * 
	 * @memberof! ws.suid.Suid
	 */
	Suid.next = function Suid_next() {
		var pool = Pool.get();
		if ((pool.length < config.min) || ((!currentBlock && pool.length === config.min))) {
			if (config.server) {Server.fetch();}
			else {log && log.warn('No suid server configured. Please add the data-suid-server attribute to the script tag or call Suid.config before generating IDs.');}
		}
		if (! currentBlock) {
			if (pool.length === 0) {
				throw new Error('Unable to generate IDs. Suid block pool exhausted.');
			}
			currentId = 0;
			currentBlock = pool.splice(0, 1)[0];
			Pool.set(pool);
		}

		var result = currentBlock + currentId * SHARDSIZE;
		currentId++;
		if (currentId >= IDSIZE) {
			currentBlock = null;
		}
		return new Suid(result);
	};

	/**
	 * Configures the suid generator or gets the current config.
	 * 
	 * <p>This method can be used as an alternative for, or in addition to, specifying
	 * the configuration in the <tt>data-suid-server</tt> and <tt>data-suid-options</tt>
	 * script attributes.</p>
	 * 
	 * <p><b>Examples:</b></p>
	 * 
	 * <code><pre>
	 * // Assuming no config was set yet... (defaults)
	 * var config = Suid.config();       // config => {server:'/suid/suid.json', min:3, max:4}  
	 * 
	 * Suid.config({
	 *     server: '/suid.json',
	 *     min: 2,
	 *     max: 6,
	 *     seed: ['14she', '14sky']
	 * });
	 * 
	 * // The seed is used to fill the pool and then discarded: 
	 * var config = Suid.config();       // config => {server:'/suid.json', min:2, max:6}
	 * 
	 * // Config does a merge:  
	 * config = Suid.config({max: 8});   // config => {server:'/suid.json', min:2, max:8}
	 * </pre></code> 
	 * 
	 * @return The current config after the given <tt>cfg</tt> object has been processed, if any.
	 * 
	 * @memberof! ws.suid.Suid
	 */
	Suid.config = function Suid_config(cfg) {
		if (cfg) {
			config = merge(config, cfg);
			if (cfg.seed) {
				var pool = Pool.get();
				pool.push(cfg.seed);
				Pool.set(pool);
				delete config.seed;
			}
			Suid.ready();
		}
		return config;
	};

	/**
	 * Indicates if Suid is ready to generate IDs, attaches the given callback listener.
	 * 
	 * <p>This method can be used to find out whether Suid is ready to generate ID's, or
	 * to attach an event listener to the ready event. The given <tt>callback</tt> function
	 * is guaranteed to fire asynchronously, meaning this method will always return before
	 * the callback is fired. Due to this, this method may already return <tt>true</tt> even
	 * though the ready event hasn't fired yet.</p>
	 * 
	 * <p><b>Examples:</b></p>
	 * 
	 * <code><pre>
	 * if (Suid.ready()) {
	 *   // Suid is ready!
	 * } else {
	 *   // Suid is not ready yet...
	 * }
	 * 
	 * Suid.ready(function(){
	 *   // Suid is ready!
	 * });
	 * </pre></code>
	 * 
	 * @param callback The optional callback function that will be called once Suid is ready.
	 * @return <tt>true</tt> if Suid is ready, <tt>false</tt> otherwise.
	 */
	Suid.ready = function Suid_ready(callback) {
		var ready = Pool.get().length > 0;
		if (callback) {readyListeners.push(callback);}
		if (ready) {setTimeout(function(){for (var l; l=readyListeners.shift();) {l();}}, 0);}
		// give client 100ms to configure the server using Suid.configure
		else {setTimeout(function(){Server.fetch();}, 100);}
		return ready;
	};

	return Suid;
})();

var Pool = (function(){
	var pool = [];
	return {
		get: function() {
			if (localStorageSupported) {
				pool = Pool.from(localStorage.getItem(POOL));
			}
			return pool;
		},
		set: function(values){
			pool = values;
			if (localStorageSupported) {
				localStorage.setItem(POOL, Pool.to(pool));
			}
			return Pool;
		},
		from: function(str){
			var results = [];
			if (str) {
				var strings = str.split(',');
				for (var i=0, s; s=strings[i]; i++) {
					results.push(new Suid(s));
				}
			}
			return results;
		},
		to: function(obj){
			return obj.join(',');
		}
	};
})();

var Server = (function(){
	var retries = 0,
		started = 0;

	function handleSuccess(text) {
		retries = 0;
		var pool = Pool.get();
		pool.push(JSON.parse(text));
		Pool.set(pool);
		Suid.ready();
	}

	function handleError(status, request) {
		// status code 5xx ? possibly recoverable.
		switch(status) {
			case 500: // Internal server error
			case 502: // Bad Gateway
			case 503: // Service unavailable
			case 504: // Gateway Timeout
				retry(request);
				break;
			default: // unrecoverable? give up 
				log && log.error('Unable to fetch suid data from server. ', request, status);
				retries = 0;
		}
	}

	function retry(request) {
		if (retries === 0) {
			log && log.error('Giving up fetching suid data from server: ' + config.server);
			return;
		}

		retries--;
		var after = 300000; // 5 minutes
		var retryAfter = request.getResponseHeader('Retry-After');
		if (retryAfter) {
			after = parseInt(retryAfter, 10);
			if (! isNaN(after)) {
				after = after * 1000; // seconds to ms.
			}
		}
		// Is this urgent?
		if (! Pool.get().length) { // Pool is out of blocks
			if (after > 60000) {
				after = 60000; // 1 min
			}
		}
		if (currentId > (IDSIZE/2)) { // less than half of current block left
			if (after > 30000) {
				after = 30000; // 30 sec
			}
		}
		if (! currentBlock) { // completely out
			if (after > 2000) {
				after = 2000; // 2 sec
			}
		}
		
		setTimeout(function(){
			ajax(config.server, {blocks: config.max - Pool.get().length}, handleSuccess, handleError);
		}, after);
	}

	function ajax(url, data, success, error, sync) {
		var xhr = new XMLHttpRequest(), query = [], params, key; 
		for (key in data) {
			if (data.hasOwnProperty(key)) {
				query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
			}
		}
		params = query.join('&');
		xhr.open('get', url + (params ? (url.indexOf('?') !== -1 ? '&' : '?') + params : ''), !sync); 
		xhr.addEventListener('readystatechange', function(){ 
			if (this.readyState === 4) { 
				this.status === 200 ? success(this.responseText, this) : error(this.status, this);
			}
		}); 
		xhr.addEventListener('error', function () { 
			error(this, this.status); 
		}); 
		xhr.send(); 
	}

	return {
		fetch: function Server_fetch() {
			if (retries && ((new Date().getTime() - started < THROTTLE) || 
							(currentId && (currentId < (IDSIZE/2))))) {
				return; // already fetching and still recent or not urgent
			} 
			var pool = Pool.get();
			if (pool.length < config.min) {
				retries = 3;
				started = Date.now();
				ajax(config.server, {blocks: config.max - pool.length}, handleSuccess, handleError);
			}
		}
	};
})();

function merge() {
	var res={}, i, arg, key;
	for (i=0; arg=arguments[i++];) {
		for (key in arg) {res[key] = arg[key];}
	}
	return res;
}

function getConfig(cfg, def) {
	var options = {},
		config = merge(def, cfg),
		script = document.querySelector('script[data-suid-server]');
	if (!script) {script = document.querySelector('script[data-suid-options]');}
	if (script) {
		var attr = script.getAttribute('data-suid-options');
		if (attr) {
			try {
				options = JSON.parse(attr.split('\'').join('"'));
			}
			catch(error) {
				log && log.error('Unable to parse suid options as JSON: \'' + attr + '\'. Error was: ', error);
			}
		}
		options.url = script.getAttribute('data-suid-server');
		config = merge(config, options);
	}
	config.configured = cfg || script;
	return config;
}

if (config.configured) {Suid.configure(config);}
else {Suid.ready();}

log && log.info('Suid.js started.');

// EXPOSE
return Suid;

}));