diff --git a/Readme.md b/Readme.md index 04e20f8..58d996c 100644 --- a/Readme.md +++ b/Readme.md @@ -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( ) @@ -20,28 +37,56 @@ 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, @@ -49,11 +94,20 @@ start of the epoch (1970-01-01 UTC) // 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 diff --git a/mongoid.js b/mongoid.js index 5273adc..2482aa8 100644 --- a/mongoid.js +++ b/mongoid.js @@ -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 @@ -23,6 +23,7 @@ module.exports = MongoId; module.exports.mongoid = mongoid; module.exports.MongoId = MongoId; +module.exports._singleton = globalSingleton; var globalSingleton = null; @@ -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() { @@ -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 @@ -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]; }; @@ -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), @@ -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()); }; diff --git a/package.json b/package.json index 1142673..2a6a19f 100644 --- a/package.json +++ b/package.json @@ -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" @@ -30,6 +30,6 @@ "dependencies": { }, "devDependencies": { - "nodeunit": "0.9.0" + "qnit": "*" } } diff --git a/test/test-mongoid.js b/test/test-mongoid.js index 665236c..ab28e71 100644 --- a/test/test-mongoid.js +++ b/test/test-mongoid.js @@ -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(); }, @@ -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 = { @@ -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); @@ -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;