Files
PotentialStorage/src/core/path.ts
T
Claude 69108d1557 M1: core path escaping, order-preserving index value encoding, errors
Scaffold TypeScript/Vitest project, add DESIGN.md (brief) and CLAUDE.md.
Property tests cover escape round-trips, separator-freedom and order preservation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
2026-10-23 06:52:06 +00:00

110 lines
3.7 KiB
TypeScript

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),
}
}