summaryrefslogtreecommitdiffstats
path: root/javascript/src/snowflake.ts
diff options
context:
space:
mode:
authorEthan Werner <mail@ethanwerner.com>2026-08-07 17:17:32 +0400
committerEthan Werner <mail@ethanwerner.com>2026-08-07 17:17:32 +0400
commite4e84c7aad9fb887150ba1fcfaf64463765419c9 (patch)
treecdafa934aa61d8b646435d2a7b4ed0d7ac5c9f55 /javascript/src/snowflake.ts
parente0f9cc69f9f7f524610d65b288eecdbf2ad958d6 (diff)
javascript portHEADmaster
Diffstat (limited to 'javascript/src/snowflake.ts')
-rw-r--r--javascript/src/snowflake.ts169
1 files changed, 169 insertions, 0 deletions
diff --git a/javascript/src/snowflake.ts b/javascript/src/snowflake.ts
new file mode 100644
index 0000000..5da7683
--- /dev/null
+++ b/javascript/src/snowflake.ts
@@ -0,0 +1,169 @@
+const _crypto: Crypto = globalThis.crypto ?? (globalThis as any).crypto ?? (await import("node:crypto")).webcrypto;
+
+const ID_BYTE_SIZE = 16;
+const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+const BASE = BigInt(ALPHABET.length);
+
+class InvalidEncodingError extends Error {
+ constructor(message = "snowflake: invalid encoding string") {
+ super(message);
+ this.name = "InvalidEncodingError";
+ }
+}
+
+class InvalidIDLengthError extends Error {
+ constructor(message = "snowflake: invalid ID byte length") {
+ super(message);
+ this.name = "InvalidIDLengthError";
+ }
+}
+
+const DECODE_LUT = new Map<string, number>();
+for (let i = 0; i < ALPHABET.length; i++) {
+ DECODE_LUT.set(ALPHABET[i], i);
+}
+
+function bytesToBigInt(bytes: Uint8Array): bigint {
+ let result = 0n;
+ for (const b of bytes) {
+ result = (result << 8n) | BigInt(b);
+ }
+ return result;
+}
+
+function bigIntToBytes(value: bigint, length: number): Uint8Array {
+ const bytes = new Uint8Array(length);
+ for (let i = length - 1; i >= 0; i--) {
+ bytes[i] = Number(value & 0xffn);
+ value >>= 8n;
+ }
+ return bytes;
+}
+
+class Snowflake {
+ readonly bytes: Uint8Array;
+
+ constructor(bytes: Uint8Array) {
+ this.bytes = bytes;
+ }
+
+ static generate(): Snowflake {
+ const bytes = new Uint8Array(ID_BYTE_SIZE);
+ _crypto.getRandomValues(bytes);
+ return new Snowflake(bytes);
+ }
+
+ static zero(): Snowflake {
+ return new Snowflake(new Uint8Array(ID_BYTE_SIZE));
+ }
+
+ isZero(): boolean {
+ for (const b of this.bytes) {
+ if (b !== 0) return false;
+ }
+ return true;
+ }
+
+ equals(other: Snowflake): boolean {
+ for (let i = 0; i < ID_BYTE_SIZE; i++) {
+ if (this.bytes[i] !== other.bytes[i]) return false;
+ }
+ return true;
+ }
+
+ toString(): string {
+ if (this.isZero()) return "0";
+
+ let value = bytesToBigInt(this.bytes);
+ let result = "";
+
+ while (value > 0n) {
+ const remainder = Number(value % BASE);
+ result = ALPHABET[remainder] + result;
+ value /= BASE;
+ }
+
+ return result;
+ }
+
+ static fromString(s: string): Snowflake {
+ if (s.length === 0) return Snowflake.zero();
+
+ let value = 0n;
+ for (const ch of s) {
+ const idx = DECODE_LUT.get(ch);
+ if (idx === undefined) {
+ throw new InvalidEncodingError();
+ }
+ value = value * BASE + BigInt(idx);
+ }
+
+ if (value >= (1n << 128n)) {
+ throw new InvalidEncodingError();
+ }
+
+ return new Snowflake(bigIntToBytes(value, ID_BYTE_SIZE));
+ }
+
+ toHex(): string {
+ return Array.from(this.bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+ }
+
+ static fromHex(h: string): Snowflake {
+ if (!/^[0-9a-f]+$/i.test(h)) {
+ throw new InvalidEncodingError();
+ }
+ if (h.length !== ID_BYTE_SIZE * 2) {
+ throw new InvalidIDLengthError();
+ }
+
+ const bytes = new Uint8Array(ID_BYTE_SIZE);
+ for (let i = 0; i < ID_BYTE_SIZE; i++) {
+ bytes[i] = parseInt(h.substring(i * 2, i * 2 + 2), 16);
+ }
+ return new Snowflake(bytes);
+ }
+
+ toJSON(): string {
+ return this.toString();
+ }
+
+ static fromJSON(json: string): Snowflake {
+ if (!json.startsWith('"') || !json.endsWith('"')) {
+ throw new InvalidEncodingError();
+ }
+ return Snowflake.fromString(json.slice(1, -1));
+ }
+
+ toBytes(): Uint8Array {
+ return new Uint8Array(this.bytes);
+ }
+
+ static fromBytes(source: Uint8Array | string): Snowflake {
+ let bytes: Uint8Array;
+ if (source instanceof Uint8Array) {
+ bytes = source;
+ } else {
+ bytes = new Uint8Array(source.length);
+ for (let i = 0; i < source.length; i++) {
+ bytes[i] = source.charCodeAt(i) & 0xff;
+ }
+ }
+
+ if (bytes.length !== ID_BYTE_SIZE) {
+ throw new InvalidIDLengthError();
+ }
+
+ return new Snowflake(new Uint8Array(bytes));
+ }
+}
+
+export {
+ Snowflake,
+ InvalidEncodingError,
+ InvalidIDLengthError,
+ ID_BYTE_SIZE,
+ ALPHABET,
+};