Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
86
fork

Configure Feed

Select the types of activity you want to include in your feed.

caching consolidation, fix tiered storage

+270 -315
+6 -5
apps/hosting-service/src/index.ts
··· 2 2 import { serve } from '@hono/node-server'; 3 3 import { initializeGrafanaExporters, createLogger } from '@wispplace/observability'; 4 4 import { mkdirSync, existsSync } from 'fs'; 5 - import { startDomainCacheCleanup, stopDomainCacheCleanup, closeDatabase, CACHE_ONLY } from './lib/db'; 5 + import { closeDatabase, CACHE_ONLY } from './lib/db'; 6 + import { cache } from './lib/cache-manager'; 6 7 import { closeRevalidateQueue } from './lib/revalidate-queue'; 7 8 import { startCacheInvalidationSubscriber, stopCacheInvalidationSubscriber } from './lib/cache-invalidation'; 8 9 import { storage, getStorageConfig } from './lib/storage'; ··· 24 25 logger.info('Created cache directory', { CACHE_DIR }); 25 26 } 26 27 27 - // Start domain cache cleanup 28 - startDomainCacheCleanup(); 28 + // Start in-memory cache cleanup 29 + cache.startCleanup(); 29 30 30 31 // Start cache invalidation subscriber (listens for firehose-service updates via Redis pub/sub) 31 32 startCacheInvalidationSubscriber(); ··· 87 88 // Graceful shutdown 88 89 process.on('SIGINT', async () => { 89 90 logger.info('Shutting down...'); 90 - stopDomainCacheCleanup(); 91 + cache.stopCleanup(); 91 92 await stopCacheInvalidationSubscriber(); 92 93 await closeRevalidateQueue(); 93 94 await closeDatabase(); ··· 97 98 98 99 process.on('SIGTERM', async () => { 99 100 logger.info('Shutting down...'); 100 - stopDomainCacheCleanup(); 101 + cache.stopCleanup(); 101 102 await stopCacheInvalidationSubscriber(); 102 103 await closeRevalidateQueue(); 103 104 await closeDatabase();
+4 -3
apps/hosting-service/src/lib/cache-invalidation.ts
··· 8 8 9 9 import Redis from 'ioredis'; 10 10 import { storage } from './storage'; 11 - import { clearRedirectRulesCache } from './site-cache'; 11 + import { cache } from './cache-manager'; 12 12 13 13 const CHANNEL = 'wisp:cache-invalidate'; 14 14 ··· 63 63 const deleted = await storage.invalidate(prefix); 64 64 console.log(`[CacheInvalidation] Cleared ${deleted} keys from tiered storage for ${did}/${rkey}`); 65 65 66 - // Clear redirect rules cache 67 - clearRedirectRulesCache(did, rkey); 66 + // Clear in-memory caches for this site 67 + cache.delete('redirectRules', `${did}:${rkey}`); 68 + cache.delete('settings', `${did}:${rkey}`); 68 69 } catch (err) { 69 70 console.error('[CacheInvalidation] Error processing message:', err); 70 71 }
+208
apps/hosting-service/src/lib/cache-manager.ts
··· 1 + /** 2 + * Centralized in-memory cache manager for the hosting service. 3 + * 4 + * Replaces the scattered TTL Maps (domains, customDomains, settings, handles) 5 + * and the LRU redirect-rules cache with a single, namespace-aware cache. 6 + */ 7 + 8 + interface NamespaceConfig { 9 + /** Time-to-live in milliseconds. Entries older than this are stale. */ 10 + ttl?: number; 11 + /** Maximum number of entries before LRU eviction kicks in. */ 12 + maxEntries?: number; 13 + /** Maximum total estimated size (bytes) before LRU eviction kicks in. */ 14 + maxSize?: number; 15 + /** Estimate the byte size of a value. Required when maxSize is set. */ 16 + estimateSize?: (value: unknown) => number; 17 + } 18 + 19 + interface CacheEntry { 20 + value: unknown; 21 + timestamp: number; 22 + size: number; 23 + } 24 + 25 + interface NamespaceStats { 26 + hits: number; 27 + misses: number; 28 + evictions: number; 29 + entries: number; 30 + sizeBytes: number; 31 + } 32 + 33 + interface GetOrFetchOpts<T> { 34 + /** Skip caching when predicate returns false (e.g. don't cache null). */ 35 + cacheIf?: (value: T) => boolean; 36 + } 37 + 38 + export class CacheManager<NS extends string = string> { 39 + private namespaces: Map<NS, Map<string, CacheEntry>> = new Map(); 40 + private configs: Map<NS, NamespaceConfig> = new Map(); 41 + private stats: Map<NS, NamespaceStats> = new Map(); 42 + private cleanupTimer: ReturnType<typeof setInterval> | null = null; 43 + 44 + constructor(config: Record<NS, NamespaceConfig>) { 45 + for (const [ns, cfg] of Object.entries(config) as [NS, NamespaceConfig][]) { 46 + this.namespaces.set(ns, new Map()); 47 + this.configs.set(ns, cfg); 48 + this.stats.set(ns, { hits: 0, misses: 0, evictions: 0, entries: 0, sizeBytes: 0 }); 49 + } 50 + } 51 + 52 + // ── Primary API ────────────────────────────────────────────────────── 53 + 54 + async getOrFetch<T>( 55 + ns: NS, 56 + key: string, 57 + fetcher: () => T | Promise<T>, 58 + opts?: GetOrFetchOpts<T>, 59 + ): Promise<T> { 60 + const existing = this.get<T>(ns, key); 61 + if (existing !== undefined) return existing; 62 + 63 + const value = await fetcher(); 64 + 65 + if (!opts?.cacheIf || opts.cacheIf(value)) { 66 + this.set(ns, key, value); 67 + } 68 + 69 + return value; 70 + } 71 + 72 + get<T>(ns: NS, key: string): T | undefined { 73 + const map = this.namespaces.get(ns); 74 + const cfg = this.configs.get(ns); 75 + const st = this.stats.get(ns); 76 + if (!map || !cfg || !st) return undefined; 77 + 78 + const entry = map.get(key); 79 + if (!entry) { 80 + st.misses++; 81 + return undefined; 82 + } 83 + 84 + // TTL check 85 + if (cfg.ttl && Date.now() - entry.timestamp > cfg.ttl) { 86 + map.delete(key); 87 + st.entries = map.size; 88 + st.sizeBytes -= entry.size; 89 + st.misses++; 90 + return undefined; 91 + } 92 + 93 + // Touch for LRU: delete + re-insert moves to end of Map iteration order 94 + map.delete(key); 95 + map.set(key, entry); 96 + 97 + st.hits++; 98 + return entry.value as T; 99 + } 100 + 101 + set(ns: NS, key: string, value: unknown): void { 102 + const map = this.namespaces.get(ns); 103 + const cfg = this.configs.get(ns); 104 + const st = this.stats.get(ns); 105 + if (!map || !cfg || !st) return; 106 + 107 + const size = cfg.estimateSize ? cfg.estimateSize(value) : 0; 108 + 109 + // Remove existing entry first 110 + const existing = map.get(key); 111 + if (existing) { 112 + st.sizeBytes -= existing.size; 113 + map.delete(key); 114 + } 115 + 116 + // LRU eviction 117 + while (map.size > 0) { 118 + const overCount = cfg.maxEntries !== undefined && map.size >= cfg.maxEntries; 119 + const overSize = cfg.maxSize !== undefined && st.sizeBytes + size > cfg.maxSize; 120 + if (!overCount && !overSize) break; 121 + 122 + const oldest = map.keys().next().value; 123 + if (oldest === undefined) break; 124 + const evicted = map.get(oldest)!; 125 + map.delete(oldest); 126 + st.sizeBytes -= evicted.size; 127 + st.evictions++; 128 + } 129 + 130 + map.set(key, { value, timestamp: Date.now(), size }); 131 + st.sizeBytes += size; 132 + st.entries = map.size; 133 + } 134 + 135 + delete(ns: NS, key: string): void { 136 + const map = this.namespaces.get(ns); 137 + const st = this.stats.get(ns); 138 + if (!map || !st) return; 139 + 140 + const entry = map.get(key); 141 + if (entry) { 142 + map.delete(key); 143 + st.sizeBytes -= entry.size; 144 + st.entries = map.size; 145 + } 146 + } 147 + 148 + clear(ns: NS): void { 149 + const map = this.namespaces.get(ns); 150 + const st = this.stats.get(ns); 151 + if (!map || !st) return; 152 + 153 + map.clear(); 154 + st.entries = 0; 155 + st.sizeBytes = 0; 156 + } 157 + 158 + // ── Stats ──────────────────────────────────────────────────────────── 159 + 160 + getStats(): Record<NS, NamespaceStats> { 161 + const out = {} as Record<NS, NamespaceStats>; 162 + for (const [ns, st] of this.stats) { 163 + out[ns] = { ...st }; 164 + } 165 + return out; 166 + } 167 + 168 + // ── Periodic cleanup ───────────────────────────────────────────────── 169 + 170 + startCleanup(intervalMs = 30 * 60_000): void { 171 + if (this.cleanupTimer) return; 172 + 173 + this.cleanupTimer = setInterval(() => { 174 + const now = Date.now(); 175 + for (const [ns, cfg] of this.configs) { 176 + if (!cfg.ttl) continue; 177 + const map = this.namespaces.get(ns)!; 178 + const st = this.stats.get(ns)!; 179 + for (const [key, entry] of map) { 180 + if (now - entry.timestamp > cfg.ttl) { 181 + map.delete(key); 182 + st.sizeBytes -= entry.size; 183 + } 184 + } 185 + st.entries = map.size; 186 + } 187 + }, intervalMs); 188 + } 189 + 190 + stopCleanup(): void { 191 + if (this.cleanupTimer) { 192 + clearInterval(this.cleanupTimer); 193 + this.cleanupTimer = null; 194 + } 195 + } 196 + } 197 + 198 + // ── Singleton ──────────────────────────────────────────────────────────── 199 + 200 + type CacheNamespace = 'domains' | 'customDomains' | 'settings' | 'handles' | 'redirectRules'; 201 + 202 + export const cache = new CacheManager<CacheNamespace>({ 203 + domains: { ttl: 5 * 60_000, maxEntries: 5000 }, 204 + customDomains: { ttl: 5 * 60_000, maxEntries: 5000 }, 205 + settings: { ttl: 5 * 60_000, maxEntries: 1000 }, 206 + handles: { ttl: 10 * 60_000, maxEntries: 5000 }, 207 + redirectRules: { maxEntries: 1000, maxSize: 10 * 1024 * 1024, estimateSize: (v) => (v as unknown[]).length * 100 }, 208 + });
+4 -120
apps/hosting-service/src/lib/cache.ts
··· 1 1 /** 2 - * Cache management for wisp-hosting-service 2 + * Cache statistics for wisp-hosting-service 3 3 * 4 4 * With tiered storage, most caching is handled transparently. 5 - * This module provides a generic LRU cache and exposes storage stats. 5 + * In-memory caches are managed by the centralized CacheManager. 6 6 */ 7 7 8 8 import { storage } from './storage'; 9 - 10 - // In-memory LRU cache for rewritten HTML (for path rewriting in subdomain routes) 11 - interface CacheEntry<T> { 12 - value: T; 13 - size: number; 14 - timestamp: number; 15 - } 16 - 17 - interface CacheStats { 18 - hits: number; 19 - misses: number; 20 - evictions: number; 21 - currentSize: number; 22 - currentCount: number; 23 - } 24 - 25 - export class LRUCache<T> { 26 - private cache: Map<string, CacheEntry<T>>; 27 - private maxSize: number; 28 - private maxCount: number; 29 - private currentSize: number; 30 - private stats: CacheStats; 31 - 32 - constructor(maxSize: number, maxCount: number) { 33 - this.cache = new Map(); 34 - this.maxSize = maxSize; 35 - this.maxCount = maxCount; 36 - this.currentSize = 0; 37 - this.stats = { 38 - hits: 0, 39 - misses: 0, 40 - evictions: 0, 41 - currentSize: 0, 42 - currentCount: 0, 43 - }; 44 - } 45 - 46 - get(key: string): T | null { 47 - const entry = this.cache.get(key); 48 - if (!entry) { 49 - this.stats.misses++; 50 - return null; 51 - } 52 - 53 - // Move to end (most recently used) 54 - this.cache.delete(key); 55 - this.cache.set(key, entry); 56 - 57 - this.stats.hits++; 58 - return entry.value; 59 - } 60 - 61 - set(key: string, value: T, size: number): void { 62 - // Remove existing entry if present 63 - if (this.cache.has(key)) { 64 - const existing = this.cache.get(key)!; 65 - this.currentSize -= existing.size; 66 - this.cache.delete(key); 67 - } 68 - 69 - // Evict entries if needed 70 - while ( 71 - (this.cache.size >= this.maxCount || this.currentSize + size > this.maxSize) && 72 - this.cache.size > 0 73 - ) { 74 - const firstKey = this.cache.keys().next().value; 75 - if (!firstKey) break; // Should never happen, but satisfy TypeScript 76 - const firstEntry = this.cache.get(firstKey); 77 - if (!firstEntry) break; // Should never happen, but satisfy TypeScript 78 - this.cache.delete(firstKey); 79 - this.currentSize -= firstEntry.size; 80 - this.stats.evictions++; 81 - } 82 - 83 - // Add new entry 84 - this.cache.set(key, { 85 - value, 86 - size, 87 - timestamp: Date.now(), 88 - }); 89 - this.currentSize += size; 90 - 91 - // Update stats 92 - this.stats.currentSize = this.currentSize; 93 - this.stats.currentCount = this.cache.size; 94 - } 95 - 96 - delete(key: string): boolean { 97 - const entry = this.cache.get(key); 98 - if (!entry) return false; 99 - 100 - this.cache.delete(key); 101 - this.currentSize -= entry.size; 102 - this.stats.currentSize = this.currentSize; 103 - this.stats.currentCount = this.cache.size; 104 - return true; 105 - } 106 - 107 - size(): number { 108 - return this.cache.size; 109 - } 110 - 111 - clear(): void { 112 - this.cache.clear(); 113 - this.currentSize = 0; 114 - this.stats.currentSize = 0; 115 - this.stats.currentCount = 0; 116 - } 117 - 118 - getStats(): CacheStats { 119 - return { ...this.stats }; 120 - } 121 - 122 - getHitRate(): number { 123 - const total = this.stats.hits + this.stats.misses; 124 - return total === 0 ? 0 : (this.stats.hits / total) * 100; 125 - } 126 - } 9 + import { cache } from './cache-manager'; 127 10 128 11 // Get overall cache statistics 129 12 export async function getCacheStats() { ··· 131 14 132 15 return { 133 16 tieredStorage: tieredStats, 17 + inMemory: cache.getStats(), 134 18 }; 135 19 }
+30 -120
apps/hosting-service/src/lib/db.ts
··· 1 1 import postgres from 'postgres'; 2 2 import { createHash } from 'crypto'; 3 3 import type { DomainLookup, CustomDomainLookup, SiteCache, SiteSettingsCache } from '@wispplace/database'; 4 + import { cache } from './cache-manager'; 4 5 5 6 const sql = postgres( 6 7 process.env.DATABASE_URL || 'postgres://postgres:postgres@localhost:5432/wisp', ··· 13 14 // Cache-only mode: skip all DB writes and only use tiered storage 14 15 export const CACHE_ONLY = process.env.CACHE_ONLY === 'true'; 15 16 16 - // Domain lookup cache with TTL 17 - const DOMAIN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes 18 - 19 - interface CachedDomain<T> { 20 - value: T; 21 - timestamp: number; 22 - } 23 - 24 - const domainCache = new Map<string, CachedDomain<DomainLookup | null>>(); 25 - const customDomainCache = new Map<string, CachedDomain<CustomDomainLookup | null>>(); 26 - 27 - let cleanupInterval: NodeJS.Timeout | null = null; 28 - 29 - export function startDomainCacheCleanup() { 30 - if (cleanupInterval) return; 31 - 32 - cleanupInterval = setInterval(() => { 33 - const now = Date.now(); 34 - 35 - for (const [key, entry] of domainCache.entries()) { 36 - if (now - entry.timestamp > DOMAIN_CACHE_TTL) { 37 - domainCache.delete(key); 38 - } 39 - } 40 - 41 - for (const [key, entry] of customDomainCache.entries()) { 42 - if (now - entry.timestamp > DOMAIN_CACHE_TTL) { 43 - customDomainCache.delete(key); 44 - } 45 - } 46 - 47 - for (const [key, entry] of settingsCache.entries()) { 48 - if (now - entry.timestamp > SETTINGS_CACHE_TTL) { 49 - settingsCache.delete(key); 50 - } 51 - } 52 - }, 30 * 60 * 1000); // Run every 30 minutes 53 - } 54 - 55 - export function stopDomainCacheCleanup() { 56 - if (cleanupInterval) { 57 - clearInterval(cleanupInterval); 58 - cleanupInterval = null; 59 - } 60 - } 61 - 62 17 export async function getWispDomain(domain: string): Promise<DomainLookup | null> { 63 18 const key = domain.toLowerCase(); 64 - 65 - // Check cache first 66 - const cached = domainCache.get(key); 67 - if (cached && Date.now() - cached.timestamp < DOMAIN_CACHE_TTL) { 68 - return cached.value; 69 - } 70 - 71 - // Query database 72 - const result = await sql<DomainLookup[]>` 73 - SELECT did, rkey FROM domains WHERE domain = ${key} LIMIT 1 74 - `; 75 - const data = result[0] || null; 76 - 77 - // Cache the result 78 - domainCache.set(key, { value: data, timestamp: Date.now() }); 79 - 80 - return data; 19 + return cache.getOrFetch('domains', key, async () => { 20 + const result = await sql<DomainLookup[]>` 21 + SELECT did, rkey FROM domains WHERE domain = ${key} LIMIT 1 22 + `; 23 + return result[0] || null; 24 + }); 81 25 } 82 26 83 27 export async function getCustomDomain(domain: string): Promise<CustomDomainLookup | null> { 84 28 const key = domain.toLowerCase(); 85 - 86 - // Check cache first 87 - const cached = customDomainCache.get(key); 88 - if (cached && Date.now() - cached.timestamp < DOMAIN_CACHE_TTL) { 89 - return cached.value; 90 - } 91 - 92 - // Query database 93 - const result = await sql<CustomDomainLookup[]>` 94 - SELECT id, domain, did, rkey, verified FROM custom_domains 95 - WHERE domain = ${key} AND verified = true LIMIT 1 96 - `; 97 - const data = result[0] || null; 98 - 99 - // Cache the result 100 - customDomainCache.set(key, { value: data, timestamp: Date.now() }); 101 - 102 - return data; 29 + return cache.getOrFetch('customDomains', key, async () => { 30 + const result = await sql<CustomDomainLookup[]>` 31 + SELECT id, domain, did, rkey, verified FROM custom_domains 32 + WHERE domain = ${key} AND verified = true LIMIT 1 33 + `; 34 + return result[0] || null; 35 + }); 103 36 } 104 37 105 38 export async function getCustomDomainByHash(hash: string): Promise<CustomDomainLookup | null> { 106 - const key = `hash:${hash}`; 107 - 108 - // Check cache first 109 - const cached = customDomainCache.get(key); 110 - if (cached && Date.now() - cached.timestamp < DOMAIN_CACHE_TTL) { 111 - return cached.value; 112 - } 113 - 114 - // Query database 115 - const result = await sql<CustomDomainLookup[]>` 116 - SELECT id, domain, did, rkey, verified FROM custom_domains 117 - WHERE id = ${hash} AND verified = true LIMIT 1 118 - `; 119 - const data = result[0] || null; 120 - 121 - // Cache the result 122 - customDomainCache.set(key, { value: data, timestamp: Date.now() }); 123 - 124 - return data; 39 + return cache.getOrFetch('customDomains', `hash:${hash}`, async () => { 40 + const result = await sql<CustomDomainLookup[]>` 41 + SELECT id, domain, did, rkey, verified FROM custom_domains 42 + WHERE id = ${hash} AND verified = true LIMIT 1 43 + `; 44 + return result[0] || null; 45 + }); 125 46 } 126 47 127 48 export async function upsertSite(did: string, rkey: string, displayName?: string) { ··· 253 174 254 175 // Site cache queries 255 176 256 - const SETTINGS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes 257 - const settingsCache = new Map<string, CachedDomain<SiteSettingsCache | null>>(); 258 - 259 177 export async function getSiteSettingsCache(did: string, rkey: string): Promise<SiteSettingsCache | null> { 260 - const key = `${did}:${rkey}`; 261 - 262 - const cached = settingsCache.get(key); 263 - if (cached && Date.now() - cached.timestamp < SETTINGS_CACHE_TTL) { 264 - return cached.value; 265 - } 266 - 267 - const result = await sql<SiteSettingsCache[]>` 268 - SELECT did, rkey, record_cid, directory_listing, spa_mode, custom_404, index_files, clean_urls, headers, cached_at, updated_at 269 - FROM site_settings_cache 270 - WHERE did = ${did} AND rkey = ${rkey} 271 - LIMIT 1 272 - `; 273 - const data = result[0] || null; 274 - 275 - settingsCache.set(key, { value: data, timestamp: Date.now() }); 276 - return data; 178 + return cache.getOrFetch('settings', `${did}:${rkey}`, async () => { 179 + const result = await sql<SiteSettingsCache[]>` 180 + SELECT did, rkey, record_cid, directory_listing, spa_mode, custom_404, index_files, clean_urls, headers, cached_at, updated_at 181 + FROM site_settings_cache 182 + WHERE did = ${did} AND rkey = ${rkey} 183 + LIMIT 1 184 + `; 185 + return result[0] || null; 186 + }); 277 187 } 278 188 279 189 export async function getSiteCache(did: string, rkey: string): Promise<SiteCache | null> {
+7 -15
apps/hosting-service/src/lib/file-serving.ts
··· 12 12 import { isHtmlContent } from './html-rewriter'; 13 13 import { generate404Page, generateDirectoryListing } from './page-generators'; 14 14 import { getIndexFiles, applyCustomHeaders } from './request-utils'; 15 - import { getRedirectRulesFromCache, setRedirectRulesInCache } from './site-cache'; 15 + import { cache } from './cache-manager'; 16 16 import { storage } from './storage'; 17 17 import { getSiteCache } from './db'; 18 18 import { enqueueRevalidate } from './revalidate-queue'; ··· 187 187 const indexFiles = getIndexFiles(settings); 188 188 189 189 // Check for redirect rules first (_redirects wins over settings) 190 - let redirectRules = getRedirectRulesFromCache(did, rkey); 191 - 192 - if (redirectRules === null) { 193 - // Load rules (not in cache or evicted) 194 - redirectRules = await span(trace, 'storage:redirectRules', () => loadRedirectRules(did, rkey)); 195 - setRedirectRulesInCache(did, rkey, redirectRules); 196 - } 190 + const redirectRules = await cache.getOrFetch('redirectRules', `${did}:${rkey}`, () => 191 + span(trace, 'storage:redirectRules', () => loadRedirectRules(did, rkey)) 192 + ); 197 193 198 194 // Apply redirect rules if any exist 199 195 if (redirectRules.length > 0) { ··· 488 484 const indexFiles = getIndexFiles(settings); 489 485 490 486 // Check for redirect rules first (_redirects wins over settings) 491 - let redirectRules = getRedirectRulesFromCache(did, rkey); 492 - 493 - if (redirectRules === null) { 494 - // Load rules (not in cache or evicted) 495 - redirectRules = await span(trace, 'storage:redirectRules', () => loadRedirectRules(did, rkey)); 496 - setRedirectRulesInCache(did, rkey, redirectRules); 497 - } 487 + const redirectRules = await cache.getOrFetch('redirectRules', `${did}:${rkey}`, () => 488 + span(trace, 'storage:redirectRules', () => loadRedirectRules(did, rkey)) 489 + ); 498 490 499 491 // Apply redirect rules if any exist 500 492 if (redirectRules.length > 0) {
-37
apps/hosting-service/src/lib/site-cache.ts
··· 1 - /** 2 - * Redirect rules cache utilities 3 - */ 4 - 5 - import { LRUCache } from './cache'; 6 - import type { RedirectRule } from './redirects'; 7 - 8 - // Cache for redirect rules (per site) - LRU with 1000 site limit 9 - // Each entry is relatively small (array of redirect rules), so 1000 sites should be < 10MB 10 - const redirectRulesCache = new LRUCache<RedirectRule[]>(10 * 1024 * 1024, 1000); // 10MB, 1000 sites 11 - 12 - /** 13 - * Clear redirect rules cache for a specific site 14 - * Should be called when a site is updated/recached 15 - */ 16 - export function clearRedirectRulesCache(did: string, rkey: string) { 17 - const cacheKey = `${did}:${rkey}`; 18 - redirectRulesCache.delete(cacheKey); 19 - } 20 - 21 - /** 22 - * Get redirect rules from cache 23 - */ 24 - export function getRedirectRulesFromCache(did: string, rkey: string): RedirectRule[] | null { 25 - const cacheKey = `${did}:${rkey}`; 26 - return redirectRulesCache.get(cacheKey); 27 - } 28 - 29 - /** 30 - * Set redirect rules in cache 31 - */ 32 - export function setRedirectRulesInCache(did: string, rkey: string, rules: RedirectRule[]) { 33 - const cacheKey = `${did}:${rkey}`; 34 - // Estimate size: roughly 100 bytes per rule 35 - const estimatedSize = rules.length * 100; 36 - redirectRulesCache.set(cacheKey, rules, estimatedSize); 37 - }
+4 -11
apps/hosting-service/src/server.ts
··· 13 13 import { isValidRkey, extractHeaders } from './lib/request-utils'; 14 14 import { serveFromCache, serveFromCacheWithRewrite } from './lib/file-serving'; 15 15 import { getRevalidateMetrics } from './lib/revalidate-metrics'; 16 + import { cache } from './lib/cache-manager'; 16 17 17 18 const logger = createLogger('hosting-service'); 18 19 19 - // Cache handle → DID resolutions for 10 minutes to avoid hitting bsky API on every request 20 - const HANDLE_CACHE_TTL = 10 * 60 * 1000; 21 - const handleCache = new Map<string, { did: string; timestamp: number }>(); 22 - 23 20 async function resolveDidCached(identifier: string): Promise<string | null> { 24 21 if (identifier.startsWith('did:')) return identifier; 25 - const cached = handleCache.get(identifier); 26 - if (cached && Date.now() - cached.timestamp < HANDLE_CACHE_TTL) { 27 - return cached.did; 28 - } 29 - const did = await resolveDid(identifier); 30 - if (did) handleCache.set(identifier, { did, timestamp: Date.now() }); 31 - return did; 22 + return cache.getOrFetch('handles', identifier, () => resolveDid(identifier), { 23 + cacheIf: (v) => v !== null, 24 + }); 32 25 } 33 26 34 27 const BASE_HOST_ENV = process.env.BASE_HOST || 'wisp.place';
+7 -4
packages/@wispplace/tiered-storage/src/tiers/DiskStorageTier.ts
··· 180 180 const data = await readFile(filePath); 181 181 return new Uint8Array(data); 182 182 } catch (error) { 183 - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 183 + const code = (error as NodeJS.ErrnoException).code; 184 + if (code === 'ENOENT' || code === 'ENOTDIR') { 184 185 return null; 185 186 } 186 187 throw error; ··· 218 219 219 220 return { data: new Uint8Array(dataBuffer), metadata }; 220 221 } catch (error) { 221 - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 222 + const code = (error as NodeJS.ErrnoException).code; 223 + if (code === 'ENOENT' || code === 'ENOTDIR') { 222 224 return null; 223 225 } 224 226 throw error; ··· 256 258 257 259 return { stream, metadata }; 258 260 } catch (error) { 259 - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 261 + const code = (error as NodeJS.ErrnoException).code; 262 + if (code === 'ENOENT' || code === 'ENOTDIR') { 260 263 return null; 261 264 } 262 265 throw error; ··· 435 438 return metadata; 436 439 } catch (error) { 437 440 const code = (error as NodeJS.ErrnoException).code; 438 - if (code === 'ENOENT') { 441 + if (code === 'ENOENT' || code === 'ENOTDIR') { 439 442 return null; 440 443 } 441 444 if (error instanceof SyntaxError) {