Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ulid): port /x/ulid module #3582

Merged
merged 30 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
61692fb
feat(ulid): port kt3k's ulid module
lino-levan Aug 26, 2023
e7566ee
chore: fmt
lino-levan Aug 26, 2023
7e0f57a
chore: address iuioiua's comments
lino-levan Aug 28, 2023
9532da2
Merge branch 'main' into feat-ulid
kt3k Aug 29, 2023
5e0cfdc
Merge branch 'main' into feat-ulid
lino-levan Aug 29, 2023
2c1c9fe
fix doc type errors
kt3k Aug 29, 2023
e87c39d
fix typo in test name
kt3k Aug 29, 2023
6380e32
Merge branch 'main' into feat-ulid
lino-levan Aug 30, 2023
6645d05
Merge branch 'main' into feat-ulid
lino-levan Sep 1, 2023
dd5011d
chore: rework ulid module to only export three items
lino-levan Sep 1, 2023
a5159d6
Merge branch 'main' into feat-ulid
lino-levan Sep 1, 2023
c3416ed
chore: possibly fix CI failure
lino-levan Sep 1, 2023
94d6d10
chore: really fix ci failures
lino-levan Sep 1, 2023
64f2245
chore: fix test import
lino-levan Sep 1, 2023
eae5dc4
Merge branch 'main' into feat-ulid
lino-levan Sep 3, 2023
6006727
perf: improve performance by like 10x
lino-levan Sep 3, 2023
b266ffe
chore: fmt
lino-levan Sep 3, 2023
a6ccf64
chore: fix outdated jsdoc
lino-levan Sep 3, 2023
e45a9af
Update ulid/_util.ts
lino-levan Sep 3, 2023
30cf5f7
Update ulid/_util.ts
lino-levan Sep 3, 2023
ef26f18
Update ulid/_util.ts
lino-levan Sep 3, 2023
06d3ac5
chore: remove unnecessary factory and fmt
lino-levan Sep 3, 2023
8596de6
chore: fix bug with incrementbase32
lino-levan Sep 4, 2023
679c3aa
Merge branch 'main' into feat-ulid
lino-levan Sep 4, 2023
cc18a1d
Merge branch 'main' into feat-ulid
lino-levan Sep 5, 2023
a311232
Merge branch 'main' into feat-ulid
lino-levan Sep 6, 2023
100c2eb
Merge branch 'main' into feat-ulid
lino-levan Sep 6, 2023
1742eaa
Merge branch 'main' into feat-ulid
lino-levan Sep 9, 2023
4d8073c
chore: add test
lino-levan Sep 9, 2023
3419320
chore: fmt
lino-levan Sep 9, 2023
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
78 changes: 78 additions & 0 deletions ulid/_util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

import type { PRNG } from "./mod.ts";

// These values should NEVER change. If
// they do, we're no longer making ulids!
export const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32
export const ENCODING_LEN = ENCODING.length;
export const TIME_MAX = Math.pow(2, 48) - 1;
export const TIME_LEN = 10;
export const RANDOM_LEN = 16;

export function replaceCharAt(str: string, index: number, char: string) {
return str.substring(0, index) + char + str.substring(index + 1);
}

export function randomChar(prng: PRNG): string {
let rand = Math.floor(prng() * ENCODING_LEN);
if (rand === ENCODING_LEN) {
rand = ENCODING_LEN - 1;
}
return ENCODING.charAt(rand);
}

export function encodeTime(now: number, len: number = TIME_LEN): string {
if (now > TIME_MAX) {
throw new Error("cannot encode time greater than " + TIME_MAX);
}
if (now < 0) {
throw new Error("time must be positive");
}
if (Number.isInteger(now) === false) {
throw new Error("time must be an integer");
}
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
let str = "";
for (; len > 0; len--) {
const mod = now % ENCODING_LEN;
str = ENCODING[mod] + str;
now = (now - mod) / ENCODING_LEN;
}
return str;
}

export function encodeRandom(len: number, prng: PRNG): string {
let str = "";
for (; len > 0; len--) {
str = randomChar(prng) + str;
}
return str;
}
lino-levan marked this conversation as resolved.
Show resolved Hide resolved

export function detectPrng(): PRNG {
return () => {
const buffer = new Uint8Array(1);
crypto.getRandomValues(buffer);
return buffer[0] / 0xff;
};
}

