agent-smith/dist-in/commons/decorators.js
2026-02-26 19:41:09 +01:00

195 lines
16 KiB
JavaScript

import { trackUsage, updateUsageRecord } from '../middleware/usageTracking.js';
import { FunctionRegistry, PublicEndpointRegistry, AdminEndpointRegistry } from './registry.js';
import { logger } from './logger.js';
/**
* Decorator/Wrapper to mark an endpoint as public
* Registers the route in PublicEndpointRegistry
*/
export function Public(route) {
PublicEndpointRegistry.register(route.path, route.method);
return route;
}
/**
* Decorator/Wrapper to mark an endpoint as admin-only
* Registers the route in AdminEndpointRegistry
*/
export function Admin(route) {
AdminEndpointRegistry.register(route.path, route.method);
return route;
}
/**
* Decorator to mark a method as billable
* Handles usage tracking, context injection, and cancellation
*/
export function Billable(options) {
return function (target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args) {
// 1. Extract context
// Assumes the first argument is BillableContext, or it's part of the first argument object
let context;
if (args.length > 0 && typeof args[0] === 'object') {
// Check if first arg is context
if ('userId' in args[0] && 'jobId' in args[0]) {
context = args[0];
}
}
if (!context) {
// If no context provided, we can't track usage properly
// For now, we'll log a warning and proceed without tracking
// In strict mode, we might want to throw an error
logger.warn(`[Billable] No context provided for ${options.productId}:${options.actionId}`);
return originalMethod.apply(this, args);
}
// 2. Get config
const config = FunctionRegistry.get(options.productId, options.actionId);
if (!config) {
logger.warn(`[Billable] No config found for ${options.productId}:${options.actionId}`);
return originalMethod.apply(this, args);
}
// 3. Start tracking
const usageId = await trackUsage({
userId: context.userId,
endpoint: 'function', // Internal function call
method: 'CALL',
product: options.productId,
action: options.actionId,
costUnits: config.costUnits,
cancellable: options.cancellable || false,
jobId: context.jobId,
metadata: context.metadata
});
const startTime = Date.now();
let error = null;
let result;
try {
// 4. Execute method
// If cancellable, we should ideally wrap the execution or check signal
if (options.cancellable && context.signal) {
if (context.signal.aborted) {
throw new Error('Operation cancelled');
}
// Add abort listener
context.signal.addEventListener('abort', () => {
logger.info(`[Billable] Job ${context?.jobId} aborted via signal`);
});
}
result = await originalMethod.apply(this, args);
return result;
}
catch (err) {
error = err;
throw err;
}
finally {
// 5. End tracking
if (usageId) {
const endTime = Date.now();
await updateUsageRecord({
usageId,
responseStatus: error ? 500 : 200,
responseTimeMs: endTime - startTime,
error
});
}
}
};
return descriptor;
};
}
/**
* Class Decorator: Registers the worker queue name
*/
export function Worker(queueName) {
return function (constructor) {
// We can't easily access the instance method 'handler' here without instantiating
// So we assume the class has a 'handler' method or we register the class itself
// For simplicity, let's assume we'll instantiate it later or the registry handles it.
// But wait, pg-boss needs a function.
// Let's store the constructor in the registry, and the registry (or bootstrap) will instantiate and bind.
// Actually, let's just attach the queue name to the class for now,
// and let a separate scanner or manual registration use it.
// OR, we can register a factory.
// Better approach for now: Register the prototype's handler if it exists.
// But 'handler' is on the instance usually.
// Let's just modify the class to have a static 'queueName' property
// and register it.
constructor.queueName = queueName;
};
}
import { getCache } from './cache/index.js';
const defaultKeyInfo = (c) => {
const url = new URL(c.req.url);
url.searchParams.sort();
return `auto-cache:${c.req.method}:${url.pathname}${url.search}`;
};
export const CachedHandler = (handler, options) => async (c) => {
const opts = options || {};
const ttl = opts.ttl || 300;
const varyByAuth = opts.varyByAuth || false;
const skipAuth = opts.skipAuth !== undefined ? opts.skipAuth : !varyByAuth; // Default true unless varyByAuth is true
const maxSizeBytes = opts.maxSizeBytes || 1024 * 1024; // 1MB
const keyGen = opts.keyGenerator || defaultKeyInfo;
// 1. Auth Bypass
const authHeader = c.req.header('Authorization');
if (skipAuth && authHeader) {
// Explicitly mark as skipped due to auth
c.header('X-Cache', 'SKIP');
return handler(c);
}
const cache = getCache();
let key = keyGen(c);
// Append Auth to key if requested (User Isolation)
if (varyByAuth && authHeader) {
key += `|auth=${authHeader}`;
}
const bypass = c.req.query('cache') === 'false' || c.req.query('nocache') === 'true';
// 2. Hit
if (!bypass) {
const cached = await cache.get(key);
if (cached) {
c.header('X-Cache', 'HIT');
const cachedVal = cached;
if (cachedVal.contentType)
c.header('Content-Type', cachedVal.contentType);
if (varyByAuth)
c.header('Vary', 'Authorization');
return c.body(cachedVal.data);
}
}
// 3. Miss
const response = await handler(c);
// 4. Save
if (response instanceof Response && response.ok) {
const cloned = response.clone();
try {
const contentType = response.headers.get('Content-Type') || 'application/json';
let data;
// Check content length if available
const contentLength = cloned.headers.get('Content-Length');
if (contentLength && parseInt(contentLength) > maxSizeBytes) {
return response;
}
if (contentType.includes('application/json')) {
const jsonObj = await cloned.json();
data = JSON.stringify(jsonObj);
}
else {
data = await cloned.text();
}
// Double check actual size after reading
if (data.length > maxSizeBytes) {
return response;
}
await cache.set(key, { data, contentType }, ttl);
c.header('X-Cache', bypass ? 'BYPASS' : 'MISS');
if (varyByAuth)
c.header('Vary', 'Authorization');
}
catch (e) {
logger.error({ err: e }, 'Cache interception failed');
}
}
return response;
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVjb3JhdG9ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9jb21tb25zL2RlY29yYXRvcnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQy9FLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxzQkFBc0IsRUFBRSxxQkFBcUIsRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUNoRyxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sYUFBYSxDQUFDO0FBRXJDOzs7R0FHRztBQUNILE1BQU0sVUFBVSxNQUFNLENBQTZDLEtBQVE7SUFDdkUsc0JBQXNCLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQzFELE9BQU8sS0FBSyxDQUFDO0FBQ2pCLENBQUM7QUFFRDs7O0dBR0c7QUFDSCxNQUFNLFVBQVUsS0FBSyxDQUE2QyxLQUFRO0lBQ3RFLHFCQUFxQixDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUN6RCxPQUFPLEtBQUssQ0FBQztBQUNqQixDQUFDO0FBZUQ7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLFFBQVEsQ0FBQyxPQUF3QjtJQUM3QyxPQUFPLFVBQ0gsTUFBVyxFQUNYLFdBQW1CLEVBQ25CLFVBQThCO1FBRTlCLE1BQU0sY0FBYyxHQUFHLFVBQVUsQ0FBQyxLQUFLLENBQUM7UUFFeEMsVUFBVSxDQUFDLEtBQUssR0FBRyxLQUFLLFdBQVcsR0FBRyxJQUFXO1lBQzdDLHFCQUFxQjtZQUNyQiwyRkFBMkY7WUFDM0YsSUFBSSxPQUFvQyxDQUFDO1lBRXpDLElBQUksSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksT0FBTyxJQUFJLENBQUMsQ0FBQyxDQUFDLEtBQUssUUFBUSxFQUFFLENBQUM7Z0JBQ2pELGdDQUFn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