/** * Self resolution stays parse-only: `_self` becomes a Path/IdPath with no graph * read. The dispatcher later attaches lazy node()/call helpers once it has a kernel. */ import type { BindCtx, BoundNode, InstanceDispatch, NodeBinder, ProxyCaller, TypeMethods, TypeNames, } from '@astrale-os/kernel-client/schema' import type { Node } from '@astrale-os/kernel-core/graph' import type { Schema } from '@astrale-os/kernel-dsl' import { createKnownInstanceDispatch, defaultCaller, extractMethodsByType, } from '@astrale-os/kernel-client/schema' import { IdPath, K, Path, type NodeId } from '@astrale-os/kernel-core' import { classPathSchema } from '@astrale-os/kernel-core/domain' import { absolutePathSchema } from '@astrale-os/kernel-core/tree' import { z } from 'zod' import type { Kernel } from '../method/context.js' /** The node a non-static method runs on — its full record, id guaranteed. */ export type ResolvedSelfNode = Node & { id: NodeId } /** The bare parse of a self target — no kernel, no fetch. */ export type ParsedSelf = { path: Path /** Set when `path` is an `IdPath` (i.e. `@::method` calls). */ id?: NodeId } export type SelfResult = ParsedSelf & { /** * Lazy point-in-time self read. Memoized per dispatch; `{ reload: true }` * forces a fresh `function.get`. */ node(opts?: { reload?: boolean }): Promise } /** Schema-typed self: short-keyed props and typed same-instance call proxy. */ export type TypedSelf = ParsedSelf & { /** * Fetch THIS node as a typed `BoundNode`. Memoized exactly like * {@link SelfResult.node} — one in-flight `function.get`, rejection-clearing, * `{ reload: true }` refetches — so the zero-extra-graph-read dispatch * invariant holds: nothing is read until called, then at most once. */ node(opts?: { reload?: boolean }): Promise> /** Own instance methods — typed, address-only (`::method`), no read. */ readonly call: InstanceDispatch } export function resolveSelf(ref: string): ParsedSelf { const path = Path.parse(ref) return path instanceof IdPath ? { path, id: path.id } : { path } } /** Attach memoized node(); unauthenticated contexts reject instead of reading. */ export function withNode(parsed: ParsedSelf, kernel: Kernel | null): SelfResult { let cached: Promise | undefined const node = (opts?: { reload?: boolean }): Promise => { if (opts?.reload) cached = undefined if (!cached) { cached = fetchSelfNode(parsed.path, kernel).catch((err: unknown) => { cached = undefined // never cache a rejection — a later call may succeed throw err }) } return cached } return { ...parsed, node } } const EMPTY_METHODS: TypeMethods = { kind: 'class', methods: new Set(), staticMethods: new Set(), } const methodMapCache = new WeakMap>() /** The type's method allow-list + kind, computed once per binder. */ function methodsFor(binder: NodeBinder, typeName: string): TypeMethods { let map = methodMapCache.get(binder) if (!map) { map = extractMethodsByType(binder.schema) methodMapCache.set(binder, map) } return map.get(typeName) ?? EMPTY_METHODS } function noKernelCaller(ref: string): ProxyCaller { return () => Promise.reject( new Error( `self.call needs an authenticated kernel to dispatch on "${ref}", but this method ran ` + `without one (its auth policy yields no kernel — e.g. 'public').`, ), ) } /** * Schema-typed self helper: lazy BoundNode plus address-only call proxy. With no * kernel, both node() and call thunks fail loudly. */ export function withBoundNode( parsed: ParsedSelf, kernel: Kernel | null, binder: NodeBinder, typeName: string, ): TypedSelf { const ctx: BindCtx = kernel ? { caller: defaultCaller(kernel), read: kernel } : { caller: null, read: null } let cached: Promise> | undefined const node = (opts?: { reload?: boolean }): Promise> => { if (opts?.reload) cached = undefined if (!cached) { cached = fetchSelfNode(parsed.path, kernel) .then( (record) => binder.bind(typeName as TypeNames, record, ctx, { validate: 'warn', }) as unknown as BoundNode, ) .catch((err: unknown) => { cached = undefined // never cache a rejection — a later call may succeed throw err }) } return cached } const info = methodsFor(binder, typeName) const call = createKnownInstanceDispatch( ctx.caller ?? noKernelCaller(parsed.path.raw), parsed.path.raw, info.methods, info.kind === 'class' ? typeName : undefined, ) as InstanceDispatch return { ...parsed, node, call } } /** * Boundary schema for a node record off the wire. Structurally identical to * kernel-core's `nodeSchema`, re-declared here from its public coercers * (`classPathSchema` from `/domain`, `absolutePathSchema` from `/tree`) on * purpose: importing the schema from `#graph` would pull zod into the graph * barrel and cycle with `AbsolutePath` init (see `kernel/core/graph/index.ts`). * Coercing through the canonical schemas yields a real `ClassPath` / `AbsolutePath`. */ const nodeRecordSchema = z.object({ class: classPathSchema(), path: absolutePathSchema(), props: z.record(z.string(), z.unknown()), id: z.string().optional(), __labels: z.array(z.string()).optional(), }) async function fetchSelfNode(path: Path, kernel: Kernel | null): Promise { if (!kernel) { throw new Error( `self.node() needs an authenticated kernel to read "${path.raw}", but this method ran ` + `without one (its auth policy yields no kernel — e.g. 'public'). Read the node from a ` + `method whose auth is 'required', or supply a credential.`, ) } // function.get soft-masks missing/unreadable roots; empty page means no self node. const result = (await kernel.call(K.$.f('get').path.domain.raw, { roots: [path.raw], depth: 0, })) as { nodes?: unknown[] } const first = result?.nodes?.[0] if (first === undefined) { throw new Error( `self.node(): "${path.raw}" was not found or is not visible to the caller ` + `(function.get returned no node).`, ) } const record = nodeRecordSchema.parse(first) if (record.id === undefined) { throw new Error(`self.node(): "${path.raw}" returned a record with no id`) } // Validated at the wire boundary: id is now known-present, props are // structurally checked. Bridge the parse output to the canonical `Node`. return { ...record, id: record.id as NodeId } as ResolvedSelfNode }