/** * Method resolution — the index the dispatcher queries on every call. * * Owns both the index's construction and its lookup contract: * - `buildMethodIndex` is called once at server startup. * - `resolveMethod` is called once per inbound call. * * Each method is indexed under its `BoundMethod.ref` * (`namespace.Owner.method.name`) and under a short alias `Owner.name`. * The short alias is only registered when it is unambiguous — two methods * whose owners live in different namespaces (e.g. class vs interface) and * share the same local name would collide, and we refuse to register the * ambiguous key rather than silently overwriting. * * Inbound names may arrive as `Path`, full `ref`, short alias, a tree path * `///`, or the typed colon-form MethodPath * `/:::` (what the kernel forwards when a * caller addresses the method in `:`-delimited form). All shapes normalize to * the same lookup key. */ import type { Path } from '@astrale-os/kernel-core' import type { BoundMethod } from '@astrale-os/kernel-core/domain' import type { AnyRemoteHandler } from '../method/single.js' export type MethodIndex = Map> const NAMESPACE_PREFIXES = ['class.', 'interface.'] as const const METHOD_INFIX = 'method' export function buildMethodIndex(methods: BoundMethod[]): MethodIndex { const index: MethodIndex = new Map() const shortFormAmbiguous = new Set() for (const m of methods) { index.set(m.ref, m) const shortForm = `${m.owner}.${m.method}` if (shortFormAmbiguous.has(shortForm)) continue const existing = index.get(shortForm) if (existing && existing !== m) { index.delete(shortForm) shortFormAmbiguous.add(shortForm) continue } index.set(shortForm, m) } return index } export function resolveMethod( index: MethodIndex, name: Path | string, ): BoundMethod | null { return index.get(normalizeMethod(name)) ?? null } function normalizeMethod(name: Path | string): string { const raw = typeof name === 'string' ? name : name.raw if (!raw.startsWith('/')) return raw // Typed colon-form MethodPath (`/:::`) vs. tree // slash-form (`///`). The owner segment carries its own // `.` (e.g. `interface.NoteOps`), so the path separator is unambiguous. const separator = raw.startsWith('/:') ? ':' : '/' const segments = raw .replace(/^\/+/, '') .split(separator) .filter((s) => s.length > 0) if (segments.length < 3) return raw const owner = segments[segments.length - 2]! const method = segments[segments.length - 1]! if (NAMESPACE_PREFIXES.some((p) => owner.startsWith(p))) { return `${owner}.${METHOD_INFIX}.${method}` } return `${owner}.${method}` }