@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
634 lines • 19.8 kB
JavaScript
/**
* Dynamic Argument Resolution Utilities
*
* Provides utilities for resolving dynamic arguments to their actual values,
* with support for caching, memoization, fallbacks, and conditional resolution.
*
* @module dynamic/dynamicResolver
*/
import { isDynamicFunction, isContextAwareFunction } from "./resolution.js";
import { logger } from "../utils/logger.js";
import { withTimeout } from "../utils/errorHandling.js";
/**
* Default resolution options
*/
const DEFAULT_RESOLUTION_OPTIONS = {
timeout: 5000,
cache: false,
cacheKey: "",
cacheTtl: 60000, // 1 minute
defaultValue: undefined,
throwOnError: true,
};
/**
* Resolution cache for dynamic arguments
*/
class ResolutionCache {
cache = new Map();
cleanupInterval = null;
constructor(cleanupIntervalMs = 60000) {
this.startCleanup(cleanupIntervalMs);
}
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return undefined;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}
set(key, value, ttl) {
const now = Date.now();
this.cache.set(key, {
value,
resolvedAt: now,
expiresAt: now + ttl,
key,
});
}
delete(key) {
return this.cache.delete(key);
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
startCleanup(intervalMs) {
const timer = setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(key);
}
}
}, intervalMs);
if (typeof timer.unref === "function") {
timer.unref();
}
this.cleanupInterval = timer;
}
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.cache.clear();
}
}
// Global resolution cache
const globalCache = new ResolutionCache();
// Stable per-instance ids for function arguments — `String(fn)` is unsafe
// because two distinct closures with identical source text collide.
const functionIds = new WeakMap();
let nextFunctionId = 0;
function getArgumentId(argument) {
if (typeof argument !== "function") {
return String(argument);
}
let id = functionIds.get(argument);
if (id === undefined) {
id = `fn:${++nextFunctionId}`;
functionIds.set(argument, id);
}
return id;
}
/**
* Generate cache key for dynamic argument resolution
*/
function generateCacheKey(argumentId, context, options) {
if (options?.cacheKey) {
return options.cacheKey;
}
const parts = [argumentId];
if (context?.requestContext) {
// Extract cache-scoping ids from generic context (consumer-defined shape)
const rc = context.requestContext;
if (rc.user?.id) {
parts.push(`user:${rc.user.id}`);
}
if (rc.tenant?.id) {
parts.push(`tenant:${rc.tenant.id}`);
}
if (rc.session?.id) {
parts.push(`session:${rc.session.id}`);
}
}
return parts.join(":");
}
/**
* Resolve a dynamic argument to its actual value
*
* @template T - The expected resolved type
* @param argument - The dynamic argument to resolve
* @param context - Resolution context (optional for static values)
* @param options - Resolution options
* @returns Resolution result with value and metadata
*
* @example Resolve static value
* ```typescript
* const result = await resolveDynamicArgument("gpt-4o");
* console.log(result.value); // "gpt-4o"
* console.log(result.resolutionType); // "static"
* ```
*
* @example Resolve context-aware function
* ```typescript
* const modelSelector = ({ requestContext }) =>
* requestContext.tenant?.plan === "enterprise" ? "claude-3-opus" : "claude-3-sonnet";
*
* const result = await resolveDynamicArgument(modelSelector, {
* requestContext: { requestId: "123", tenant: { id: "t1", plan: "enterprise" } }
* });
* console.log(result.value); // "claude-3-opus"
* ```
*/
export async function resolveDynamicArgument(argument, context, options) {
const startTime = Date.now();
const opts = { ...DEFAULT_RESOLUTION_OPTIONS, ...options };
// Check cache first
if (opts.cache) {
const cacheKey = generateCacheKey(getArgumentId(argument), context, options);
const cached = globalCache.get(cacheKey);
if (cached !== undefined) {
return {
value: cached,
fromCache: true,
resolutionTime: Date.now() - startTime,
resolutionType: "static", // Cached value, original type unknown
};
}
}
try {
// Static value
if (!isDynamicFunction(argument)) {
const result = {
value: argument,
fromCache: false,
resolutionTime: Date.now() - startTime,
resolutionType: "static",
};
if (opts.cache) {
const cacheKey = generateCacheKey(getArgumentId(argument), context, options);
globalCache.set(cacheKey, argument, opts.cacheTtl);
}
return result;
}
// Function value
let resolvedValue;
let resolutionType = "async-function";
const resolutionPromise = (async () => {
if (isContextAwareFunction(argument)) {
// Context-aware function — pass an empty context if caller didn't
// provide one. Combinators (withFallback, conditional, etc.) return
// arity-1 functions even when their inputs don't need context, so
// requiring a context here would force callers to invent one.
resolutionType = "context-aware";
return argument(context ?? { requestContext: {} });
}
else {
// No-argument function
const fn = argument;
const result = fn();
if (result instanceof Promise) {
resolutionType = "async-function";
return result;
}
else {
resolutionType = "sync-function";
return result;
}
}
})();
// Apply timeout if specified
if (opts.timeout > 0) {
resolvedValue = await withTimeout(resolutionPromise, opts.timeout, new Error(`Dynamic argument resolution timed out after ${opts.timeout}ms`));
}
else {
resolvedValue = await resolutionPromise;
}
const result = {
value: resolvedValue,
fromCache: false,
resolutionTime: Date.now() - startTime,
resolutionType: resolutionType,
};
// Cache if enabled
if (opts.cache) {
const cacheKey = generateCacheKey(getArgumentId(argument), context, options);
globalCache.set(cacheKey, resolvedValue, opts.cacheTtl);
}
return result;
}
catch (error) {
logger.error("Dynamic argument resolution failed", {
error: error instanceof Error ? error.message : String(error),
resolutionTime: Date.now() - startTime,
});
if (opts.throwOnError) {
throw error;
}
// Return default value on error
return {
value: opts.defaultValue,
fromCache: false,
resolutionTime: Date.now() - startTime,
resolutionType: "static",
};
}
}
/**
* Resolve multiple dynamic arguments in parallel
*
* @example
* ```typescript
* const [model, temperature] = await resolveDynamicArguments(
* [
* ({ requestContext }) => requestContext.user?.preferences?.preferredModel || "gpt-4o",
* 0.7,
* ],
* context
* );
* ```
*/
export async function resolveDynamicArguments(arguments_, context, options) {
const results = await Promise.all(arguments_.map((arg) => resolveDynamicArgument(arg, context, options)));
return results.map((r) => r.value);
}
/**
* Resolve all properties of a dynamic configuration object
*
* @example
* ```typescript
* const dynamicConfig = {
* model: ({ requestContext }) => requestContext.tenant?.settings?.defaultModel || "gpt-4o",
* temperature: 0.7,
* maxTokens: async () => (await fetchConfig()).maxTokens,
* };
*
* const resolved = await resolveDynamicConfig(dynamicConfig, context);
* // resolved.model, resolved.temperature, resolved.maxTokens are all resolved values
* ```
*/
export async function resolveDynamicConfig(config, context, options) {
const entries = Object.entries(config);
const resolvedEntries = await Promise.all(entries.map(async ([key, value]) => {
const result = await resolveDynamicArgument(value, context, options);
return [key, result.value];
}));
return Object.fromEntries(resolvedEntries);
}
/**
* Create a memoized dynamic argument that caches its result
*
* @example
* ```typescript
* const expensiveModelSelector = memoizeDynamicArgument(
* async ({ requestContext }) => {
* const config = await fetchTenantConfig(requestContext.tenant?.id);
* return config.preferredModel;
* },
* { cacheTtl: 300000 } // Cache for 5 minutes
* );
* ```
*/
export function memoizeDynamicArgument(argument, options) {
if (!isDynamicFunction(argument)) {
return argument; // Static values don't need memoization
}
const cache = new Map();
const ttl = options?.cacheTtl || 60000;
return async (context) => {
const key = options?.cacheKey ||
generateCacheKey("memoized", context, { cacheKey: options?.cacheKey });
const cached = cache.get(key);
if (cached && Date.now() < cached.expiresAt) {
return cached.value;
}
const result = await resolveDynamicArgument(argument, context);
cache.set(key, { value: result.value, expiresAt: Date.now() + ttl });
return result.value;
};
}
/**
* Create a dynamic argument with fallback chain
*
* @example
* ```typescript
* const modelWithFallback = withFallback(
* ({ requestContext }) => requestContext.user?.preferences?.preferredModel,
* ({ requestContext }) => requestContext.tenant?.settings?.defaultModel,
* "gpt-4o" // Final static fallback
* );
* ```
*/
export function withFallback(...arguments_) {
return async (context) => {
let lastError;
for (const arg of arguments_) {
try {
const result = await resolveDynamicArgument(arg, context, {
throwOnError: false,
});
if (result.value !== undefined && result.value !== null) {
return result.value;
}
}
catch (err) {
lastError = err;
}
}
const msg = lastError instanceof Error ? lastError.message : String(lastError || "");
throw new Error(`All fallbacks failed${msg ? `: ${msg}` : ""}`, {
cause: lastError,
});
};
}
/**
* Create a conditional dynamic argument
*
* @example
* ```typescript
* const conditionalModel = conditional(
* ({ requestContext }) => requestContext.tenant?.plan === "enterprise",
* "claude-3-opus", // If true
* "claude-3-sonnet" // If false
* );
* ```
*/
export function conditional(condition, ifTrue, ifFalse) {
return async (context) => {
const conditionResult = await resolveDynamicArgument(condition, context);
if (conditionResult.value) {
return (await resolveDynamicArgument(ifTrue, context)).value;
}
return (await resolveDynamicArgument(ifFalse, context)).value;
};
}
/**
* Create a mapped dynamic argument that transforms the result
*
* @example
* ```typescript
* const upperCaseModel = mapDynamicArgument(
* ({ requestContext }) => requestContext.user?.preferences?.preferredModel,
* (model) => model?.toUpperCase()
* );
* ```
*/
export function mapDynamicArgument(argument, transform) {
return async (context) => {
const result = await resolveDynamicArgument(argument, context);
return transform(result.value);
};
}
/**
* Create a dynamic argument that combines multiple arguments
*
* @example
* ```typescript
* const combinedConfig = combineDynamicArguments(
* [
* ({ requestContext }) => requestContext.user?.preferences?.preferredModel,
* ({ requestContext }) => requestContext.tenant?.settings?.defaultTemperature,
* ],
* ([model, temperature]) => ({ model: model || "gpt-4o", temperature: temperature || 0.7 })
* );
* ```
*/
export function combineDynamicArguments(arguments_, combiner) {
return async (context) => {
const results = await resolveDynamicArguments(arguments_, context);
return combiner(results);
};
}
/**
* Check if a value contains any dynamic arguments (is a function)
*/
export function hasDynamicArgument(value) {
return isDynamicFunction(value);
}
/**
* Check if an object has any dynamic properties
*/
export function hasDynamicProperties(config) {
return Object.values(config).some((value) => isDynamicFunction(value));
}
/**
* Clear the global resolution cache
*/
export function clearResolutionCache() {
globalCache.clear();
}
/**
* Get resolution cache statistics
*/
export function getResolutionCacheStats() {
return { size: globalCache.size() };
}
/**
* Destroy the resolver (cleanup intervals, etc.)
*/
export function destroyResolver() {
globalCache.destroy();
}
// ============================================================================
// Environment Variable Interpolation
// ============================================================================
/**
* Pattern for environment variable interpolation
* Supports: ${ENV_VAR}, ${ENV_VAR:-default}, ${ENV_VAR:+replacement}
*/
const ENV_VAR_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::([+-]?)([^}]*))?\}/g;
/**
* Interpolate environment variables in a string
*
* Supports syntax:
* - ${VAR} - Simple substitution
* - ${VAR:-default} - Use default if VAR is unset or empty
* - ${VAR:+replacement} - Use replacement if VAR is set and non-empty
*
* @example
* ```typescript
* interpolateEnvVars("Model: ${DEFAULT_MODEL:-gpt-4o}");
* // Returns "Model: gpt-4o" if DEFAULT_MODEL is not set
*
* interpolateEnvVars("API Key: ${OPENAI_API_KEY}");
* // Returns "API Key: sk-xxx..." if OPENAI_API_KEY is set
*
* interpolateEnvVars("Debug: ${DEBUG:+enabled}");
* // Returns "Debug: enabled" if DEBUG is set, "Debug: " otherwise
* ```
*/
export function interpolateEnvVars(input, customEnv) {
const env = customEnv || process.env;
return input.replace(ENV_VAR_PATTERN, (match, varName, modifier, value) => {
const envValue = env[varName];
const isSet = envValue !== undefined && envValue !== "";
if (modifier === "-") {
// ${VAR:-default} - Use default if unset or empty
return isSet ? envValue : value || "";
}
else if (modifier === "+") {
// ${VAR:+replacement} - Use replacement if set and non-empty
return isSet ? value || "" : "";
}
else {
// ${VAR} - Simple substitution
return envValue || "";
}
});
}
/**
* Create a dynamic argument that interpolates environment variables
*
* @example
* ```typescript
* const model = fromEnv("${PREFERRED_MODEL:-gpt-4o}");
* // Resolves to value of PREFERRED_MODEL or "gpt-4o" as fallback
* ```
*/
export function fromEnv(template) {
return () => interpolateEnvVars(template);
}
/**
* Create a dynamic argument from a single environment variable
*
* @example
* ```typescript
* const apiKey = envVar("OPENAI_API_KEY");
* // Resolves to value of OPENAI_API_KEY or undefined
*
* const model = envVar("DEFAULT_MODEL", "gpt-4o");
* // Resolves to DEFAULT_MODEL value or "gpt-4o" as default
* ```
*/
export function envVar(name, defaultValue) {
return () => {
const value = process.env[name];
return (value !== undefined && value !== "" ? value : defaultValue);
};
}
/**
* Create a dynamic argument that selects from environment-based configurations
*
* @example
* ```typescript
* const model = envSwitch("NODE_ENV", {
* development: "gpt-4o-mini",
* production: "gpt-4o",
* test: "gpt-3.5-turbo",
* }, "gpt-4o-mini");
* ```
*/
export function envSwitch(envVarName, options, defaultValue) {
return () => {
const envValue = process.env[envVarName];
if (envValue && envValue in options) {
return options[envValue];
}
return defaultValue;
};
}
/**
* Create a dynamic argument that parses a JSON value from an environment variable
*
* @example
* ```typescript
* // If RATE_LIMITS='{"requestsPerMinute": 100, "tokensPerDay": 50000}'
* const rateLimits = envJson<RateLimits>("RATE_LIMITS", { requestsPerMinute: 10 });
* ```
*/
export function envJson(name, defaultValue) {
return () => {
const value = process.env[name];
if (!value) {
return defaultValue;
}
try {
return JSON.parse(value);
}
catch {
logger.warn(`Failed to parse JSON from environment variable ${name}`);
return defaultValue;
}
};
}
/**
* Create a dynamic argument that reads a number from an environment variable
*
* @example
* ```typescript
* const maxTokens = envNumber("MAX_TOKENS", 1000);
* const temperature = envNumber("TEMPERATURE", 0.7);
* ```
*/
export function envNumber(name, defaultValue) {
return () => {
const value = process.env[name];
if (!value) {
return defaultValue;
}
const parsed = parseFloat(value);
return isNaN(parsed) ? defaultValue : parsed;
};
}
/**
* Create a dynamic argument that reads a boolean from an environment variable
*
* @example
* ```typescript
* const enableDebug = envBoolean("DEBUG", false);
* const enableTools = envBoolean("ENABLE_TOOLS", true);
* ```
*/
export function envBoolean(name, defaultValue) {
return () => {
const value = process.env[name];
if (!value) {
return defaultValue;
}
const lower = value.toLowerCase();
if (lower === "true" ||
lower === "1" ||
lower === "yes" ||
lower === "on") {
return true;
}
if (lower === "false" ||
lower === "0" ||
lower === "no" ||
lower === "off") {
return false;
}
return defaultValue;
};
}
/**
* Create a dynamic argument that reads a comma-separated list from an environment variable
*
* @example
* ```typescript
* // If ALLOWED_PROVIDERS='openai,anthropic,vertex'
* const providers = envList("ALLOWED_PROVIDERS", ["openai"]);
* // Returns ["openai", "anthropic", "vertex"]
* ```
*/
export function envList(name, defaultValue, separator = ",") {
return () => {
const value = process.env[name];
if (!value) {
return defaultValue;
}
return value
.split(separator)
.map((s) => s.trim())
.filter(Boolean);
};
}
export { globalCache as resolutionCache };
//# sourceMappingURL=dynamicResolver.js.map