/** * Validated Protomux channel alias registry: logical name → actual channel name. * Each actual channel name may be claimed by at most one logical alias at a time. * Re-registering the same logical with a different actual overwrites and records history. */ const NAME_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,127}$/ /** * @param {string} s */ export function assertValidProtomuxAliasName(s) { const t = String(s || '').trim() if (!t || t.length > 128) { throw new Error('protomux alias: name length must be 1..128') } if (!NAME_RE.test(t)) { throw new Error( 'protomux alias: name must match ' + NAME_RE.source + ' (logical/actual)' ) } return t } export function createBareOsProtomuxAliasRegistry() { /** @type {Map} */ const logicalToActual = new Map() /** @type {Record} */ const legacyMap = Object.create(null) /** @type {{ atMs: number, logical: string, previousActual: string | null, actual: string }[]} */ const changeLog = [] /** @type {Map} */ const actualToLogicals = new Map() function reindexActual(logical, oldAct, newAct) { if (oldAct) { const arr = actualToLogicals.get(oldAct) if (arr) { const i = arr.indexOf(logical) if (i >= 0) arr.splice(i, 1) if (!arr.length) actualToLogicals.delete(oldAct) } } if (newAct) { const arr = actualToLogicals.get(newAct) || [] if (!arr.includes(logical)) arr.push(logical) actualToLogicals.set(newAct, arr) } } return { /** * @param {string} logicalName * @param {string} actualName * @returns {{ ok: boolean, conflict?: string, replaced?: boolean }} */ register(logicalName, actualName) { const a = assertValidProtomuxAliasName(logicalName) const b = assertValidProtomuxAliasName(actualName) const prev = logicalToActual.get(a) || null if (prev === b) { return { ok: true, replaced: false } } const others = actualToLogicals.get(b) || [] if (others.some((o) => o !== a)) { return { ok: false, conflict: `actual ${b} already mapped from logical: ${others.filter((o) => o !== a).join(', ')}` } } reindexActual(a, prev, b) logicalToActual.set(a, b) legacyMap[a] = b changeLog.push({ atMs: Date.now(), logical: a, previousActual: prev, actual: b }) if (changeLog.length > 256) changeLog.splice(0, changeLog.length - 256) return { ok: true, replaced: Boolean(prev) } }, get(logicalName) { const a = String(logicalName || '').trim() return logicalToActual.get(a) || null }, snapshot() { const aliases = {} for (const [k, v] of logicalToActual.entries()) aliases[k] = v return { schema: 2, aliases, changeLogTail: changeLog.slice(-32), reverseIndex: Object.fromEntries( [...actualToLogicals.entries()].map(([act, logs]) => [act, [...logs]]) ), atMs: Date.now() } }, /** @returns {Record} same object reference; updated on each register */ legacyMapView() { return legacyMap } } }