Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Typescript

A Unique Identifier Usable Across Disconnected Systems

5.00/5 (3 votes)
19 Apr 2024MIT2 min read 7.6K  
Just a simple routine and helper routine to generate unique IDs suitable for disconnected systems.
It's straightforward enough to generate a unique ID in a closed system, but every now and again one may need to generate a unique ID that stays unique across different systems, regardless if they're in a connected state or not. Had to cook up this shiny routine to handle just that and well here it is in case anyone else wants to save a few keystrokes.

Introduction

Typically when someone wants to create a unique identifier that works across distributed/disconnected systems a UUID (GUID in Microsoft parlance) is leveraged. In the past, for JavaScript/TypeScript in Node, we had to use a UUID library for this or write one ourselves. These days, we have native a implementation of version 4 of the UUID specification using Crypto.randomUUID however. As such, we should take advantage of that.

Now, UUIDs are great, but they're also long, 36 characters in fact. In order to help with that, here is a routine and associated helper routine to generate a UUID but shorten it into a 24 character case sensitive string first. It also adds a teeny, tiny bit of extra entropy in the process. And while it's a straightforward process; hopefully, it'll help save you a few keystrokes should you need this functionality.

Also, given the fact that Crypto.randomUUID still isn't ubiquitously supported on older browsers, this code should be considered server-side. Be aware it's only available in Node 19 or higher. But, using the native implantation also means this code runs fast enough to generate 100,000 IDs in 340ms in a WSL environment.

Profiled via:

TypeScript
// result = 340 ms
console.time('createUniqueId');
for (let x = 0; x < 100000; x++) createUniqueId();
console.timeEnd('createUniqueId');

The Goodies

This exposes two functions, one for the ID generation and one to convert a number into base62 to keep it short as possible. This follows in-line with sites such as YouTube or URL shorteners where otherwise longer IDs are kept short. JavaScript natively supports up to base36, but not 62. So, we have to roll that one ourselves.

It's implemented as an ESM module that's intended to run on the server via Node. In theory this code could be used on the client side as well after transpilation into JavaScript; however, at the time of this writing Crypto.randomUUID isn't still widely supported in older browsers. Using a polyfill would defeat the purpose since there are already uuid libraries out there and the goal here is speed. So, server-side for the win given the native implementation should run faster.

TypeScript
import crypto from 'node:crypto';

/**
 * This module contains routines to assist with creating unique identifiers.
 * @module Identify
 */

// used for the base62 conversion
const CHARSET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

// no need to go to Number.MAX_SAFE_INTEGER for this as 65,535 is plenty of entropy
const MAX_COUNT = 0xFFFF;

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * This will calculate a UUID that is able to identify a resource in a globally safe manner. It should be unique
 * and safe to distribute across disconnected systems. To save space however, the UUID is converted to base 62
 * after removing any unnecessary characters such as hyphens. Do be aware that it uses version 4 of the UUID
 * specification, which has a teeny, tiny chance of a collision.
 *
 * The odds of collision between any two UUIDs is 1 in 2.71 x 10^18 though. To put it another way, one would need
 * to generate 1 billion v4 UUIDs per second for 85 years to have a 50% chance of a single collision. So, the odds
 * are very low; however, this routine will also internally store an incremented counter and add it to part of
 * the result. This will effectively guarantee the returned ID will be unique across any system.
 *
 * The nature of base 62 encoding means the returned ID is case sensitive.
 * @see {@link https://en.wikipedia.org/wiki/Universally_unique_identifier WikiPedia} for more information on the
 * v4 UUID format.
 * @returns {string} Returns a globally unique ID encoded as a base 62 string or null on error.
 */

export function createUniqueId(): string | null {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const self = createUniqueId as any;
  const count: number = (self.count && (self.count < MAX_COUNT)) ? self.count : 0;

  let result = '';
  const uuid = crypto.randomUUID();
  const parts = uuid.split('-');

  if (parts.length === 5) {
    // do not use nullish coalescing for these, also only add the counter to the second
    // part as doing this will allow us to deconstruct the UUID later on if needed
    const one = toBase62(parseInt(parts[0], 16) || 0).padStart(6, '0');
    const two = toBase62((parseInt(parts[1], 16) || 0) + count).padStart(3, '0');
    const three = toBase62(parseInt(parts[2], 16) || 0).padStart(3, '0');
    const four = toBase62(parseInt(parts[3], 16) || 0).padStart(3, '0');
    const five = toBase62(parseInt(parts[4], 16) || 0).padStart(9, '0');

    result = `${one}${two}${three}${four}${five}`;
  }

  // we have to store this here to have it available for the next invocation of this routine
  self.count = count + 1;

  return result || null;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * This will a encode an integral number as a base62 value, that is to say a value that uses all the characters
 * 0-9, a-z, and A-Z to represent a digit. By the nature of mixing lowercase and uppercase letters, the return
 * value is inherently case sensitive. The algorithm used was borrowed from the `base62.js` library but we added
 * one extra sanity check for decimals and leverage ES6 string indexing to avoid the array conversion.
 *
 * Also note, that when using letters as digits, as with hex, the uppercase version of a letter was traditionally
 * used. But, in modern times the lowercase version is used. Since both cases are used in base62, we start with
 * the lowercase version to better align with more modern formats. As such, the number `10` would equal `a` and
 * not `A`. This is important to be aware of because unlike with hex case matters in base 62.
 * @see {@link https://github.com/base62/base62.js base62.js} for more information on the basis of this algorithm.
 * @see {@link https://dev.to/joshduffney/what-is-base62-conversion-13o0 dev.to} for even more information about
 * the algorithm being implemented.
 * @see {@link https://microsoft.github.io/makecode-csp/unit-6/day-14/base63-url-shorteners Microsoft's GitHub Page}
 * for even more, more information about base62 and how they are used in URL shorteners.
 * @see {@link https://normaltool.com/dencoders/base62-encoder normaltool.com} for an online convert to test with.
 * @returns {string} Returns a case sensitive base 62 number as a string.
 */

export function toBase62(number: number): string {
  let n = Math.floor(number);
  if (n === 0) return CHARSET[0];

  let result = '';

  // js engines will optimize the division these days
  while (n > 0) {
    result = CHARSET[n % 62] + result;
    n = Math.floor(n / 62);
  }

  return result;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Using the code

Using the code is pretty straightforward, as one routine takes zero parameters and the other just takes one.

TypeScript
createUniqueId() // returns something like '2JoUqI7KA4PYaOZ12LayQjMO'
toBase62(30456) // returns '7Ve'

Credits

The algorithm for the base62 conversation came from base62.js. However, it was modified to leverage ES6 string indexing to avoid the array cast and adds an additional sanity check.

History

2024-04-19: Initial release.
2024-04-25: Updated introduction.

License

This article, along with any associated source code and files, is licensed under The MIT License