···
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.
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
npm install tiered-storage
22
-
bun add tiered-storage
28
-
import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage';
23
+
import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage'
const storage = new TieredStorage({
32
-
hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), // 100MB
27
+
hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }),
warm: new DiskStorageTier({ directory: './cache' }),
34
-
cold: new S3StorageTier({
35
-
bucket: 'my-bucket',
36
-
region: 'us-east-1',
38
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
39
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
29
+
cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }),
44
-
defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days
45
-
promotionStrategy: 'lazy',
32
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
33
+
{ pattern: '**/*.{jpg,png,gif,mp4}', tiers: ['warm', 'cold'] },
34
+
{ pattern: '**', tiers: ['warm', 'cold'] },
48
-
// Store data (cascades to all tiers)
49
-
await storage.set('user:123', { name: 'Alice', email: 'alice@example.com' });
38
+
// just set - rules decide where it goes
39
+
await storage.set('site:abc/index.html', indexHtml) // → hot + warm + cold
40
+
await storage.set('site:abc/hero.png', imageData) // → warm + cold
41
+
await storage.set('site:abc/video.mp4', videoData) // → warm + cold
51
-
// Retrieve data (bubbles up from cold → warm → hot)
52
-
const user = await storage.get('user:123');
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'
43
+
// reads bubble up from wherever it lives
44
+
const page = await storage.getWithMetadata('site:abc/index.html')
45
+
console.log(page.source) // 'hot'
58
-
// Invalidate all keys with prefix
59
-
await storage.invalidate('user:');
47
+
const video = await storage.getWithMetadata('site:abc/video.mp4')
48
+
console.log(video.source) // 'warm'
62
-
await storage.touch('user:123');
51
+
await storage.invalidate('site:abc/')
54
+
Hot tier stays small and fast. Warm tier has everything. Cold tier is the source of truth.
67
-
### Cascading Containment Model
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
-
└──────────────────────────────────────────────────────┘
59
+
┌─────────────────────────────────────────────┐
60
+
│ Cold (S3) - source of truth, all data │
62
+
│ Warm (disk) - everything hot has + more │
64
+
│ Hot (memory) - just the hottest stuff │
65
+
└─────────────────────────────────────────────┘
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**
68
+
Writes cascade **down**. Reads bubble **up**.
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
94
-
### Selective Tier Placement
96
-
For use cases like static site hosting, you can control which files go into which tiers:
72
+
Items leave upper tiers through eviction or TTL expiration:
99
-
// Small, critical file (index.html) - store in all tiers for instant serving
100
-
await storage.set('site:abc/index.html', htmlContent);
75
+
const storage = new TieredStorage({
77
+
// hot: LRU eviction when size/count limits hit
78
+
hot: new MemoryStorageTier({
79
+
maxSizeBytes: 100 * 1024 * 1024,
102
-
// Large file (video) - skip hot tier to avoid memory bloat
103
-
await storage.set('site:abc/video.mp4', videoData, { skipTiers: ['hot'] });
83
+
// warm: evicts when maxSizeBytes hit, policy controls which items go
84
+
warm: new DiskStorageTier({
85
+
directory: './cache',
86
+
maxSizeBytes: 10 * 1024 * 1024 * 1024,
87
+
evictionPolicy: 'lru', // 'lru' | 'fifo' | 'size'
105
-
// Medium files (images, CSS) - skip hot, use warm + cold
106
-
await storage.set('site:abc/style.css', cssData, { skipTiers: ['hot'] });
90
+
// cold: never evicts, keeps everything
91
+
cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }),
93
+
defaultTTL: 14 * 24 * 60 * 60 * 1000, // TTL checked on read
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)
116
-
### `TieredStorage`
97
+
A file that hasn't been accessed eventually gets evicted from hot (LRU), then warm (size limit + policy). Next request fetches from cold and promotes it back up.
118
-
Main orchestrator class for tiered storage.
101
+
Define once which keys go where, instead of passing `skipTiers` on every `set()`:
123
-
new TieredStorage<T>(config: TieredStorageConfig)
126
-
**Config Options:**
129
-
interface TieredStorageConfig {
104
+
const storage = new TieredStorage({
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)
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>;
106
+
hot: new MemoryStorageTier({ maxSizeBytes: 50 * 1024 * 1024 }),
107
+
warm: new DiskStorageTier({ directory: './cache' }),
108
+
cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }),
111
+
// index.html goes everywhere for instant serving
112
+
{ pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] },
114
+
// images and video skip hot
115
+
{ pattern: '**/*.{jpg,png,gif,webp,mp4}', tiers: ['warm', 'cold'] },
147
-
**`get(key: string): Promise<T | null>`**
117
+
// assets directory skips hot
118
+
{ pattern: 'assets/**', tiers: ['warm', 'cold'] },
149
-
Retrieve data for a key. Returns null if not found or expired.
120
+
// everything else: warm + cold only
121
+
{ pattern: '**', tiers: ['warm', 'cold'] },
151
-
**`getWithMetadata(key: string): Promise<StorageResult<T> | null>`**
153
-
Retrieve data with metadata and source tier information.
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.)
125
+
// just call set() - rules handle placement
126
+
await storage.set('site:abc/index.html', html) // → hot + warm + cold
127
+
await storage.set('site:abc/hero.png', image) // → warm + cold
128
+
await storage.set('site:abc/assets/font.woff', font) // → warm + cold
129
+
await storage.set('site:abc/about.html', html) // → warm + cold
162
-
**`set(key: string, data: T, options?: SetOptions): Promise<SetResult>`**
164
-
Store data with optional configuration.
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
174
-
**`delete(key: string): Promise<void>`**
132
+
Rules are evaluated in order. First match wins. Cold is always included.
176
-
Delete data from all tiers.
178
-
**`exists(key: string): Promise<boolean>`**
136
+
### `storage.get(key)`
180
-
Check if a key exists (and hasn't expired).
138
+
Get data. Returns `null` if missing or expired.
182
-
**`touch(key: string, ttlMs?: number): Promise<void>`**
140
+
### `storage.getWithMetadata(key)`
184
-
Renew TTL for a key. Useful for "keep alive" behavior.
142
+
Get data plus which tier served it.
186
-
**`invalidate(prefix: string): Promise<number>`**
144
+
### `storage.set(key, data, options?)`
188
-
Delete all keys matching a prefix. Returns number of keys deleted.
146
+
Store data. Options:
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
196
-
**`listKeys(prefix?: string): AsyncIterableIterator<string>`**
198
-
List all keys, optionally filtered by prefix.
201
-
for await (const key of storage.listKeys('user:')) {
202
-
console.log(key); // 'user:123', 'user:456', etc.
150
+
ttl: 86400000, // custom TTL
151
+
skipTiers: ['hot'], // skip specific tiers
152
+
metadata: { ... }, // custom metadata
206
-
**`getStats(): Promise<AllTierStats>`**
208
-
Get aggregated statistics across all tiers.
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)
156
+
### `storage.delete(key)`
216
-
**`bootstrapHot(limit?: number): Promise<number>`**
158
+
Delete from all tiers.
218
-
Load most frequently accessed items from warm into hot. Returns number of items loaded.
160
+
### `storage.invalidate(prefix)`
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`);
162
+
Delete all keys matching prefix. Returns count.
226
-
**`bootstrapWarm(options?: { limit?: number; sinceDate?: Date }): Promise<number>`**
228
-
Load recent items from cold into warm. Returns number of items loaded.
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),
164
+
### `storage.touch(key, ttl?)`
238
-
**`export(): Promise<StorageSnapshot>`**
240
-
Export metadata snapshot for backup or migration.
168
+
### `storage.listKeys(prefix?)`
242
-
**`import(snapshot: StorageSnapshot): Promise<void>`**
170
+
Async iterator over keys.
244
-
Import metadata snapshot.
172
+
### `storage.getStats()`
246
-
**`clear(): Promise<void>`**
174
+
Stats across all tiers.
248
-
Clear all data from all tiers. ⚠️ Use with extreme caution!
176
+
### `storage.bootstrapHot(limit?)`
250
-
**`clearTier(tier: 'hot' | 'warm' | 'cold'): Promise<void>`**
178
+
Warm up hot tier from warm tier. Run on startup.
252
-
Clear a specific tier.
180
+
### `storage.bootstrapWarm(options?)`
254
-
### Built-in Storage Tiers
182
+
Warm up warm tier from cold tier.
256
-
#### `MemoryStorageTier`
258
-
In-memory storage using TinyLRU for efficient LRU eviction.
186
+
### MemoryStorageTier
261
-
import { MemoryStorageTier } from 'tiered-storage';
263
-
const tier = new MemoryStorageTier({
264
-
maxSizeBytes: 100 * 1024 * 1024, // 100MB
265
-
maxItems: 1000, // Optional: max number of items
189
+
new MemoryStorageTier({
190
+
maxSizeBytes: 100 * 1024 * 1024,
270
-
- Battle-tested TinyLRU library
271
-
- Automatic LRU eviction
272
-
- Size-based and count-based limits
273
-
- Single process only (not distributed)
275
-
#### `DiskStorageTier`
195
+
LRU eviction. Fast. Single process only.
277
-
Filesystem-based storage with `.meta` files.
197
+
### DiskStorageTier
280
-
import { DiskStorageTier } from 'tiered-storage';
282
-
const tier = new DiskStorageTier({
200
+
new DiskStorageTier({
284
-
maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB (optional)
285
-
evictionPolicy: 'lru', // 'lru' | 'fifo' | 'size'
202
+
maxSizeBytes: 10 * 1024 * 1024 * 1024,
203
+
evictionPolicy: 'lru', // or 'fifo', 'size'
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
207
+
Files on disk with `.meta` sidecars.
296
-
**File structure:**
299
-
├── user%3A123 # Data file (encoded key)
300
-
├── user%3A123.meta # Metadata JSON
301
-
├── site%3Aabc%2Findex.html
302
-
└── site%3Aabc%2Findex.html.meta
305
-
#### `S3StorageTier`
307
-
AWS S3 or S3-compatible object storage.
310
-
import { S3StorageTier } from 'tiered-storage';
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',
318
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
319
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
321
-
prefix: 'cache/', // Optional key prefix
324
-
// Cloudflare R2 with metadata bucket
325
-
const r2Tier = new S3StorageTier({
326
-
bucket: 'my-r2-data-bucket',
327
-
metadataBucket: 'my-r2-metadata-bucket',
329
-
endpoint: 'https://account-id.r2.cloudflarestorage.com',
331
-
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
332
-
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
336
-
// Without metadata bucket (legacy mode - slower, more expensive)
337
-
const legacyTier = new S3StorageTier({
338
-
bucket: 'my-bucket',
212
+
new S3StorageTier({
214
+
metadataBucket: 'metadata', // recommended!
340
-
// No metadataBucket - metadata stored in S3 object metadata fields
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)
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!
219
+
Works with AWS S3, Cloudflare R2, MinIO. Use a separate metadata bucket—otherwise updating access counts requires copying entire objects.
356
-
### Pattern 1: Simple Single-Server Setup
223
+
Implement `StorageTier`:
359
-
import { TieredStorage, MemoryStorageTier, DiskStorageTier } from 'tiered-storage';
226
+
interface StorageTier {
227
+
get(key: string): Promise<Uint8Array | null>
228
+
set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void>
229
+
delete(key: string): Promise<void>
230
+
exists(key: string): Promise<boolean>
231
+
listKeys(prefix?: string): AsyncIterableIterator<string>
232
+
deleteMany(keys: string[]): Promise<void>
233
+
getMetadata(key: string): Promise<StorageMetadata | null>
234
+
setMetadata(key: string, metadata: StorageMetadata): Promise<void>
235
+
getStats(): Promise<TierStats>
236
+
clear(): Promise<void>
361
-
const storage = new TieredStorage({
363
-
hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }),
364
-
warm: new DiskStorageTier({ directory: './cache' }),
365
-
cold: new DiskStorageTier({ directory: './storage' }),
368
-
defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days
371
-
await storage.set('user:123', { name: 'Alice', email: 'alice@example.com' });
372
-
const user = await storage.get('user:123');
375
-
### Pattern 2: Static Site Hosting (wisp.place-style)
378
-
import { TieredStorage, MemoryStorageTier, DiskStorageTier } from 'tiered-storage';
380
-
const storage = new TieredStorage({
382
-
hot: new MemoryStorageTier({
383
-
maxSizeBytes: 100 * 1024 * 1024, // 100MB
386
-
warm: new DiskStorageTier({
387
-
directory: './cache/sites',
388
-
maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB
390
-
// Cold tier is PDS (fetched on demand via custom tier implementation)
393
-
defaultTTL: 14 * 24 * 60 * 60 * 1000,
394
-
promotionStrategy: 'lazy', // Don't auto-promote large files to hot
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' },
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' },
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
412
-
// Invalidate entire site
413
-
await storage.invalidate(`${did}/${rkey}/`);
415
-
// Renew TTL when site is accessed
416
-
await storage.touch(`${did}/${rkey}/index.html`);
419
-
### Pattern 3: Custom Backend (SQLite)
421
-
Implement the `StorageTier` interface to use any backend:
424
-
import { StorageTier, StorageMetadata, TierStats } from 'tiered-storage';
425
-
import Database from 'better-sqlite3';
427
-
class SQLiteStorageTier implements StorageTier {
428
-
private db: Database.Database;
430
-
constructor(dbPath: string) {
431
-
this.db = new Database(dbPath);
433
-
CREATE TABLE IF NOT EXISTS cache (
434
-
key TEXT PRIMARY KEY,
435
-
data BLOB NOT NULL,
436
-
metadata TEXT NOT NULL
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;
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));
451
-
async delete(key: string): Promise<void> {
452
-
this.db.prepare('DELETE FROM cache WHERE key = ?').run(key);
455
-
async exists(key: string): Promise<boolean> {
456
-
const row = this.db.prepare('SELECT 1 FROM cache WHERE key = ?').get(key);
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');
465
-
const rows = prefix ? query.all(`${prefix}%`) : query.all();
467
-
for (const row of rows) {
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);
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;
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);
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 };
492
-
async clear(): Promise<void> {
493
-
this.db.prepare('DELETE FROM cache').run();
238
+
// Optional: combine get + getMetadata for better performance
239
+
getWithMetadata?(key: string): Promise<{ data: Uint8Array; metadata: StorageMetadata } | null>
498
-
const storage = new TieredStorage({
500
-
warm: new SQLiteStorageTier('./cache.db'),
501
-
cold: new DiskStorageTier({ directory: './storage' }),
506
-
## Running Examples
243
+
The optional `getWithMetadata` method returns both data and metadata in a single call. Implement it if your backend can fetch both efficiently (e.g., parallel I/O, single query). Falls back to separate `get()` + `getMetadata()` calls if not implemented.
508
-
### Interactive Demo Server
510
-
Run a **real HTTP server** that serves the example site using tiered storage:
245
+
## Running the demo
513
-
# Configure S3 credentials first (copy .env.example to .env and fill in)
514
-
cp .env.example .env
516
-
# Start the demo server
248
+
cp .env.example .env # add S3 creds
521
-
- **http://localhost:3000/** - The demo site served from tiered storage
522
-
- **http://localhost:3000/admin/stats** - Live cache statistics dashboard
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
529
-
### Command-Line Examples
531
-
Or run the non-interactive examples:
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)
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)
548
-
This demonstrates the exact pattern you'd use for wisp.place: critical files in memory, everything else on disk/S3.
559
-
# Install dependencies
252
+
Visit http://localhost:3000 to see it work. Check http://localhost:3000/admin/stats for live cache stats.