/** * @module ui/router/cosmos/cosmos-router-helpers * * Route parsing, formatting, and equality helpers for the Universal Cosmos Router. * Pure isomorphic functions with zero DOM dependency. */ import { CosmosRouteInfo, CosmosTenant, CosmosGalaxyRouteInfo, ParseCosmosRouteOptions, } from './cosmos-router-types.mjs'; import { getGalaxyInfoFromCosmos } from '@ibgib/core-gib/dist/cosmology/cosmos/cosmos-helpers.mjs'; import { getGalaxyPresetConfig } from '@ibgib/core-gib/dist/cosmology/galaxy/galaxy-configs.mjs'; /** * Safely decodes a URI component, returning the raw string if decoding fails. */ export function safeDecode(str: string): string { try { return decodeURIComponent(str); } catch { return str; } } /** * Extracts tenant and cosmos name from a pathname without full route parsing. * Used for pre-fetching the Cosmos manifest before route parsing. */ export function extractCosmosAndTenant(pathname: string): { cosmos?: string; tenant?: CosmosTenant } { let p = (pathname || '').trim(); const qIdx = p.indexOf('?'); if (qIdx !== -1) { p = p.substring(0, qIdx); } const hIdx = p.indexOf('#'); if (hIdx !== -1) { p = p.substring(0, hIdx); } const segments = p.split('/').filter(s => s.length > 0); if (segments.length === 0) { return {}; } let tenant: CosmosTenant | undefined; let segIdx = 0; if (segments[0].startsWith('@')) { const username = safeDecode(segments[0].slice(1)).trim(); if (username) { tenant = { type: 'username', siteUsername: username }; segIdx = 1; } } else if (segments[0] === 'keystone' && segments.length > 1) { const keystoneTjp = safeDecode(segments[1]).trim(); if (keystoneTjp) { tenant = { type: 'keystone', keystoneTjp }; segIdx = 2; } } const remaining = segments.slice(segIdx); if (remaining.length > 0 && remaining[0] !== 'polycosmos') { return { cosmos: safeDecode(remaining[0]).trim(), tenant }; } return { tenant }; } /** * Parses a browser pathname into a structured CosmosRouteInfo. * * Supports: * - Polycosmos: / or /polycosmos or /@:siteUsername or /keystone/:keystoneTjp * - Cosmos: /:cosmos or /@:siteUsername/:cosmos or /keystone/:keystoneTjp/:cosmos * - Universal Galaxy (Option B): /:cosmos/:galaxy[/:branch[/*filePath]] (branching) * /:cosmos/:galaxy[/tjp/*trail] or [/addr/*trail] (linear) * - Raw Addr (Snapshot): /:cosmos/:spaceId/addr/:ibGibAddr * - Raw Tjp (Living Tip): /:cosmos/:spaceId/tjp/:tjpAddr * * Note: Hardcoded /code/ and /notes/ intermediary segments fail fast. */ export function parseCosmosRoute(pathname: string, opts?: ParseCosmosRouteOptions): CosmosRouteInfo { let p = (pathname || '').trim(); let queryObj: Record | undefined; const qIdx = p.indexOf('?'); if (qIdx !== -1) { const qStr = p.substring(qIdx + 1); p = p.substring(0, qIdx); const searchParams = new URLSearchParams(qStr); const q: Record = {}; searchParams.forEach((val, key) => { q[key] = val; }); if (Object.keys(q).length > 0) { queryObj = q; } } const hIdx = p.indexOf('#'); if (hIdx !== -1) { p = p.substring(0, hIdx); } if (!p || p === '/') { return { kind: 'polycosmos' }; } const rawSegments = p.split('/').filter(s => s.length > 0); if (rawSegments.length === 0) { return { kind: 'polycosmos' }; } // Extract tenant prefix if present (@username or /keystone/tjp) let tenant: CosmosTenant | undefined; let segIdx = 0; if (rawSegments[0].startsWith('@')) { const username = safeDecode(rawSegments[0].slice(1)).trim(); if (username) { tenant = { type: 'username', siteUsername: username }; segIdx = 1; } } else if (rawSegments[0] === 'keystone' && rawSegments.length > 1) { const keystoneTjp = safeDecode(rawSegments[1]).trim(); if (keystoneTjp) { tenant = { type: 'keystone', keystoneTjp }; segIdx = 2; } } const remaining = rawSegments.slice(segIdx); // If only tenant specifier was given (e.g. /@bill or /keystone/tjp123) if (remaining.length === 0) { return { kind: 'polycosmos', ...(tenant ? { tenant } : {}) }; } if (remaining.length === 1 && remaining[0] === 'polycosmos') { return { kind: 'polycosmos', ...(tenant ? { tenant } : {}) }; } const cosmos = safeDecode(remaining[0]).trim(); if (remaining.length === 1) { return { kind: 'cosmos', ...(tenant ? { tenant } : {}), cosmos }; } const second = safeDecode(remaining[1]).trim(); // FAIL FAST: Explicit rejection of legacy /code/ and /notes/ literal route segments if (second === 'code' || second === 'notes') { throw new Error(`Legacy route rejected: intermediate "/${second}/" segment is obsolete. Use "/${cosmos}/${remaining[2] || '...'}/..." directly without "/${second}/". (FAIL FAST)`); } // Mechanism 1: Space-Qualified Raw Addrs // /:cosmos/:spaceId/addr/:ibGibAddr // /:cosmos/:spaceId/tjp/:tjpAddr if (remaining.length >= 4) { const third = safeDecode(remaining[2]).trim(); const spaceId = second; const fourth = safeDecode(remaining.slice(3).join('/')).trim(); // Only parse as raw if not a registered galaxy const isKnownGalaxy = Boolean( opts?.cosmosManifest ? getGalaxyInfoFromCosmos(opts.cosmosManifest, second) : (getGalaxyPresetConfig(second) || second.startsWith('notes') || second.startsWith('code') || second.startsWith('curriculum') || second.startsWith('telemetry')) ); if (!isKnownGalaxy && (third === 'addr' || third === 'tjp')) { if (third === 'addr') { return { kind: 'raw-addr', ...(tenant ? { tenant } : {}), cosmos, spaceId, ibGibAddr: fourth, ...(queryObj ? { query: queryObj } : {}), }; } if (third === 'tjp') { return { kind: 'raw-tjp', ...(tenant ? { tenant } : {}), cosmos, spaceId, tjpAddr: fourth, ...(queryObj ? { query: queryObj } : {}), }; } } } // Mechanism 2: Universal Galaxy Archetype Projections // /:cosmos/:galaxy/... const galaxy = second; // Option B: Resolve policy from Cosmos Manifest if provided, else fallback to preset configs or heuristics let topology: 'branching' | 'linear'; let discriminator: string | undefined; if (opts?.cosmosManifest) { const entry = getGalaxyInfoFromCosmos(opts.cosmosManifest, galaxy); if (entry) { topology = entry.policy.topology; discriminator = entry.discriminator; } else { const preset = getGalaxyPresetConfig(galaxy); topology = preset?.policy?.topology || (galaxy.startsWith('notes') || galaxy.startsWith('curriculum') || galaxy.startsWith('telemetry') ? 'linear' : 'branching'); discriminator = preset?.discriminator; } } else { const preset = getGalaxyPresetConfig(galaxy); if (preset) { topology = preset.policy.topology; discriminator = preset.discriminator; } else { // Heuristic for offline parsing: if subsequent token is tjp/addr or prefix suggests linear const third = remaining[2] ? safeDecode(remaining[2]).trim() : undefined; const isExplicitLinear = third === 'tjp' || third === 'addr' || galaxy.startsWith('notes') || galaxy.startsWith('curriculum') || galaxy.startsWith('telemetry'); topology = isExplicitLinear ? 'linear' : 'branching'; } } if (topology === 'branching') { const branch = remaining[2] ? safeDecode(remaining[2]).trim() : undefined; const filePath = remaining.length > 3 ? remaining.slice(3).map(safeDecode).join('/') : undefined; return { kind: 'galaxy', ...(tenant ? { tenant } : {}), cosmos, galaxy, topology: 'branching', ...(discriminator ? { discriminator } : {}), ...(branch ? { branch } : {}), ...(filePath ? { filePath } : {}), ...(queryObj ? { query: queryObj } : {}), }; } else { let trailMode: 'tjp' | 'addr' | undefined; let trail: string[] | undefined; let topicTjp: string | undefined; if (remaining.length > 2) { const modeCandidate = safeDecode(remaining[2]).trim(); if (modeCandidate === 'tjp' || modeCandidate === 'addr') { trailMode = modeCandidate; const trailSegments = remaining.slice(3).map(s => safeDecode(s).trim()).filter(s => s.length > 0); if (trailSegments.length > 0) { trail = trailSegments; topicTjp = trailSegments[trailSegments.length - 1]; } } else { topicTjp = modeCandidate; trailMode = 'tjp'; trail = [modeCandidate]; } } return { kind: 'galaxy', ...(tenant ? { tenant } : {}), cosmos, galaxy, topology: 'linear', ...(discriminator ? { discriminator } : {}), ...(trailMode ? { trailMode } : {}), ...(trail ? { trail } : {}), ...(topicTjp ? { topicTjp } : {}), ...(queryObj ? { query: queryObj } : {}), }; } } /** * Formats a CosmosRouteInfo into a canonical URL pathname. */ export function formatCosmosRoute(route: CosmosRouteInfo): string { let prefix = ''; if (route.tenant) { if (route.tenant.type === 'username') { prefix = `/@${encodeURIComponent(route.tenant.siteUsername)}`; } else if (route.tenant.type === 'keystone') { prefix = `/keystone/${encodeURIComponent(route.tenant.keystoneTjp)}`; } } switch (route.kind) { case 'polycosmos': return prefix ? `${prefix}/polycosmos` : '/'; case 'cosmos': return `${prefix}/${encodeURIComponent(route.cosmos)}`; case 'galaxy': { let res = `${prefix}/${encodeURIComponent(route.cosmos)}/${encodeURIComponent(route.galaxy)}`; if (route.topology === 'branching') { if (route.branch) { res += `/${encodeURIComponent(route.branch)}`; if (route.filePath) { res += `/${route.filePath.split('/').map(encodeURIComponent).join('/')}`; } } } else { if (route.trail && route.trail.length > 0) { const mode = route.trailMode || 'tjp'; res += `/${mode}/${route.trail.map(encodeURIComponent).join('/')}`; } else if (route.topicTjp) { res += `/${encodeURIComponent(route.topicTjp)}`; } } if (route.query && Object.keys(route.query).length > 0) { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(route.query)) { if (v !== undefined) { sp.set(k, v); } } const qs = sp.toString(); if (qs) { res += `?${qs}`; } } return res; } case 'raw-addr': { let res = `${prefix}/${encodeURIComponent(route.cosmos)}/${encodeURIComponent(route.spaceId)}/addr/${encodeURIComponent(route.ibGibAddr)}`; if (route.query && Object.keys(route.query).length > 0) { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(route.query)) { if (v !== undefined) { sp.set(k, v); } } const qs = sp.toString(); if (qs) { res += `?${qs}`; } } return res; } case 'raw-tjp': { let res = `${prefix}/${encodeURIComponent(route.cosmos)}/${encodeURIComponent(route.spaceId)}/tjp/${encodeURIComponent(route.tjpAddr)}`; if (route.query && Object.keys(route.query).length > 0) { const sp = new URLSearchParams(); for (const [k, v] of Object.entries(route.query)) { if (v !== undefined) { sp.set(k, v); } } const qs = sp.toString(); if (qs) { res += `?${qs}`; } } return res; } default: return '/'; } } /** * Compares two Cosmos routes for equivalence by comparing their canonical formatted paths. */ export function isSameCosmosRoute(a?: CosmosRouteInfo, b?: CosmosRouteInfo): boolean { if (!a && !b) { return true; } if (!a || !b) { return false; } return formatCosmosRoute(a) === formatCosmosRoute(b); }