/** * Mizan @client decorator and function wrapper. * * Two registration styles: * * 1. Function wrapper (standalone functions): * const userProfile = client({ context: UserCtx }, async (userId: number) => { ... }) * * 2. Class decorator (methods): * class Handlers { * @client({ context: UserCtx }) * async userProfile(userId: number) { ... } * } */ 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 } }) } function extractParams(fn: Function): ParamDef[] { // Extract parameter names from function.toString() 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 => { // Handle destructured defaults: name = default, name: type const name = p.split(/[=:]/)[0].trim() return { name, type: 'any', required: !p.includes('=') } }) } function isResponseReturn(result: any): boolean { return result instanceof Response } /** * Function wrapper — registers a standalone function. * * const userProfile = client({ context: UserCtx }, async (userId: number) => { ... }) */ export function client Promise>( options: ClientOptions, fn: T, ): T /** * Class method decorator. * * class Handlers { * @client({ context: UserCtx }) * async userProfile(userId: number) { ... } * } */ export function client(options: ClientOptions): MethodDecorator export function client(optionsOrFn: ClientOptions | 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 isView = false // Determined at call time for function wrappers const entry: RegistryEntry = { name, fn: fn as any, context, affects, params, private: options.private ?? false, viewPath: isView, 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 } }