Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ The ids are guaranteed unique on any one server, and can be configured
to be unique across a cluster of up to 16 million (2^24) servers.
Uniqueness is guaranteed by unique {server, process} id pairs.


## Installation

npm install mongoid-js
npm test mongoid-js


## Summary

var mongoid = require('mongoid-js');
var id = mondoid(); // => 543f376340e2816497000001

var MongoId = require('mongoid-js').MongoId;
var idFactory = new MongoId(/*systemId:*/ 0x123);
var id = idFactory.fetch(); // => 543f3789001230649f000001


## Functions

### mongoid( )
Expand All @@ -20,40 +37,77 @@ MongoId singleton initialized with a random machine id. All subsequent calls
to mongoid() in this process will fetch ids from this singleton.

// ids with a randomly chosen system id (here 0x40e281)
var mongoid = require('mongoid-js');
var mongoid = require('mongoid-js').mongoid;
var id1 = mongoid(); // => 543f376340e2816497000001
var id2 = mongoid(); // => 543f376340e2816497000002

### new MongoId( systemId ).fetch( )
### MongoId( systemId )

unique id factory that embeds the given system id in each generated unique id.
By a systematic assignment of system ids to servers, this approach can guarantee
globally unique ids (ie, globally for an installation).

The systemId must be an integer between 0 and 16777215 (0xFFFFFF), inclusive.

// ids with a unique system id
var MongoId = require('mongoid-js').MongoId;
var systemId = 4656;
var idFactory = new MongoId(systemId);

#### id.fetch( )

generate and return the next id in the sequence. Up to 16 million distinct
ids (16777216) can be fetched during the same wallclock second; trying to
fetch more throws an error. The second starts when the clock reads _*000_
milliseconds, not when the first id is fetched. The second ends 1000
milliseconds after the start, when the clock next reads _*000_ milliseconds.

var id1 = idFactory().fetch(); // => 543f3789001230649f000001
var id2 = idFactory().fetch(); // => 543f3789001230649f000002

### MongoId.parse( idString )
#### id.parse( )

same as MongoId.parse(id.toString()), see below

#### id.getTimestamp( )

same as MongoId.getTimestamp(id.toString()), see below

#### id.toString( )

each MongoId object itself can have a distinct unique id, created on demand
when toString() is called. The object invokes itself as an id factory and
fetches for itself the next id in the sequence. The

### Class Methods

#### MongoId.parse( idString )

Decompose the id string into its parts -- unix timestamp, machine id,
process id and sequence number. Unix timestamps are seconds since the
start of the epoch (1970-01-01 UTC)
start of the epoch (1970-01-01 GMT). Note that parse() returns seconds,
while getTimestamp() returns milliseconds.

var parts = MongoId.parse("543f376340e2816497000013");
// => { timestamp: 1413429091,
// machineid: 4252289,
// pid: 25751,
// sequence: 19 }

### MongoId.getTimestamp( idString )
#### MongoId.getTimestamp( idString )

Return just the javascript timestamp part of the id. Javascript timestamps
are milliseconds since the start of the epoch (they are 1000 x more than the
unix timestamp.)
are milliseconds since the start of the epoch. Each mongoid embeds a seconds
precision unix timestamp; getTimestamp() returns that multiplied by 1000.

MongoId.getTimestamp("543f376340e2816497000013");
// => 1413429091000


Change Log
----------

- 1.1.0 - tentative `browserify` support: use a random pid if process.pid is not set, avoid object methods in constructor, 100% unit test coverage
- 1.0.7 - fix getTimestamp and quantize correctly, deprecate index.js, test with qnit, fix sequence wrapping
- 1.0.6 - doc edits
- 1.0.5 - stable, fast version
36 changes: 25 additions & 11 deletions mongoid.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Ids are a hex number built out of the timestamp, a per-server unique id,
* the process id and a sequence number.
*
* Copyright (C) 2014 Andras Radics
* Copyright (C) 2014,2016 Andras Radics
* Licensed under the Apache License, Version 2.0
*
* MongoDB object ids are 12 bytes (24 hexadecimal chars), composed out of
Expand All @@ -23,6 +23,7 @@
module.exports = MongoId;
module.exports.mongoid = mongoid;
module.exports.MongoId = MongoId;
module.exports._singleton = globalSingleton;

