195 lines
16 KiB
JavaScript
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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
|