wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs typescript
at main 19 kB view raw
1/** 2 * Example usage of the tiered-storage library 3 * 4 * Run with: bun run example 5 * 6 * Note: This example uses S3 for cold storage. You'll need to configure 7 * AWS credentials and an S3 bucket in .env (see .env.example) 8 */ 9 10import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from './src/index.js'; 11import { rm } from 'node:fs/promises'; 12 13// Configuration from environment variables 14const S3_BUCKET = process.env.S3_BUCKET || 'tiered-storage-example'; 15const S3_REGION = process.env.S3_REGION || 'us-east-1'; 16const S3_ENDPOINT = process.env.S3_ENDPOINT; 17const S3_FORCE_PATH_STYLE = process.env.S3_FORCE_PATH_STYLE !== 'false'; // Default true 18const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; 19const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; 20 21async function basicExample() { 22 console.log('\n=== Basic Example ===\n'); 23 24 const storage = new TieredStorage({ 25 tiers: { 26 hot: new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }), // 10MB 27 warm: new DiskStorageTier({ directory: './example-cache/basic/warm' }), 28 cold: new S3StorageTier({ 29 bucket: S3_BUCKET, 30 region: S3_REGION, 31 endpoint: S3_ENDPOINT, 32 forcePathStyle: S3_FORCE_PATH_STYLE, 33 credentials: 34 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 35 ? { 36 accessKeyId: AWS_ACCESS_KEY_ID, 37 secretAccessKey: AWS_SECRET_ACCESS_KEY, 38 } 39 : undefined, 40 prefix: 'example/basic/', 41 }), 42 }, 43 compression: true, 44 defaultTTL: 60 * 60 * 1000, // 1 hour 45 }); 46 47 // Store some data 48 console.log('Storing user data...'); 49 await storage.set('user:alice', { 50 name: 'Alice', 51 email: 'alice@example.com', 52 role: 'admin', 53 }); 54 55 await storage.set('user:bob', { 56 name: 'Bob', 57 email: 'bob@example.com', 58 role: 'user', 59 }); 60 61 // Retrieve with metadata 62 const result = await storage.getWithMetadata('user:alice'); 63 if (result) { 64 console.log(`Retrieved user:alice from ${result.source} tier:`); 65 console.log(result.data); 66 console.log('Metadata:', { 67 size: result.metadata.size, 68 compressed: result.metadata.compressed, 69 accessCount: result.metadata.accessCount, 70 }); 71 } 72 73 // Get statistics 74 const stats = await storage.getStats(); 75 console.log('\nStorage Statistics:'); 76 console.log(`Hot tier: ${stats.hot?.items} items, ${stats.hot?.bytes} bytes`); 77 console.log(`Warm tier: ${stats.warm?.items} items, ${stats.warm?.bytes} bytes`); 78 console.log(`Cold tier (S3): ${stats.cold.items} items, ${stats.cold.bytes} bytes`); 79 console.log(`Hit rate: ${(stats.hitRate * 100).toFixed(2)}%`); 80 81 // List all keys with prefix 82 console.log('\nAll user keys:'); 83 for await (const key of storage.listKeys('user:')) { 84 console.log(` - ${key}`); 85 } 86 87 // Invalidate by prefix 88 console.log('\nInvalidating all user keys...'); 89 const deleted = await storage.invalidate('user:'); 90 console.log(`Deleted ${deleted} keys`); 91} 92 93async function staticSiteHostingExample() { 94 console.log('\n=== Static Site Hosting Example (wisp.place pattern) ===\n'); 95 96 const storage = new TieredStorage({ 97 tiers: { 98 hot: new MemoryStorageTier({ 99 maxSizeBytes: 50 * 1024 * 1024, // 50MB 100 maxItems: 500, 101 }), 102 warm: new DiskStorageTier({ 103 directory: './example-cache/sites/warm', 104 maxSizeBytes: 1024 * 1024 * 1024, // 1GB 105 }), 106 cold: new S3StorageTier({ 107 bucket: S3_BUCKET, 108 region: S3_REGION, 109 endpoint: S3_ENDPOINT, 110 forcePathStyle: S3_FORCE_PATH_STYLE, 111 credentials: 112 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 113 ? { 114 accessKeyId: AWS_ACCESS_KEY_ID, 115 secretAccessKey: AWS_SECRET_ACCESS_KEY, 116 } 117 : undefined, 118 prefix: 'example/sites/', 119 }), 120 }, 121 compression: true, 122 defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days 123 promotionStrategy: 'lazy', // Don't auto-promote large files 124 }); 125 126 const siteId = 'did:plc:abc123'; 127 const siteName = 'tiered-cache-demo'; 128 129 console.log('Loading real static site from example-site/...\n'); 130 131 // Load actual site files 132 const { readFile } = await import('node:fs/promises'); 133 134 const files = [ 135 { name: 'index.html', skipTiers: [], mimeType: 'text/html' }, 136 { name: 'about.html', skipTiers: ['hot'], mimeType: 'text/html' }, 137 { name: 'docs.html', skipTiers: ['hot'], mimeType: 'text/html' }, 138 { name: 'style.css', skipTiers: ['hot'], mimeType: 'text/css' }, 139 { name: 'script.js', skipTiers: ['hot'], mimeType: 'application/javascript' }, 140 ]; 141 142 console.log('Storing site files with selective tier placement:\n'); 143 144 for (const file of files) { 145 const content = await readFile(`./example-site/${file.name}`, 'utf-8'); 146 const key = `${siteId}/${siteName}/${file.name}`; 147 148 await storage.set(key, content, { 149 skipTiers: file.skipTiers as ('hot' | 'warm')[], 150 metadata: { mimeType: file.mimeType }, 151 }); 152 153 const tierInfo = 154 file.skipTiers.length === 0 155 ? 'hot + warm + cold (S3)' 156 : `warm + cold (S3) - skipped ${file.skipTiers.join(', ')}`; 157 const sizeKB = (content.length / 1024).toFixed(2); 158 console.log(`${file.name} (${sizeKB} KB) → ${tierInfo}`); 159 } 160 161 // Check where each file is served from 162 console.log('\nServing files (checking which tier):'); 163 for (const file of files) { 164 const result = await storage.getWithMetadata(`${siteId}/${siteName}/${file.name}`); 165 if (result) { 166 const sizeKB = (result.metadata.size / 1024).toFixed(2); 167 console.log(` ${file.name}: served from ${result.source} (${sizeKB} KB)`); 168 } 169 } 170 171 // Show hot tier only has index.html 172 console.log('\nHot tier contents (should only contain index.html):'); 173 const stats = await storage.getStats(); 174 console.log(` Items: ${stats.hot?.items}`); 175 console.log(` Size: ${((stats.hot?.bytes ?? 0) / 1024).toFixed(2)} KB`); 176 console.log(` Files: index.html only`); 177 178 console.log('\nWarm tier contents (all site files):'); 179 console.log(` Items: ${stats.warm?.items}`); 180 console.log(` Size: ${((stats.warm?.bytes ?? 0) / 1024).toFixed(2)} KB`); 181 console.log(` Files: all ${files.length} files`); 182 183 // Demonstrate accessing a page 184 console.log('\nSimulating page request for about.html:'); 185 const aboutPage = await storage.getWithMetadata(`${siteId}/${siteName}/about.html`); 186 if (aboutPage) { 187 console.log(` Source: ${aboutPage.source} tier`); 188 console.log(` Access count: ${aboutPage.metadata.accessCount}`); 189 console.log(` Preview: ${aboutPage.data.toString().slice(0, 100)}...`); 190 } 191 192 // Invalidate entire site 193 console.log(`\nInvalidating entire site: ${siteId}/${siteName}/`); 194 const deleted = await storage.invalidate(`${siteId}/${siteName}/`); 195 console.log(`Deleted ${deleted} files from all tiers`); 196} 197 198async function bootstrapExample() { 199 console.log('\n=== Bootstrap Example ===\n'); 200 201 const hot = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }); 202 const warm = new DiskStorageTier({ directory: './example-cache/bootstrap/warm' }); 203 const cold = new S3StorageTier({ 204 bucket: S3_BUCKET, 205 region: S3_REGION, 206 endpoint: S3_ENDPOINT, 207 forcePathStyle: S3_FORCE_PATH_STYLE, 208 credentials: 209 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 210 ? { 211 accessKeyId: AWS_ACCESS_KEY_ID, 212 secretAccessKey: AWS_SECRET_ACCESS_KEY, 213 } 214 : undefined, 215 prefix: 'example/bootstrap/', 216 }); 217 218 const storage = new TieredStorage({ 219 tiers: { hot, warm, cold }, 220 }); 221 222 // Populate with some data 223 console.log('Populating storage with test data...'); 224 for (let i = 0; i < 10; i++) { 225 await storage.set(`item:${i}`, { 226 id: i, 227 name: `Item ${i}`, 228 description: `This is item number ${i}`, 229 }); 230 } 231 232 // Access some items to build up access counts 233 console.log('Accessing some items to simulate usage patterns...'); 234 await storage.get('item:0'); // Most accessed 235 await storage.get('item:0'); 236 await storage.get('item:0'); 237 await storage.get('item:1'); // Second most accessed 238 await storage.get('item:1'); 239 await storage.get('item:2'); // Third most accessed 240 241 // Clear hot tier to simulate server restart 242 console.log('\nSimulating server restart (clearing hot tier)...'); 243 await hot.clear(); 244 245 let hotStats = await hot.getStats(); 246 console.log(`Hot tier after clear: ${hotStats.items} items`); 247 248 // Bootstrap hot from warm (loads most accessed items) 249 console.log('\nBootstrapping hot tier from warm (loading top 3 items)...'); 250 const loaded = await storage.bootstrapHot(3); 251 console.log(`Loaded ${loaded} items into hot tier`); 252 253 hotStats = await hot.getStats(); 254 console.log(`Hot tier after bootstrap: ${hotStats.items} items`); 255 256 // Verify the right items were loaded 257 console.log('\nVerifying loaded items are served from hot:'); 258 for (let i = 0; i < 3; i++) { 259 const result = await storage.getWithMetadata(`item:${i}`); 260 console.log(` item:${i}: ${result?.source}`); 261 } 262 263 // Cleanup this example's data 264 console.log('\nCleaning up bootstrap example data...'); 265 await storage.invalidate('item:'); 266} 267 268async function streamingExample() { 269 console.log('\n=== Streaming Example ===\n'); 270 271 const { createReadStream, statSync } = await import('node:fs'); 272 const { pipeline } = await import('node:stream/promises'); 273 274 const storage = new TieredStorage({ 275 tiers: { 276 hot: new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }), // 10MB 277 warm: new DiskStorageTier({ directory: './example-cache/streaming/warm' }), 278 cold: new S3StorageTier({ 279 bucket: S3_BUCKET, 280 region: S3_REGION, 281 endpoint: S3_ENDPOINT, 282 forcePathStyle: S3_FORCE_PATH_STYLE, 283 credentials: 284 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 285 ? { 286 accessKeyId: AWS_ACCESS_KEY_ID, 287 secretAccessKey: AWS_SECRET_ACCESS_KEY, 288 } 289 : undefined, 290 prefix: 'example/streaming/', 291 }), 292 }, 293 compression: true, // Streams will be compressed automatically 294 defaultTTL: 60 * 60 * 1000, // 1 hour 295 }); 296 297 // Stream a file to storage 298 const filePath = './example-site/index.html'; 299 const fileStats = statSync(filePath); 300 301 console.log(`Streaming ${filePath} (${fileStats.size} bytes) with compression...`); 302 303 const readStream = createReadStream(filePath); 304 const result = await storage.setStream('streaming/index.html', readStream, { 305 size: fileStats.size, 306 mimeType: 'text/html', 307 }); 308 309 console.log(`✓ Stored with key: ${result.key}`); 310 console.log(` Original size: ${result.metadata.size} bytes`); 311 console.log(` Compressed: ${result.metadata.compressed}`); 312 console.log(` Checksum (original data): ${result.metadata.checksum.slice(0, 16)}...`); 313 console.log(` Written to tiers: ${result.tiersWritten.join(', ')}`); 314 315 // Stream the file back (automatically decompressed) 316 console.log('\nStreaming back the file (with automatic decompression)...'); 317 318 const streamResult = await storage.getStream('streaming/index.html'); 319 if (streamResult) { 320 console.log(`✓ Streaming from: ${streamResult.source} tier`); 321 console.log(` Metadata size: ${streamResult.metadata.size} bytes`); 322 console.log(` Compressed in storage: ${streamResult.metadata.compressed}`); 323 324 // Collect stream data to verify content 325 const chunks: Buffer[] = []; 326 for await (const chunk of streamResult.stream) { 327 chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); 328 } 329 const content = Buffer.concat(chunks); 330 331 console.log(` Retrieved size: ${content.length} bytes`); 332 console.log(` Content preview: ${content.toString('utf-8').slice(0, 100)}...`); 333 334 // Verify the content matches the original 335 const { readFile } = await import('node:fs/promises'); 336 const original = await readFile(filePath); 337 if (content.equals(original)) { 338 console.log(' ✓ Content matches original file!'); 339 } else { 340 console.log(' ✗ Content does NOT match original file'); 341 } 342 } 343 344 // Example: Stream to a writable destination (like an HTTP response) 345 console.log('\nStreaming to destination (simulated HTTP response)...'); 346 const streamResult2 = await storage.getStream('streaming/index.html'); 347 if (streamResult2) { 348 // In a real server, you would do: streamResult2.stream.pipe(res); 349 // Here we just demonstrate the pattern 350 const { Writable } = await import('node:stream'); 351 let totalBytes = 0; 352 const mockResponse = new Writable({ 353 write(chunk, _encoding, callback) { 354 totalBytes += chunk.length; 355 callback(); 356 }, 357 }); 358 359 await pipeline(streamResult2.stream, mockResponse); 360 console.log(`✓ Streamed ${totalBytes} bytes to destination`); 361 } 362 363 // Cleanup 364 console.log('\nCleaning up streaming example data...'); 365 await storage.invalidate('streaming/'); 366} 367 368async function promotionStrategyExample() { 369 console.log('\n=== Promotion Strategy Example ===\n'); 370 371 // Lazy promotion (default) 372 console.log('Testing LAZY promotion:'); 373 const lazyStorage = new TieredStorage({ 374 tiers: { 375 hot: new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }), 376 warm: new DiskStorageTier({ directory: './example-cache/promo-lazy/warm' }), 377 cold: new S3StorageTier({ 378 bucket: S3_BUCKET, 379 region: S3_REGION, 380 endpoint: S3_ENDPOINT, 381 forcePathStyle: S3_FORCE_PATH_STYLE, 382 credentials: 383 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 384 ? { 385 accessKeyId: AWS_ACCESS_KEY_ID, 386 secretAccessKey: AWS_SECRET_ACCESS_KEY, 387 } 388 : undefined, 389 prefix: 'example/promo-lazy/', 390 }), 391 }, 392 promotionStrategy: 'lazy', 393 }); 394 395 // Write data and clear hot 396 await lazyStorage.set('test:lazy', { value: 'lazy test' }); 397 await lazyStorage.clearTier('hot'); 398 399 // Read from cold (should NOT auto-promote to hot) 400 const lazyResult = await lazyStorage.getWithMetadata('test:lazy'); 401 console.log(` First read served from: ${lazyResult?.source}`); 402 403 const lazyResult2 = await lazyStorage.getWithMetadata('test:lazy'); 404 console.log(` Second read served from: ${lazyResult2?.source} (lazy = no auto-promotion)`); 405 406 // Eager promotion 407 console.log('\nTesting EAGER promotion:'); 408 const eagerStorage = new TieredStorage({ 409 tiers: { 410 hot: new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }), 411 warm: new DiskStorageTier({ directory: './example-cache/promo-eager/warm' }), 412 cold: new S3StorageTier({ 413 bucket: S3_BUCKET, 414 region: S3_REGION, 415 endpoint: S3_ENDPOINT, 416 forcePathStyle: S3_FORCE_PATH_STYLE, 417 credentials: 418 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 419 ? { 420 accessKeyId: AWS_ACCESS_KEY_ID, 421 secretAccessKey: AWS_SECRET_ACCESS_KEY, 422 } 423 : undefined, 424 prefix: 'example/promo-eager/', 425 }), 426 }, 427 promotionStrategy: 'eager', 428 }); 429 430 // Write data and clear hot 431 await eagerStorage.set('test:eager', { value: 'eager test' }); 432 await eagerStorage.clearTier('hot'); 433 434 // Read from cold (SHOULD auto-promote to hot) 435 const eagerResult = await eagerStorage.getWithMetadata('test:eager'); 436 console.log(` First read served from: ${eagerResult?.source}`); 437 438 const eagerResult2 = await eagerStorage.getWithMetadata('test:eager'); 439 console.log(` Second read served from: ${eagerResult2?.source} (eager = promoted to hot)`); 440 441 // Cleanup 442 await lazyStorage.invalidate('test:'); 443 await eagerStorage.invalidate('test:'); 444} 445 446async function cleanup() { 447 console.log('\n=== Cleanup ===\n'); 448 console.log('Removing example cache directories...'); 449 await rm('./example-cache', { recursive: true, force: true }); 450 console.log('✓ Local cache directories removed'); 451 console.log('\nNote: S3 objects with prefix "example/" remain in bucket'); 452 console.log(' (remove manually if needed)'); 453} 454 455async function main() { 456 console.log('╔════════════════════════════════════════════════╗'); 457 console.log('║ Tiered Storage Library - Usage Examples ║'); 458 console.log('║ Cold Tier: S3 (or S3-compatible storage) ║'); 459 console.log('╚════════════════════════════════════════════════╝'); 460 461 // Check for S3 configuration 462 if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { 463 console.log('\n⚠️ Warning: AWS credentials not configured'); 464 console.log(' Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env'); 465 console.log(' (See .env.example for configuration options)\n'); 466 } 467 468 console.log('\nConfiguration:'); 469 console.log(` S3 Bucket: ${S3_BUCKET}`); 470 console.log(` S3 Region: ${S3_REGION}`); 471 console.log(` S3 Endpoint: ${S3_ENDPOINT || '(default AWS S3)'}`); 472 console.log(` Force Path Style: ${S3_FORCE_PATH_STYLE}`); 473 console.log(` Credentials: ${AWS_ACCESS_KEY_ID ? '✓ Configured' : '✗ Not configured (using IAM role)'}`); 474 475 try { 476 // Test S3 connection first 477 console.log('\nTesting S3 connection...'); 478 const testStorage = new S3StorageTier({ 479 bucket: S3_BUCKET, 480 region: S3_REGION, 481 endpoint: S3_ENDPOINT, 482 forcePathStyle: S3_FORCE_PATH_STYLE, 483 credentials: 484 AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY 485 ? { 486 accessKeyId: AWS_ACCESS_KEY_ID, 487 secretAccessKey: AWS_SECRET_ACCESS_KEY, 488 } 489 : undefined, 490 prefix: 'test/', 491 }); 492 493 try { 494 await testStorage.set('connection-test', new TextEncoder().encode('test'), { 495 key: 'connection-test', 496 size: 4, 497 createdAt: new Date(), 498 lastAccessed: new Date(), 499 accessCount: 0, 500 compressed: false, 501 checksum: 'test', 502 }); 503 console.log('✓ S3 connection successful!\n'); 504 await testStorage.delete('connection-test'); 505 } catch (error: any) { 506 console.error('✗ S3 connection failed:', error.message); 507 console.error('\nPossible issues:'); 508 console.error(' 1. Check that the bucket exists on your S3 service'); 509 console.error(' 2. Verify credentials have read/write permissions'); 510 console.error(' 3. Confirm the endpoint URL is correct'); 511 console.error(' 4. Try setting S3_REGION to a different value (e.g., "us-east-1" or "auto")'); 512 console.error('\nSkipping examples due to S3 connection error.\n'); 513 return; 514 } 515 516 await basicExample(); 517 await staticSiteHostingExample(); 518 await streamingExample(); 519 await bootstrapExample(); 520 await promotionStrategyExample(); 521 } catch (error: any) { 522 console.error('\n❌ Error:', error.message); 523 if (error.name === 'NoSuchBucket') { 524 console.error(`\n The S3 bucket "${S3_BUCKET}" does not exist.`); 525 console.error(' Create it first or set S3_BUCKET in .env to an existing bucket.\n'); 526 } 527 } finally { 528 await cleanup(); 529 } 530 531 console.log('\n✅ All examples completed successfully!'); 532 console.log('\nTry modifying this file to experiment with different patterns.'); 533} 534 535main().catch(console.error);