node-hilo
Version:
NHibernate-style hi/lo ID generator for node.js & SQL Server
132 lines (125 loc) • 3.63 kB
JavaScript
"use strict";
const machina = require( "machina" );
const bigInt = require( "big-integer" );
const tedious = require( "tedious" );
const HiloGenerationError = require( "./HiloGenerationError" );
const fs = require( "fs" );
const path = require( "path" );
const createTediousConfig = require( "./createTediousConfig" );
module.exports = ( { sql, hilo: { maxLo = 100, maxRetryDelay = 5000, table = "dbo.hibernate_unique_key" } = {} } ) => {
const tediousConfig = createTediousConfig( sql );
let retryDelay = 1;
var fsm = new machina.Fsm( {
initialize() {
const query = fs.readFileSync( path.join( __dirname, "/sql/nexthi.sql" ), "utf8" ).replace( /{{TABLE}}/g, table );
this.getNextHival = function() {
return new Promise( ( resolve, reject ) => {
const request = new tedious.Request( query, ( requestError, rowCount, results ) => {
if ( requestError ) {
return reject( requestError );
}
const [ { next_hi: { value } } ] = results;
resolve( value );
} );
this.connection.execSql( request );
} );
};
},
initialState: "uninitialized",
_transitionToFailure( err ) {
this.err = err;
this.transition( "dbFailure" );
},
_processHiValResponse( val ) {
if ( val === null || typeof val === "undefined" || val.length === 0 ) {
return this._transitionToFailure( new HiloGenerationError( `Invalid hival returned from database: ${ val }` ) );
}
this.hival = bigInt( val );
this.lo = ( this.hival.equals( 0 ) ) ? 1 : 0;
this.hi = this.hival.times( maxLo + 1 );
retryDelay = 1;
this.transition( "ready" );
},
states: {
uninitialized: {
nextId() {
this.deferUntilTransition();
this.transition( "connecting" );
}
},
connecting: {
_onEnter() {
try {
this.connection = new tedious.Connection( tediousConfig );
this.connection.once( "connect", connError => {
if ( connError ) {
this.err = connError;
return this.handle( "connectionFailure" );
}
this.connection.once( "error", () => {
this.connection = null;
} );
this.connection.once( "end", () => {
this.connection = null;
} );
this.handle( "connectionSuccess" );
} );
this.connection.connect();
} catch ( e ) {
this.err = e;
this.handle( "connectionFailure" );
}
},
nextId() {
this.deferUntilTransition();
},
connectionFailure: "dbFailure",
connectionSuccess: "acquiring"
},
acquiring: {
_onEnter() {
this.getNextHival()
.then( this._processHiValResponse.bind( this ), this._transitionToFailure.bind( this ) );
},
nextId() {
this.deferUntilTransition();
}
},
dbFailure: {
_onEnter() {
this.timer = setTimeout( () => {
this.handle( "clearErrorState" );
}, retryDelay );
retryDelay = Math.min( retryDelay * 2, maxRetryDelay );
},
clearErrorState: "uninitialized",
_onExit() {
clearTimeout( this.timer );
},
nextId( done ) {
done( this.err || new HiloGenerationError( "An unknown error has occurred." ) );
}
},
ready: {
nextId: function getNextId( done ) {
const val = this.hi.add( this.lo++ );
if ( this.lo > maxLo ) {
this.transition( this.connection ? "acquiring" : "connecting" );
}
done( null, val.toString() );
}
}
},
nextId() {
return new Promise( ( resolve, reject ) => {
this.handle( "nextId", ( err, val ) => {
if ( err ) {
return reject( err );
}
resolve( val );
} );
} );
}
} );
return fsm;
};