+96
-483
README.md
+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