wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs typescript

humans write the best readmes, not bots

nekomimi.pet b4de0bdf 34ec0d34

verified
Changed files
+96 -483
+96 -483
README.md
··· 1 - # Tiered Storage 2 3 - A lightweight, pluggable tiered storage library that orchestrates caching across hot (memory), warm (disk/database), and cold (S3/object storage) tiers. 4 5 ## Features 6 7 - - **Cascading Containment Model**: Hot ⊆ Warm ⊆ Cold (lower tiers contain all data from upper tiers) 8 - - **Pluggable Backends**: Bring your own Redis, Postgres, SQLite, or use built-in implementations 9 - - **Automatic Promotion**: Configurable eager/lazy promotion strategies for cache warming 10 - - **TTL Management**: Per-key TTL with automatic expiration and renewal 11 - - **Prefix Invalidation**: Efficiently delete groups of keys by prefix 12 - - **Bootstrap Support**: Warm up caches from lower tiers on startup 13 - - **Compression**: Optional transparent gzip compression 14 - - **TypeScript First**: Full type safety with comprehensive TSDoc comments 15 - - **Zero Forced Dependencies**: Only require what you use 16 17 - ## Installation 18 19 ```bash 20 npm install tiered-storage 21 - # or 22 - bun add tiered-storage 23 ``` 24 25 - ## Quick Start 26 27 ```typescript 28 - import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage'; 29 30 const storage = new TieredStorage({ 31 tiers: { 32 - hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), // 100MB 33 warm: new DiskStorageTier({ directory: './cache' }), 34 - cold: new S3StorageTier({ 35 - bucket: 'my-bucket', 36 - region: 'us-east-1', 37 - credentials: { 38 - accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 39 - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 40 - }, 41 - }), 42 }, 43 compression: true, 44 - defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days 45 - promotionStrategy: 'lazy', 46 - }); 47 - 48 - // Store data (cascades to all tiers) 49 - await storage.set('user:123', { name: 'Alice', email: 'alice@example.com' }); 50 - 51 - // Retrieve data (bubbles up from cold → warm → hot) 52 - const user = await storage.get('user:123'); 53 - 54 - // Get data with metadata and source tier 55 - const result = await storage.getWithMetadata('user:123'); 56 - console.log(`Served from ${result.source}`); // 'hot', 'warm', or 'cold' 57 - 58 - // Invalidate all keys with prefix 59 - await storage.invalidate('user:'); 60 61 - // Renew TTL 62 - await storage.touch('user:123'); 63 - ``` 64 65 - ## Core Concepts 66 67 - ### Cascading Containment Model 68 69 ``` 70 - ┌──────────────────────────────────────────────────────┐ 71 - │ Cold Storage (S3/Object Storage) │ 72 - │ • Contains ALL objects (source of truth) │ 73 - │ • Slowest access, unlimited capacity │ 74 - ├──────────────────────────────────────────────────────┤ 75 - │ Warm Storage (Disk/Database) │ 76 - │ • Contains ALL hot objects + additional warm objects │ 77 - │ • Medium access speed, large capacity │ 78 - ├──────────────────────────────────────────────────────┤ 79 - │ Hot Storage (Memory) │ 80 - │ • Contains only the hottest objects │ 81 - │ • Fastest access, limited capacity │ 82 - └──────────────────────────────────────────────────────┘ 83 - ``` 84 - 85 - **Write Strategy (Cascading Down):** 86 - - Write to **hot** → also writes to **warm** and **cold** 87 - - Write to **warm** → also writes to **cold** 88 - - Write to **cold** → only writes to **cold** 89 - 90 - **Read Strategy (Bubbling Up):** 91 - - Check **hot** first → if miss, check **warm** → if miss, check **cold** 92 - - On cache miss, optionally promote data up through tiers 93 - 94 - ### Selective Tier Placement 95 - 96 - For use cases like static site hosting, you can control which files go into which tiers: 97 - 98 - ```typescript 99 - // Small, critical file (index.html) - store in all tiers for instant serving 100 - await storage.set('site:abc/index.html', htmlContent); 101 - 102 - // Large file (video) - skip hot tier to avoid memory bloat 103 - await storage.set('site:abc/video.mp4', videoData, { skipTiers: ['hot'] }); 104 - 105 - // Medium files (images, CSS) - skip hot, use warm + cold 106 - await storage.set('site:abc/style.css', cssData, { skipTiers: ['hot'] }); 107 - ``` 108 - 109 - This pattern ensures: 110 - - Hot tier stays small and fast (only critical files) 111 - - Warm tier caches everything (all site files on disk) 112 - - Cold tier is source of truth (all data) 113 - 114 - ## API Reference 115 - 116 - ### `TieredStorage` 117 - 118 - Main orchestrator class for tiered storage. 119 - 120 - #### Constructor 121 - 122 - ```typescript 123 - new TieredStorage<T>(config: TieredStorageConfig) 124 - ``` 125 - 126 - **Config Options:** 127 - 128 - ```typescript 129 - interface TieredStorageConfig { 130 - tiers: { 131 - hot?: StorageTier; // Optional: fastest tier (memory/Redis) 132 - warm?: StorageTier; // Optional: medium tier (disk/SQLite/Postgres) 133 - cold: StorageTier; // Required: slowest tier (S3/object storage) 134 - }; 135 - compression?: boolean; // Auto-compress before storing (default: false) 136 - defaultTTL?: number; // Default TTL in milliseconds 137 - promotionStrategy?: 'eager' | 'lazy'; // When to promote to upper tiers (default: 'lazy') 138 - serialization?: { // Custom serialization (default: JSON) 139 - serialize: (data: unknown) => Promise<Uint8Array>; 140 - deserialize: (data: Uint8Array) => Promise<unknown>; 141 - }; 142 - } 143 - ``` 144 - 145 - #### Methods 146 147 - **`get(key: string): Promise<T | null>`** 148 - 149 - Retrieve data for a key. Returns null if not found or expired. 150 - 151 - **`getWithMetadata(key: string): Promise<StorageResult<T> | null>`** 152 - 153 - Retrieve data with metadata and source tier information. 154 155 - ```typescript 156 - const result = await storage.getWithMetadata('user:123'); 157 - console.log(result.data); // The actual data 158 - console.log(result.source); // 'hot' | 'warm' | 'cold' 159 - console.log(result.metadata); // Metadata (size, timestamps, TTL, etc.) 160 ``` 161 - 162 - **`set(key: string, data: T, options?: SetOptions): Promise<SetResult>`** 163 - 164 - Store data with optional configuration. 165 - 166 - ```typescript 167 - await storage.set('key', data, { 168 - ttl: 24 * 60 * 60 * 1000, // Custom TTL (24 hours) 169 - metadata: { contentType: 'application/json' }, // Custom metadata 170 - skipTiers: ['hot'], // Skip specific tiers 171 - }); 172 ``` 173 174 - **`delete(key: string): Promise<void>`** 175 176 - Delete data from all tiers. 177 178 - **`exists(key: string): Promise<boolean>`** 179 180 - Check if a key exists (and hasn't expired). 181 182 - **`touch(key: string, ttlMs?: number): Promise<void>`** 183 - 184 - Renew TTL for a key. Useful for "keep alive" behavior. 185 - 186 - **`invalidate(prefix: string): Promise<number>`** 187 - 188 - Delete all keys matching a prefix. Returns number of keys deleted. 189 190 - ```typescript 191 - await storage.invalidate('user:'); // Delete all user keys 192 - await storage.invalidate('site:abc/'); // Delete all files for site 'abc' 193 - await storage.invalidate(''); // Delete everything 194 - ``` 195 196 - **`listKeys(prefix?: string): AsyncIterableIterator<string>`** 197 198 - List all keys, optionally filtered by prefix. 199 200 ```typescript 201 - for await (const key of storage.listKeys('user:')) { 202 - console.log(key); // 'user:123', 'user:456', etc. 203 } 204 ``` 205 206 - **`getStats(): Promise<AllTierStats>`** 207 208 - Get aggregated statistics across all tiers. 209 210 - ```typescript 211 - const stats = await storage.getStats(); 212 - console.log(stats.hot); // Hot tier stats (size, items, hits, misses) 213 - console.log(stats.hitRate); // Overall hit rate (0-1) 214 - ``` 215 216 - **`bootstrapHot(limit?: number): Promise<number>`** 217 - 218 - Load most frequently accessed items from warm into hot. Returns number of items loaded. 219 - 220 - ```typescript 221 - // On server startup: warm up hot tier 222 - const loaded = await storage.bootstrapHot(1000); // Load top 1000 items 223 - console.log(`Loaded ${loaded} items into hot tier`); 224 - ``` 225 - 226 - **`bootstrapWarm(options?: { limit?: number; sinceDate?: Date }): Promise<number>`** 227 - 228 - Load recent items from cold into warm. Returns number of items loaded. 229 230 - ```typescript 231 - // Load items accessed in last 7 days 232 - const loaded = await storage.bootstrapWarm({ 233 - sinceDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), 234 - limit: 10000, 235 - }); 236 - ``` 237 238 - **`export(): Promise<StorageSnapshot>`** 239 240 - Export metadata snapshot for backup or migration. 241 242 - **`import(snapshot: StorageSnapshot): Promise<void>`** 243 244 - Import metadata snapshot. 245 246 - **`clear(): Promise<void>`** 247 248 - Clear all data from all tiers. ⚠️ Use with extreme caution! 249 250 - **`clearTier(tier: 'hot' | 'warm' | 'cold'): Promise<void>`** 251 252 - Clear a specific tier. 253 254 - ### Built-in Storage Tiers 255 256 - #### `MemoryStorageTier` 257 258 - In-memory storage using TinyLRU for efficient LRU eviction. 259 260 ```typescript 261 - import { MemoryStorageTier } from 'tiered-storage'; 262 - 263 - const tier = new MemoryStorageTier({ 264 - maxSizeBytes: 100 * 1024 * 1024, // 100MB 265 - maxItems: 1000, // Optional: max number of items 266 - }); 267 ``` 268 269 - **Features:** 270 - - Battle-tested TinyLRU library 271 - - Automatic LRU eviction 272 - - Size-based and count-based limits 273 - - Single process only (not distributed) 274 275 - #### `DiskStorageTier` 276 - 277 - Filesystem-based storage with `.meta` files. 278 279 ```typescript 280 - import { DiskStorageTier } from 'tiered-storage'; 281 - 282 - const tier = new DiskStorageTier({ 283 directory: './cache', 284 - maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB (optional) 285 - evictionPolicy: 'lru', // 'lru' | 'fifo' | 'size' 286 - }); 287 ``` 288 289 - **Features:** 290 - - Human-readable file structure 291 - - Optional size-based eviction 292 - - Three eviction policies: LRU, FIFO, size-based 293 - - Atomic writes with `.meta` files 294 - - Zero external dependencies 295 296 - **File structure:** 297 - ``` 298 - cache/ 299 - ├── user%3A123 # Data file (encoded key) 300 - ├── user%3A123.meta # Metadata JSON 301 - ├── site%3Aabc%2Findex.html 302 - └── site%3Aabc%2Findex.html.meta 303 - ``` 304 - 305 - #### `S3StorageTier` 306 - 307 - AWS S3 or S3-compatible object storage. 308 309 ```typescript 310 - import { S3StorageTier } from 'tiered-storage'; 311 - 312 - // AWS S3 with separate metadata bucket (RECOMMENDED!) 313 - const tier = new S3StorageTier({ 314 - bucket: 'my-data-bucket', 315 - metadataBucket: 'my-metadata-bucket', // Stores metadata separately for fast updates 316 region: 'us-east-1', 317 - credentials: { 318 - accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 319 - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 320 - }, 321 - prefix: 'cache/', // Optional key prefix 322 - }); 323 - 324 - // Cloudflare R2 with metadata bucket 325 - const r2Tier = new S3StorageTier({ 326 - bucket: 'my-r2-data-bucket', 327 - metadataBucket: 'my-r2-metadata-bucket', 328 - region: 'auto', 329 - endpoint: 'https://account-id.r2.cloudflarestorage.com', 330 - credentials: { 331 - accessKeyId: process.env.R2_ACCESS_KEY_ID!, 332 - secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, 333 - }, 334 - }); 335 - 336 - // Without metadata bucket (legacy mode - slower, more expensive) 337 - const legacyTier = new S3StorageTier({ 338 - bucket: 'my-bucket', 339 - region: 'us-east-1', 340 - // No metadataBucket - metadata stored in S3 object metadata fields 341 - }); 342 ``` 343 344 - **Features:** 345 - - Compatible with AWS S3, Cloudflare R2, MinIO, and other S3-compatible services 346 - - **Separate metadata bucket support (RECOMMENDED)** - stores metadata as JSON objects for fast, cheap updates 347 - - Legacy mode: metadata in S3 object metadata fields (requires object copying for updates) 348 - - Efficient batch deletions (up to 1000 keys per request) 349 - - Optional key prefixing for multi-tenant scenarios 350 - - Typically used as cold tier (source of truth) 351 352 - **⚠️ Important:** Without `metadataBucket`, updating metadata (e.g., access counts) requires copying the entire object, which is slow and expensive for large files. Use a separate metadata bucket in production! 353 354 - ## Usage Patterns 355 - 356 - ### Pattern 1: Simple Single-Server Setup 357 358 ```typescript 359 - import { TieredStorage, MemoryStorageTier, DiskStorageTier } from 'tiered-storage'; 360 - 361 - const storage = new TieredStorage({ 362 - tiers: { 363 - hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), 364 - warm: new DiskStorageTier({ directory: './cache' }), 365 - cold: new DiskStorageTier({ directory: './storage' }), 366 - }, 367 - compression: true, 368 - defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days 369 - }); 370 - 371 - await storage.set('user:123', { name: 'Alice', email: 'alice@example.com' }); 372 - const user = await storage.get('user:123'); 373 - ``` 374 - 375 - ### Pattern 2: Static Site Hosting (wisp.place-style) 376 - 377 - ```typescript 378 - import { TieredStorage, MemoryStorageTier, DiskStorageTier } from 'tiered-storage'; 379 - 380 - const storage = new TieredStorage({ 381 - tiers: { 382 - hot: new MemoryStorageTier({ 383 - maxSizeBytes: 100 * 1024 * 1024, // 100MB 384 - maxItems: 500, 385 - }), 386 - warm: new DiskStorageTier({ 387 - directory: './cache/sites', 388 - maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB 389 - }), 390 - // Cold tier is PDS (fetched on demand via custom tier implementation) 391 - }, 392 - compression: true, 393 - defaultTTL: 14 * 24 * 60 * 60 * 1000, 394 - promotionStrategy: 'lazy', // Don't auto-promote large files to hot 395 - }); 396 - 397 - // Store index.html in all tiers (fast access) 398 - await storage.set(`${did}/${rkey}/index.html`, htmlBuffer, { 399 - metadata: { mimeType: 'text/html', encoding: 'gzip' }, 400 - }); 401 - 402 - // Store large files only in warm + cold (skip hot) 403 - await storage.set(`${did}/${rkey}/video.mp4`, videoBuffer, { 404 - skipTiers: ['hot'], 405 - metadata: { mimeType: 'video/mp4' }, 406 - }); 407 - 408 - // Get file with source tracking 409 - const result = await storage.getWithMetadata(`${did}/${rkey}/index.html`); 410 - console.log(`Served from ${result.source}`); // Likely 'hot' for index.html 411 - 412 - // Invalidate entire site 413 - await storage.invalidate(`${did}/${rkey}/`); 414 - 415 - // Renew TTL when site is accessed 416 - await storage.touch(`${did}/${rkey}/index.html`); 417 ``` 418 419 - ### Pattern 3: Custom Backend (SQLite) 420 421 - Implement the `StorageTier` interface to use any backend: 422 423 ```typescript 424 - import { StorageTier, StorageMetadata, TierStats } from 'tiered-storage'; 425 - import Database from 'better-sqlite3'; 426 - 427 - class SQLiteStorageTier implements StorageTier { 428 - private db: Database.Database; 429 - 430 - constructor(dbPath: string) { 431 - this.db = new Database(dbPath); 432 - this.db.exec(` 433 - CREATE TABLE IF NOT EXISTS cache ( 434 - key TEXT PRIMARY KEY, 435 - data BLOB NOT NULL, 436 - metadata TEXT NOT NULL 437 - ) 438 - `); 439 - } 440 - 441 - async get(key: string): Promise<Uint8Array | null> { 442 - const row = this.db.prepare('SELECT data FROM cache WHERE key = ?').get(key); 443 - return row ? new Uint8Array(row.data) : null; 444 - } 445 - 446 - async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 447 - this.db.prepare('INSERT OR REPLACE INTO cache (key, data, metadata) VALUES (?, ?, ?)') 448 - .run(key, Buffer.from(data), JSON.stringify(metadata)); 449 - } 450 - 451 - async delete(key: string): Promise<void> { 452 - this.db.prepare('DELETE FROM cache WHERE key = ?').run(key); 453 - } 454 - 455 - async exists(key: string): Promise<boolean> { 456 - const row = this.db.prepare('SELECT 1 FROM cache WHERE key = ?').get(key); 457 - return !!row; 458 - } 459 - 460 - async *listKeys(prefix?: string): AsyncIterableIterator<string> { 461 - const query = prefix 462 - ? this.db.prepare('SELECT key FROM cache WHERE key LIKE ?') 463 - : this.db.prepare('SELECT key FROM cache'); 464 - 465 - const rows = prefix ? query.all(`${prefix}%`) : query.all(); 466 - 467 - for (const row of rows) { 468 - yield row.key; 469 - } 470 - } 471 - 472 - async deleteMany(keys: string[]): Promise<void> { 473 - const placeholders = keys.map(() => '?').join(','); 474 - this.db.prepare(`DELETE FROM cache WHERE key IN (${placeholders})`).run(...keys); 475 - } 476 - 477 - async getMetadata(key: string): Promise<StorageMetadata | null> { 478 - const row = this.db.prepare('SELECT metadata FROM cache WHERE key = ?').get(key); 479 - return row ? JSON.parse(row.metadata) : null; 480 - } 481 - 482 - async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 483 - this.db.prepare('UPDATE cache SET metadata = ? WHERE key = ?') 484 - .run(JSON.stringify(metadata), key); 485 - } 486 - 487 - async getStats(): Promise<TierStats> { 488 - const row = this.db.prepare('SELECT COUNT(*) as count, SUM(LENGTH(data)) as bytes FROM cache').get(); 489 - return { items: row.count, bytes: row.bytes || 0 }; 490 - } 491 - 492 - async clear(): Promise<void> { 493 - this.db.prepare('DELETE FROM cache').run(); 494 - } 495 - } 496 - 497 - // Use it 498 - const storage = new TieredStorage({ 499 - tiers: { 500 - warm: new SQLiteStorageTier('./cache.db'), 501 - cold: new DiskStorageTier({ directory: './storage' }), 502 - }, 503 - }); 504 ``` 505 506 - ## Running Examples 507 - 508 - ### Interactive Demo Server 509 - 510 - Run a **real HTTP server** that serves the example site using tiered storage: 511 512 ```bash 513 - # Configure S3 credentials first (copy .env.example to .env and fill in) 514 - cp .env.example .env 515 - 516 - # Start the demo server 517 bun run serve 518 ``` 519 520 - Then visit: 521 - - **http://localhost:3000/** - The demo site served from tiered storage 522 - - **http://localhost:3000/admin/stats** - Live cache statistics dashboard 523 - 524 - Watch the console to see which tier serves each request: 525 - - 🔥 **Hot tier (memory)** - index.html served instantly 526 - - 💾 **Warm tier (disk)** - Other pages served from disk cache 527 - - ☁️ **Cold tier (S3)** - First access fetches from S3, then cached 528 - 529 - ### Command-Line Examples 530 - 531 - Or run the non-interactive examples: 532 - 533 - ```bash 534 - bun run example 535 - ``` 536 - 537 - The examples include: 538 - - **Basic CRUD operations** with statistics tracking 539 - - **Static site hosting** using the real site in `example-site/` directory 540 - - **Bootstrap demonstrations** (warming caches from lower tiers) 541 - - **Promotion strategy comparisons** (eager vs lazy) 542 - 543 - The `example-site/` directory contains a complete static website with: 544 - - `index.html` - Stored in hot + warm + cold (instant serving) 545 - - `about.html`, `docs.html` - Stored in warm + cold (skips hot) 546 - - `style.css`, `script.js` - Stored in warm + cold (skips hot) 547 - 548 - This demonstrates the exact pattern you'd use for wisp.place: critical files in memory, everything else on disk/S3. 549 - 550 - ## Testing 551 - 552 - ```bash 553 - bun test 554 - ``` 555 - 556 - ## Development 557 - 558 - ```bash 559 - # Install dependencies 560 - bun install 561 - 562 - # Type check 563 - bun run check 564 - 565 - # Build 566 - bun run build 567 568 - # Run tests 569 - bun test 570 - 571 - ``` 572 ## License 573 574 MIT
··· 1 + # tiered-storage 2 3 + Cascading cache that flows hot → warm → cold. Memory, disk, S3—or bring your own. 4 5 ## Features 6 7 + - **Cascading writes** - data flows down through all tiers 8 + - **Bubbling reads** - check hot first, fall back to warm, then cold 9 + - **Pluggable backends** - memory, disk, S3, or implement your own 10 + - **Selective placement** - skip tiers for big files that don't need memory caching 11 + - **Prefix invalidation** - `invalidate('user:')` nukes all user keys 12 + - **Optional compression** - transparent gzip 13 14 + ## Install 15 16 ```bash 17 npm install tiered-storage 18 ``` 19 20 + ## Example 21 22 ```typescript 23 + import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage' 24 25 const storage = new TieredStorage({ 26 tiers: { 27 + hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), 28 warm: new DiskStorageTier({ directory: './cache' }), 29 + cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }), 30 }, 31 compression: true, 32 + }) 33 34 + // write cascades down 35 + await storage.set('user:123', { name: 'Alice' }) 36 37 + // read bubbles up 38 + const user = await storage.get('user:123') 39 40 + // see where it came from 41 + const result = await storage.getWithMetadata('user:123') 42 + console.log(result.source) // 'hot', 'warm', or 'cold' 43 44 + // nuke by prefix 45 + await storage.invalidate('user:') 46 ``` 47 48 + ## How it works 49 50 ``` 51 + ┌─────────────────────────────────────────────┐ 52 + │ Cold (S3) - source of truth, all data │ 53 + │ ↑ │ 54 + │ Warm (disk) - everything hot has + more │ 55 + │ ↑ │ 56 + │ Hot (memory) - just the hottest stuff │ 57 + └─────────────────────────────────────────────┘ 58 ``` 59 60 + Writes cascade **down**. Reads bubble **up**. 61 62 + ## API 63 64 + ### `storage.get(key)` 65 66 + Get data. Returns `null` if missing or expired. 67 68 + ### `storage.getWithMetadata(key)` 69 70 + Get data plus which tier served it. 71 72 + ### `storage.set(key, data, options?)` 73 74 + Store data. Options: 75 76 ```typescript 77 + { 78 + ttl: 86400000, // custom TTL 79 + skipTiers: ['hot'], // skip specific tiers 80 + metadata: { ... }, // custom metadata 81 } 82 ``` 83 84 + ### `storage.delete(key)` 85 86 + Delete from all tiers. 87 88 + ### `storage.invalidate(prefix)` 89 90 + Delete all keys matching prefix. Returns count. 91 92 + ### `storage.touch(key, ttl?)` 93 94 + Renew TTL. 95 96 + ### `storage.listKeys(prefix?)` 97 98 + Async iterator over keys. 99 100 + ### `storage.getStats()` 101 102 + Stats across all tiers. 103 104 + ### `storage.bootstrapHot(limit?)` 105 106 + Warm up hot tier from warm tier. Run on startup. 107 108 + ### `storage.bootstrapWarm(options?)` 109 110 + Warm up warm tier from cold tier. 111 112 + ## Built-in tiers 113 114 + ### MemoryStorageTier 115 116 ```typescript 117 + new MemoryStorageTier({ 118 + maxSizeBytes: 100 * 1024 * 1024, 119 + maxItems: 1000, 120 + }) 121 ``` 122 123 + LRU eviction. Fast. Single process only. 124 125 + ### DiskStorageTier 126 127 ```typescript 128 + new DiskStorageTier({ 129 directory: './cache', 130 + maxSizeBytes: 10 * 1024 * 1024 * 1024, 131 + evictionPolicy: 'lru', // or 'fifo', 'size' 132 + }) 133 ``` 134 135 + Files on disk with `.meta` sidecars. 136 137 + ### S3StorageTier 138 139 ```typescript 140 + new S3StorageTier({ 141 + bucket: 'data', 142 + metadataBucket: 'metadata', // recommended! 143 region: 'us-east-1', 144 + }) 145 ``` 146 147 + Works with AWS S3, Cloudflare R2, MinIO. Use a separate metadata bucket—otherwise updating access counts requires copying entire objects. 148 149 + ## Custom tiers 150 151 + Implement `StorageTier`: 152 153 ```typescript 154 + interface StorageTier { 155 + get(key: string): Promise<Uint8Array | null> 156 + set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> 157 + delete(key: string): Promise<void> 158 + exists(key: string): Promise<boolean> 159 + listKeys(prefix?: string): AsyncIterableIterator<string> 160 + deleteMany(keys: string[]): Promise<void> 161 + getMetadata(key: string): Promise<StorageMetadata | null> 162 + setMetadata(key: string, metadata: StorageMetadata): Promise<void> 163 + getStats(): Promise<TierStats> 164 + clear(): Promise<void> 165 + } 166 ``` 167 168 + ## Skipping tiers 169 170 + Don't want big videos in memory? Skip hot: 171 172 ```typescript 173 + await storage.set('video.mp4', data, { skipTiers: ['hot'] }) 174 ``` 175 176 + ## Running the demo 177 178 ```bash 179 + cp .env.example .env # add S3 creds 180 bun run serve 181 ``` 182 183 + Visit http://localhost:3000 to see it work. Check http://localhost:3000/admin/stats for live cache stats. 184 185 ## License 186 187 MIT