var globalSingleton = null;

Expand All @@ -32,24 +33,32 @@ function mongoid( ) {
}
else {
globalSingleton = new MongoId();
module.exports._singleton = globalSingleton;
return globalSingleton.fetch();
}
}

var _getTimestamp = null;
var _getTimestampStr = null;

function MongoId( machineId ) {
// if called as a function, return an id from the singleton
if (this === global || !this) return mongoid();

// if no machine id specified, use a 3-byte random number
if (!machineId) machineId = Math.floor(Math.random() * 0x1000000);
else if (machineId < 0 || machineId > 0x1000000)
else if (machineId < 0 || machineId >= 0x1000000)
throw new Error("machine id out of range 0.." + parseInt(0x1000000));

this.processIdStr = this.hexFormat(machineId, 6) + this.hexFormat(process.pid, 4);
// if process.pid not available, use a random 2-byte number between 10k and 30k
// suggestions for better browserify support from @cordovapolymer at github
var processId = process.pid || 10000 + Math.floor(Math.random() * 20000);

this.processIdStr = hexFormat(machineId, 6) + hexFormat(processId, 4);
this.sequenceId = 0;
this.sequencePrefix = "00000";
this.id = null;
this.sequenceStartTimestamp = this._getTimestamp();
this.sequenceStartTimestamp = _getTimestamp();
}

