├── .gitignore ├── Makefile ├── .github ├── workflows │ ├── eslint.yml │ └── codeql.yml └── codeql │ └── codeql-config.yml ├── package.json ├── LICENSE ├── index.js ├── README.md └── test └── druuid.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | tmp -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = "spec" 2 | 3 | default: install 4 | 5 | install: node_modules 6 | 7 | node_modules: package.json 8 | @npm -s install 9 | 10 | test: 11 | @NODE_ENV=test ./node_modules/.bin/mocha \ 12 | --require "should" \ 13 | --reporter $(REPORTER) \ 14 | --check-leaks \ 15 | 16 | clean: 17 | @rm -rf node_modules 18 | 19 | .PHONY: install test clean 20 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: Security check - ESLint 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - staging 9 | - main 10 | - master 11 | - qa 12 | 13 | jobs: 14 | eslint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: stefanoeb/eslint-action@1.0.2 19 | continue-on-error: true -------------------------------------------------------------------------------- /.github/codeql/codeql-config.yml: -------------------------------------------------------------------------------- 1 | # Recurly Security and Quality checks 2 | # 3 | # Customize this file for your project. 4 | # https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#specifying-directories-to-scan 5 | name: "Recurly Security Tests" 6 | query-filters: 7 | # - exclude: 8 | # id: js/redundant-query 9 | paths-ignore: 10 | - '**/node_modules' 11 | - '**/*test*' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "druuid", 3 | "version": "1.4.0", 4 | "description": "Date-relative UUIDs", 5 | "contributors": [ 6 | "Stephen Celis " 7 | ], 8 | "main": "index.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/recurly/node-druuid.git" 12 | }, 13 | "scripts": { 14 | "test": "make test" 15 | }, 16 | "dependencies": { 17 | "bignum": "^0.13.0" 18 | }, 19 | "devDependencies": { 20 | "mocha": "1.16.0", 21 | "should": "2.1.1" 22 | }, 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Recurly, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Module dependencies. 4 | */ 5 | 6 | var bignum = require('bignum'); 7 | 8 | /** 9 | * The offset from which druuid UUIDs are generated (in milliseconds). 10 | */ 11 | 12 | exports.epoch = 0; 13 | 14 | /** 15 | * Generates a time-sortable, 64-bit UUID. 16 | * 17 | * Examples: 18 | * 19 | * druuid.gen() 20 | * // => 21 | * 22 | * @param {Date} [date=new Date()] of UUID 23 | * @param {Number} [epoch=druuid.epoch] offset 24 | * @return {BigInt} UUID 25 | * @public 26 | */ 27 | 28 | exports.gen = function gen(date, epoch){ 29 | if (!date) date = new Date(); 30 | if (!epoch) epoch = exports.epoch; 31 | var id = bignum(date - epoch).shiftLeft(64 - 41); 32 | return id.or(Math.round(Math.random() * 1e16) % Math.pow(2, 64 - 41)); 33 | }; 34 | 35 | /** 36 | * Determines when a given UUID was generated. 37 | * 38 | * Examples: 39 | * 40 | * druuid.time(11142943683383068069); 41 | * // => Sat Feb 04 2012 00:00:00 GMT-0800 (PST) 42 | * 43 | * @param {BigInt|Number|String} uuid 44 | * @param {Number} [epoch=druuid.epoch] offset 45 | * @return {Date} when UUID was generated 46 | * @public 47 | */ 48 | 49 | exports.time = function(uuid, epoch){ 50 | if (!epoch) epoch = exports.epoch; 51 | var ms = bignum(uuid).shiftRight(64 - 41).toNumber(); 52 | return new Date(ms + epoch); 53 | }; 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-druuid 2 | 3 | Date-relative (and relatively universally unique) UUID generation. 4 | 5 | ## Install 6 | 7 | ``` sh 8 | $ npm install druuid 9 | ``` 10 | 11 | ## Overview 12 | 13 | Druuid generates 64-bit, time-sortable IDs inspired by [Snowflake][1] 14 | and [Instagram][2]. 15 | 16 | [1]: https://github.com/twitter/snowflake 17 | [2]: http://www.tumblr.com/ZElL-wA6vd-t 18 | 19 | 20 | A druuid comprises: 21 | 22 | - A 41-bit timestamp (which has millisecond precision for over 69 years 23 | after a defined epoch); and 24 | 25 | - 23 random bits. 26 | 27 | 28 | For example, a druuid generated at midnight on February 4, 2012, may 29 | look something like 11142943683383068069. In binary: 30 | 31 | | Timestamp | Randomness | 32 | |-------------------------------------------|-------------------------| 33 | | 10011010101000111011000000111110000000000 | 01110110000010110100101 | 34 | 35 | This ID can be displayed compactly in base 36: 2cnpvvfkm56ed. 36 | 37 | 38 | ### Pros 39 | 40 | - 64-bit IDs can be stored in BIGINT database columns, which are 41 | generally more efficient to index (and index uniquely) than VARCHAR. 42 | 43 | - The timestamp component allows for efficient date-based queries and 44 | easy cursor-based pagination. 45 | 46 | 47 | ### Cons 48 | 49 | - 23 bits of randomness contains much less entropy than traditional, 50 | 128-bit UUIDs, so precautions must be taken to avoid collisions 51 | between druuids generated in the same millisecond (e.g., a 52 | database constraint). The probability, within a millisecond, can be 53 | calculated with [the Birthday problem][3] (where n is the 54 | number of IDs generated per millisecond and 23 represents the number 55 | of random bits): 56 | 57 | p(n)≈1-e^(-(n^2)/(2*2^23)) 58 | 59 | IDs generated in different milliseconds cannot collide, but at a rate 60 | of 10 IDs per millisecond (10,000 IDs per second), the probability a 61 | collision will occur within any given millisecond approaches 62 | 0.000596%, which is about once every few minutes if that rate is 63 | constant. 64 | 65 | [3]: http://en.wikipedia.org/wiki/Birthday_problem 66 | 67 | ## Examples 68 | 69 | ``` js 70 | var druuid = require('druuid'); 71 | // druuid.epoch = Date.UTC(1970, 0); // change the default (Unix) epoch 72 | 73 | var uuid = druuid.gen(); 74 | // => 75 | druuid.time(uuid); 76 | // => Sat Feb 04 2012 00:00:00 GMT-0800 (PST) 77 | ``` 78 | 79 | ## License 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # Managed by Security 2 | name: "Security check - CodeQL" 3 | 4 | on: 5 | pull_request: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - staging 10 | - main 11 | - master 12 | - qa 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | language: [ 'javascript' ] 27 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 28 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v3 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v2 37 | with: 38 | languages: ${{ matrix.language }} 39 | config-file: ./.github/codeql/codeql-config.yml 40 | # If you wish to specify custom queries, you can do so here or in a config file. 41 | # By default, queries listed here will override any specified in a config file. 42 | # Prefix the list here with "+" to use these queries and those in the config file. 43 | 44 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 45 | queries: + security-and-quality 46 | 47 | 48 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 49 | # If this step fails, then you should remove it and run the build manually (see below) 50 | - name: Autobuild 51 | uses: github/codeql-action/autobuild@v2 52 | 53 | # ℹ️ Command-line programs to run using the OS shell. 54 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 55 | 56 | # If the Autobuild fails above, remove it and uncomment the following three lines. 57 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 58 | 59 | # - run: | 60 | # echo "Run, Build Application using script" 61 | # ./location_of_script_within_repo/buildscript.sh 62 | 63 | - name: Perform CodeQL Analysis 64 | uses: github/codeql-action/analyze@v2 65 | continue-on-error: true 66 | with: 67 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /test/druuid.test.js: -------------------------------------------------------------------------------- 1 | 2 | var druuid = require('..') 3 | , bignum = require('bignum'); 4 | 5 | describe('druuid', function(){ 6 | describe('.gen', function(){ 7 | it('generates a UUID', function(){ 8 | var uuid = druuid.gen(); 9 | uuid.should.be.instanceOf(bignum); 10 | uuid.should.not.equal(druuid.gen()); 11 | }); 12 | 13 | var datetime = new Date(Date.UTC(2012, 1, 4, 8)) 14 | , prefix = '111429436833'; 15 | context('with a given time', function(){ 16 | it('generates the UUID against the time', function(){ 17 | var uuid = druuid.gen(datetime).toString(); 18 | uuid.substr(0, 12).should.equal(prefix); 19 | }); 20 | }); 21 | 22 | var offset = 1000 * 60 * 60 * 24; 23 | context('with a given epoch', function(){ 24 | it('generates the UUID against the offset', function(){ 25 | var dateoffset = new Date(datetime); 26 | dateoffset.setMilliseconds(dateoffset.getMilliseconds() + offset); 27 | var uuid = druuid.gen(dateoffset, offset).toString(); 28 | uuid.substr(0, 12).should.equal(prefix); 29 | }); 30 | }); 31 | 32 | context('with a default epoch', function(){ 33 | var oldEpoch; 34 | before(function(){ oldEpoch = druuid.epoch, druuid.epoch = offset; }); 35 | it('generates the UUID against the offset', function(){ 36 | var dateoffset = new Date(datetime); 37 | dateoffset.setMilliseconds(dateoffset.getMilliseconds() + offset); 38 | var uuid = druuid.gen(dateoffset).toString(); 39 | uuid.substr(0, 12).should.equal(prefix); 40 | }); 41 | after(function(){ druuid.epoch = oldEpoch; }); 42 | }); 43 | }); 44 | 45 | describe('.time', function(){ 46 | var datetime = new Date(Date.UTC(2012, 1, 4, 8)) 47 | , uuid = '11142943683383068069'; 48 | it('determines when a UUID was generated', function(){ 49 | druuid.time(uuid).should.eql(datetime); 50 | }); 51 | 52 | var offset = 1000 * 60 * 60 * 24 53 | , dateoffset = new Date(datetime); 54 | dateoffset.setMilliseconds(dateoffset.getMilliseconds() + offset); 55 | context('with a given epoch', function(){ 56 | it('determines UUID date against the offset', function(){ 57 | druuid.time(uuid, offset).should.eql(dateoffset); 58 | }); 59 | }); 60 | 61 | context('with a default epoch', function(){ 62 | var oldEpoch; 63 | before(function(){ oldEpoch = druuid.epoch, druuid.epoch = offset; }); 64 | it('generates the UUID against the offset', function(){ 65 | druuid.time(uuid).should.eql(dateoffset); 66 | }); 67 | after(function(){ druuid.epoch = oldEpoch; }); 68 | }); 69 | }); 70 | }); 71 | --------------------------------------------------------------------------------