/**!
* 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;
}));