var timestampCache = (function() {
Expand All @@ -64,19 +73,23 @@ var timestampCache = (function() {
if (!_timestamp || ++_ncalls > 1000) {
_ncalls = 0;
_timestamp = Date.now();
_timestampStr = hexFormat(Math.floor(_timestamp/1000), 8);
setTimeout(function(){ _timestamp = null; }, 10);
var msToNextTimestamp = 1000 - _timestamp % 1000;
setTimeout(function(){ _timestamp = null; }, Math.min(msToNextTimestamp - 1, 100));
_timestamp -= _timestamp % 1000;
_timestampStr = hexFormat(_timestamp/1000, 8);
}
return _timestampStr;
}
return [getTimestamp, getTimestampStr];
})();
MongoId.prototype._getTimestamp = timestampCache[0];
MongoId.prototype._getTimestampStr = timestampCache[1];
_getTimestamp = MongoId.prototype._getTimestamp = timestampCache[0];
_getTimestampStr = MongoId.prototype._getTimestampStr = timestampCache[1];

var _hexDigits = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'];
MongoId.prototype.fetch = function() {
this.sequenceId += 1;
if (this.sequenceId >= 0x1000000) {
// sequence wrapped, we can make an id only if the timestamp advanced
var _timestamp = this._getTimestamp();
if (_timestamp === this.sequenceStartTimestamp) {
// TODO: find a more elegant way to deal with overflow
Expand All @@ -86,8 +99,8 @@ MongoId.prototype.fetch = function() {
this.sequenceStartTimestamp = _timestamp;
}

if (++this.sequenceId % 16 === 0) {
this.sequencePrefix = hexFormat((this.sequenceId / 16 | 0).toString(16), 5);
if ((this.sequenceId & 0xF) === 0) {
this.sequencePrefix = hexFormat((this.sequenceId >>> 4).toString(16), 5);
}
return this._getTimestampStr() + this.processIdStr + this.sequencePrefix + _hexDigits[this.sequenceId % 16];
};
Expand All @@ -106,6 +119,7 @@ MongoId.prototype.toString = function( ) {
};

MongoId.parse = function( idstring ) {
// TODO: should throw an Error not coerce, but is a breaking change
if (typeof idstring !== 'string') idstring = "" + idstring;
return {
timestamp: parseInt(idstring.slice( 0, 0+8), 16),
Expand All @@ -124,7 +138,7 @@ MongoId.prototype.parse = function( idstring ) {
MongoId.getTimestamp = function( idstring ) {
return parseInt(idstring.slice(0, 8), 16) * 1000;
};
MongoId.prototype.getTimestamp = function( idstring ) {
MongoId.prototype.getTimestamp = function( ) {
return MongoId.getTimestamp(this.toString());
};

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"name": "mongoid-js",
"version": "1.0.5",
"version": "1.1.0",
"description": "very fast mongoid compatible unique ids",
"license": "Apache-2.0",
"main": "index.js",
"main": "mongoid.js",
"author": {
"name": "Andras",
"url": "http://github.com/andrasq"
},
"repository": {
"type": "git",
"url": "git://github.com/andrasq/node-mongoid-js"
"url": "git://github.com/andrasq/node-mongoid-js.git"
},
"engines": {
"node": ">=0.0.0"
Expand All @@ -30,6 +30,6 @@
"dependencies": {
},
"devDependencies": {
"nodeunit": "0.9.0"
"qnit": "*"
}
}
85 changes: 84 additions & 1 deletion test/test-mongoid.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module.exports.require = {
module.exports.mongoid_function = {
testShouldReturn24CharHexString: function(test) {
var id = mongoid();
test.ok(id.match(/[0-9a-fA-F]{24}/), "should return a 24-char id string");
test.ok(id.match(/^[0-9a-fA-F]{24}$/), "should return a 24-char id string");
test.done();
},

Expand All @@ -76,6 +76,20 @@ module.exports.mongoid_function = {
test.ok(t2-t1 < 100, "should generate > 100k ids / sec");
test.done();
},

'it should use the global singleton': function(t) {
mongoid();
var called = false;
var actualFetch = mongoid._singleton.fetch;
mongoid._singleton.fetch = function(){
called = true;
return actualFetch.call(mongoid._singleton)
};
mongoid();
mongoid._singleton.fetch = actualFetch;
t.equal(called, true);
t.done();
},
};

module.exports.MongoId_class = {
Expand Down Expand Up @@ -107,6 +121,68 @@ module.exports.MongoId_class = {
test.done();
},

'should throw Error if wrapped in same second': function(t) {
factory = new MongoId(0x111111);
factory.sequenceId = 0xffffff;
factory.sequencePrefix = "fffff";
// note: race condition: this test will fail if the seconds increase before the fetch
t.throws(function(){ factory.fetch() }, 'should throw');
t.done();
},

'should wrap at max id': function(t) {
factory = new MongoId(0x222222);
factory.sequenceId = 0xfffffe;
factory.sequencePrefix = "fffff";
factory.sequenceStartTimestamp -= 1000;
t.equal(factory.fetch().slice(-6), 'ffffff');
t.equal(factory.fetch().slice(-6), '000000');
t.equal(factory.fetch().slice(-6), '000001');
t.done();
},

'id should include timestamp': function(t) {
var t1 = Date.now();
var id = new MongoId().toString();
var timestamp = MongoId.getTimestamp(id);
t.ok(t1 - t1 % 1000 <= timestamp && timestamp <= Date.now());
t.done();
},

'id should include pid': function(t) {
var id = new MongoId().toString();
t.ok(id.indexOf(process.pid.toString(16)) == 14);
t.done();
},

'id should include a random pid if process.pid is not set': function(t) {
var processPid = process.pid;
delete process.pid;
var id = new MongoId().toString();
var pid = parseInt(id.slice(14, 18), 16);
t.ok(pid >= 10000 && pid <= 32767);
process.pid = processPid;
t.done();
},

'it should reject a machine id out of range': function(t) {
t.throws(function(){ new MongoId(-1) });
t.throws(function(){ new MongoId(0xffffff + 1) });
t.done();
},

'_getTimestamp should return second precision timestamps 100ms apart': function(t) {
var factory = new MongoId();
var t1 = factory._getTimestamp();
setTimeout(function(){
var t2 = factory._getTimestamp();
t.equal(t1 % 1000, 0);
t.equal(t2 % 1000, 0);
t.ok(t2 >= t1);
t.done();
}, 100 + 5);
},

testShouldParseId: function(test) {
var timestamp = Math.floor(Date.now()/1000);
var obj = new MongoId(0x123456);
Expand All @@ -118,6 +194,13 @@ module.exports.MongoId_class = {
test.done();
},

testShouldParseNonString: function(t) {
// TODO: should throw, not coerce (but is a breaking change)
var hash = MongoId.parse(0x12345678);
t.equal(hash.timestamp, parseInt(("" + 0x12345678).slice(0, 8), 16));
t.done();
},

testIdShouldContainParsedParts: function(test) {
var obj = new MongoId();
var hexFormat = obj.hexFormat;
Expand Down