wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs
typescript
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);