@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
366 lines (365 loc) • 10.5 kB
JavaScript
/**
* Cache Middleware
* Provides response caching for server adapters
*/
/**
* Cache entry
*/
/**
* Cache store interface
*/
/**
* In-memory LRU cache store
*/
export class InMemoryCacheStore {
cache = new Map();
accessOrder = [];
maxSize;
constructor(maxSize = 1000) {
this.maxSize = maxSize;
}
async get(key) {
const entry = this.cache.get(key);
if (!entry) {
return undefined;
}
// Check if expired
if (Date.now() - entry.createdAt > entry.ttlMs) {
await this.delete(key);
return undefined;
}
// Update access order (LRU)
this.updateAccessOrder(key);
return entry;
}
async set(key, entry) {
// Check size limit
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
// Remove least recently used
const lruKey = this.accessOrder.shift();
if (lruKey) {
this.cache.delete(lruKey);
}
}
this.cache.set(key, entry);
this.updateAccessOrder(key);
}
async delete(key) {
this.cache.delete(key);
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
}
async clear() {
this.cache.clear();
this.accessOrder = [];
}
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
}
/**
* Create cache middleware
*
* Response headers set by this middleware:
* - `X-Cache`: "HIT" if served from cache, "MISS" if freshly generated
* - `X-Cache-Age`: Seconds since the response was cached (only on HIT)
* - `Cache-Control`: Caching directive with max-age (only on MISS)
*
* @example
* ```typescript
* const cacheMiddleware = createCacheMiddleware({
* ttlMs: 60 * 1000, // 1 minute
* methods: ["GET"],
* excludePaths: ["/api/health"],
* });
*
* server.registerMiddleware(cacheMiddleware);
* ```
*/
export function createCacheMiddleware(config) {
const { ttlMs, maxSize = 1000, keyGenerator = defaultKeyGenerator, methods = ["GET"], paths, excludePaths = [], store = new InMemoryCacheStore(maxSize), includeQuery: _includeQuery = true, ttlByPath = {}, } = config;
return {
name: "cache",
order: 20, // Run after auth but before route handlers
excludePaths,
handler: async (ctx, next) => {
// Only cache specified methods
if (!methods.includes(ctx.method.toUpperCase())) {
return next();
}
// Check if path should be cached
if (paths && !paths.some((p) => ctx.path.startsWith(p))) {
return next();
}
// Generate cache key
const cacheKey = keyGenerator(ctx);
// Check cache
const cached = await store.get(cacheKey);
if (cached) {
// Set cache headers in responseHeaders for actual HTTP response
ctx.responseHeaders = ctx.responseHeaders || {};
ctx.responseHeaders["X-Cache"] = "HIT";
ctx.responseHeaders["X-Cache-Age"] = String(Math.floor((Date.now() - cached.createdAt) / 1000));
// Return cached data (middleware should handle this)
ctx.metadata.cachedResponse = cached.data;
return cached.data;
}
// Execute handler and cache result
const result = await next();
// Determine TTL for this path
let pathTtl = ttlMs;
for (const [pattern, patternTtl] of Object.entries(ttlByPath)) {
if (ctx.path.startsWith(pattern)) {
pathTtl = patternTtl;
break;
}
}
// Store in cache
await store.set(cacheKey, {
data: result,
createdAt: Date.now(),
ttlMs: pathTtl,
});
// Set cache headers in responseHeaders for actual HTTP response
ctx.responseHeaders = ctx.responseHeaders || {};
ctx.responseHeaders["X-Cache"] = "MISS";
ctx.responseHeaders["Cache-Control"] =
`max-age=${Math.floor(pathTtl / 1000)}`;
return result;
},
};
}
/**
* Default cache key generator
*/
function defaultKeyGenerator(ctx) {
const parts = [ctx.method, ctx.path];
// Include sorted query params
const queryKeys = Object.keys(ctx.query).sort();
if (queryKeys.length > 0) {
const queryPart = queryKeys.map((k) => `${k}=${ctx.query[k]}`).join("&");
parts.push(queryPart);
}
return parts.join(":");
}
/**
* Create a cache invalidation helper
*/
export function createCacheInvalidator(store) {
return {
invalidate: async (pattern) => {
// For in-memory store, this would need iteration
// For Redis, you could use pattern matching
await store.delete(pattern);
},
clear: async () => {
await store.clear();
},
};
}
// ============================================
// LRU Cache (Synchronous)
// ============================================
/**
* Generic LRU (Least Recently Used) Cache
*
* Provides a simple in-memory cache with LRU eviction policy.
*
* @example
* ```typescript
* const cache = new LRUCache<string, number>(100);
*
* cache.set("key1", 42);
* cache.get("key1"); // => 42
* cache.has("key1"); // => true
* cache.delete("key1");
* ```
*/
export class LRUCache {
cache = new Map();
accessOrder = [];
maxSize;
constructor(maxSize = 1000) {
this.maxSize = maxSize;
}
/**
* Get a value from the cache
*/
get(key) {
const value = this.cache.get(key);
if (value !== undefined) {
this.updateAccessOrder(key);
}
return value;
}
/**
* Set a value in the cache
*/
set(key, value) {
// Check size limit
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
// Remove least recently used
const lruKey = this.accessOrder.shift();
if (lruKey !== undefined) {
this.cache.delete(lruKey);
}
}
this.cache.set(key, value);
this.updateAccessOrder(key);
}
/**
* Check if a key exists in the cache
*/
has(key) {
return this.cache.has(key);
}
/**
* Delete a key from the cache
*/
delete(key) {
const deleted = this.cache.delete(key);
if (deleted) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
}
return deleted;
}
/**
* Clear the cache
*/
clear() {
this.cache.clear();
this.accessOrder = [];
}
/**
* Get the current size of the cache
*/
get size() {
return this.cache.size;
}
updateAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index > -1) {
this.accessOrder.splice(index, 1);
}
this.accessOrder.push(key);
}
}
// ============================================
// Response Cache Store (Synchronous)
// ============================================
/**
* Synchronous response cache store with TTL support
*
* Designed for caching HTTP responses with automatic expiration.
*
* @example
* ```typescript
* const store = new ResponseCacheStore(100, 60000); // 100 entries, 60s TTL
*
* store.set("GET:/api/users", { status: 200, data: [...] });
* const cached = store.get("GET:/api/users");
*
* // Invalidate specific key
* store.invalidate("GET:/api/users");
*
* // Invalidate by pattern (e.g., all user endpoints)
* store.invalidateByPattern("/api/users");
* ```
*/
export class ResponseCacheStore {
cache;
ttlMs;
constructor(maxSize = 1000, ttlMs = 60000) {
this.cache = new LRUCache(maxSize);
this.ttlMs = ttlMs;
}
/**
* Get a value from the cache
*/
get(key) {
const entry = this.cache.get(key);
if (!entry) {
return undefined;
}
// Check if expired
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}
/**
* Set a value in the cache
*/
set(key, value, ttlMs) {
const effectiveTtl = ttlMs ?? this.ttlMs;
this.cache.set(key, {
value,
expiresAt: Date.now() + effectiveTtl,
});
}
/**
* Check if a key exists and is not expired
*/
has(key) {
return this.get(key) !== undefined;
}
/**
* Invalidate (delete) a specific key
*/
invalidate(key) {
return this.cache.delete(key);
}
/**
* Invalidate all keys matching a pattern (substring match or regex)
* @param pattern - String to match or RegExp
*/
invalidateByPattern(pattern) {
return this.invalidatePattern(pattern);
}
/**
* Invalidate all keys matching a pattern (substring match or regex)
* @param pattern - String to match or RegExp
*/
invalidatePattern(pattern) {
// Note: This is O(n) because we need to iterate through all keys
// For production, consider using a more efficient data structure
let invalidated = 0;
const keysToDelete = [];
// Get all keys (requires accessing internal cache)
const internalCache = this.cache.cache;
for (const key of internalCache.keys()) {
const matches = pattern instanceof RegExp ? pattern.test(key) : key.includes(pattern);
if (matches) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
if (this.cache.delete(key)) {
invalidated++;
}
}
return invalidated;
}
/**
* Clear the entire cache
*/
clear() {
this.cache.clear();
}
/**
* Get the current size of the cache
*/
get size() {
return this.cache.size;
}
}