Add GET endpoint support, response caching, and update landing page
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m11s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m11s
- Add GET /v1/screenshot endpoint with query parameter support - Support API key authentication via ?key= query parameter - Implement in-memory LRU cache with configurable TTL (5min) and size limits (100MB) - Add X-Cache headers (HIT/MISS) to indicate cache status - Add cache bypass option via ?cache=false parameter - Update OpenAPI documentation with GET endpoint and caching info - Add GET/Embed code examples to landing page hero section - Add Response Caching and GET Request Support feature cards - Update features grid layout to accommodate new features
This commit is contained in:
parent
609e7d0808
commit
44e31e355c
5 changed files with 433 additions and 18 deletions
195
src/services/cache.ts
Normal file
195
src/services/cache.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
import crypto from "crypto";
|
||||
import logger from "./logger.js";
|
||||
|
||||
interface CacheItem {
|
||||
buffer: Buffer;
|
||||
contentType: string;
|
||||
timestamp: number;
|
||||
lastAccessed: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export class ScreenshotCache {
|
||||
private cache = new Map<string, CacheItem>();
|
||||
private ttlMs: number;
|
||||
private maxSizeBytes: number;
|
||||
private currentSizeBytes = 0;
|
||||
|
||||
constructor() {
|
||||
// Default TTL: 5 minutes (configurable via env)
|
||||
this.ttlMs = parseInt(process.env.CACHE_TTL_MS || "300000", 10);
|
||||
// Default max size: 100MB (configurable via env)
|
||||
this.maxSizeBytes = parseInt(process.env.CACHE_MAX_MB || "100", 10) * 1024 * 1024;
|
||||
|
||||
// Start cleanup interval every minute
|
||||
setInterval(() => this.cleanup(), 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key from URL and screenshot parameters
|
||||
*/
|
||||
private generateKey(params: any): string {
|
||||
// Hash all parameters that affect the screenshot result
|
||||
const keyData = {
|
||||
url: params.url,
|
||||
format: params.format || "png",
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
fullPage: params.fullPage,
|
||||
quality: params.quality,
|
||||
waitForSelector: params.waitForSelector,
|
||||
deviceScale: params.deviceScale,
|
||||
delay: params.delay,
|
||||
waitUntil: params.waitUntil,
|
||||
};
|
||||
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(JSON.stringify(keyData));
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached screenshot if available and not expired
|
||||
*/
|
||||
get(params: any): CacheItem | null {
|
||||
const key = this.generateKey(params);
|
||||
const item = this.cache.get(key);
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() - item.timestamp > this.ttlMs) {
|
||||
this.cache.delete(key);
|
||||
this.currentSizeBytes -= item.size;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last accessed time for LRU
|
||||
item.lastAccessed = Date.now();
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store screenshot in cache
|
||||
*/
|
||||
put(params: any, buffer: Buffer, contentType: string): void {
|
||||
const key = this.generateKey(params);
|
||||
const size = buffer.length;
|
||||
const now = Date.now();
|
||||
|
||||
// Don't cache if item is larger than 50% of max cache size
|
||||
if (size > this.maxSizeBytes * 0.5) {
|
||||
logger.warn({ size, maxSize: this.maxSizeBytes }, "Screenshot too large to cache");
|
||||
return;
|
||||
}
|
||||
|
||||
// Make room if needed
|
||||
this.evictToFit(size);
|
||||
|
||||
const item: CacheItem = {
|
||||
buffer,
|
||||
contentType,
|
||||
timestamp: now,
|
||||
lastAccessed: now,
|
||||
size,
|
||||
};
|
||||
|
||||
this.cache.set(key, item);
|
||||
this.currentSizeBytes += size;
|
||||
|
||||
logger.debug({
|
||||
key: key.substring(0, 8),
|
||||
size,
|
||||
totalItems: this.cache.size,
|
||||
totalSize: this.currentSizeBytes
|
||||
}, "Screenshot cached");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if caching should be bypassed
|
||||
*/
|
||||
shouldBypass(params: any): boolean {
|
||||
// Check for cache=false in params (both GET query and POST body)
|
||||
return params.cache === false || params.cache === "false";
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict items to make room for new item
|
||||
*/
|
||||
private evictToFit(newItemSize: number): void {
|
||||
// Calculate how much space we need
|
||||
const availableSpace = this.maxSizeBytes - this.currentSizeBytes;
|
||||
if (availableSpace >= newItemSize) {
|
||||
return; // No eviction needed
|
||||
}
|
||||
|
||||
const spaceNeeded = newItemSize - availableSpace;
|
||||
let spaceFreed = 0;
|
||||
|
||||
// Sort items by last accessed time (oldest first)
|
||||
const items = Array.from(this.cache.entries()).sort(
|
||||
([, a], [, b]) => a.lastAccessed - b.lastAccessed
|
||||
);
|
||||
|
||||
for (const [key, item] of items) {
|
||||
if (spaceFreed >= spaceNeeded) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.cache.delete(key);
|
||||
this.currentSizeBytes -= item.size;
|
||||
spaceFreed += item.size;
|
||||
|
||||
logger.debug({
|
||||
key: key.substring(0, 8),
|
||||
size: item.size,
|
||||
spaceFreed,
|
||||
spaceNeeded
|
||||
}, "Evicted cache item");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired items
|
||||
*/
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
let expiredCount = 0;
|
||||
let freedBytes = 0;
|
||||
|
||||
for (const [key, item] of this.cache.entries()) {
|
||||
if (now - item.timestamp > this.ttlMs) {
|
||||
this.cache.delete(key);
|
||||
this.currentSizeBytes -= item.size;
|
||||
expiredCount++;
|
||||
freedBytes += item.size;
|
||||
}
|
||||
}
|
||||
|
||||
if (expiredCount > 0) {
|
||||
logger.debug({
|
||||
expiredCount,
|
||||
freedBytes,
|
||||
remainingItems: this.cache.size,
|
||||
totalSize: this.currentSizeBytes
|
||||
}, "Cache cleanup completed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
items: this.cache.size,
|
||||
sizeBytes: this.currentSizeBytes,
|
||||
maxSizeBytes: this.maxSizeBytes,
|
||||
ttlMs: this.ttlMs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const screenshotCache = new ScreenshotCache();
|
||||
Loading…
Add table
Add a link
Reference in a new issue