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 1 + # tiered-storage 2 2 3 - A lightweight, pluggable tiered storage library that orchestrates caching across hot (memory), warm (disk/database), and cold (S3/object storage) tiers. 3 + Cascading cache that flows hot → warm → cold. Memory, disk, S3—or bring your own. 4 4 5 5 ## Features 6 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 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 16 13 17 - ## Installation 14 + ## Install 18 15 19 16 ```bash 20 17 npm install tiered-storage 21 - # or 22 - bun add tiered-storage 23 18 ``` 24 19 25 - ## Quick Start 20 + ## Example 26 21 27 22 ```typescript 28 - import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage'; 23 + import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage' 29 24 30 25 const storage = new TieredStorage({ 31 26 tiers: { 32 - hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), // 100MB 27 + hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), 33 28 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 - }), 29 + cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }), 42 30 }, 43 31 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:'); 32 + }) 60 33 61 - // Renew TTL 62 - await storage.touch('user:123'); 63 - ``` 34 + // write cascades down 35 + await storage.set('user:123', { name: 'Alice' }) 64 36 65 - ## Core Concepts 37 + // read bubbles up 38 + const user = await storage.get('user:123') 66 39 67 - ### Cascading Containment Model 40 + // see where it came from 41 + const result = await storage.getWithMetadata('user:123') 42 + console.log(result.source) // 'hot', 'warm', or 'cold' 68 43 44 + // nuke by prefix 45 + await storage.invalidate('user:') 69 46 ``` 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 47 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. 48 + ## How it works 154 49 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 50 ``` 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 - }); 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 + └─────────────────────────────────────────────┘ 172 58 ``` 173 59 174 - **`delete(key: string): Promise<void>`** 60 + Writes cascade **down**. Reads bubble **up**. 175 61 176 - Delete data from all tiers. 62 + ## API 177 63 178 - **`exists(key: string): Promise<boolean>`** 64 + ### `storage.get(key)` 179 65 180 - Check if a key exists (and hasn't expired). 66 + Get data. Returns `null` if missing or expired. 181 67 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. 68 + ### `storage.getWithMetadata(key)` 189 69 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 - ``` 70 + Get data plus which tier served it. 195 71 196 - **`listKeys(prefix?: string): AsyncIterableIterator<string>`** 72 + ### `storage.set(key, data, options?)` 197 73 198 - List all keys, optionally filtered by prefix. 74 + Store data. Options: 199 75 200 76 ```typescript 201 - for await (const key of storage.listKeys('user:')) { 202 - console.log(key); // 'user:123', 'user:456', etc. 77 + { 78 + ttl: 86400000, // custom TTL 79 + skipTiers: ['hot'], // skip specific tiers 80 + metadata: { ... }, // custom metadata 203 81 } 204 82 ``` 205 83 206 - **`getStats(): Promise<AllTierStats>`** 84 + ### `storage.delete(key)` 207 85 208 - Get aggregated statistics across all tiers. 86 + Delete from all tiers. 209 87 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 - ``` 88 + ### `storage.invalidate(prefix)` 215 89 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. 90 + Delete all keys matching prefix. Returns count. 229 91 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 - ``` 92 + ### `storage.touch(key, ttl?)` 237 93 238 - **`export(): Promise<StorageSnapshot>`** 94 + Renew TTL. 239 95 240 - Export metadata snapshot for backup or migration. 96 + ### `storage.listKeys(prefix?)` 241 97 242 - **`import(snapshot: StorageSnapshot): Promise<void>`** 98 + Async iterator over keys. 243 99 244 - Import metadata snapshot. 100 + ### `storage.getStats()` 245 101 246 - **`clear(): Promise<void>`** 102 + Stats across all tiers. 247 103 248 - Clear all data from all tiers. ⚠️ Use with extreme caution! 104 + ### `storage.bootstrapHot(limit?)` 249 105 250 - **`clearTier(tier: 'hot' | 'warm' | 'cold'): Promise<void>`** 106 + Warm up hot tier from warm tier. Run on startup. 251 107 252 - Clear a specific tier. 108 + ### `storage.bootstrapWarm(options?)` 253 109 254 - ### Built-in Storage Tiers 110 + Warm up warm tier from cold tier. 255 111 256 - #### `MemoryStorageTier` 112 + ## Built-in tiers 257 113 258 - In-memory storage using TinyLRU for efficient LRU eviction. 114 + ### MemoryStorageTier 259 115 260 116 ```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 - }); 117 + new MemoryStorageTier({ 118 + maxSizeBytes: 100 * 1024 * 1024, 119 + maxItems: 1000, 120 + }) 267 121 ``` 268 122 269 - **Features:** 270 - - Battle-tested TinyLRU library 271 - - Automatic LRU eviction 272 - - Size-based and count-based limits 273 - - Single process only (not distributed) 123 + LRU eviction. Fast. Single process only. 274 124 275 - #### `DiskStorageTier` 276 - 277 - Filesystem-based storage with `.meta` files. 125 + ### DiskStorageTier 278 126 279 127 ```typescript 280 - import { DiskStorageTier } from 'tiered-storage'; 281 - 282 - const tier = new DiskStorageTier({ 128 + new DiskStorageTier({ 283 129 directory: './cache', 284 - maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB (optional) 285 - evictionPolicy: 'lru', // 'lru' | 'fifo' | 'size' 286 - }); 130 + maxSizeBytes: 10 * 1024 * 1024 * 1024, 131 + evictionPolicy: 'lru', // or 'fifo', 'size' 132 + }) 287 133 ``` 288 134 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 135 + Files on disk with `.meta` sidecars. 295 136 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. 137 + ### S3StorageTier 308 138 309 139 ```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 140 + new S3StorageTier({ 141 + bucket: 'data', 142 + metadataBucket: 'metadata', // recommended! 316 143 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 - }); 144 + }) 342 145 ``` 343 146 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) 147 + Works with AWS S3, Cloudflare R2, MinIO. Use a separate metadata bucket—otherwise updating access counts requires copying entire objects. 351 148 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! 149 + ## Custom tiers 353 150 354 - ## Usage Patterns 355 - 356 - ### Pattern 1: Simple Single-Server Setup 151 + Implement `StorageTier`: 357 152 358 153 ```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`); 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 + } 417 166 ``` 418 167 419 - ### Pattern 3: Custom Backend (SQLite) 168 + ## Skipping tiers 420 169 421 - Implement the `StorageTier` interface to use any backend: 170 + Don't want big videos in memory? Skip hot: 422 171 423 172 ```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 - }); 173 + await storage.set('video.mp4', data, { skipTiers: ['hot'] }) 504 174 ``` 505 175 506 - ## Running Examples 507 - 508 - ### Interactive Demo Server 509 - 510 - Run a **real HTTP server** that serves the example site using tiered storage: 176 + ## Running the demo 511 177 512 178 ```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 179 + cp .env.example .env # add S3 creds 517 180 bun run serve 518 181 ``` 519 182 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 183 + Visit http://localhost:3000 to see it work. Check http://localhost:3000/admin/stats for live cache stats. 567 184 568 - # Run tests 569 - bun test 570 - 571 - ``` 572 185 ## License 573 186 574 187 MIT