import { ReactContext, type ClientOptions, type RegistryEntry, type ParamDef } from './types' import { register } from './registry' function resolveContext(ctx: ReactContext | string | undefined): string | undefined { if (ctx instanceof ReactContext) return ctx.name return ctx } function normalizeAffects( affects: ClientOptions['affects'], ): RegistryEntry['affects'] | undefined { if (!affects) return undefined const items = Array.isArray(affects) ? affects : [affects] return items.map(item => { if (item instanceof ReactContext) { return { type: 'context' as const, name: item.name } } return { type: 'context' as const, name: item } }) } /** * Recover parameter names by parsing the function's own source text. * * Names are the wire contract — the dispatcher matches request params to * positional arguments by name — and JS erases them at runtime, so the * source string is the only place they survive. */ function extractParams(fn: Function): ParamDef[] { const source = fn.toString() const match = source.match(/\(([^)]*)\)/) if (!match || !match[1].trim()) return [] return match[1] .split(',') .map(p => p.trim()) .filter(p => p && !p.startsWith('...')) .map(p => { // Strips a default value or a type annotation off the name const name = p.split(/[=:]/)[0].trim() return { name, type: 'any', required: !p.includes('=') } }) } /** Wrap and register a standalone function. */ export function client Promise>( options: ClientOptions, fn: T, ): T /** Register a class method. */ export function client(options: ClientOptions): MethodDecorator export function client(optionsOrFn: ClientOptions, fn?: Function): any { // Function wrapper form: client(options, fn) if (fn && typeof fn === 'function') { const options = optionsOrFn as ClientOptions const context = resolveContext(options.context) const affects = normalizeAffects(options.affects) if (context && affects) { throw new Error('context and affects are mutually exclusive') } const name = fn.name || 'anonymous' const params = extractParams(fn) const entry: RegistryEntry = { name, fn: fn as RegistryEntry['fn'], context, affects, params, private: options.private ?? false, // A wrapped function's return is only known once called, so the // view-vs-RPC split is decided by the dispatcher, not here. viewPath: false, route: options.route, methods: options.methods, auth: options.auth, rev: options.rev, cache: options.cache, } register(entry) return fn } // Decorator form: @client(options) const options = optionsOrFn as ClientOptions return function (_target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value const context = resolveContext(options.context) const affects = normalizeAffects(options.affects) if (context && affects) { throw new Error('context and affects are mutually exclusive') } const params = extractParams(originalMethod) const entry: RegistryEntry = { name: propertyKey, fn: originalMethod, context, affects, params, private: options.private ?? false, viewPath: false, route: options.route, methods: options.methods, auth: options.auth, rev: options.rev, cache: options.cache, } register(entry) return descriptor } }