wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs typescript
1# tiered-storage 2 3Cascading cache that flows hot → warm → cold. Memory, disk, S3—or bring your own. 4 5## Features 6 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 13 14## Install 15 16```bash 17npm install tiered-storage 18``` 19 20## Example 21 22```typescript 23import { TieredStorage, MemoryStorageTier, DiskStorageTier, S3StorageTier } from 'tiered-storage' 24 25const storage = new TieredStorage({ 26 tiers: { 27 hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), 28 warm: new DiskStorageTier({ directory: './cache' }), 29 cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }), 30 }, 31 placementRules: [ 32 { pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] }, 33 { pattern: '**/*.{jpg,png,gif,mp4}', tiers: ['warm', 'cold'] }, 34 { pattern: '**', tiers: ['warm', 'cold'] }, 35 ], 36}) 37 38// just set - rules decide where it goes 39await storage.set('site:abc/index.html', indexHtml) // → hot + warm + cold 40await storage.set('site:abc/hero.png', imageData) // → warm + cold 41await storage.set('site:abc/video.mp4', videoData) // → warm + cold 42 43// reads bubble up from wherever it lives 44const page = await storage.getWithMetadata('site:abc/index.html') 45console.log(page.source) // 'hot' 46 47const video = await storage.getWithMetadata('site:abc/video.mp4') 48console.log(video.source) // 'warm' 49 50// nuke entire site 51await storage.invalidate('site:abc/') 52``` 53 54Hot tier stays small and fast. Warm tier has everything. Cold tier is the source of truth. 55 56## How it works 57 58``` 59┌─────────────────────────────────────────────┐ 60│ Cold (S3) - source of truth, all data │ 61│ ↑ │ 62│ Warm (disk) - everything hot has + more │ 63│ ↑ │ 64│ Hot (memory) - just the hottest stuff │ 65└─────────────────────────────────────────────┘ 66``` 67 68Writes cascade **down**. Reads bubble **up**. 69 70## Eviction 71 72Items leave upper tiers through eviction or TTL expiration: 73 74```typescript 75const storage = new TieredStorage({ 76 tiers: { 77 // hot: LRU eviction when size/count limits hit 78 hot: new MemoryStorageTier({ 79 maxSizeBytes: 100 * 1024 * 1024, 80 maxItems: 500, 81 }), 82 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' 88 }), 89 90 // cold: never evicts, keeps everything 91 cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }), 92 }, 93 defaultTTL: 14 * 24 * 60 * 60 * 1000, // TTL checked on read 94}) 95``` 96 97A 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. 98 99## Placement rules 100 101Define once which keys go where, instead of passing `skipTiers` on every `set()`: 102 103```typescript 104const storage = new TieredStorage({ 105 tiers: { 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' }), 109 }, 110 placementRules: [ 111 // index.html goes everywhere for instant serving 112 { pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] }, 113 114 // images and video skip hot 115 { pattern: '**/*.{jpg,png,gif,webp,mp4}', tiers: ['warm', 'cold'] }, 116 117 // assets directory skips hot 118 { pattern: 'assets/**', tiers: ['warm', 'cold'] }, 119 120 // everything else: warm + cold only 121 { pattern: '**', tiers: ['warm', 'cold'] }, 122 ], 123}) 124 125// just call set() - rules handle placement 126await storage.set('site:abc/index.html', html) // → hot + warm + cold 127await storage.set('site:abc/hero.png', image) // → warm + cold 128await storage.set('site:abc/assets/font.woff', font) // → warm + cold 129await storage.set('site:abc/about.html', html) // → warm + cold 130``` 131 132Rules are evaluated in order. First match wins. Cold is always included. 133 134## API 135 136### `storage.get(key)` 137 138Get data. Returns `null` if missing or expired. 139 140### `storage.getWithMetadata(key)` 141 142Get data plus which tier served it. 143 144### `storage.set(key, data, options?)` 145 146Store data. Options: 147 148```typescript 149{ 150 ttl: 86400000, // custom TTL 151 skipTiers: ['hot'], // skip specific tiers 152 metadata: { ... }, // custom metadata 153} 154``` 155 156### `storage.delete(key)` 157 158Delete from all tiers. 159 160### `storage.invalidate(prefix)` 161 162Delete all keys matching prefix. Returns count. 163 164### `storage.touch(key, ttl?)` 165 166Renew TTL. 167 168### `storage.listKeys(prefix?)` 169 170Async iterator over keys. 171 172### `storage.getStats()` 173 174Stats across all tiers. 175 176### `storage.bootstrapHot(limit?)` 177 178Warm up hot tier from warm tier. Run on startup. 179 180### `storage.bootstrapWarm(options?)` 181 182Warm up warm tier from cold tier. 183 184## Built-in tiers 185 186### MemoryStorageTier 187 188```typescript 189new MemoryStorageTier({ 190 maxSizeBytes: 100 * 1024 * 1024, 191 maxItems: 1000, 192}) 193``` 194 195LRU eviction. Fast. Single process only. 196 197### DiskStorageTier 198 199```typescript 200new DiskStorageTier({ 201 directory: './cache', 202 maxSizeBytes: 10 * 1024 * 1024 * 1024, 203 evictionPolicy: 'lru', // or 'fifo', 'size' 204}) 205``` 206 207Files on disk with `.meta` sidecars. 208 209### S3StorageTier 210 211```typescript 212new S3StorageTier({ 213 bucket: 'data', 214 metadataBucket: 'metadata', // recommended! 215 region: 'us-east-1', 216}) 217``` 218 219Works with AWS S3, Cloudflare R2, MinIO. Use a separate metadata bucket—otherwise updating access counts requires copying entire objects. 220 221## Custom tiers 222 223Implement `StorageTier`: 224 225```typescript 226interface 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> 237 238 // Optional: combine get + getMetadata for better performance 239 getWithMetadata?(key: string): Promise<{ data: Uint8Array; metadata: StorageMetadata } | null> 240} 241``` 242 243The 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. 244 245## Running the demo 246 247```bash 248cp .env.example .env # add S3 creds 249bun run serve 250``` 251 252Visit http://localhost:3000 to see it work. Check http://localhost:3000/admin/stats for live cache stats. 253 254## License 255 256MIT