export function incrementBase32(str: string): string {
let index = str.length;
let char;
let charIndex;
const maxCharIndex = ENCODING_LEN - 1;
while (index-- >= 0) {
char = str[index];
charIndex = ENCODING.indexOf(char);
if (charIndex === -1) {
throw new Error("incorrectly encoded string");
}
if (charIndex === maxCharIndex) {
str = replaceCharAt(str, index, ENCODING[0]);
continue;
}
return replaceCharAt(str, index, ENCODING[charIndex + 1]);
}
throw new Error("cannot increment this string");
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
}
129 changes: 129 additions & 0 deletions ulid/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Copyright 2023 Yoshiya Hinosawa. All rights reserved. MIT license.
// Copyright 2017 Alizain Feerasta. All rights reserved. MIT license.
// This module is browser compatible.

/**
* @module
* @example
* ```ts
* import { ulid } from "https://deno.land/std@$STD_VERSION/ulid/mod.ts";
* ulid(); // 01ARZ3NDEKTSV4RRFFQ69G5FAV
* ```
*/

import {
detectPrng,
encodeRandom,
encodeTime,
ENCODING,
ENCODING_LEN,
incrementBase32,
RANDOM_LEN,
TIME_LEN,
TIME_MAX,
} from "./_util.ts";

export interface PRNG {
(): number;
}

export interface ULID {
(seedTime?: number): string;
}

/**
* Extracts the timestamp given a ULID
*/
export function decodeTime(id: string): number {
if (id.length !== TIME_LEN + RANDOM_LEN) {
throw new Error("malformed ulid");
}
const time = id
.substring(0, TIME_LEN)
.split("")
.reverse()
.reduce((carry, char, index) => {
const encodingIndex = ENCODING.indexOf(char);
if (encodingIndex === -1) {
throw new Error("invalid character found: " + char);
}
return (carry += encodingIndex * Math.pow(ENCODING_LEN, index));
}, 0);
if (time > TIME_MAX) {
throw new Error("malformed ulid, timestamp too large");
}
return time;
}

/**
* Generates a ULID function given a PRNG
*
* @example To use your own pseudo-random number generator, import the factory, and pass it your generator function.
* ```ts
* import { factory } from "https://deno.land/std@$STD_VERSION/ulid/mod.ts";
* import prng from "somewhere";
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
*
* const ulid = factory(prng);
* ulid(); // 01BXAVRG61YJ5YSBRM51702F6M
* ```
*/
export function factory(prng: PRNG = detectPrng()): ULID {
return function ulid(seedTime: number = Date.now()): string {
return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN, prng);
};
}

/**
* Generates a monotonically increasing ULID, optionally given a PRNG.
*
* @example To generate monotonically increasing ULIDs, create a monotonic counter.
* ```ts
* import { monotonicFactory } from "https://deno.land/std@$STD_VERSION/ulid/mod.ts";
*
* const ulid = monotonicFactory();
* // Strict ordering for the same timestamp, by incrementing the least-significant random bit by 1
* ulid(150000); // 000XAL6S41ACTAV9WEVGEMMVR8
* ulid(150000); // 000XAL6S41ACTAV9WEVGEMMVR9
* ulid(150000); // 000XAL6S41ACTAV9WEVGEMMVRA
* ulid(150000); // 000XAL6S41ACTAV9WEVGEMMVRB
* ulid(150000); // 000XAL6S41ACTAV9WEVGEMMVRC
*
* // Even if a lower timestamp is passed (or generated), it will preserve sort order
* ulid(100000); // 000XAL6S41ACTAV9WEVGEMMVRD
* ```
*
* @example You can also pass in a prng to the monotonicFactory function.
* ```ts
* import { monotonicFactory } from "https://deno.land/std@$STD_VERSION/ulid/mod.ts";
* import prng from "somewhere";
lino-levan marked this conversation as resolved.
Show resolved Hide resolved
*
* const ulid = monotonicFactory(prng);
* ulid(); // 01BXAVRG61YJ5YSBRM51702F6M
* ```
*/
export function monotonicFactory(prng: PRNG = detectPrng()): ULID {
let lastTime = 0;
let lastRandom: string;
return function ulid(seedTime: number = Date.now()): string {
if (seedTime <= lastTime) {
const incrementedRandom = (lastRandom = incrementBase32(lastRandom));
return encodeTime(lastTime, TIME_LEN) + incrementedRandom;
}
lastTime = seedTime;
const newRandom = (lastRandom = encodeRandom(RANDOM_LEN, prng));
return encodeTime(seedTime, TIME_LEN) + newRandom;
};
}

/**
* @example
* ```ts
* import { ulid } from "https://deno.land/std@$STD_VERSION/ulid/mod.ts";
* ulid(); // 01ARZ3NDEKTSV4RRFFQ69G5FAV
*
* // You can also input a seed time which will consistently give you the same string for the time component
* ulid(1469918176385); // 01ARYZ6S41TSV4RRFFQ69G5FAV
* ```
*/
export const ulid = factory();
Loading