summaryrefslogtreecommitdiffhomepage
path: root/packages/util/src/identifier.ts
blob: 272507f0a5cc581130749f5ce1781e346b838817 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import z from "zod"

export namespace Identifier {
  const prefixes = {
    session: "ses",
    message: "msg",
    permission: "per",
    user: "usr",
    part: "prt",
    pty: "pty",
  } as const

  export type Prefix = keyof typeof prefixes
  type CryptoLike = {
    getRandomValues<T extends ArrayBufferView>(array: T): T
  }

  const TOTAL_LENGTH = 26
  const RANDOM_LENGTH = TOTAL_LENGTH - 12
  const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

  let lastTimestamp = 0
  let counter = 0

  const fillRandomBytes = (buffer: Uint8Array) => {
    const cryptoLike = (globalThis as { crypto?: CryptoLike }).crypto
    if (cryptoLike?.getRandomValues) {
      cryptoLike.getRandomValues(buffer)
      return buffer
    }
    for (let i = 0; i < buffer.length; i++) {
      buffer[i] = Math.floor(Math.random() * 256)
    }
    return buffer
  }

  const randomBase62 = (length: number) => {
    const bytes = fillRandomBytes(new Uint8Array(length))
    let result = ""
    for (let i = 0; i < length; i++) {
      result += BASE62[bytes[i] % BASE62.length]
    }
    return result
  }

  const createSuffix = (descending: boolean, timestamp?: number) => {
    const currentTimestamp = timestamp ?? Date.now()
    if (currentTimestamp !== lastTimestamp) {
      lastTimestamp = currentTimestamp
      counter = 0
    }
    counter += 1

    let value = BigInt(currentTimestamp) * 0x1000n + BigInt(counter)
    if (descending) value = ~value

    const timeBytes = new Uint8Array(6)
    for (let i = 0; i < 6; i++) {
      timeBytes[i] = Number((value >> BigInt(40 - 8 * i)) & 0xffn)
    }
    const hex = Array.from(timeBytes)
      .map((byte) => byte.toString(16).padStart(2, "0"))
      .join("")
    return hex + randomBase62(RANDOM_LENGTH)
  }

  const generateID = (prefix: Prefix, descending: boolean, given?: string, timestamp?: number) => {
    if (given) {
      const expected = `${prefixes[prefix]}_`
      if (!given.startsWith(expected)) throw new Error(`ID ${given} does not start with ${expected}`)
      return given
    }
    return `${prefixes[prefix]}_${createSuffix(descending, timestamp)}`
  }

  export const schema = (prefix: Prefix) => z.string().startsWith(`${prefixes[prefix]}_`)

  export function ascending(): string
  export function ascending(prefix: Prefix, given?: string): string
  export function ascending(prefix?: Prefix, given?: string) {
    if (prefix) return generateID(prefix, false, given)
    return create(false)
  }

  export function descending(): string
  export function descending(prefix: Prefix, given?: string): string
  export function descending(prefix?: Prefix, given?: string) {
    if (prefix) return generateID(prefix, true, given)
    return create(true)
  }

  export function create(descending: boolean, timestamp?: number) {
    return createSuffix(descending, timestamp)
  }

  export function createPrefixed(prefix: Prefix, descending: boolean, timestamp?: number) {
    return generateID(prefix, descending, undefined, timestamp)
  }
}