feat: add core domain types for Photo Gallery SDK including media items, albums, and annotations

This commit is contained in:
2026-07-23 07:39:01 +05:30
parent 7bcd1a2a2d
commit 2613c767a6
107 changed files with 20699 additions and 7 deletions
+86
View File
@@ -0,0 +1,86 @@
/**
* Tiny in-process fixed-window rate limiter for the Smart Gallery AI routes.
*
* ---------------------------------------------------------------------------
* SCOPE AND LIMITATIONS — READ BEFORE RELYING ON THIS
* ---------------------------------------------------------------------------
* State lives in a plain `Map` in THIS process's memory. That means:
*
* - PER-INSTANCE, NOT GLOBAL. With N app instances behind a load balancer a
* caller gets up to N x the configured budget. On serverless platforms each
* cold start begins with an empty map, so the effective limit is weaker
* still.
* - NOT DURABLE. A restart or redeploy clears every counter.
* - FIXED WINDOW, NOT SLIDING. A caller can burst `max` at the very end of one
* window and `max` again at the start of the next — up to 2x `max` across a
* window boundary. Acceptable here; the goal is to bound runaway cost, not
* to meter precisely.
*
* It exists because the routes it guards spend real money on GPU inference and
* shipping them with NO limit at all is worse than shipping an imperfect one.
*
* REPLACE WITH REDIS (or the platform's rate limiter) BEFORE RUNNING MORE THAN
* ONE INSTANCE. The `limit()` signature is deliberately narrow so a Redis-backed
* implementation can drop straight in — the only change needed is making it
* async at the call sites.
*
* This is a throttle, not an authorization check. See src/lib/server/session.ts.
*/
interface Window {
/** Requests counted so far in the current window. */
count: number;
/** Epoch ms at which the current window ends. */
resetAt: number;
}
const windows = new Map<string, Window>();
/** Drop expired entries so the map cannot grow without bound. */
const SWEEP_INTERVAL_MS = 60_000;
let lastSweep = 0;
function sweep(now: number): void {
if (now - lastSweep < SWEEP_INTERVAL_MS) return;
lastSweep = now;
for (const [key, w] of windows) {
if (w.resetAt <= now) windows.delete(key);
}
}
export interface LimitResult {
ok: boolean;
/** Seconds until the window resets. Send as `Retry-After` when `ok` is false. */
retryAfter: number;
}
/**
* Count one request against `key` and report whether it is allowed.
*
* @param key Caller identity — use `rateLimitKey()` from ./session.
* @param max Requests allowed per window.
* @param windowMs Window length in ms.
*/
export function limit(key: string, max: number, windowMs: number): LimitResult {
const now = Date.now();
sweep(now);
const existing = windows.get(key);
if (!existing || existing.resetAt <= now) {
windows.set(key, { count: 1, resetAt: now + windowMs });
return { ok: true, retryAfter: 0 };
}
if (existing.count >= max) {
return { ok: false, retryAfter: Math.max(1, Math.ceil((existing.resetAt - now) / 1000)) };
}
existing.count += 1;
return { ok: true, retryAfter: 0 };
}
/** Test/maintenance helper — clears all counters. */
export function resetAllLimits(): void {
windows.clear();
lastSweep = 0;
}