import { randomBytes } from 'node:crypto'; const ALPHABET = '0123456789abcdefghijkmnpqrstvwxyz'; // Crockford-ish: no i, l, o, u const BASE = ALPHABET.length; /** Milliseconds, base-33, fixed width — nine digits reach the year 3400. */ const STAMP_WIDTH = 9; /** * Bumped instead of reused when two ids land in one millisecond, so ids sort * in the order they were made even inside a single import loop. */ let lastStamp = 0; /** * Collision-resistant, URL-safe, time-ordered row identifier. * * Time-ordered is load-bearing: an unsorted grid is `ORDER BY id`, so ids made * in sequence have to sort in sequence — an imported spreadsheet keeps its * file order, and a new row lands at the bottom instead of wherever twelve * random characters happen to fall. The random tail keeps two servers that * share a millisecond from sharing an id. */ export function newId(prefix = ''): string { const now = Date.now(); lastStamp = now > lastStamp ? now : lastStamp + 1; let stamp = ''; for (let rest = lastStamp, i = 0; i < STAMP_WIDTH; i++) { stamp = ALPHABET[rest % BASE] + stamp; rest = Math.floor(rest / BASE); } let out = stamp; for (const b of randomBytes(7)) out += ALPHABET[b % BASE]; return prefix ? `${prefix}_${out}` : out; } /** Row ids are user-visible in the address bar; keep them tightly shaped. */ export function isValidId(id: string): boolean { return /^[A-Za-z0-9_-]{1,64}$/.test(id); }