All files / core/blong-gogo/src handlerProxy.ts

69.52% Statements 146/210
69.69% Branches 23/33
77.77% Functions 7/9
69.52% Lines 146/210

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 2111x 1x 1x 1x 1x 1x 1x 1x 1x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1991x 1991x 1991x 1907x 1991x 1600x 391x   391x 1991x 1991x 1x 1x 1x 1x 1x 1x 1x 1x 1x 445x 445x 445x 445x 445x 445x 445x 445x 445x 445x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1991x 1382x 4x 4x 4x 4x 4x 1382x 1382x 1991x 259x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x 1382x     1382x 1382x 1382x 1382x 1382x 1382x 259x 1732x 1732x 1732x 1732x 1732x 700x 700x 700x 700x 700x 700x 700x 700x 700x 1732x 1991x 1991x                                       1991x 1991x 1991x                                                               1991x 1991x 1991x 1991x 1991x 1991x 1991x                       1991x 2082x 2082x 1991x 1991x 445x 445x  
import type {Adapter, ILib, IMeta} from '@feasibleone/blong/types';
import merge from 'ut-function.merge';
 
import {camelToSentence, legName, parseAnnotatedKey, recordedCall} from './lib.ts';
import {declareCall} from './semanticContext.ts';
 
/**
 * Rename a function's `.name` property.
 */
function rename<T>(value: string, fn: T): T {
    Object.defineProperty(fn, 'name', {
        value,
        configurable: true,
        enumerable: false,
    });
    return fn;
}
 
/**
 * The unit that declares a call: the namespace the calling port serves.
 *
 * A leg id names the **logical unit** that made the call - the namespace, which is what a
 * call targets and what becomes a Kubernetes service - so it cannot name the port. A port id
 * is an artefact of how one process happens to be wired (`srv.db` hosts the db handlers of
 * every realm, `login.loginDispatch` is one realm's dispatch), and a diagram drawn from it
 * would label its arrows with names nothing is deployed as. The process's own name is
 * deliberately not a candidate either: one process hosts many namespaces, so it would draw
 * every participant of a monolith as one.
 *
 * A port that serves several namespaces - the shared subject orchestrator answers for each
 * realm that contributes to it - declares its calls under the first it serves. A layer-built
 * proxy whose port names no namespace falls back to the layer's name, and the placeholder is
 * the last resort rather than the default: a leg whose first segment is not a usable name is
 * rejected by the grammar, and that rejection is silent (F-198).
 */
function callerOf(port: unknown, layerName: string | undefined): string {
    const namespace = (
        port as {config?: {namespace?: string | string[] | Record<string, unknown>}} | undefined
    )?.config?.namespace;
    const first = Array.isArray(namespace)
        ? namespace.find(entry => typeof entry === 'string')
        : typeof namespace === 'object' && namespace !== null
          ? Object.keys(namespace)[0]
          : namespace;
    return String(first || layerName || 'port');
}
 
/**
 * Create the handler proxy — the IoC mechanism that resolves handler calls
 * at runtime through the registry (local or remote).
 *
 * This is the most important abstraction in the framework: handlers never
 * import each other directly; they access dependencies through this proxy.
 */
