identifier.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import z from "zod"
  2. export namespace Identifier {
  3. const prefixes = {
  4. session: "ses",
  5. message: "msg",
  6. permission: "per",
  7. user: "usr",
  8. part: "prt",
  9. pty: "pty",
  10. } as const
  11. export type Prefix = keyof typeof prefixes
  12. type CryptoLike = {
  13. getRandomValues<T extends ArrayBufferView>(array: T): T
  14. }
  15. const TOTAL_LENGTH = 26
  16. const RANDOM_LENGTH = TOTAL_LENGTH - 12
  17. const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  18. let lastTimestamp = 0
  19. let counter = 0
  20. const fillRandomBytes = (buffer: Uint8Array) => {
  21. const cryptoLike = (globalThis as { crypto?: CryptoLike }).crypto
  22. if (cryptoLike?.getRandomValues) {
  23. cryptoLike.getRandomValues(buffer)
  24. return buffer
  25. }
  26. for (let i = 0; i < buffer.length; i++) {
  27. buffer[i] = Math.floor(Math.random() * 256)
  28. }
  29. return buffer
  30. }
  31. const randomBase62 = (length: number) => {
  32. const bytes = fillRandomBytes(new Uint8Array(length))
  33. let result = ""
  34. for (let i = 0; i < length; i++) {
  35. result += BASE62[bytes[i] % BASE62.length]
  36. }
  37. return result
  38. }
  39. const createSuffix = (descending: boolean, timestamp?: number) => {
  40. const currentTimestamp = timestamp ?? Date.now()
  41. if (currentTimestamp !== lastTimestamp) {
  42. lastTimestamp = currentTimestamp
  43. counter = 0
  44. }
  45. counter += 1
  46. let value = BigInt(currentTimestamp) * 0x1000n + BigInt(counter)
  47. if (descending) value = ~value
  48. const timeBytes = new Uint8Array(6)
  49. for (let i = 0; i < 6; i++) {
  50. timeBytes[i] = Number((value >> BigInt(40 - 8 * i)) & 0xffn)
  51. }
  52. const hex = Array.from(timeBytes)
  53. .map((byte) => byte.toString(16).padStart(2, "0"))
  54. .join("")
  55. return hex + randomBase62(RANDOM_LENGTH)
  56. }
  57. const generateID = (prefix: Prefix, descending: boolean, given?: string, timestamp?: number) => {
  58. if (given) {
  59. const expected = `${prefixes[prefix]}_`
  60. if (!given.startsWith(expected)) throw new Error(`ID ${given} does not start with ${expected}`)
  61. return given
  62. }
  63. return `${prefixes[prefix]}_${createSuffix(descending, timestamp)}`
  64. }
  65. export const schema = (prefix: Prefix) => z.string().startsWith(`${prefixes[prefix]}_`)
  66. export function ascending(): string
  67. export function ascending(prefix: Prefix, given?: string): string
  68. export function ascending(prefix?: Prefix, given?: string) {
  69. if (prefix) return generateID(prefix, false, given)
  70. return create(false)
  71. }
  72. export function descending(): string
  73. export function descending(prefix: Prefix, given?: string): string
  74. export function descending(prefix?: Prefix, given?: string) {
  75. if (prefix) return generateID(prefix, true, given)
  76. return create(true)
  77. }
  78. export function create(descending: boolean, timestamp?: number) {
  79. return createSuffix(descending, timestamp)
  80. }
  81. export function createPrefixed(prefix: Prefix, descending: boolean, timestamp?: number) {
  82. return generateID(prefix, descending, undefined, timestamp)
  83. }
  84. }