import { InvalidKeyError } from '../errors.js' export const DEFAULT_SEPARATOR = '/' /** Reserved segments. User segments can never start with `~` (a leading `~` is escaped). */ export const RESERVED = { index: '~i', unique: '~u', events: '~e', projections: '~p', } as const export type Key = string | number /** The character immediately after `c` (by UTF-16 code unit). */ export function succ(c: string): string { return String.fromCharCode(c.charCodeAt(0) + 1) } export function validateSeparator(sep: string): void { if (sep.length !== 1) throw new TypeError(`Separator must be a single character, got ${JSON.stringify(sep)}`) const code = sep.charCodeAt(0) if (code <= 0x20 || code >= 0x7f || sep === '%' || sep === '~') { throw new TypeError(`Separator must be printable ASCII other than "%" and "~", got ${JSON.stringify(sep)}`) } } function hex2(code: number): string { return '%' + code.toString(16).toUpperCase().padStart(2, '0') } /** Converts a user key to a segment string, rejecting `undefined`/`null`/empty. */ export function toSegment(key: unknown): string { if (key === undefined || key === null) throw new InvalidKeyError(`Key must not be ${key}`) if (typeof key === 'number') { if (!Number.isFinite(key)) throw new InvalidKeyError(`Numeric key must be finite, got ${key}`) return String(key) } if (typeof key !== 'string') throw new InvalidKeyError(`Key must be a string or number, got ${typeof key}`) if (key === '') throw new InvalidKeyError('Key must not be empty') return key } /** * Escapes one path segment: percent-encodes `%`, the separator, and a leading `~`. * The result never contains a raw separator and never starts with `~`. */ export function escapeSegment(segment: string, sep: string = DEFAULT_SEPARATOR): string { // Fast path: nothing to escape (the overwhelmingly common case). if (segment.indexOf('%') === -1 && segment.indexOf(sep) === -1 && segment.charCodeAt(0) !== 0x7e) return segment let out = '' for (let i = 0; i < segment.length; i++) { const c = segment[i]! if (c === '%' || c === sep || (i === 0 && c === '~')) out += hex2(c.charCodeAt(0)) else out += c } return out } export function unescapeSegment(escaped: string): string { if (escaped.indexOf('%') === -1) return escaped return escaped.replace(/%([0-9A-Fa-f]{2})/g, (_, h: string) => String.fromCharCode(parseInt(h, 16))) } export function isReservedSegment(escaped: string): boolean { return escaped.charCodeAt(0) === 0x7e // '~' } /** Joins unescaped segments into an escaped key. */ export function joinPath(path: readonly string[], sep: string = DEFAULT_SEPARATOR): string { let out = '' for (let i = 0; i < path.length; i++) { if (i > 0) out += sep out += escapeSegment(path[i]!, sep) } return out } /** Splits an escaped key into unescaped segments. */ export function splitPath(key: string, sep: string = DEFAULT_SEPARATOR): string[] { if (key === '') return [] return key.split(sep).map(unescapeSegment) } /** * Precomputed key strings for a node path. * - `selfKey`: escaped segments joined by the separator (`''` for the root) * - `prefix`: `selfKey + sep` (`''` for the root) — every descendant key starts with it * - `end`: exclusive upper bound of the subtree range (`selfKey + succ(sep)`; `undefined` for the root) */ export interface PathKeys { readonly path: readonly string[] readonly sep: string readonly selfKey: string readonly prefix: string readonly end: string | undefined } export function pathKeys(path: readonly string[], sep: string): PathKeys { const selfKey = joinPath(path, sep) const root = path.length === 0 return { path, sep, selfKey, prefix: root ? '' : selfKey + sep, end: root ? undefined : selfKey + succ(sep), } }