export default function createHandlerProxy(
    local: object,
    port: Adapter,
    remote: (methodName: string) => () => unknown,
    attachCheckpoint: ((meta: IMeta) => void) | undefined,
    lib: ILib,
    mergedConfig: Record<string, unknown>,
    layerName?: string,
): object {
    return new Proxy(local, {
        get(target: unknown, handlerName: string) {
            if (typeof handlerName !== 'string') return undefined;
 
            // The namespace this port serves names the leg it declares, so the arrow a diagram
            // draws starts at a namespace rather than at the process or at the port. The route
            // that owns the call is the exception and declares itself (`public`, see
            // `Gateway.ts`).
            const callerId = callerOf(port, layerName);
 
            function resolveHandler(resolvedName: string): (...params: unknown[]) => unknown {
                let fn: (() => unknown) | undefined;
                const sentence = camelToSentence(resolvedName);
                function nameSteps(result: unknown): unknown {
                    if (Array.isArray(result) && !(result as {name?: string}).name) {
                        Object.defineProperty(result, 'name', {
                            value: sentence,
                            configurable: true,
                        });
                    }
                    return result;
                }
                if (port?.handles?.(resolvedName)) {
                    return rename(resolvedName, function (...params: unknown[]) {
                        fn ||= port.findHandler?.(resolvedName) as (() => unknown) | undefined;
                        if (!fn) throw new Error(`Handler '${resolvedName}' not found`);
                        const $meta = params.length > 1 ? (params[1] as IMeta) : undefined;
                        if ($meta && typeof $meta === 'object') {
                            attachCheckpoint?.($meta);
                        }
                        // The caller declares the call, so the records its handler
                        // writes inside it name the same leg the callee's do. A same-port
                        // call is not a hop, so its own records are written only for a
                        // port that asks for them.
                        const record =
                            (port as unknown as {config?: {recordCalls?: boolean}})?.config
                                ?.recordCalls === true;
                        return nameSteps(
                            declareCall(
                                callerId,
                                legName(resolvedName),
                                () =>
                                    record
                                        ? recordedCall(port, resolvedName, () =>
                                              fn?.apply(port, params as []),
                                          )
                                        : fn?.apply(port, params as []),
                                $meta,
                            ),
                        );
                    });
                }
                // A remote call carries its identity in `$meta`, which travels in
                // the request body and in the outbound headers — so the declaration
                // has to be made before the transport builds the request.
                const remoteCall = remote(resolvedName) as (...params: unknown[]) => unknown;
                return rename(resolvedName, function (...params: unknown[]) {
                    const $meta = params.length > 1 ? (params[1] as IMeta) : undefined;
                    return declareCall(
                        callerId,
                        legName(resolvedName),
                        () =>
                            recordedCall(port, legName(resolvedName), () => remoteCall(...params)),
                        $meta,
                    );
                });
            }
 
            function wrapWithMeta(
                baseFn: (...params: unknown[]) => unknown,
                metaOverrides: Record<string, unknown>,
                aliasName?: string,
            ): (...params: unknown[]) => unknown {
                return rename(aliasName || baseFn.name, function (...params: unknown[]) {
                    const $meta = params.length > 1 ? (params[1] as IMeta) : undefined;
                    if ($meta && typeof $meta === 'object') {
                        merge($meta, metaOverrides);
                    }
                    const result = baseFn(...params);
                    if (Array.isArray(result) && metaOverrides.name) {
                        Object.defineProperty(result, 'name', {
                            value: metaOverrides.name,
                            configurable: true,
                        });
                    }
                    return result;
                });
            }
 
            // Approach 2: Annotation syntax
            if (handlerName.startsWith('@')) {
                const parsed = parseAnnotatedKey(handlerName);
                const baseFn = resolveHandler(parsed.handlerName);
                const metaOverrides: Record<string, unknown> = {};
                for (const ann of parsed.annotations) {
                    const hasKeyValue = ann.params.some(p => p.includes('='));
                    if (ann.params.length > 0 && !hasKeyValue) {
                        // Mode A: $meta injection
                        metaOverrides[ann.name] = ann.params.join(' ');
                    } else {
                        // Mode B: config-object reference with deep merge
                        const handlerConfig = mergedConfig?.handler as
                            | Record<string, unknown>
                            | undefined;
                        const configObj = handlerConfig?.[ann.name];
                        if (configObj && typeof configObj === 'object') {
                            merge(metaOverrides, configObj as Record<string, unknown>);
                        }
                        for (const p of ann.params) {
                            const eqIdx = p.indexOf('=');
                            if (eqIdx > 0) {
                                lib.setProperty(
                                    metaOverrides,
                                    p.slice(0, eqIdx),
                                    p.slice(eqIdx + 1),
                                );
                            }
                        }
                    }
                }
                return wrapWithMeta(baseFn, metaOverrides);
            }
 
            // Resolve handler (local or remote)
            const resolved = resolveHandler(handlerName);
 
            // Approach 1: Wrap with naming proxy for sub-property destructuring
            return new Proxy(resolved, {
                get(proxyTarget, prop, receiver) {
                    if (typeof prop !== 'string' || prop in proxyTarget) {
                        return Reflect.get(proxyTarget, prop, receiver);
                    }
                    return wrapWithMeta(
                        proxyTarget as (...params: unknown[]) => unknown,
                        {
                            name: camelToSentence(prop),
                        },
                        prop,
                    );
                },
                apply(proxyTarget, thisArg, args) {
                    return Reflect.apply(proxyTarget, thisArg, args);
                },
            });
        },
    });
}