wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs typescript

Compare changes

Choose any two refs to compare.

-28
.eslintrc.cjs
··· 1 - module.exports = { 2 - parser: '@typescript-eslint/parser', 3 - parserOptions: { 4 - ecmaVersion: 2022, 5 - sourceType: 'module', 6 - project: './tsconfig.json', 7 - }, 8 - plugins: ['@typescript-eslint'], 9 - extends: [ 10 - 'eslint:recommended', 11 - '@typescript-eslint/recommended', 12 - '@typescript-eslint/recommended-requiring-type-checking', 13 - ], 14 - root: true, 15 - env: { 16 - node: true, 17 - es2022: true, 18 - }, 19 - rules: { 20 - '@typescript-eslint/no-unused-vars': 'error', 21 - '@typescript-eslint/explicit-function-return-type': 'warn', 22 - '@typescript-eslint/no-explicit-any': 'warn', 23 - '@typescript-eslint/prefer-nullish-coalescing': 'error', 24 - '@typescript-eslint/prefer-optional-chain': 'error', 25 - '@typescript-eslint/no-floating-promises': 'error', 26 - '@typescript-eslint/await-thenable': 'error', 27 - }, 28 - };
+9
.prettierrc.json
··· 1 + { 2 + "useTabs": true, 3 + "tabWidth": 4, 4 + "singleQuote": true, 5 + "trailingComma": "all", 6 + "printWidth": 100, 7 + "arrowParens": "always", 8 + "endOfLine": "lf" 9 + }
+148 -466
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 - compression: true, 44 - defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days 45 - promotionStrategy: 'lazy', 46 - }); 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 + }) 47 37 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 50 42 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' 43 + // reads bubble up from wherever it lives 44 + const page = await storage.getWithMetadata('site:abc/index.html') 45 + console.log(page.source) // 'hot' 57 46 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' 60 49 61 - // Renew TTL 62 - await storage.touch('user:123'); 50 + // nuke entire site 51 + await storage.invalidate('site:abc/') 63 52 ``` 64 53 65 - ## Core Concepts 54 + Hot tier stays small and fast. Warm tier has everything. Cold tier is the source of truth. 66 55 67 - ### Cascading Containment Model 56 + ## How it works 68 57 69 58 ``` 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 โ”‚ 61 + โ”‚ โ†‘ โ”‚ 62 + โ”‚ Warm (disk) - everything hot has + more โ”‚ 63 + โ”‚ โ†‘ โ”‚ 64 + โ”‚ Hot (memory) - just the hottest stuff โ”‚ 65 + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 83 66 ``` 84 67 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**. 89 69 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 70 + ## Eviction 93 71 94 - ### Selective Tier Placement 95 - 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: 97 73 98 74 ```typescript 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({ 76 + tiers: { 77 + // hot: LRU eviction when size/count limits hit 78 + hot: new MemoryStorageTier({ 79 + maxSizeBytes: 100 * 1024 * 1024, 80 + maxItems: 500, 81 + }), 101 82 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' 88 + }), 104 89 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' }), 92 + }, 93 + defaultTTL: 14 * 24 * 60 * 60 * 1000, // TTL checked on read 94 + }) 107 95 ``` 108 96 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` 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. 117 98 118 - Main orchestrator class for tiered storage. 99 + ## Placement rules 119 100 120 - #### Constructor 101 + Define once which keys go where, instead of passing `skipTiers` on every `set()`: 121 102 122 103 ```typescript 123 - new TieredStorage<T>(config: TieredStorageConfig) 124 - ``` 125 - 126 - **Config Options:** 127 - 128 - ```typescript 129 - interface TieredStorageConfig { 104 + const storage = new TieredStorage({ 130 105 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 - ``` 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'] }, 144 113 145 - #### Methods 114 + // images and video skip hot 115 + { pattern: '**/*.{jpg,png,gif,webp,mp4}', tiers: ['warm', 'cold'] }, 146 116 147 - **`get(key: string): Promise<T | null>`** 117 + // assets directory skips hot 118 + { pattern: 'assets/**', tiers: ['warm', 'cold'] }, 148 119 149 - Retrieve data for a key. Returns null if not found or expired. 120 + // everything else: warm + cold only 121 + { pattern: '**', tiers: ['warm', 'cold'] }, 122 + ], 123 + }) 150 124 151 - **`getWithMetadata(key: string): Promise<StorageResult<T> | null>`** 152 - 153 - Retrieve data with metadata and source tier information. 154 - 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.) 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 160 130 ``` 161 131 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 - }); 172 - ``` 173 - 174 - **`delete(key: string): Promise<void>`** 132 + Rules are evaluated in order. First match wins. Cold is always included. 175 133 176 - Delete data from all tiers. 134 + ## API 177 135 178 - **`exists(key: string): Promise<boolean>`** 136 + ### `storage.get(key)` 179 137 180 - Check if a key exists (and hasn't expired). 138 + Get data. Returns `null` if missing or expired. 181 139 182 - **`touch(key: string, ttlMs?: number): Promise<void>`** 140 + ### `storage.getWithMetadata(key)` 183 141 184 - Renew TTL for a key. Useful for "keep alive" behavior. 142 + Get data plus which tier served it. 185 143 186 - **`invalidate(prefix: string): Promise<number>`** 144 + ### `storage.set(key, data, options?)` 187 145 188 - Delete all keys matching a prefix. Returns number of keys deleted. 146 + Store data. Options: 189 147 190 148 ```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 - ``` 195 - 196 - **`listKeys(prefix?: string): AsyncIterableIterator<string>`** 197 - 198 - List all keys, optionally filtered by prefix. 199 - 200 - ```typescript 201 - for await (const key of storage.listKeys('user:')) { 202 - console.log(key); // 'user:123', 'user:456', etc. 149 + { 150 + ttl: 86400000, // custom TTL 151 + skipTiers: ['hot'], // skip specific tiers 152 + metadata: { ... }, // custom metadata 203 153 } 204 154 ``` 205 155 206 - **`getStats(): Promise<AllTierStats>`** 207 - 208 - Get aggregated statistics across all tiers. 209 - 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 - ``` 156 + ### `storage.delete(key)` 215 157 216 - **`bootstrapHot(limit?: number): Promise<number>`** 158 + Delete from all tiers. 217 159 218 - Load most frequently accessed items from warm into hot. Returns number of items loaded. 160 + ### `storage.invalidate(prefix)` 219 161 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 - ``` 162 + Delete all keys matching prefix. Returns count. 225 163 226 - **`bootstrapWarm(options?: { limit?: number; sinceDate?: Date }): Promise<number>`** 227 - 228 - Load recent items from cold into warm. Returns number of items loaded. 229 - 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 - ``` 164 + ### `storage.touch(key, ttl?)` 237 165 238 - **`export(): Promise<StorageSnapshot>`** 166 + Renew TTL. 239 167 240 - Export metadata snapshot for backup or migration. 168 + ### `storage.listKeys(prefix?)` 241 169 242 - **`import(snapshot: StorageSnapshot): Promise<void>`** 170 + Async iterator over keys. 243 171 244 - Import metadata snapshot. 172 + ### `storage.getStats()` 245 173 246 - **`clear(): Promise<void>`** 174 + Stats across all tiers. 247 175 248 - Clear all data from all tiers. โš ๏ธ Use with extreme caution! 176 + ### `storage.bootstrapHot(limit?)` 249 177 250 - **`clearTier(tier: 'hot' | 'warm' | 'cold'): Promise<void>`** 178 + Warm up hot tier from warm tier. Run on startup. 251 179 252 - Clear a specific tier. 180 + ### `storage.bootstrapWarm(options?)` 253 181 254 - ### Built-in Storage Tiers 182 + Warm up warm tier from cold tier. 255 183 256 - #### `MemoryStorageTier` 184 + ## Built-in tiers 257 185 258 - In-memory storage using TinyLRU for efficient LRU eviction. 186 + ### MemoryStorageTier 259 187 260 188 ```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 - }); 189 + new MemoryStorageTier({ 190 + maxSizeBytes: 100 * 1024 * 1024, 191 + maxItems: 1000, 192 + }) 267 193 ``` 268 194 269 - **Features:** 270 - - Battle-tested TinyLRU library 271 - - Automatic LRU eviction 272 - - Size-based and count-based limits 273 - - Single process only (not distributed) 274 - 275 - #### `DiskStorageTier` 195 + LRU eviction. Fast. Single process only. 276 196 277 - Filesystem-based storage with `.meta` files. 197 + ### DiskStorageTier 278 198 279 199 ```typescript 280 - import { DiskStorageTier } from 'tiered-storage'; 281 - 282 - const tier = new DiskStorageTier({ 200 + new DiskStorageTier({ 283 201 directory: './cache', 284 - maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB (optional) 285 - evictionPolicy: 'lru', // 'lru' | 'fifo' | 'size' 286 - }); 202 + maxSizeBytes: 10 * 1024 * 1024 * 1024, 203 + evictionPolicy: 'lru', // or 'fifo', 'size' 204 + }) 287 205 ``` 288 206 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 207 + Files on disk with `.meta` sidecars. 295 208 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. 209 + ### S3StorageTier 308 210 309 211 ```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 316 - 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', 212 + new S3StorageTier({ 213 + bucket: 'data', 214 + metadataBucket: 'metadata', // recommended! 339 215 region: 'us-east-1', 340 - // No metadataBucket - metadata stored in S3 object metadata fields 341 - }); 216 + }) 342 217 ``` 343 218 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) 351 - 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. 353 220 354 - ## Usage Patterns 221 + ## Custom tiers 355 222 356 - ### Pattern 1: Simple Single-Server Setup 223 + Implement `StorageTier`: 357 224 358 225 ```typescript 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> 360 237 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`); 417 - ``` 418 - 419 - ### Pattern 3: Custom Backend (SQLite) 420 - 421 - Implement the `StorageTier` interface to use any backend: 422 - 423 - ```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 - } 238 + // Optional: combine get + getMetadata for better performance 239 + getWithMetadata?(key: string): Promise<{ data: Uint8Array; metadata: StorageMetadata } | null> 495 240 } 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 - }); 504 241 ``` 505 242 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. 507 244 508 - ### Interactive Demo Server 509 - 510 - Run a **real HTTP server** that serves the example site using tiered storage: 245 + ## Running the demo 511 246 512 247 ```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 248 + cp .env.example .env # add S3 creds 517 249 bun run serve 518 250 ``` 519 251 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 252 + Visit http://localhost:3000 to see it work. Check http://localhost:3000/admin/stats for live cache stats. 561 253 562 - # Type check 563 - bun run check 564 - 565 - # Build 566 - bun run build 567 - 568 - # Run tests 569 - bun test 570 - 571 - ``` 572 254 ## License 573 255 574 256 MIT
+181 -54
bun.lock
··· 6 6 "name": "tiered-storage", 7 7 "dependencies": { 8 8 "@aws-sdk/client-s3": "^3.500.0", 9 + "@aws-sdk/lib-storage": "^3.500.0", 9 10 "hono": "^4.10.7", 10 11 "mime-types": "^3.0.2", 11 12 "tiny-lru": "^11.0.0", 12 13 }, 13 14 "devDependencies": { 15 + "@types/bun": "^1.3.4", 16 + "@types/mime-types": "^3.0.1", 14 17 "@types/node": "^24.10.1", 15 18 "@typescript-eslint/eslint-plugin": "^8.48.1", 16 19 "@typescript-eslint/parser": "^8.48.1", 17 - "eslint": "^8.0.0", 20 + "eslint": "^9.39.1", 21 + "eslint-config-prettier": "^10.1.8", 22 + "eslint-plugin-prettier": "^5.5.4", 23 + "prettier": "^3.7.4", 18 24 "tsx": "^4.0.0", 19 25 "typescript": "^5.3.0", 26 + "typescript-eslint": "^8.50.0", 20 27 "vitest": "^4.0.15", 21 28 }, 22 29 }, ··· 57 64 "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.946.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.946.0", "@aws-sdk/core": "3.946.0", "@aws-sdk/token-providers": "3.946.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-LeGSSt2V5iwYey1ENGY75RmoDP3bA2iE/py8QBKW8EDA8hn74XBLkprhrK5iccOvU3UGWY8WrEKFAFGNjJOL9g=="], 58 65 59 66 "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.946.0", "", { "dependencies": { "@aws-sdk/core": "3.946.0", "@aws-sdk/nested-clients": "3.946.0", "@aws-sdk/types": "3.936.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-ocBCvjWfkbjxElBI1QUxOnHldsNhoU0uOICFvuRDAZAoxvypJHN3m5BJkqb7gqorBbcv3LRgmBdEnWXOAvq+7Q=="], 67 + 68 + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.962.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/smithy-client": "^4.10.2", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.962.0" } }, "sha512-Ai5gWRQkzsUMQ6NPoZZoiLXoQ6/yPRcR4oracIVjyWcu48TfBpsRgbqY/5zNOM55ag1wPX9TtJJGOhK3TNk45g=="], 60 69 61 70 "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.936.0", "", { "dependencies": { "@aws-sdk/types": "3.936.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg=="], 62 71 ··· 158 167 159 168 "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], 160 169 161 - "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], 170 + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], 171 + 172 + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], 173 + 174 + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], 175 + 176 + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], 177 + 178 + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], 179 + 180 + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], 181 + 182 + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], 162 183 163 - "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], 184 + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 164 185 165 - "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], 186 + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], 166 187 167 188 "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 168 189 169 - "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], 190 + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], 170 191 171 192 "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 172 193 173 - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], 174 - 175 - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 176 - 177 - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 194 + "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], 178 195 179 196 "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], 180 197 ··· 220 237 221 238 "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], 222 239 223 - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="], 240 + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw=="], 224 241 225 242 "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA=="], 226 243 ··· 324 341 325 342 "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 326 343 344 + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 345 + 327 346 "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], 328 347 329 348 "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], 330 349 331 350 "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 351 + 352 + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 353 + 354 + "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], 332 355 333 356 "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 334 357 ··· 352 375 353 376 "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA=="], 354 377 355 - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], 356 - 357 378 "@vitest/expect": ["@vitest/expect@4.0.15", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.15", "@vitest/utils": "4.0.15", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w=="], 358 379 359 380 "@vitest/mocker": ["@vitest/mocker@4.0.15", "", { "dependencies": { "@vitest/spy": "4.0.15", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ=="], ··· 374 395 375 396 "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], 376 397 377 - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 378 - 379 398 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 380 399 381 400 "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], ··· 384 403 385 404 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 386 405 406 + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], 407 + 387 408 "bowser": ["bowser@2.13.1", "", {}, "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw=="], 388 409 389 410 "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], 390 411 412 + "buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], 413 + 414 + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 415 + 391 416 "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 392 417 393 418 "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="], ··· 406 431 407 432 "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 408 433 409 - "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], 410 - 411 434 "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], 412 435 413 436 "esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="], 414 437 415 438 "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 416 439 417 - "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], 440 + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], 418 441 419 - "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], 442 + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], 420 443 421 - "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 444 + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg=="], 445 + 446 + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 447 + 448 + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 422 449 423 - "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], 450 + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], 424 451 425 452 "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], 426 453 ··· 431 458 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 432 459 433 460 "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 461 + 462 + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], 434 463 435 464 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], 436 465 437 466 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 438 467 468 + "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], 469 + 439 470 "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 440 471 441 472 "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 442 473 443 474 "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], 444 475 445 - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], 446 - 447 476 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 448 477 449 - "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], 478 + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 450 479 451 480 "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], 452 481 453 - "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], 482 + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 454 483 455 484 "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], 456 - 457 - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], 458 485 459 486 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 460 487 461 488 "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], 462 489 463 - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], 464 - 465 490 "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 466 491 467 - "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], 468 - 469 - "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], 492 + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 470 493 471 494 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 472 495 473 496 "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], 497 + 498 + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], 474 499 475 500 "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 476 501 ··· 478 503 479 504 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 480 505 481 - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], 482 - 483 506 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 484 507 485 508 "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 486 509 487 510 "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 488 - 489 - "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], 490 511 491 512 "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 492 513 ··· 522 543 523 544 "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], 524 545 525 - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 526 - 527 546 "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], 528 547 529 548 "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], ··· 533 552 "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 534 553 535 554 "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 536 - 537 - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], 538 555 539 556 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 540 557 ··· 548 565 549 566 "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 550 567 568 + "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], 569 + 570 + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], 571 + 551 572 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 552 573 553 - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 574 + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], 554 575 555 576 "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 556 577 557 578 "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 558 579 559 - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 560 - 561 - "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], 562 - 563 580 "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], 564 581 565 - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 582 + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 566 583 567 584 "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 568 585 ··· 578 595 579 596 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], 580 597 581 - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 598 + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], 599 + 600 + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 582 601 583 602 "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 584 603 ··· 586 605 587 606 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 588 607 589 - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], 608 + "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], 590 609 591 610 "tiny-lru": ["tiny-lru@11.4.5", "", {}, "sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw=="], 592 611 ··· 606 625 607 626 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 608 627 609 - "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], 628 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 610 629 611 - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 630 + "typescript-eslint": ["typescript-eslint@8.50.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.0", "@typescript-eslint/parser": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A=="], 612 631 613 632 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 614 633 615 634 "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 635 + 636 + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 616 637 617 638 "vite": ["vite@7.2.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ=="], 618 639 ··· 624 645 625 646 "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 626 647 627 - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 628 - 629 648 "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 630 649 631 650 "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], ··· 634 653 635 654 "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], 636 655 656 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.1", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "@smithy/url-parser": "^4.2.7", "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" } }, "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg=="], 657 + 658 + "@aws-sdk/lib-storage/@smithy/smithy-client": ["@smithy/smithy-client@4.10.2", "", { "dependencies": { "@smithy/core": "^3.20.0", "@smithy/middleware-endpoint": "^4.4.1", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" } }, "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g=="], 659 + 660 + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 661 + 637 662 "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 638 663 639 - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 664 + "@smithy/abort-controller/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], 640 665 641 - "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 666 + "@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="], 667 + 668 + "@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="], 669 + 670 + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 642 671 643 672 "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 644 673 674 + "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="], 675 + 676 + "typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.50.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q=="], 677 + 678 + "typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.50.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.50.0", "@typescript-eslint/tsconfig-utils": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ=="], 679 + 680 + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.50.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg=="], 681 + 645 682 "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], 646 683 647 684 "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], ··· 650 687 651 688 "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], 652 689 690 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core": ["@smithy/core@3.20.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ=="], 691 + 692 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="], 693 + 694 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.7", "", { "dependencies": { "@smithy/property-provider": "^4.2.7", "@smithy/shared-ini-file-loader": "^4.4.2", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw=="], 695 + 696 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.2", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg=="], 697 + 698 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], 699 + 700 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/url-parser@4.2.7", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg=="], 701 + 702 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w=="], 703 + 704 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core": ["@smithy/core@3.20.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.8", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ=="], 705 + 706 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw=="], 707 + 708 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="], 709 + 710 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/types": ["@smithy/types@4.11.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA=="], 711 + 712 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream": ["@smithy/util-stream@4.5.8", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w=="], 713 + 653 714 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 654 715 716 + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="], 717 + 718 + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/typescript-estree": "8.50.0", "@typescript-eslint/utils": "8.50.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw=="], 719 + 720 + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], 721 + 722 + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="], 723 + 724 + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], 725 + 726 + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], 727 + 728 + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.50.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.50.0", "@typescript-eslint/types": "^8.50.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ=="], 729 + 730 + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.50.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w=="], 731 + 732 + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], 733 + 734 + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], 735 + 736 + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 737 + 738 + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0" } }, "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A=="], 739 + 740 + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], 741 + 655 742 "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 656 743 657 744 "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], ··· 709 796 "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 710 797 711 798 "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 799 + 800 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="], 801 + 802 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.8", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.8", "@smithy/node-http-handler": "^4.4.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w=="], 803 + 804 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/middleware-serde/@smithy/protocol-http": ["@smithy/protocol-http@5.3.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA=="], 805 + 806 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA=="], 807 + 808 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w=="], 809 + 810 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w=="], 811 + 812 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/util-middleware@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w=="], 813 + 814 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg=="], 815 + 816 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ=="], 817 + 818 + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], 819 + 820 + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], 821 + 822 + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.50.0", "", {}, "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w=="], 823 + 824 + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 825 + 826 + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="], 827 + 828 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg=="], 829 + 830 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.7", "", { "dependencies": { "@smithy/abort-controller": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/querystring-builder": "^4.2.7", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" } }, "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ=="], 831 + 832 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="], 833 + 834 + "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="], 835 + 836 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="], 837 + 838 + "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.7", "", { "dependencies": { "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg=="], 712 839 } 713 840 }
+33
eslint.config.js
··· 1 + import js from '@eslint/js'; 2 + import tseslint from 'typescript-eslint'; 3 + import prettier from 'eslint-plugin-prettier/recommended'; 4 + 5 + export default tseslint.config( 6 + js.configs.recommended, 7 + ...tseslint.configs.recommendedTypeChecked, 8 + prettier, 9 + { 10 + languageOptions: { 11 + parserOptions: { 12 + project: './tsconfig.eslint.json', 13 + tsconfigRootDir: import.meta.dirname, 14 + }, 15 + }, 16 + rules: { 17 + '@typescript-eslint/no-unused-vars': 'error', 18 + '@typescript-eslint/no-explicit-any': 'warn', 19 + '@typescript-eslint/prefer-nullish-coalescing': 'error', 20 + '@typescript-eslint/prefer-optional-chain': 'error', 21 + '@typescript-eslint/no-floating-promises': 'error', 22 + '@typescript-eslint/await-thenable': 'error', 23 + '@typescript-eslint/require-await': 'off', // Interface methods can be async for compatibility 24 + '@typescript-eslint/explicit-function-return-type': 'off', // Too noisy for test files 25 + 'prettier/prettier': 'error', 26 + 'indent': ['error', 'tab', { 'SwitchCase': 1 }], 27 + '@typescript-eslint/indent': 'off', // Prettier handles this 28 + }, 29 + }, 30 + { 31 + ignores: ['dist/', 'node_modules/', '*.js', '*.cjs', '*.mjs'], 32 + }, 33 + );
+101
example.ts
··· 265 265 await storage.invalidate('item:'); 266 266 } 267 267 268 + async 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 + 268 368 async function promotionStrategyExample() { 269 369 console.log('\n=== Promotion Strategy Example ===\n'); 270 370 ··· 415 515 416 516 await basicExample(); 417 517 await staticSiteHostingExample(); 518 + await streamingExample(); 418 519 await bootstrapExample(); 419 520 await promotionStrategyExample(); 420 521 } catch (error: any) {
+969 -467
package-lock.json
··· 1 1 { 2 2 "name": "tiered-storage", 3 - "version": "1.0.0", 3 + "version": "1.0.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "tiered-storage", 9 - "version": "1.0.0", 9 + "version": "1.0.1", 10 10 "dependencies": { 11 11 "@aws-sdk/client-s3": "^3.500.0", 12 + "@aws-sdk/lib-storage": "^3.500.0", 12 13 "hono": "^4.10.7", 13 14 "mime-types": "^3.0.2", 14 15 "tiny-lru": "^11.0.0" ··· 20 21 "@typescript-eslint/eslint-plugin": "^8.48.1", 21 22 "@typescript-eslint/parser": "^8.48.1", 22 23 "eslint": "^9.39.1", 24 + "eslint-config-prettier": "^10.1.8", 25 + "eslint-plugin-prettier": "^5.5.4", 26 + "prettier": "^3.7.4", 23 27 "tsx": "^4.0.0", 24 28 "typescript": "^5.3.0", 29 + "typescript-eslint": "^8.50.0", 25 30 "vitest": "^4.0.15" 26 31 }, 27 32 "engines": { ··· 30 35 }, 31 36 "node_modules/@aws-crypto/crc32": { 32 37 "version": "5.2.0", 38 + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", 39 + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", 33 40 "license": "Apache-2.0", 34 41 "dependencies": { 35 42 "@aws-crypto/util": "^5.2.0", ··· 42 49 }, 43 50 "node_modules/@aws-crypto/crc32c": { 44 51 "version": "5.2.0", 52 + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", 53 + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", 45 54 "license": "Apache-2.0", 46 55 "dependencies": { 47 56 "@aws-crypto/util": "^5.2.0", ··· 95 104 }, 96 105 "node_modules/@aws-crypto/sha256-browser": { 97 106 "version": "5.2.0", 107 + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", 108 + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", 98 109 "license": "Apache-2.0", 99 110 "dependencies": { 100 111 "@aws-crypto/sha256-js": "^5.2.0", ··· 106 117 "tslib": "^2.6.2" 107 118 } 108 119 }, 109 - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { 110 - "version": "2.3.0", 120 + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { 121 + "version": "2.2.0", 122 + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", 123 + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 111 124 "license": "Apache-2.0", 112 125 "dependencies": { 113 - "@smithy/util-buffer-from": "^2.2.0", 114 126 "tslib": "^2.6.2" 115 127 }, 116 128 "engines": { 117 129 "node": ">=14.0.0" 118 130 } 119 131 }, 120 - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from": { 132 + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { 121 133 "version": "2.2.0", 134 + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", 135 + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 122 136 "license": "Apache-2.0", 123 137 "dependencies": { 124 138 "@smithy/is-array-buffer": "^2.2.0", ··· 128 142 "node": ">=14.0.0" 129 143 } 130 144 }, 131 - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8/node_modules/@smithy/util-buffer-from/node_modules/@smithy/is-array-buffer": { 132 - "version": "2.2.0", 145 + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { 146 + "version": "2.3.0", 147 + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", 148 + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 133 149 "license": "Apache-2.0", 134 150 "dependencies": { 151 + "@smithy/util-buffer-from": "^2.2.0", 135 152 "tslib": "^2.6.2" 136 153 }, 137 154 "engines": { ··· 140 157 }, 141 158 "node_modules/@aws-crypto/sha256-js": { 142 159 "version": "5.2.0", 160 + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", 161 + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", 143 162 "license": "Apache-2.0", 144 163 "dependencies": { 145 164 "@aws-crypto/util": "^5.2.0", ··· 199 218 } 200 219 }, 201 220 "node_modules/@aws-sdk/client-s3": { 202 - "version": "3.946.0", 221 + "version": "3.956.0", 222 + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.956.0.tgz", 223 + "integrity": "sha512-O+Z7PSY9TjaqJcZSDMvVmXBuV/jmFRJIu7ga+9XgWv4+qfjhAX2N2s4kgsRnIdjIO4xgkN3O/BugTCyjIRrIDQ==", 203 224 "license": "Apache-2.0", 225 + "peer": true, 204 226 "dependencies": { 205 227 "@aws-crypto/sha1-browser": "5.2.0", 206 228 "@aws-crypto/sha256-browser": "5.2.0", 207 229 "@aws-crypto/sha256-js": "5.2.0", 208 - "@aws-sdk/core": "3.946.0", 209 - "@aws-sdk/credential-provider-node": "3.946.0", 210 - "@aws-sdk/middleware-bucket-endpoint": "3.936.0", 211 - "@aws-sdk/middleware-expect-continue": "3.936.0", 212 - "@aws-sdk/middleware-flexible-checksums": "3.946.0", 213 - "@aws-sdk/middleware-host-header": "3.936.0", 214 - "@aws-sdk/middleware-location-constraint": "3.936.0", 215 - "@aws-sdk/middleware-logger": "3.936.0", 216 - "@aws-sdk/middleware-recursion-detection": "3.936.0", 217 - "@aws-sdk/middleware-sdk-s3": "3.946.0", 218 - "@aws-sdk/middleware-ssec": "3.936.0", 219 - "@aws-sdk/middleware-user-agent": "3.946.0", 220 - "@aws-sdk/region-config-resolver": "3.936.0", 221 - "@aws-sdk/signature-v4-multi-region": "3.946.0", 222 - "@aws-sdk/types": "3.936.0", 223 - "@aws-sdk/util-endpoints": "3.936.0", 224 - "@aws-sdk/util-user-agent-browser": "3.936.0", 225 - "@aws-sdk/util-user-agent-node": "3.946.0", 226 - "@smithy/config-resolver": "^4.4.3", 227 - "@smithy/core": "^3.18.7", 228 - "@smithy/eventstream-serde-browser": "^4.2.5", 229 - "@smithy/eventstream-serde-config-resolver": "^4.3.5", 230 - "@smithy/eventstream-serde-node": "^4.2.5", 231 - "@smithy/fetch-http-handler": "^5.3.6", 232 - "@smithy/hash-blob-browser": "^4.2.6", 233 - "@smithy/hash-node": "^4.2.5", 234 - "@smithy/hash-stream-node": "^4.2.5", 235 - "@smithy/invalid-dependency": "^4.2.5", 236 - "@smithy/md5-js": "^4.2.5", 237 - "@smithy/middleware-content-length": "^4.2.5", 238 - "@smithy/middleware-endpoint": "^4.3.14", 239 - "@smithy/middleware-retry": "^4.4.14", 240 - "@smithy/middleware-serde": "^4.2.6", 241 - "@smithy/middleware-stack": "^4.2.5", 242 - "@smithy/node-config-provider": "^4.3.5", 243 - "@smithy/node-http-handler": "^4.4.5", 244 - "@smithy/protocol-http": "^5.3.5", 245 - "@smithy/smithy-client": "^4.9.10", 246 - "@smithy/types": "^4.9.0", 247 - "@smithy/url-parser": "^4.2.5", 230 + "@aws-sdk/core": "3.956.0", 231 + "@aws-sdk/credential-provider-node": "3.956.0", 232 + "@aws-sdk/middleware-bucket-endpoint": "3.956.0", 233 + "@aws-sdk/middleware-expect-continue": "3.956.0", 234 + "@aws-sdk/middleware-flexible-checksums": "3.956.0", 235 + "@aws-sdk/middleware-host-header": "3.956.0", 236 + "@aws-sdk/middleware-location-constraint": "3.956.0", 237 + "@aws-sdk/middleware-logger": "3.956.0", 238 + "@aws-sdk/middleware-recursion-detection": "3.956.0", 239 + "@aws-sdk/middleware-sdk-s3": "3.956.0", 240 + "@aws-sdk/middleware-ssec": "3.956.0", 241 + "@aws-sdk/middleware-user-agent": "3.956.0", 242 + "@aws-sdk/region-config-resolver": "3.956.0", 243 + "@aws-sdk/signature-v4-multi-region": "3.956.0", 244 + "@aws-sdk/types": "3.956.0", 245 + "@aws-sdk/util-endpoints": "3.956.0", 246 + "@aws-sdk/util-user-agent-browser": "3.956.0", 247 + "@aws-sdk/util-user-agent-node": "3.956.0", 248 + "@smithy/config-resolver": "^4.4.5", 249 + "@smithy/core": "^3.20.0", 250 + "@smithy/eventstream-serde-browser": "^4.2.7", 251 + "@smithy/eventstream-serde-config-resolver": "^4.3.7", 252 + "@smithy/eventstream-serde-node": "^4.2.7", 253 + "@smithy/fetch-http-handler": "^5.3.8", 254 + "@smithy/hash-blob-browser": "^4.2.8", 255 + "@smithy/hash-node": "^4.2.7", 256 + "@smithy/hash-stream-node": "^4.2.7", 257 + "@smithy/invalid-dependency": "^4.2.7", 258 + "@smithy/md5-js": "^4.2.7", 259 + "@smithy/middleware-content-length": "^4.2.7", 260 + "@smithy/middleware-endpoint": "^4.4.1", 261 + "@smithy/middleware-retry": "^4.4.17", 262 + "@smithy/middleware-serde": "^4.2.8", 263 + "@smithy/middleware-stack": "^4.2.7", 264 + "@smithy/node-config-provider": "^4.3.7", 265 + "@smithy/node-http-handler": "^4.4.7", 266 + "@smithy/protocol-http": "^5.3.7", 267 + "@smithy/smithy-client": "^4.10.2", 268 + "@smithy/types": "^4.11.0", 269 + "@smithy/url-parser": "^4.2.7", 248 270 "@smithy/util-base64": "^4.3.0", 249 271 "@smithy/util-body-length-browser": "^4.2.0", 250 272 "@smithy/util-body-length-node": "^4.2.1", 251 - "@smithy/util-defaults-mode-browser": "^4.3.13", 252 - "@smithy/util-defaults-mode-node": "^4.2.16", 253 - "@smithy/util-endpoints": "^3.2.5", 254 - "@smithy/util-middleware": "^4.2.5", 255 - "@smithy/util-retry": "^4.2.5", 256 - "@smithy/util-stream": "^4.5.6", 273 + "@smithy/util-defaults-mode-browser": "^4.3.16", 274 + "@smithy/util-defaults-mode-node": "^4.2.19", 275 + "@smithy/util-endpoints": "^3.2.7", 276 + "@smithy/util-middleware": "^4.2.7", 277 + "@smithy/util-retry": "^4.2.7", 278 + "@smithy/util-stream": "^4.5.8", 257 279 "@smithy/util-utf8": "^4.2.0", 258 - "@smithy/util-waiter": "^4.2.5", 280 + "@smithy/util-waiter": "^4.2.7", 259 281 "tslib": "^2.6.2" 260 282 }, 261 283 "engines": { ··· 263 285 } 264 286 }, 265 287 "node_modules/@aws-sdk/client-sso": { 266 - "version": "3.946.0", 288 + "version": "3.956.0", 289 + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.956.0.tgz", 290 + "integrity": "sha512-TCxCa9B1IMILvk/7sig0fRQzff+M2zBQVZGWOJL8SAZq/gfElIMAf/nYjQwMhXjyq8PFDRGm4GN8ZhNKPeNleQ==", 267 291 "license": "Apache-2.0", 268 292 "dependencies": { 269 293 "@aws-crypto/sha256-browser": "5.2.0", 270 294 "@aws-crypto/sha256-js": "5.2.0", 271 - "@aws-sdk/core": "3.946.0", 272 - "@aws-sdk/middleware-host-header": "3.936.0", 273 - "@aws-sdk/middleware-logger": "3.936.0", 274 - "@aws-sdk/middleware-recursion-detection": "3.936.0", 275 - "@aws-sdk/middleware-user-agent": "3.946.0", 276 - "@aws-sdk/region-config-resolver": "3.936.0", 277 - "@aws-sdk/types": "3.936.0", 278 - "@aws-sdk/util-endpoints": "3.936.0", 279 - "@aws-sdk/util-user-agent-browser": "3.936.0", 280 - "@aws-sdk/util-user-agent-node": "3.946.0", 281 - "@smithy/config-resolver": "^4.4.3", 282 - "@smithy/core": "^3.18.7", 283 - "@smithy/fetch-http-handler": "^5.3.6", 284 - "@smithy/hash-node": "^4.2.5", 285 - "@smithy/invalid-dependency": "^4.2.5", 286 - "@smithy/middleware-content-length": "^4.2.5", 287 - "@smithy/middleware-endpoint": "^4.3.14", 288 - "@smithy/middleware-retry": "^4.4.14", 289 - "@smithy/middleware-serde": "^4.2.6", 290 - "@smithy/middleware-stack": "^4.2.5", 291 - "@smithy/node-config-provider": "^4.3.5", 292 - "@smithy/node-http-handler": "^4.4.5", 293 - "@smithy/protocol-http": "^5.3.5", 294 - "@smithy/smithy-client": "^4.9.10", 295 - "@smithy/types": "^4.9.0", 296 - "@smithy/url-parser": "^4.2.5", 295 + "@aws-sdk/core": "3.956.0", 296 + "@aws-sdk/middleware-host-header": "3.956.0", 297 + "@aws-sdk/middleware-logger": "3.956.0", 298 + "@aws-sdk/middleware-recursion-detection": "3.956.0", 299 + "@aws-sdk/middleware-user-agent": "3.956.0", 300 + "@aws-sdk/region-config-resolver": "3.956.0", 301 + "@aws-sdk/types": "3.956.0", 302 + "@aws-sdk/util-endpoints": "3.956.0", 303 + "@aws-sdk/util-user-agent-browser": "3.956.0", 304 + "@aws-sdk/util-user-agent-node": "3.956.0", 305 + "@smithy/config-resolver": "^4.4.5", 306 + "@smithy/core": "^3.20.0", 307 + "@smithy/fetch-http-handler": "^5.3.8", 308 + "@smithy/hash-node": "^4.2.7", 309 + "@smithy/invalid-dependency": "^4.2.7", 310 + "@smithy/middleware-content-length": "^4.2.7", 311 + "@smithy/middleware-endpoint": "^4.4.1", 312 + "@smithy/middleware-retry": "^4.4.17", 313 + "@smithy/middleware-serde": "^4.2.8", 314 + "@smithy/middleware-stack": "^4.2.7", 315 + "@smithy/node-config-provider": "^4.3.7", 316 + "@smithy/node-http-handler": "^4.4.7", 317 + "@smithy/protocol-http": "^5.3.7", 318 + "@smithy/smithy-client": "^4.10.2", 319 + "@smithy/types": "^4.11.0", 320 + "@smithy/url-parser": "^4.2.7", 297 321 "@smithy/util-base64": "^4.3.0", 298 322 "@smithy/util-body-length-browser": "^4.2.0", 299 323 "@smithy/util-body-length-node": "^4.2.1", 300 - "@smithy/util-defaults-mode-browser": "^4.3.13", 301 - "@smithy/util-defaults-mode-node": "^4.2.16", 302 - "@smithy/util-endpoints": "^3.2.5", 303 - "@smithy/util-middleware": "^4.2.5", 304 - "@smithy/util-retry": "^4.2.5", 324 + "@smithy/util-defaults-mode-browser": "^4.3.16", 325 + "@smithy/util-defaults-mode-node": "^4.2.19", 326 + "@smithy/util-endpoints": "^3.2.7", 327 + "@smithy/util-middleware": "^4.2.7", 328 + "@smithy/util-retry": "^4.2.7", 305 329 "@smithy/util-utf8": "^4.2.0", 306 330 "tslib": "^2.6.2" 307 331 }, ··· 310 334 } 311 335 }, 312 336 "node_modules/@aws-sdk/core": { 313 - "version": "3.946.0", 337 + "version": "3.956.0", 338 + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.956.0.tgz", 339 + "integrity": "sha512-BMOCXZNz5z4cR3/SaNHUfeoZQUG/y39bLscdLUgg3RL6mDOhuINIqMc0qc6G3kpwDTLVdXikF4nmx2UrRK9y5A==", 314 340 "license": "Apache-2.0", 315 341 "dependencies": { 316 - "@aws-sdk/types": "3.936.0", 317 - "@aws-sdk/xml-builder": "3.930.0", 318 - "@smithy/core": "^3.18.7", 319 - "@smithy/node-config-provider": "^4.3.5", 320 - "@smithy/property-provider": "^4.2.5", 321 - "@smithy/protocol-http": "^5.3.5", 322 - "@smithy/signature-v4": "^5.3.5", 323 - "@smithy/smithy-client": "^4.9.10", 324 - "@smithy/types": "^4.9.0", 342 + "@aws-sdk/types": "3.956.0", 343 + "@aws-sdk/xml-builder": "3.956.0", 344 + "@smithy/core": "^3.20.0", 345 + "@smithy/node-config-provider": "^4.3.7", 346 + "@smithy/property-provider": "^4.2.7", 347 + "@smithy/protocol-http": "^5.3.7", 348 + "@smithy/signature-v4": "^5.3.7", 349 + "@smithy/smithy-client": "^4.10.2", 350 + "@smithy/types": "^4.11.0", 325 351 "@smithy/util-base64": "^4.3.0", 326 - "@smithy/util-middleware": "^4.2.5", 352 + "@smithy/util-middleware": "^4.2.7", 327 353 "@smithy/util-utf8": "^4.2.0", 328 354 "tslib": "^2.6.2" 329 355 }, ··· 332 358 } 333 359 }, 334 360 "node_modules/@aws-sdk/credential-provider-env": { 335 - "version": "3.946.0", 361 + "version": "3.956.0", 362 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.956.0.tgz", 363 + "integrity": "sha512-aLJavJMPVTvhmggJ0pcdCKEWJk3sL9QkJkUIEoTzOou7HnxWS66N4sC5e8y27AF2nlnYfIxq3hkEiZlGi/vlfA==", 336 364 "license": "Apache-2.0", 337 365 "dependencies": { 338 - "@aws-sdk/core": "3.946.0", 339 - "@aws-sdk/types": "3.936.0", 340 - "@smithy/property-provider": "^4.2.5", 341 - "@smithy/types": "^4.9.0", 366 + "@aws-sdk/core": "3.956.0", 367 + "@aws-sdk/types": "3.956.0", 368 + "@smithy/property-provider": "^4.2.7", 369 + "@smithy/types": "^4.11.0", 342 370 "tslib": "^2.6.2" 343 371 }, 344 372 "engines": { ··· 346 374 } 347 375 }, 348 376 "node_modules/@aws-sdk/credential-provider-http": { 349 - "version": "3.946.0", 377 + "version": "3.956.0", 378 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.956.0.tgz", 379 + "integrity": "sha512-VsKzBNhwT6XJdW3HQX6o4KOHj1MAzSwA8/zCsT9mOGecozw1yeCcQPtlWDSlfsfygKVCXz7fiJzU03yl11NKMA==", 350 380 "license": "Apache-2.0", 351 381 "dependencies": { 352 - "@aws-sdk/core": "3.946.0", 353 - "@aws-sdk/types": "3.936.0", 354 - "@smithy/fetch-http-handler": "^5.3.6", 355 - "@smithy/node-http-handler": "^4.4.5", 356 - "@smithy/property-provider": "^4.2.5", 357 - "@smithy/protocol-http": "^5.3.5", 358 - "@smithy/smithy-client": "^4.9.10", 359 - "@smithy/types": "^4.9.0", 360 - "@smithy/util-stream": "^4.5.6", 382 + "@aws-sdk/core": "3.956.0", 383 + "@aws-sdk/types": "3.956.0", 384 + "@smithy/fetch-http-handler": "^5.3.8", 385 + "@smithy/node-http-handler": "^4.4.7", 386 + "@smithy/property-provider": "^4.2.7", 387 + "@smithy/protocol-http": "^5.3.7", 388 + "@smithy/smithy-client": "^4.10.2", 389 + "@smithy/types": "^4.11.0", 390 + "@smithy/util-stream": "^4.5.8", 361 391 "tslib": "^2.6.2" 362 392 }, 363 393 "engines": { ··· 365 395 } 366 396 }, 367 397 "node_modules/@aws-sdk/credential-provider-ini": { 368 - "version": "3.946.0", 398 + "version": "3.956.0", 399 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.956.0.tgz", 400 + "integrity": "sha512-TlDy+IGr0JIRBwnPdV31J1kWXEcfsR3OzcNVWQrguQdHeTw2lU5eft16kdizo6OruqcZRF/LvHBDwAWx4u51ww==", 369 401 "license": "Apache-2.0", 370 402 "dependencies": { 371 - "@aws-sdk/core": "3.946.0", 372 - "@aws-sdk/credential-provider-env": "3.946.0", 373 - "@aws-sdk/credential-provider-http": "3.946.0", 374 - "@aws-sdk/credential-provider-login": "3.946.0", 375 - "@aws-sdk/credential-provider-process": "3.946.0", 376 - "@aws-sdk/credential-provider-sso": "3.946.0", 377 - "@aws-sdk/credential-provider-web-identity": "3.946.0", 378 - "@aws-sdk/nested-clients": "3.946.0", 379 - "@aws-sdk/types": "3.936.0", 380 - "@smithy/credential-provider-imds": "^4.2.5", 381 - "@smithy/property-provider": "^4.2.5", 382 - "@smithy/shared-ini-file-loader": "^4.4.0", 383 - "@smithy/types": "^4.9.0", 403 + "@aws-sdk/core": "3.956.0", 404 + "@aws-sdk/credential-provider-env": "3.956.0", 405 + "@aws-sdk/credential-provider-http": "3.956.0", 406 + "@aws-sdk/credential-provider-login": "3.956.0", 407 + "@aws-sdk/credential-provider-process": "3.956.0", 408 + "@aws-sdk/credential-provider-sso": "3.956.0", 409 + "@aws-sdk/credential-provider-web-identity": "3.956.0", 410 + "@aws-sdk/nested-clients": "3.956.0", 411 + "@aws-sdk/types": "3.956.0", 412 + "@smithy/credential-provider-imds": "^4.2.7", 413 + "@smithy/property-provider": "^4.2.7", 414 + "@smithy/shared-ini-file-loader": "^4.4.2", 415 + "@smithy/types": "^4.11.0", 384 416 "tslib": "^2.6.2" 385 417 }, 386 418 "engines": { ··· 388 420 } 389 421 }, 390 422 "node_modules/@aws-sdk/credential-provider-login": { 391 - "version": "3.946.0", 423 + "version": "3.956.0", 424 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.956.0.tgz", 425 + "integrity": "sha512-p2Y62mdIlUpiyi5tvn8cKTja5kq1e3Rm5gm4wpNQ9caTayfkIEXyKrbP07iepTv60Coaylq9Fx6b5En/siAeGA==", 392 426 "license": "Apache-2.0", 393 427 "dependencies": { 394 - "@aws-sdk/core": "3.946.0", 395 - "@aws-sdk/nested-clients": "3.946.0", 396 - "@aws-sdk/types": "3.936.0", 397 - "@smithy/property-provider": "^4.2.5", 398 - "@smithy/protocol-http": "^5.3.5", 399 - "@smithy/shared-ini-file-loader": "^4.4.0", 400 - "@smithy/types": "^4.9.0", 428 + "@aws-sdk/core": "3.956.0", 429 + "@aws-sdk/nested-clients": "3.956.0", 430 + "@aws-sdk/types": "3.956.0", 431 + "@smithy/property-provider": "^4.2.7", 432 + "@smithy/protocol-http": "^5.3.7", 433 + "@smithy/shared-ini-file-loader": "^4.4.2", 434 + "@smithy/types": "^4.11.0", 401 435 "tslib": "^2.6.2" 402 436 }, 403 437 "engines": { ··· 405 439 } 406 440 }, 407 441 "node_modules/@aws-sdk/credential-provider-node": { 408 - "version": "3.946.0", 442 + "version": "3.956.0", 443 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.956.0.tgz", 444 + "integrity": "sha512-ITjp7uAQh17ljUsCWkPRmLjyFfupGlJVUfTLHnZJ+c7G0P0PDRquaM+fBSh0y33tauHsBa5fGnCCLRo5hy9sGQ==", 409 445 "license": "Apache-2.0", 410 446 "dependencies": { 411 - "@aws-sdk/credential-provider-env": "3.946.0", 412 - "@aws-sdk/credential-provider-http": "3.946.0", 413 - "@aws-sdk/credential-provider-ini": "3.946.0", 414 - "@aws-sdk/credential-provider-process": "3.946.0", 415 - "@aws-sdk/credential-provider-sso": "3.946.0", 416 - "@aws-sdk/credential-provider-web-identity": "3.946.0", 417 - "@aws-sdk/types": "3.936.0", 418 - "@smithy/credential-provider-imds": "^4.2.5", 419 - "@smithy/property-provider": "^4.2.5", 420 - "@smithy/shared-ini-file-loader": "^4.4.0", 421 - "@smithy/types": "^4.9.0", 447 + "@aws-sdk/credential-provider-env": "3.956.0", 448 + "@aws-sdk/credential-provider-http": "3.956.0", 449 + "@aws-sdk/credential-provider-ini": "3.956.0", 450 + "@aws-sdk/credential-provider-process": "3.956.0", 451 + "@aws-sdk/credential-provider-sso": "3.956.0", 452 + "@aws-sdk/credential-provider-web-identity": "3.956.0", 453 + "@aws-sdk/types": "3.956.0", 454 + "@smithy/credential-provider-imds": "^4.2.7", 455 + "@smithy/property-provider": "^4.2.7", 456 + "@smithy/shared-ini-file-loader": "^4.4.2", 457 + "@smithy/types": "^4.11.0", 422 458 "tslib": "^2.6.2" 423 459 }, 424 460 "engines": { ··· 426 462 } 427 463 }, 428 464 "node_modules/@aws-sdk/credential-provider-process": { 429 - "version": "3.946.0", 465 + "version": "3.956.0", 466 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.956.0.tgz", 467 + "integrity": "sha512-wpAex+/LGVWkHPchsn9FWy1ahFualIeSYq3ADFc262ljJjrltOWGh3+cu3OK3gTMkX6VEsl+lFvy1P7Bk7cgXA==", 430 468 "license": "Apache-2.0", 431 469 "dependencies": { 432 - "@aws-sdk/core": "3.946.0", 433 - "@aws-sdk/types": "3.936.0", 434 - "@smithy/property-provider": "^4.2.5", 435 - "@smithy/shared-ini-file-loader": "^4.4.0", 436 - "@smithy/types": "^4.9.0", 470 + "@aws-sdk/core": "3.956.0", 471 + "@aws-sdk/types": "3.956.0", 472 + "@smithy/property-provider": "^4.2.7", 473 + "@smithy/shared-ini-file-loader": "^4.4.2", 474 + "@smithy/types": "^4.11.0", 437 475 "tslib": "^2.6.2" 438 476 }, 439 477 "engines": { ··· 441 479 } 442 480 }, 443 481 "node_modules/@aws-sdk/credential-provider-sso": { 444 - "version": "3.946.0", 482 + "version": "3.956.0", 483 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.956.0.tgz", 484 + "integrity": "sha512-IRFSDF32x8TpOEYSGMcGQVJUiYuJaFkek0aCjW0klNIZHBF1YpflVpUarK9DJe4v4ryfVq3c0bqR/JFui8QFmw==", 445 485 "license": "Apache-2.0", 446 486 "dependencies": { 447 - "@aws-sdk/client-sso": "3.946.0", 448 - "@aws-sdk/core": "3.946.0", 449 - "@aws-sdk/token-providers": "3.946.0", 450 - "@aws-sdk/types": "3.936.0", 451 - "@smithy/property-provider": "^4.2.5", 452 - "@smithy/shared-ini-file-loader": "^4.4.0", 453 - "@smithy/types": "^4.9.0", 487 + "@aws-sdk/client-sso": "3.956.0", 488 + "@aws-sdk/core": "3.956.0", 489 + "@aws-sdk/token-providers": "3.956.0", 490 + "@aws-sdk/types": "3.956.0", 491 + "@smithy/property-provider": "^4.2.7", 492 + "@smithy/shared-ini-file-loader": "^4.4.2", 493 + "@smithy/types": "^4.11.0", 454 494 "tslib": "^2.6.2" 455 495 }, 456 496 "engines": { ··· 458 498 } 459 499 }, 460 500 "node_modules/@aws-sdk/credential-provider-web-identity": { 461 - "version": "3.946.0", 501 + "version": "3.956.0", 502 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.956.0.tgz", 503 + "integrity": "sha512-4YkmjwZC+qoUKlVOY9xNx7BTKRdJ1R1/Zjk2QSW5aWtwkk2e07ZUQvUpbW4vGpAxGm1K4EgRcowuSpOsDTh44Q==", 504 + "license": "Apache-2.0", 505 + "dependencies": { 506 + "@aws-sdk/core": "3.956.0", 507 + "@aws-sdk/nested-clients": "3.956.0", 508 + "@aws-sdk/types": "3.956.0", 509 + "@smithy/property-provider": "^4.2.7", 510 + "@smithy/shared-ini-file-loader": "^4.4.2", 511 + "@smithy/types": "^4.11.0", 512 + "tslib": "^2.6.2" 513 + }, 514 + "engines": { 515 + "node": ">=18.0.0" 516 + } 517 + }, 518 + "node_modules/@aws-sdk/lib-storage": { 519 + "version": "3.956.0", 520 + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.956.0.tgz", 521 + "integrity": "sha512-CbRdMmB0lAf+InlqGUm7ly6qIO8g6661Xpre3OtZcwfTFSJ8TjQWfj0KLZoNvk605XKxhWR43yd5I6MrmE5jng==", 462 522 "license": "Apache-2.0", 463 523 "dependencies": { 464 - "@aws-sdk/core": "3.946.0", 465 - "@aws-sdk/nested-clients": "3.946.0", 466 - "@aws-sdk/types": "3.936.0", 467 - "@smithy/property-provider": "^4.2.5", 468 - "@smithy/shared-ini-file-loader": "^4.4.0", 469 - "@smithy/types": "^4.9.0", 524 + "@smithy/abort-controller": "^4.2.7", 525 + "@smithy/middleware-endpoint": "^4.4.1", 526 + "@smithy/smithy-client": "^4.10.2", 527 + "buffer": "5.6.0", 528 + "events": "3.3.0", 529 + "stream-browserify": "3.0.0", 470 530 "tslib": "^2.6.2" 471 531 }, 472 532 "engines": { 473 533 "node": ">=18.0.0" 534 + }, 535 + "peerDependencies": { 536 + "@aws-sdk/client-s3": "^3.956.0" 474 537 } 475 538 }, 476 539 "node_modules/@aws-sdk/middleware-bucket-endpoint": { 477 - "version": "3.936.0", 540 + "version": "3.956.0", 541 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.956.0.tgz", 542 + "integrity": "sha512-+iHH9cnkNZgKkTBnPP9rbapHliKDrOuj7MDz6+wL0NV4N/XGB5tbrd+uDP608FXVeMHcWIIZtWkANADUmAI49w==", 478 543 "license": "Apache-2.0", 479 544 "dependencies": { 480 - "@aws-sdk/types": "3.936.0", 481 - "@aws-sdk/util-arn-parser": "3.893.0", 482 - "@smithy/node-config-provider": "^4.3.5", 483 - "@smithy/protocol-http": "^5.3.5", 484 - "@smithy/types": "^4.9.0", 545 + "@aws-sdk/types": "3.956.0", 546 + "@aws-sdk/util-arn-parser": "3.953.0", 547 + "@smithy/node-config-provider": "^4.3.7", 548 + "@smithy/protocol-http": "^5.3.7", 549 + "@smithy/types": "^4.11.0", 485 550 "@smithy/util-config-provider": "^4.2.0", 486 551 "tslib": "^2.6.2" 487 552 }, ··· 490 555 } 491 556 }, 492 557 "node_modules/@aws-sdk/middleware-expect-continue": { 493 - "version": "3.936.0", 558 + "version": "3.956.0", 559 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.956.0.tgz", 560 + "integrity": "sha512-97rmalK9x09Darcl6AbShZRXYxWiyCeO8ll1C9rx1xyZMs2DeIKAZ/xuAJ/bywB3l25ls6VqXO4/EuDFJHL8eA==", 494 561 "license": "Apache-2.0", 495 562 "dependencies": { 496 - "@aws-sdk/types": "3.936.0", 497 - "@smithy/protocol-http": "^5.3.5", 498 - "@smithy/types": "^4.9.0", 563 + "@aws-sdk/types": "3.956.0", 564 + "@smithy/protocol-http": "^5.3.7", 565 + "@smithy/types": "^4.11.0", 499 566 "tslib": "^2.6.2" 500 567 }, 501 568 "engines": { ··· 503 570 } 504 571 }, 505 572 "node_modules/@aws-sdk/middleware-flexible-checksums": { 506 - "version": "3.946.0", 573 + "version": "3.956.0", 574 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.956.0.tgz", 575 + "integrity": "sha512-Rd/VeVKuw+lQ1oJmJOyXV0flIkp9ouMGAS9QT28ogdQVxXriaheNo754N4z0+8+R6uTcmeojN7dN4jzt51WV2g==", 507 576 "license": "Apache-2.0", 508 577 "dependencies": { 509 578 "@aws-crypto/crc32": "5.2.0", 510 579 "@aws-crypto/crc32c": "5.2.0", 511 580 "@aws-crypto/util": "5.2.0", 512 - "@aws-sdk/core": "3.946.0", 513 - "@aws-sdk/types": "3.936.0", 581 + "@aws-sdk/core": "3.956.0", 582 + "@aws-sdk/types": "3.956.0", 514 583 "@smithy/is-array-buffer": "^4.2.0", 515 - "@smithy/node-config-provider": "^4.3.5", 516 - "@smithy/protocol-http": "^5.3.5", 517 - "@smithy/types": "^4.9.0", 518 - "@smithy/util-middleware": "^4.2.5", 519 - "@smithy/util-stream": "^4.5.6", 584 + "@smithy/node-config-provider": "^4.3.7", 585 + "@smithy/protocol-http": "^5.3.7", 586 + "@smithy/types": "^4.11.0", 587 + "@smithy/util-middleware": "^4.2.7", 588 + "@smithy/util-stream": "^4.5.8", 520 589 "@smithy/util-utf8": "^4.2.0", 521 590 "tslib": "^2.6.2" 522 591 }, ··· 525 594 } 526 595 }, 527 596 "node_modules/@aws-sdk/middleware-host-header": { 528 - "version": "3.936.0", 597 + "version": "3.956.0", 598 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.956.0.tgz", 599 + "integrity": "sha512-JujNJDp/dj1DbsI0ntzhrz2uJ4jpumcKtr743eMpEhdboYjuu/UzY8/7n1h5JbgU9TNXgqE9lgQNa5QPG0Tvsg==", 529 600 "license": "Apache-2.0", 530 601 "dependencies": { 531 - "@aws-sdk/types": "3.936.0", 532 - "@smithy/protocol-http": "^5.3.5", 533 - "@smithy/types": "^4.9.0", 602 + "@aws-sdk/types": "3.956.0", 603 + "@smithy/protocol-http": "^5.3.7", 604 + "@smithy/types": "^4.11.0", 534 605 "tslib": "^2.6.2" 535 606 }, 536 607 "engines": { ··· 538 609 } 539 610 }, 540 611 "node_modules/@aws-sdk/middleware-location-constraint": { 541 - "version": "3.936.0", 612 + "version": "3.956.0", 613 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.956.0.tgz", 614 + "integrity": "sha512-eANhYRFcVO/lI9tliitSW0DK5H1d9J7BK/9RrRz86bd5zPWteVqqzQRbMUdErVi1nwSbSIAa6YGv/ItYPswe0w==", 542 615 "license": "Apache-2.0", 543 616 "dependencies": { 544 - "@aws-sdk/types": "3.936.0", 545 - "@smithy/types": "^4.9.0", 617 + "@aws-sdk/types": "3.956.0", 618 + "@smithy/types": "^4.11.0", 546 619 "tslib": "^2.6.2" 547 620 }, 548 621 "engines": { ··· 550 623 } 551 624 }, 552 625 "node_modules/@aws-sdk/middleware-logger": { 553 - "version": "3.936.0", 626 + "version": "3.956.0", 627 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.956.0.tgz", 628 + "integrity": "sha512-Qff39yEOPYgRsm4SrkHOvS0nSoxXILYnC8Akp0uMRi2lOcZVyXL3WCWqIOtI830qVI4GPa796sleKguxx50RHg==", 554 629 "license": "Apache-2.0", 555 630 "dependencies": { 556 - "@aws-sdk/types": "3.936.0", 557 - "@smithy/types": "^4.9.0", 631 + "@aws-sdk/types": "3.956.0", 632 + "@smithy/types": "^4.11.0", 558 633 "tslib": "^2.6.2" 559 634 }, 560 635 "engines": { ··· 562 637 } 563 638 }, 564 639 "node_modules/@aws-sdk/middleware-recursion-detection": { 565 - "version": "3.936.0", 640 + "version": "3.956.0", 641 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.956.0.tgz", 642 + "integrity": "sha512-/f4JxL2kSCYhy63wovqts6SJkpalSLvuFe78ozt3ClrGoHGyr69o7tPRYx5U7azLgvrIGjsWUyTayeAk3YHIVQ==", 566 643 "license": "Apache-2.0", 567 644 "dependencies": { 568 - "@aws-sdk/types": "3.936.0", 569 - "@aws/lambda-invoke-store": "^0.2.0", 570 - "@smithy/protocol-http": "^5.3.5", 571 - "@smithy/types": "^4.9.0", 645 + "@aws-sdk/types": "3.956.0", 646 + "@aws/lambda-invoke-store": "^0.2.2", 647 + "@smithy/protocol-http": "^5.3.7", 648 + "@smithy/types": "^4.11.0", 572 649 "tslib": "^2.6.2" 573 650 }, 574 651 "engines": { ··· 576 653 } 577 654 }, 578 655 "node_modules/@aws-sdk/middleware-sdk-s3": { 579 - "version": "3.946.0", 656 + "version": "3.956.0", 657 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.956.0.tgz", 658 + "integrity": "sha512-U/+jYb4iowqqpLjB6cSYan0goAMOlh2xg2CPIdSy550o8mYnJtuajBUQ20A9AA9PYKLlEAoCNEysNZkn4o/63g==", 580 659 "license": "Apache-2.0", 581 660 "dependencies": { 582 - "@aws-sdk/core": "3.946.0", 583 - "@aws-sdk/types": "3.936.0", 584 - "@aws-sdk/util-arn-parser": "3.893.0", 585 - "@smithy/core": "^3.18.7", 586 - "@smithy/node-config-provider": "^4.3.5", 587 - "@smithy/protocol-http": "^5.3.5", 588 - "@smithy/signature-v4": "^5.3.5", 589 - "@smithy/smithy-client": "^4.9.10", 590 - "@smithy/types": "^4.9.0", 661 + "@aws-sdk/core": "3.956.0", 662 + "@aws-sdk/types": "3.956.0", 663 + "@aws-sdk/util-arn-parser": "3.953.0", 664 + "@smithy/core": "^3.20.0", 665 + "@smithy/node-config-provider": "^4.3.7", 666 + "@smithy/protocol-http": "^5.3.7", 667 + "@smithy/signature-v4": "^5.3.7", 668 + "@smithy/smithy-client": "^4.10.2", 669 + "@smithy/types": "^4.11.0", 591 670 "@smithy/util-config-provider": "^4.2.0", 592 - "@smithy/util-middleware": "^4.2.5", 593 - "@smithy/util-stream": "^4.5.6", 671 + "@smithy/util-middleware": "^4.2.7", 672 + "@smithy/util-stream": "^4.5.8", 594 673 "@smithy/util-utf8": "^4.2.0", 595 674 "tslib": "^2.6.2" 596 675 }, ··· 599 678 } 600 679 }, 601 680 "node_modules/@aws-sdk/middleware-ssec": { 602 - "version": "3.936.0", 681 + "version": "3.956.0", 682 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.956.0.tgz", 683 + "integrity": "sha512-1Et0vPoIzfhkUAdNRzu0pC25ZawFqXo5T8xpvbwkfDgfIkeVj+sm9t01iXO3pCOK52OSuLRAy7fiAo/AoHjOYg==", 603 684 "license": "Apache-2.0", 604 685 "dependencies": { 605 - "@aws-sdk/types": "3.936.0", 606 - "@smithy/types": "^4.9.0", 686 + "@aws-sdk/types": "3.956.0", 687 + "@smithy/types": "^4.11.0", 607 688 "tslib": "^2.6.2" 608 689 }, 609 690 "engines": { ··· 611 692 } 612 693 }, 613 694 "node_modules/@aws-sdk/middleware-user-agent": { 614 - "version": "3.946.0", 695 + "version": "3.956.0", 696 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.956.0.tgz", 697 + "integrity": "sha512-azH8OJ0AIe3NafaTNvJorG/ALaLNTYwVKtyaSeQKOvaL8TNuBVuDnM5iHCiWryIaRgZotomqycwyfNKLw2D3JQ==", 615 698 "license": "Apache-2.0", 616 699 "dependencies": { 617 - "@aws-sdk/core": "3.946.0", 618 - "@aws-sdk/types": "3.936.0", 619 - "@aws-sdk/util-endpoints": "3.936.0", 620 - "@smithy/core": "^3.18.7", 621 - "@smithy/protocol-http": "^5.3.5", 622 - "@smithy/types": "^4.9.0", 700 + "@aws-sdk/core": "3.956.0", 701 + "@aws-sdk/types": "3.956.0", 702 + "@aws-sdk/util-endpoints": "3.956.0", 703 + "@smithy/core": "^3.20.0", 704 + "@smithy/protocol-http": "^5.3.7", 705 + "@smithy/types": "^4.11.0", 623 706 "tslib": "^2.6.2" 624 707 }, 625 708 "engines": { ··· 627 710 } 628 711 }, 629 712 "node_modules/@aws-sdk/nested-clients": { 630 - "version": "3.946.0", 713 + "version": "3.956.0", 714 + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.956.0.tgz", 715 + "integrity": "sha512-GHDQMkxoWpi3eTrhWGmghw0gsZJ5rM1ERHfBFhlhduCdtV3TyhKVmDgFG84KhU8v18dcVpSp3Pu3KwH7j1tgIg==", 631 716 "license": "Apache-2.0", 632 717 "dependencies": { 633 718 "@aws-crypto/sha256-browser": "5.2.0", 634 719 "@aws-crypto/sha256-js": "5.2.0", 635 - "@aws-sdk/core": "3.946.0", 636 - "@aws-sdk/middleware-host-header": "3.936.0", 637 - "@aws-sdk/middleware-logger": "3.936.0", 638 - "@aws-sdk/middleware-recursion-detection": "3.936.0", 639 - "@aws-sdk/middleware-user-agent": "3.946.0", 640 - "@aws-sdk/region-config-resolver": "3.936.0", 641 - "@aws-sdk/types": "3.936.0", 642 - "@aws-sdk/util-endpoints": "3.936.0", 643 - "@aws-sdk/util-user-agent-browser": "3.936.0", 644 - "@aws-sdk/util-user-agent-node": "3.946.0", 645 - "@smithy/config-resolver": "^4.4.3", 646 - "@smithy/core": "^3.18.7", 647 - "@smithy/fetch-http-handler": "^5.3.6", 648 - "@smithy/hash-node": "^4.2.5", 649 - "@smithy/invalid-dependency": "^4.2.5", 650 - "@smithy/middleware-content-length": "^4.2.5", 651 - "@smithy/middleware-endpoint": "^4.3.14", 652 - "@smithy/middleware-retry": "^4.4.14", 653 - "@smithy/middleware-serde": "^4.2.6", 654 - "@smithy/middleware-stack": "^4.2.5", 655 - "@smithy/node-config-provider": "^4.3.5", 656 - "@smithy/node-http-handler": "^4.4.5", 657 - "@smithy/protocol-http": "^5.3.5", 658 - "@smithy/smithy-client": "^4.9.10", 659 - "@smithy/types": "^4.9.0", 660 - "@smithy/url-parser": "^4.2.5", 720 + "@aws-sdk/core": "3.956.0", 721 + "@aws-sdk/middleware-host-header": "3.956.0", 722 + "@aws-sdk/middleware-logger": "3.956.0", 723 + "@aws-sdk/middleware-recursion-detection": "3.956.0", 724 + "@aws-sdk/middleware-user-agent": "3.956.0", 725 + "@aws-sdk/region-config-resolver": "3.956.0", 726 + "@aws-sdk/types": "3.956.0", 727 + "@aws-sdk/util-endpoints": "3.956.0", 728 + "@aws-sdk/util-user-agent-browser": "3.956.0", 729 + "@aws-sdk/util-user-agent-node": "3.956.0", 730 + "@smithy/config-resolver": "^4.4.5", 731 + "@smithy/core": "^3.20.0", 732 + "@smithy/fetch-http-handler": "^5.3.8", 733 + "@smithy/hash-node": "^4.2.7", 734 + "@smithy/invalid-dependency": "^4.2.7", 735 + "@smithy/middleware-content-length": "^4.2.7", 736 + "@smithy/middleware-endpoint": "^4.4.1", 737 + "@smithy/middleware-retry": "^4.4.17", 738 + "@smithy/middleware-serde": "^4.2.8", 739 + "@smithy/middleware-stack": "^4.2.7", 740 + "@smithy/node-config-provider": "^4.3.7", 741 + "@smithy/node-http-handler": "^4.4.7", 742 + "@smithy/protocol-http": "^5.3.7", 743 + "@smithy/smithy-client": "^4.10.2", 744 + "@smithy/types": "^4.11.0", 745 + "@smithy/url-parser": "^4.2.7", 661 746 "@smithy/util-base64": "^4.3.0", 662 747 "@smithy/util-body-length-browser": "^4.2.0", 663 748 "@smithy/util-body-length-node": "^4.2.1", 664 - "@smithy/util-defaults-mode-browser": "^4.3.13", 665 - "@smithy/util-defaults-mode-node": "^4.2.16", 666 - "@smithy/util-endpoints": "^3.2.5", 667 - "@smithy/util-middleware": "^4.2.5", 668 - "@smithy/util-retry": "^4.2.5", 749 + "@smithy/util-defaults-mode-browser": "^4.3.16", 750 + "@smithy/util-defaults-mode-node": "^4.2.19", 751 + "@smithy/util-endpoints": "^3.2.7", 752 + "@smithy/util-middleware": "^4.2.7", 753 + "@smithy/util-retry": "^4.2.7", 669 754 "@smithy/util-utf8": "^4.2.0", 670 755 "tslib": "^2.6.2" 671 756 }, ··· 674 759 } 675 760 }, 676 761 "node_modules/@aws-sdk/region-config-resolver": { 677 - "version": "3.936.0", 762 + "version": "3.956.0", 763 + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.956.0.tgz", 764 + "integrity": "sha512-byU5XYekW7+rZ3e067y038wlrpnPkdI4fMxcHCHrv+TAfzl8CCk5xLyzerQtXZR8cVPVOXuaYWe1zKW0uCnXUA==", 678 765 "license": "Apache-2.0", 679 766 "dependencies": { 680 - "@aws-sdk/types": "3.936.0", 681 - "@smithy/config-resolver": "^4.4.3", 682 - "@smithy/node-config-provider": "^4.3.5", 683 - "@smithy/types": "^4.9.0", 767 + "@aws-sdk/types": "3.956.0", 768 + "@smithy/config-resolver": "^4.4.5", 769 + "@smithy/node-config-provider": "^4.3.7", 770 + "@smithy/types": "^4.11.0", 684 771 "tslib": "^2.6.2" 685 772 }, 686 773 "engines": { ··· 688 775 } 689 776 }, 690 777 "node_modules/@aws-sdk/signature-v4-multi-region": { 691 - "version": "3.946.0", 778 + "version": "3.956.0", 779 + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.956.0.tgz", 780 + "integrity": "sha512-gejlXPmor08VydGC8bx0Bv4/tPT92eK0WLe2pUPR0AaMXL+5ycDpThAi1vLWjWr0aUjCA7lXx0pMENWlJlYK3A==", 692 781 "license": "Apache-2.0", 693 782 "dependencies": { 694 - "@aws-sdk/middleware-sdk-s3": "3.946.0", 695 - "@aws-sdk/types": "3.936.0", 696 - "@smithy/protocol-http": "^5.3.5", 697 - "@smithy/signature-v4": "^5.3.5", 698 - "@smithy/types": "^4.9.0", 783 + "@aws-sdk/middleware-sdk-s3": "3.956.0", 784 + "@aws-sdk/types": "3.956.0", 785 + "@smithy/protocol-http": "^5.3.7", 786 + "@smithy/signature-v4": "^5.3.7", 787 + "@smithy/types": "^4.11.0", 699 788 "tslib": "^2.6.2" 700 789 }, 701 790 "engines": { ··· 703 792 } 704 793 }, 705 794 "node_modules/@aws-sdk/token-providers": { 706 - "version": "3.946.0", 795 + "version": "3.956.0", 796 + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.956.0.tgz", 797 + "integrity": "sha512-I01Q9yDeG9oXge14u/bubtSdBpok/rTsPp2AQwy5xj/5PatRTHPbUTP6tef3AH/lFCAqkI0nncIcgx6zikDdUQ==", 707 798 "license": "Apache-2.0", 708 799 "dependencies": { 709 - "@aws-sdk/core": "3.946.0", 710 - "@aws-sdk/nested-clients": "3.946.0", 711 - "@aws-sdk/types": "3.936.0", 712 - "@smithy/property-provider": "^4.2.5", 713 - "@smithy/shared-ini-file-loader": "^4.4.0", 714 - "@smithy/types": "^4.9.0", 800 + "@aws-sdk/core": "3.956.0", 801 + "@aws-sdk/nested-clients": "3.956.0", 802 + "@aws-sdk/types": "3.956.0", 803 + "@smithy/property-provider": "^4.2.7", 804 + "@smithy/shared-ini-file-loader": "^4.4.2", 805 + "@smithy/types": "^4.11.0", 715 806 "tslib": "^2.6.2" 716 807 }, 717 808 "engines": { ··· 719 810 } 720 811 }, 721 812 "node_modules/@aws-sdk/types": { 722 - "version": "3.936.0", 813 + "version": "3.956.0", 814 + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.956.0.tgz", 815 + "integrity": "sha512-DMRU/p9wAlAJxEjegnLwduCA8YP2pcT/sIJ+17KSF38c5cC6CbBhykwbZLECTo+zYzoFrOqeLbqE6paH8Gx3ug==", 723 816 "license": "Apache-2.0", 724 817 "dependencies": { 725 - "@smithy/types": "^4.9.0", 818 + "@smithy/types": "^4.11.0", 726 819 "tslib": "^2.6.2" 727 820 }, 728 821 "engines": { ··· 730 823 } 731 824 }, 732 825 "node_modules/@aws-sdk/util-arn-parser": { 733 - "version": "3.893.0", 826 + "version": "3.953.0", 827 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.953.0.tgz", 828 + "integrity": "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ==", 734 829 "license": "Apache-2.0", 735 830 "dependencies": { 736 831 "tslib": "^2.6.2" ··· 740 835 } 741 836 }, 742 837 "node_modules/@aws-sdk/util-endpoints": { 743 - "version": "3.936.0", 838 + "version": "3.956.0", 839 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.956.0.tgz", 840 + "integrity": "sha512-xZ5CBoubS4rs9JkFniKNShDtfqxaMUnwaebYMoybZm070q9+omFkQkJYXl7kopTViEgZgQl1sAsAkrawBM8qEQ==", 744 841 "license": "Apache-2.0", 745 842 "dependencies": { 746 - "@aws-sdk/types": "3.936.0", 747 - "@smithy/types": "^4.9.0", 748 - "@smithy/url-parser": "^4.2.5", 749 - "@smithy/util-endpoints": "^3.2.5", 843 + "@aws-sdk/types": "3.956.0", 844 + "@smithy/types": "^4.11.0", 845 + "@smithy/url-parser": "^4.2.7", 846 + "@smithy/util-endpoints": "^3.2.7", 750 847 "tslib": "^2.6.2" 751 848 }, 752 849 "engines": { ··· 764 861 } 765 862 }, 766 863 "node_modules/@aws-sdk/util-user-agent-browser": { 767 - "version": "3.936.0", 864 + "version": "3.956.0", 865 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.956.0.tgz", 866 + "integrity": "sha512-s8KwYR3HqiGNni7a1DN2P3RUog64QoBQ6VCSzJkHBWb6++8KSOpqeeDkfmEz+22y1LOne+bRrpDGKa0aqOc3rQ==", 768 867 "license": "Apache-2.0", 769 868 "dependencies": { 770 - "@aws-sdk/types": "3.936.0", 771 - "@smithy/types": "^4.9.0", 869 + "@aws-sdk/types": "3.956.0", 870 + "@smithy/types": "^4.11.0", 772 871 "bowser": "^2.11.0", 773 872 "tslib": "^2.6.2" 774 873 } 775 874 }, 776 875 "node_modules/@aws-sdk/util-user-agent-node": { 777 - "version": "3.946.0", 876 + "version": "3.956.0", 877 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.956.0.tgz", 878 + "integrity": "sha512-H0r6ol3Rr63/3xvrUsLqHps+cA7VkM7uCU5NtuTHnMbv3uYYTKf9M2XFHAdVewmmRgssTzvqemrARc8Ji3SNvg==", 778 879 "license": "Apache-2.0", 779 880 "dependencies": { 780 - "@aws-sdk/middleware-user-agent": "3.946.0", 781 - "@aws-sdk/types": "3.936.0", 782 - "@smithy/node-config-provider": "^4.3.5", 783 - "@smithy/types": "^4.9.0", 881 + "@aws-sdk/middleware-user-agent": "3.956.0", 882 + "@aws-sdk/types": "3.956.0", 883 + "@smithy/node-config-provider": "^4.3.7", 884 + "@smithy/types": "^4.11.0", 784 885 "tslib": "^2.6.2" 785 886 }, 786 887 "engines": { ··· 796 897 } 797 898 }, 798 899 "node_modules/@aws-sdk/xml-builder": { 799 - "version": "3.930.0", 900 + "version": "3.956.0", 901 + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.956.0.tgz", 902 + "integrity": "sha512-x/IvXUeQYNUEQojpRIQpFt4X7XGxqzjUlXFRdwaTCtTz3q1droXVJvYOhnX3KiMgzeHGlBJfY4Nmq3oZNEUGFw==", 800 903 "license": "Apache-2.0", 801 904 "dependencies": { 802 - "@smithy/types": "^4.9.0", 905 + "@smithy/types": "^4.11.0", 803 906 "fast-xml-parser": "5.2.5", 804 907 "tslib": "^2.6.2" 805 908 }, ··· 809 912 }, 810 913 "node_modules/@aws/lambda-invoke-store": { 811 914 "version": "0.2.2", 915 + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.2.tgz", 916 + "integrity": "sha512-C0NBLsIqzDIae8HFw9YIrIBsbc0xTiOtt7fAukGPnqQ/+zZNaq+4jhuccltK0QuWHBnNm/a6kLIRA6GFiM10eg==", 812 917 "license": "Apache-2.0", 813 918 "engines": { 814 919 "node": ">=18.0.0" ··· 1446 1551 "dev": true, 1447 1552 "license": "MIT" 1448 1553 }, 1554 + "node_modules/@pkgr/core": { 1555 + "version": "0.2.9", 1556 + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", 1557 + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", 1558 + "dev": true, 1559 + "license": "MIT", 1560 + "engines": { 1561 + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" 1562 + }, 1563 + "funding": { 1564 + "url": "https://opencollective.com/pkgr" 1565 + } 1566 + }, 1449 1567 "node_modules/@rollup/rollup-android-arm-eabi": { 1450 1568 "version": "4.53.3", 1451 1569 "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", ··· 1753 1871 ] 1754 1872 }, 1755 1873 "node_modules/@smithy/abort-controller": { 1756 - "version": "4.2.5", 1874 + "version": "4.2.7", 1875 + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", 1876 + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", 1757 1877 "license": "Apache-2.0", 1758 1878 "dependencies": { 1759 - "@smithy/types": "^4.9.0", 1879 + "@smithy/types": "^4.11.0", 1760 1880 "tslib": "^2.6.2" 1761 1881 }, 1762 1882 "engines": { ··· 1765 1885 }, 1766 1886 "node_modules/@smithy/chunked-blob-reader": { 1767 1887 "version": "5.2.0", 1888 + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", 1889 + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", 1768 1890 "license": "Apache-2.0", 1769 1891 "dependencies": { 1770 1892 "tslib": "^2.6.2" ··· 1775 1897 }, 1776 1898 "node_modules/@smithy/chunked-blob-reader-native": { 1777 1899 "version": "4.2.1", 1900 + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", 1901 + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", 1778 1902 "license": "Apache-2.0", 1779 1903 "dependencies": { 1780 1904 "@smithy/util-base64": "^4.3.0", ··· 1785 1909 } 1786 1910 }, 1787 1911 "node_modules/@smithy/config-resolver": { 1788 - "version": "4.4.3", 1912 + "version": "4.4.5", 1913 + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", 1914 + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", 1789 1915 "license": "Apache-2.0", 1790 1916 "dependencies": { 1791 - "@smithy/node-config-provider": "^4.3.5", 1792 - "@smithy/types": "^4.9.0", 1917 + "@smithy/node-config-provider": "^4.3.7", 1918 + "@smithy/types": "^4.11.0", 1793 1919 "@smithy/util-config-provider": "^4.2.0", 1794 - "@smithy/util-endpoints": "^3.2.5", 1795 - "@smithy/util-middleware": "^4.2.5", 1920 + "@smithy/util-endpoints": "^3.2.7", 1921 + "@smithy/util-middleware": "^4.2.7", 1796 1922 "tslib": "^2.6.2" 1797 1923 }, 1798 1924 "engines": { ··· 1800 1926 } 1801 1927 }, 1802 1928 "node_modules/@smithy/core": { 1803 - "version": "3.18.7", 1929 + "version": "3.20.0", 1930 + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", 1931 + "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", 1804 1932 "license": "Apache-2.0", 1805 1933 "dependencies": { 1806 - "@smithy/middleware-serde": "^4.2.6", 1807 - "@smithy/protocol-http": "^5.3.5", 1808 - "@smithy/types": "^4.9.0", 1934 + "@smithy/middleware-serde": "^4.2.8", 1935 + "@smithy/protocol-http": "^5.3.7", 1936 + "@smithy/types": "^4.11.0", 1809 1937 "@smithy/util-base64": "^4.3.0", 1810 1938 "@smithy/util-body-length-browser": "^4.2.0", 1811 - "@smithy/util-middleware": "^4.2.5", 1812 - "@smithy/util-stream": "^4.5.6", 1939 + "@smithy/util-middleware": "^4.2.7", 1940 + "@smithy/util-stream": "^4.5.8", 1813 1941 "@smithy/util-utf8": "^4.2.0", 1814 1942 "@smithy/uuid": "^1.1.0", 1815 1943 "tslib": "^2.6.2" ··· 1819 1947 } 1820 1948 }, 1821 1949 "node_modules/@smithy/credential-provider-imds": { 1822 - "version": "4.2.5", 1950 + "version": "4.2.7", 1951 + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", 1952 + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", 1823 1953 "license": "Apache-2.0", 1824 1954 "dependencies": { 1825 - "@smithy/node-config-provider": "^4.3.5", 1826 - "@smithy/property-provider": "^4.2.5", 1827 - "@smithy/types": "^4.9.0", 1828 - "@smithy/url-parser": "^4.2.5", 1955 + "@smithy/node-config-provider": "^4.3.7", 1956 + "@smithy/property-provider": "^4.2.7", 1957 + "@smithy/types": "^4.11.0", 1958 + "@smithy/url-parser": "^4.2.7", 1829 1959 "tslib": "^2.6.2" 1830 1960 }, 1831 1961 "engines": { ··· 1833 1963 } 1834 1964 }, 1835 1965 "node_modules/@smithy/eventstream-codec": { 1836 - "version": "4.2.5", 1966 + "version": "4.2.7", 1967 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.7.tgz", 1968 + "integrity": "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ==", 1837 1969 "license": "Apache-2.0", 1838 1970 "dependencies": { 1839 1971 "@aws-crypto/crc32": "5.2.0", 1840 - "@smithy/types": "^4.9.0", 1972 + "@smithy/types": "^4.11.0", 1841 1973 "@smithy/util-hex-encoding": "^4.2.0", 1842 1974 "tslib": "^2.6.2" 1843 1975 }, ··· 1846 1978 } 1847 1979 }, 1848 1980 "node_modules/@smithy/eventstream-serde-browser": { 1849 - "version": "4.2.5", 1981 + "version": "4.2.7", 1982 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.7.tgz", 1983 + "integrity": "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g==", 1850 1984 "license": "Apache-2.0", 1851 1985 "dependencies": { 1852 - "@smithy/eventstream-serde-universal": "^4.2.5", 1853 - "@smithy/types": "^4.9.0", 1986 + "@smithy/eventstream-serde-universal": "^4.2.7", 1987 + "@smithy/types": "^4.11.0", 1854 1988 "tslib": "^2.6.2" 1855 1989 }, 1856 1990 "engines": { ··· 1858 1992 } 1859 1993 }, 1860 1994 "node_modules/@smithy/eventstream-serde-config-resolver": { 1861 - "version": "4.3.5", 1995 + "version": "4.3.7", 1996 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.7.tgz", 1997 + "integrity": "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ==", 1862 1998 "license": "Apache-2.0", 1863 1999 "dependencies": { 1864 - "@smithy/types": "^4.9.0", 2000 + "@smithy/types": "^4.11.0", 1865 2001 "tslib": "^2.6.2" 1866 2002 }, 1867 2003 "engines": { ··· 1869 2005 } 1870 2006 }, 1871 2007 "node_modules/@smithy/eventstream-serde-node": { 1872 - "version": "4.2.5", 2008 + "version": "4.2.7", 2009 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.7.tgz", 2010 + "integrity": "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A==", 1873 2011 "license": "Apache-2.0", 1874 2012 "dependencies": { 1875 - "@smithy/eventstream-serde-universal": "^4.2.5", 1876 - "@smithy/types": "^4.9.0", 2013 + "@smithy/eventstream-serde-universal": "^4.2.7", 2014 + "@smithy/types": "^4.11.0", 1877 2015 "tslib": "^2.6.2" 1878 2016 }, 1879 2017 "engines": { ··· 1881 2019 } 1882 2020 }, 1883 2021 "node_modules/@smithy/eventstream-serde-universal": { 1884 - "version": "4.2.5", 2022 + "version": "4.2.7", 2023 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.7.tgz", 2024 + "integrity": "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g==", 1885 2025 "license": "Apache-2.0", 1886 2026 "dependencies": { 1887 - "@smithy/eventstream-codec": "^4.2.5", 1888 - "@smithy/types": "^4.9.0", 2027 + "@smithy/eventstream-codec": "^4.2.7", 2028 + "@smithy/types": "^4.11.0", 1889 2029 "tslib": "^2.6.2" 1890 2030 }, 1891 2031 "engines": { ··· 1893 2033 } 1894 2034 }, 1895 2035 "node_modules/@smithy/fetch-http-handler": { 1896 - "version": "5.3.6", 2036 + "version": "5.3.8", 2037 + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", 2038 + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", 1897 2039 "license": "Apache-2.0", 1898 2040 "dependencies": { 1899 - "@smithy/protocol-http": "^5.3.5", 1900 - "@smithy/querystring-builder": "^4.2.5", 1901 - "@smithy/types": "^4.9.0", 2041 + "@smithy/protocol-http": "^5.3.7", 2042 + "@smithy/querystring-builder": "^4.2.7", 2043 + "@smithy/types": "^4.11.0", 1902 2044 "@smithy/util-base64": "^4.3.0", 1903 2045 "tslib": "^2.6.2" 1904 2046 }, ··· 1907 2049 } 1908 2050 }, 1909 2051 "node_modules/@smithy/hash-blob-browser": { 1910 - "version": "4.2.6", 2052 + "version": "4.2.8", 2053 + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.8.tgz", 2054 + "integrity": "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw==", 1911 2055 "license": "Apache-2.0", 1912 2056 "dependencies": { 1913 2057 "@smithy/chunked-blob-reader": "^5.2.0", 1914 2058 "@smithy/chunked-blob-reader-native": "^4.2.1", 1915 - "@smithy/types": "^4.9.0", 2059 + "@smithy/types": "^4.11.0", 1916 2060 "tslib": "^2.6.2" 1917 2061 }, 1918 2062 "engines": { ··· 1920 2064 } 1921 2065 }, 1922 2066 "node_modules/@smithy/hash-node": { 1923 - "version": "4.2.5", 2067 + "version": "4.2.7", 2068 + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", 2069 + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", 1924 2070 "license": "Apache-2.0", 1925 2071 "dependencies": { 1926 - "@smithy/types": "^4.9.0", 2072 + "@smithy/types": "^4.11.0", 1927 2073 "@smithy/util-buffer-from": "^4.2.0", 1928 2074 "@smithy/util-utf8": "^4.2.0", 1929 2075 "tslib": "^2.6.2" ··· 1933 2079 } 1934 2080 }, 1935 2081 "node_modules/@smithy/hash-stream-node": { 1936 - "version": "4.2.5", 2082 + "version": "4.2.7", 2083 + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.7.tgz", 2084 + "integrity": "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ==", 1937 2085 "license": "Apache-2.0", 1938 2086 "dependencies": { 1939 - "@smithy/types": "^4.9.0", 2087 + "@smithy/types": "^4.11.0", 1940 2088 "@smithy/util-utf8": "^4.2.0", 1941 2089 "tslib": "^2.6.2" 1942 2090 }, ··· 1945 2093 } 1946 2094 }, 1947 2095 "node_modules/@smithy/invalid-dependency": { 1948 - "version": "4.2.5", 2096 + "version": "4.2.7", 2097 + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", 2098 + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", 1949 2099 "license": "Apache-2.0", 1950 2100 "dependencies": { 1951 - "@smithy/types": "^4.9.0", 2101 + "@smithy/types": "^4.11.0", 1952 2102 "tslib": "^2.6.2" 1953 2103 }, 1954 2104 "engines": { ··· 1957 2107 }, 1958 2108 "node_modules/@smithy/is-array-buffer": { 1959 2109 "version": "4.2.0", 2110 + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", 2111 + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", 1960 2112 "license": "Apache-2.0", 1961 2113 "dependencies": { 1962 2114 "tslib": "^2.6.2" ··· 1966 2118 } 1967 2119 }, 1968 2120 "node_modules/@smithy/md5-js": { 1969 - "version": "4.2.5", 2121 + "version": "4.2.7", 2122 + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.7.tgz", 2123 + "integrity": "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw==", 1970 2124 "license": "Apache-2.0", 1971 2125 "dependencies": { 1972 - "@smithy/types": "^4.9.0", 2126 + "@smithy/types": "^4.11.0", 1973 2127 "@smithy/util-utf8": "^4.2.0", 1974 2128 "tslib": "^2.6.2" 1975 2129 }, ··· 1978 2132 } 1979 2133 }, 1980 2134 "node_modules/@smithy/middleware-content-length": { 1981 - "version": "4.2.5", 2135 + "version": "4.2.7", 2136 + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", 2137 + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", 1982 2138 "license": "Apache-2.0", 1983 2139 "dependencies": { 1984 - "@smithy/protocol-http": "^5.3.5", 1985 - "@smithy/types": "^4.9.0", 2140 + "@smithy/protocol-http": "^5.3.7", 2141 + "@smithy/types": "^4.11.0", 1986 2142 "tslib": "^2.6.2" 1987 2143 }, 1988 2144 "engines": { ··· 1990 2146 } 1991 2147 }, 1992 2148 "node_modules/@smithy/middleware-endpoint": { 1993 - "version": "4.3.14", 2149 + "version": "4.4.1", 2150 + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", 2151 + "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", 1994 2152 "license": "Apache-2.0", 1995 2153 "dependencies": { 1996 - "@smithy/core": "^3.18.7", 1997 - "@smithy/middleware-serde": "^4.2.6", 1998 - "@smithy/node-config-provider": "^4.3.5", 1999 - "@smithy/shared-ini-file-loader": "^4.4.0", 2000 - "@smithy/types": "^4.9.0", 2001 - "@smithy/url-parser": "^4.2.5", 2002 - "@smithy/util-middleware": "^4.2.5", 2154 + "@smithy/core": "^3.20.0", 2155 + "@smithy/middleware-serde": "^4.2.8", 2156 + "@smithy/node-config-provider": "^4.3.7", 2157 + "@smithy/shared-ini-file-loader": "^4.4.2", 2158 + "@smithy/types": "^4.11.0", 2159 + "@smithy/url-parser": "^4.2.7", 2160 + "@smithy/util-middleware": "^4.2.7", 2003 2161 "tslib": "^2.6.2" 2004 2162 }, 2005 2163 "engines": { ··· 2007 2165 } 2008 2166 }, 2009 2167 "node_modules/@smithy/middleware-retry": { 2010 - "version": "4.4.14", 2168 + "version": "4.4.17", 2169 + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", 2170 + "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", 2011 2171 "license": "Apache-2.0", 2012 2172 "dependencies": { 2013 - "@smithy/node-config-provider": "^4.3.5", 2014 - "@smithy/protocol-http": "^5.3.5", 2015 - "@smithy/service-error-classification": "^4.2.5", 2016 - "@smithy/smithy-client": "^4.9.10", 2017 - "@smithy/types": "^4.9.0", 2018 - "@smithy/util-middleware": "^4.2.5", 2019 - "@smithy/util-retry": "^4.2.5", 2173 + "@smithy/node-config-provider": "^4.3.7", 2174 + "@smithy/protocol-http": "^5.3.7", 2175 + "@smithy/service-error-classification": "^4.2.7", 2176 + "@smithy/smithy-client": "^4.10.2", 2177 + "@smithy/types": "^4.11.0", 2178 + "@smithy/util-middleware": "^4.2.7", 2179 + "@smithy/util-retry": "^4.2.7", 2020 2180 "@smithy/uuid": "^1.1.0", 2021 2181 "tslib": "^2.6.2" 2022 2182 }, ··· 2025 2185 } 2026 2186 }, 2027 2187 "node_modules/@smithy/middleware-serde": { 2028 - "version": "4.2.6", 2188 + "version": "4.2.8", 2189 + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", 2190 + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", 2029 2191 "license": "Apache-2.0", 2030 2192 "dependencies": { 2031 - "@smithy/protocol-http": "^5.3.5", 2032 - "@smithy/types": "^4.9.0", 2193 + "@smithy/protocol-http": "^5.3.7", 2194 + "@smithy/types": "^4.11.0", 2033 2195 "tslib": "^2.6.2" 2034 2196 }, 2035 2197 "engines": { ··· 2037 2199 } 2038 2200 }, 2039 2201 "node_modules/@smithy/middleware-stack": { 2040 - "version": "4.2.5", 2202 + "version": "4.2.7", 2203 + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", 2204 + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", 2041 2205 "license": "Apache-2.0", 2042 2206 "dependencies": { 2043 - "@smithy/types": "^4.9.0", 2207 + "@smithy/types": "^4.11.0", 2044 2208 "tslib": "^2.6.2" 2045 2209 }, 2046 2210 "engines": { ··· 2048 2212 } 2049 2213 }, 2050 2214 "node_modules/@smithy/node-config-provider": { 2051 - "version": "4.3.5", 2215 + "version": "4.3.7", 2216 + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", 2217 + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", 2052 2218 "license": "Apache-2.0", 2053 2219 "dependencies": { 2054 - "@smithy/property-provider": "^4.2.5", 2055 - "@smithy/shared-ini-file-loader": "^4.4.0", 2056 - "@smithy/types": "^4.9.0", 2220 + "@smithy/property-provider": "^4.2.7", 2221 + "@smithy/shared-ini-file-loader": "^4.4.2", 2222 + "@smithy/types": "^4.11.0", 2057 2223 "tslib": "^2.6.2" 2058 2224 }, 2059 2225 "engines": { ··· 2061 2227 } 2062 2228 }, 2063 2229 "node_modules/@smithy/node-http-handler": { 2064 - "version": "4.4.5", 2230 + "version": "4.4.7", 2231 + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", 2232 + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", 2065 2233 "license": "Apache-2.0", 2066 2234 "dependencies": { 2067 - "@smithy/abort-controller": "^4.2.5", 2068 - "@smithy/protocol-http": "^5.3.5", 2069 - "@smithy/querystring-builder": "^4.2.5", 2070 - "@smithy/types": "^4.9.0", 2235 + "@smithy/abort-controller": "^4.2.7", 2236 + "@smithy/protocol-http": "^5.3.7", 2237 + "@smithy/querystring-builder": "^4.2.7", 2238 + "@smithy/types": "^4.11.0", 2071 2239 "tslib": "^2.6.2" 2072 2240 }, 2073 2241 "engines": { ··· 2075 2243 } 2076 2244 }, 2077 2245 "node_modules/@smithy/property-provider": { 2078 - "version": "4.2.5", 2246 + "version": "4.2.7", 2247 + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", 2248 + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", 2079 2249 "license": "Apache-2.0", 2080 2250 "dependencies": { 2081 - "@smithy/types": "^4.9.0", 2251 + "@smithy/types": "^4.11.0", 2082 2252 "tslib": "^2.6.2" 2083 2253 }, 2084 2254 "engines": { ··· 2086 2256 } 2087 2257 }, 2088 2258 "node_modules/@smithy/protocol-http": { 2089 - "version": "5.3.5", 2259 + "version": "5.3.7", 2260 + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", 2261 + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", 2090 2262 "license": "Apache-2.0", 2091 2263 "dependencies": { 2092 - "@smithy/types": "^4.9.0", 2264 + "@smithy/types": "^4.11.0", 2093 2265 "tslib": "^2.6.2" 2094 2266 }, 2095 2267 "engines": { ··· 2097 2269 } 2098 2270 }, 2099 2271 "node_modules/@smithy/querystring-builder": { 2100 - "version": "4.2.5", 2272 + "version": "4.2.7", 2273 + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", 2274 + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", 2101 2275 "license": "Apache-2.0", 2102 2276 "dependencies": { 2103 - "@smithy/types": "^4.9.0", 2277 + "@smithy/types": "^4.11.0", 2104 2278 "@smithy/util-uri-escape": "^4.2.0", 2105 2279 "tslib": "^2.6.2" 2106 2280 }, ··· 2109 2283 } 2110 2284 }, 2111 2285 "node_modules/@smithy/querystring-parser": { 2112 - "version": "4.2.5", 2286 + "version": "4.2.7", 2287 + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", 2288 + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", 2113 2289 "license": "Apache-2.0", 2114 2290 "dependencies": { 2115 - "@smithy/types": "^4.9.0", 2291 + "@smithy/types": "^4.11.0", 2116 2292 "tslib": "^2.6.2" 2117 2293 }, 2118 2294 "engines": { ··· 2120 2296 } 2121 2297 }, 2122 2298 "node_modules/@smithy/service-error-classification": { 2123 - "version": "4.2.5", 2299 + "version": "4.2.7", 2300 + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", 2301 + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", 2124 2302 "license": "Apache-2.0", 2125 2303 "dependencies": { 2126 - "@smithy/types": "^4.9.0" 2304 + "@smithy/types": "^4.11.0" 2127 2305 }, 2128 2306 "engines": { 2129 2307 "node": ">=18.0.0" 2130 2308 } 2131 2309 }, 2132 2310 "node_modules/@smithy/shared-ini-file-loader": { 2133 - "version": "4.4.0", 2311 + "version": "4.4.2", 2312 + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", 2313 + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", 2134 2314 "license": "Apache-2.0", 2135 2315 "dependencies": { 2136 - "@smithy/types": "^4.9.0", 2316 + "@smithy/types": "^4.11.0", 2137 2317 "tslib": "^2.6.2" 2138 2318 }, 2139 2319 "engines": { ··· 2141 2321 } 2142 2322 }, 2143 2323 "node_modules/@smithy/signature-v4": { 2144 - "version": "5.3.5", 2324 + "version": "5.3.7", 2325 + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", 2326 + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", 2145 2327 "license": "Apache-2.0", 2146 2328 "dependencies": { 2147 2329 "@smithy/is-array-buffer": "^4.2.0", 2148 - "@smithy/protocol-http": "^5.3.5", 2149 - "@smithy/types": "^4.9.0", 2330 + "@smithy/protocol-http": "^5.3.7", 2331 + "@smithy/types": "^4.11.0", 2150 2332 "@smithy/util-hex-encoding": "^4.2.0", 2151 - "@smithy/util-middleware": "^4.2.5", 2333 + "@smithy/util-middleware": "^4.2.7", 2152 2334 "@smithy/util-uri-escape": "^4.2.0", 2153 2335 "@smithy/util-utf8": "^4.2.0", 2154 2336 "tslib": "^2.6.2" ··· 2158 2340 } 2159 2341 }, 2160 2342 "node_modules/@smithy/smithy-client": { 2161 - "version": "4.9.10", 2343 + "version": "4.10.2", 2344 + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", 2345 + "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", 2162 2346 "license": "Apache-2.0", 2163 2347 "dependencies": { 2164 - "@smithy/core": "^3.18.7", 2165 - "@smithy/middleware-endpoint": "^4.3.14", 2166 - "@smithy/middleware-stack": "^4.2.5", 2167 - "@smithy/protocol-http": "^5.3.5", 2168 - "@smithy/types": "^4.9.0", 2169 - "@smithy/util-stream": "^4.5.6", 2348 + "@smithy/core": "^3.20.0", 2349 + "@smithy/middleware-endpoint": "^4.4.1", 2350 + "@smithy/middleware-stack": "^4.2.7", 2351 + "@smithy/protocol-http": "^5.3.7", 2352 + "@smithy/types": "^4.11.0", 2353 + "@smithy/util-stream": "^4.5.8", 2170 2354 "tslib": "^2.6.2" 2171 2355 }, 2172 2356 "engines": { ··· 2174 2358 } 2175 2359 }, 2176 2360 "node_modules/@smithy/types": { 2177 - "version": "4.9.0", 2361 + "version": "4.11.0", 2362 + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", 2363 + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", 2178 2364 "license": "Apache-2.0", 2179 2365 "dependencies": { 2180 2366 "tslib": "^2.6.2" ··· 2184 2370 } 2185 2371 }, 2186 2372 "node_modules/@smithy/url-parser": { 2187 - "version": "4.2.5", 2373 + "version": "4.2.7", 2374 + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", 2375 + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", 2188 2376 "license": "Apache-2.0", 2189 2377 "dependencies": { 2190 - "@smithy/querystring-parser": "^4.2.5", 2191 - "@smithy/types": "^4.9.0", 2378 + "@smithy/querystring-parser": "^4.2.7", 2379 + "@smithy/types": "^4.11.0", 2192 2380 "tslib": "^2.6.2" 2193 2381 }, 2194 2382 "engines": { ··· 2197 2385 }, 2198 2386 "node_modules/@smithy/util-base64": { 2199 2387 "version": "4.3.0", 2388 + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", 2389 + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", 2200 2390 "license": "Apache-2.0", 2201 2391 "dependencies": { 2202 2392 "@smithy/util-buffer-from": "^4.2.0", ··· 2209 2399 }, 2210 2400 "node_modules/@smithy/util-body-length-browser": { 2211 2401 "version": "4.2.0", 2402 + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", 2403 + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", 2212 2404 "license": "Apache-2.0", 2213 2405 "dependencies": { 2214 2406 "tslib": "^2.6.2" ··· 2219 2411 }, 2220 2412 "node_modules/@smithy/util-body-length-node": { 2221 2413 "version": "4.2.1", 2414 + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", 2415 + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", 2222 2416 "license": "Apache-2.0", 2223 2417 "dependencies": { 2224 2418 "tslib": "^2.6.2" ··· 2229 2423 }, 2230 2424 "node_modules/@smithy/util-buffer-from": { 2231 2425 "version": "4.2.0", 2426 + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", 2427 + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", 2232 2428 "license": "Apache-2.0", 2233 2429 "dependencies": { 2234 2430 "@smithy/is-array-buffer": "^4.2.0", ··· 2240 2436 }, 2241 2437 "node_modules/@smithy/util-config-provider": { 2242 2438 "version": "4.2.0", 2439 + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", 2440 + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", 2243 2441 "license": "Apache-2.0", 2244 2442 "dependencies": { 2245 2443 "tslib": "^2.6.2" ··· 2249 2447 } 2250 2448 }, 2251 2449 "node_modules/@smithy/util-defaults-mode-browser": { 2252 - "version": "4.3.13", 2450 + "version": "4.3.16", 2451 + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", 2452 + "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", 2253 2453 "license": "Apache-2.0", 2254 2454 "dependencies": { 2255 - "@smithy/property-provider": "^4.2.5", 2256 - "@smithy/smithy-client": "^4.9.10", 2257 - "@smithy/types": "^4.9.0", 2455 + "@smithy/property-provider": "^4.2.7", 2456 + "@smithy/smithy-client": "^4.10.2", 2457 + "@smithy/types": "^4.11.0", 2258 2458 "tslib": "^2.6.2" 2259 2459 }, 2260 2460 "engines": { ··· 2262 2462 } 2263 2463 }, 2264 2464 "node_modules/@smithy/util-defaults-mode-node": { 2265 - "version": "4.2.16", 2465 + "version": "4.2.19", 2466 + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", 2467 + "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", 2266 2468 "license": "Apache-2.0", 2267 2469 "dependencies": { 2268 - "@smithy/config-resolver": "^4.4.3", 2269 - "@smithy/credential-provider-imds": "^4.2.5", 2270 - "@smithy/node-config-provider": "^4.3.5", 2271 - "@smithy/property-provider": "^4.2.5", 2272 - "@smithy/smithy-client": "^4.9.10", 2273 - "@smithy/types": "^4.9.0", 2470 + "@smithy/config-resolver": "^4.4.5", 2471 + "@smithy/credential-provider-imds": "^4.2.7", 2472 + "@smithy/node-config-provider": "^4.3.7", 2473 + "@smithy/property-provider": "^4.2.7", 2474 + "@smithy/smithy-client": "^4.10.2", 2475 + "@smithy/types": "^4.11.0", 2274 2476 "tslib": "^2.6.2" 2275 2477 }, 2276 2478 "engines": { ··· 2278 2480 } 2279 2481 }, 2280 2482 "node_modules/@smithy/util-endpoints": { 2281 - "version": "3.2.5", 2483 + "version": "3.2.7", 2484 + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", 2485 + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", 2282 2486 "license": "Apache-2.0", 2283 2487 "dependencies": { 2284 - "@smithy/node-config-provider": "^4.3.5", 2285 - "@smithy/types": "^4.9.0", 2488 + "@smithy/node-config-provider": "^4.3.7", 2489 + "@smithy/types": "^4.11.0", 2286 2490 "tslib": "^2.6.2" 2287 2491 }, 2288 2492 "engines": { ··· 2291 2495 }, 2292 2496 "node_modules/@smithy/util-hex-encoding": { 2293 2497 "version": "4.2.0", 2498 + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", 2499 + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", 2294 2500 "license": "Apache-2.0", 2295 2501 "dependencies": { 2296 2502 "tslib": "^2.6.2" ··· 2300 2506 } 2301 2507 }, 2302 2508 "node_modules/@smithy/util-middleware": { 2303 - "version": "4.2.5", 2509 + "version": "4.2.7", 2510 + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", 2511 + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", 2304 2512 "license": "Apache-2.0", 2305 2513 "dependencies": { 2306 - "@smithy/types": "^4.9.0", 2514 + "@smithy/types": "^4.11.0", 2307 2515 "tslib": "^2.6.2" 2308 2516 }, 2309 2517 "engines": { ··· 2311 2519 } 2312 2520 }, 2313 2521 "node_modules/@smithy/util-retry": { 2314 - "version": "4.2.5", 2522 + "version": "4.2.7", 2523 + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", 2524 + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", 2315 2525 "license": "Apache-2.0", 2316 2526 "dependencies": { 2317 - "@smithy/service-error-classification": "^4.2.5", 2318 - "@smithy/types": "^4.9.0", 2527 + "@smithy/service-error-classification": "^4.2.7", 2528 + "@smithy/types": "^4.11.0", 2319 2529 "tslib": "^2.6.2" 2320 2530 }, 2321 2531 "engines": { ··· 2323 2533 } 2324 2534 }, 2325 2535 "node_modules/@smithy/util-stream": { 2326 - "version": "4.5.6", 2536 + "version": "4.5.8", 2537 + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", 2538 + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", 2327 2539 "license": "Apache-2.0", 2328 2540 "dependencies": { 2329 - "@smithy/fetch-http-handler": "^5.3.6", 2330 - "@smithy/node-http-handler": "^4.4.5", 2331 - "@smithy/types": "^4.9.0", 2541 + "@smithy/fetch-http-handler": "^5.3.8", 2542 + "@smithy/node-http-handler": "^4.4.7", 2543 + "@smithy/types": "^4.11.0", 2332 2544 "@smithy/util-base64": "^4.3.0", 2333 2545 "@smithy/util-buffer-from": "^4.2.0", 2334 2546 "@smithy/util-hex-encoding": "^4.2.0", ··· 2341 2553 }, 2342 2554 "node_modules/@smithy/util-uri-escape": { 2343 2555 "version": "4.2.0", 2556 + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", 2557 + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", 2344 2558 "license": "Apache-2.0", 2345 2559 "dependencies": { 2346 2560 "tslib": "^2.6.2" ··· 2351 2565 }, 2352 2566 "node_modules/@smithy/util-utf8": { 2353 2567 "version": "4.2.0", 2568 + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", 2569 + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", 2354 2570 "license": "Apache-2.0", 2355 2571 "dependencies": { 2356 2572 "@smithy/util-buffer-from": "^4.2.0", ··· 2361 2577 } 2362 2578 }, 2363 2579 "node_modules/@smithy/util-waiter": { 2364 - "version": "4.2.5", 2580 + "version": "4.2.7", 2581 + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.7.tgz", 2582 + "integrity": "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw==", 2365 2583 "license": "Apache-2.0", 2366 2584 "dependencies": { 2367 - "@smithy/abort-controller": "^4.2.5", 2368 - "@smithy/types": "^4.9.0", 2585 + "@smithy/abort-controller": "^4.2.7", 2586 + "@smithy/types": "^4.11.0", 2369 2587 "tslib": "^2.6.2" 2370 2588 }, 2371 2589 "engines": { ··· 2374 2592 }, 2375 2593 "node_modules/@smithy/uuid": { 2376 2594 "version": "1.1.0", 2595 + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", 2596 + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", 2377 2597 "license": "Apache-2.0", 2378 2598 "dependencies": { 2379 2599 "tslib": "^2.6.2" ··· 2434 2654 "version": "24.10.1", 2435 2655 "dev": true, 2436 2656 "license": "MIT", 2437 - "peer": true, 2438 2657 "dependencies": { 2439 2658 "undici-types": "~7.16.0" 2440 2659 } 2441 2660 }, 2442 2661 "node_modules/@typescript-eslint/eslint-plugin": { 2443 - "version": "8.49.0", 2662 + "version": "8.50.0", 2663 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", 2664 + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", 2444 2665 "dev": true, 2445 2666 "license": "MIT", 2446 2667 "dependencies": { 2447 2668 "@eslint-community/regexpp": "^4.10.0", 2448 - "@typescript-eslint/scope-manager": "8.49.0", 2449 - "@typescript-eslint/type-utils": "8.49.0", 2450 - "@typescript-eslint/utils": "8.49.0", 2451 - "@typescript-eslint/visitor-keys": "8.49.0", 2669 + "@typescript-eslint/scope-manager": "8.50.0", 2670 + "@typescript-eslint/type-utils": "8.50.0", 2671 + "@typescript-eslint/utils": "8.50.0", 2672 + "@typescript-eslint/visitor-keys": "8.50.0", 2452 2673 "ignore": "^7.0.0", 2453 2674 "natural-compare": "^1.4.0", 2454 2675 "ts-api-utils": "^2.1.0" ··· 2461 2682 "url": "https://opencollective.com/typescript-eslint" 2462 2683 }, 2463 2684 "peerDependencies": { 2464 - "@typescript-eslint/parser": "^8.49.0", 2685 + "@typescript-eslint/parser": "^8.50.0", 2465 2686 "eslint": "^8.57.0 || ^9.0.0", 2466 2687 "typescript": ">=4.8.4 <6.0.0" 2467 2688 } 2468 2689 }, 2469 2690 "node_modules/@typescript-eslint/parser": { 2470 - "version": "8.49.0", 2691 + "version": "8.50.0", 2692 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", 2693 + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", 2471 2694 "dev": true, 2472 2695 "license": "MIT", 2473 2696 "peer": true, 2474 2697 "dependencies": { 2475 - "@typescript-eslint/scope-manager": "8.49.0", 2476 - "@typescript-eslint/types": "8.49.0", 2477 - "@typescript-eslint/typescript-estree": "8.49.0", 2478 - "@typescript-eslint/visitor-keys": "8.49.0", 2698 + "@typescript-eslint/scope-manager": "8.50.0", 2699 + "@typescript-eslint/types": "8.50.0", 2700 + "@typescript-eslint/typescript-estree": "8.50.0", 2701 + "@typescript-eslint/visitor-keys": "8.50.0", 2479 2702 "debug": "^4.3.4" 2480 2703 }, 2481 2704 "engines": { ··· 2491 2714 } 2492 2715 }, 2493 2716 "node_modules/@typescript-eslint/project-service": { 2494 - "version": "8.49.0", 2717 + "version": "8.50.0", 2718 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", 2719 + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", 2495 2720 "dev": true, 2496 2721 "license": "MIT", 2497 2722 "dependencies": { 2498 - "@typescript-eslint/tsconfig-utils": "^8.49.0", 2499 - "@typescript-eslint/types": "^8.49.0", 2723 + "@typescript-eslint/tsconfig-utils": "^8.50.0", 2724 + "@typescript-eslint/types": "^8.50.0", 2500 2725 "debug": "^4.3.4" 2501 2726 }, 2502 2727 "engines": { ··· 2511 2736 } 2512 2737 }, 2513 2738 "node_modules/@typescript-eslint/scope-manager": { 2514 - "version": "8.49.0", 2739 + "version": "8.50.0", 2740 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", 2741 + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", 2515 2742 "dev": true, 2516 2743 "license": "MIT", 2517 2744 "dependencies": { 2518 - "@typescript-eslint/types": "8.49.0", 2519 - "@typescript-eslint/visitor-keys": "8.49.0" 2745 + "@typescript-eslint/types": "8.50.0", 2746 + "@typescript-eslint/visitor-keys": "8.50.0" 2520 2747 }, 2521 2748 "engines": { 2522 2749 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 2527 2754 } 2528 2755 }, 2529 2756 "node_modules/@typescript-eslint/tsconfig-utils": { 2530 - "version": "8.49.0", 2757 + "version": "8.50.0", 2758 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", 2759 + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", 2531 2760 "dev": true, 2532 2761 "license": "MIT", 2533 2762 "engines": { ··· 2542 2771 } 2543 2772 }, 2544 2773 "node_modules/@typescript-eslint/type-utils": { 2545 - "version": "8.49.0", 2774 + "version": "8.50.0", 2775 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", 2776 + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", 2546 2777 "dev": true, 2547 2778 "license": "MIT", 2548 2779 "dependencies": { 2549 - "@typescript-eslint/types": "8.49.0", 2550 - "@typescript-eslint/typescript-estree": "8.49.0", 2551 - "@typescript-eslint/utils": "8.49.0", 2780 + "@typescript-eslint/types": "8.50.0", 2781 + "@typescript-eslint/typescript-estree": "8.50.0", 2782 + "@typescript-eslint/utils": "8.50.0", 2552 2783 "debug": "^4.3.4", 2553 2784 "ts-api-utils": "^2.1.0" 2554 2785 }, ··· 2565 2796 } 2566 2797 }, 2567 2798 "node_modules/@typescript-eslint/types": { 2568 - "version": "8.49.0", 2799 + "version": "8.50.0", 2800 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", 2801 + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", 2569 2802 "dev": true, 2570 2803 "license": "MIT", 2571 2804 "engines": { ··· 2577 2810 } 2578 2811 }, 2579 2812 "node_modules/@typescript-eslint/typescript-estree": { 2580 - "version": "8.49.0", 2813 + "version": "8.50.0", 2814 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", 2815 + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", 2581 2816 "dev": true, 2582 2817 "license": "MIT", 2583 2818 "dependencies": { 2584 - "@typescript-eslint/project-service": "8.49.0", 2585 - "@typescript-eslint/tsconfig-utils": "8.49.0", 2586 - "@typescript-eslint/types": "8.49.0", 2587 - "@typescript-eslint/visitor-keys": "8.49.0", 2819 + "@typescript-eslint/project-service": "8.50.0", 2820 + "@typescript-eslint/tsconfig-utils": "8.50.0", 2821 + "@typescript-eslint/types": "8.50.0", 2822 + "@typescript-eslint/visitor-keys": "8.50.0", 2588 2823 "debug": "^4.3.4", 2589 2824 "minimatch": "^9.0.4", 2590 2825 "semver": "^7.6.0", ··· 2602 2837 "typescript": ">=4.8.4 <6.0.0" 2603 2838 } 2604 2839 }, 2840 + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { 2841 + "version": "2.0.2", 2842 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", 2843 + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", 2844 + "dev": true, 2845 + "license": "MIT", 2846 + "dependencies": { 2847 + "balanced-match": "^1.0.0" 2848 + } 2849 + }, 2605 2850 "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { 2606 2851 "version": "9.0.5", 2852 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 2853 + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 2607 2854 "dev": true, 2608 2855 "license": "ISC", 2609 2856 "dependencies": { ··· 2616 2863 "url": "https://github.com/sponsors/isaacs" 2617 2864 } 2618 2865 }, 2619 - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": { 2620 - "version": "2.0.2", 2621 - "dev": true, 2622 - "license": "MIT", 2623 - "dependencies": { 2624 - "balanced-match": "^1.0.0" 2625 - } 2626 - }, 2627 2866 "node_modules/@typescript-eslint/utils": { 2628 - "version": "8.49.0", 2867 + "version": "8.50.0", 2868 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", 2869 + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", 2629 2870 "dev": true, 2630 2871 "license": "MIT", 2631 2872 "dependencies": { 2632 2873 "@eslint-community/eslint-utils": "^4.7.0", 2633 - "@typescript-eslint/scope-manager": "8.49.0", 2634 - "@typescript-eslint/types": "8.49.0", 2635 - "@typescript-eslint/typescript-estree": "8.49.0" 2874 + "@typescript-eslint/scope-manager": "8.50.0", 2875 + "@typescript-eslint/types": "8.50.0", 2876 + "@typescript-eslint/typescript-estree": "8.50.0" 2636 2877 }, 2637 2878 "engines": { 2638 2879 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 2647 2888 } 2648 2889 }, 2649 2890 "node_modules/@typescript-eslint/visitor-keys": { 2650 - "version": "8.49.0", 2891 + "version": "8.50.0", 2892 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", 2893 + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", 2651 2894 "dev": true, 2652 2895 "license": "MIT", 2653 2896 "dependencies": { 2654 - "@typescript-eslint/types": "8.49.0", 2897 + "@typescript-eslint/types": "8.50.0", 2655 2898 "eslint-visitor-keys": "^4.2.1" 2656 2899 }, 2657 2900 "engines": { ··· 2664 2907 }, 2665 2908 "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { 2666 2909 "version": "4.2.1", 2910 + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 2911 + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 2667 2912 "dev": true, 2668 2913 "license": "Apache-2.0", 2669 2914 "engines": { ··· 2845 3090 "dev": true, 2846 3091 "license": "MIT" 2847 3092 }, 3093 + "node_modules/base64-js": { 3094 + "version": "1.5.1", 3095 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 3096 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 3097 + "funding": [ 3098 + { 3099 + "type": "github", 3100 + "url": "https://github.com/sponsors/feross" 3101 + }, 3102 + { 3103 + "type": "patreon", 3104 + "url": "https://www.patreon.com/feross" 3105 + }, 3106 + { 3107 + "type": "consulting", 3108 + "url": "https://feross.org/support" 3109 + } 3110 + ], 3111 + "license": "MIT" 3112 + }, 2848 3113 "node_modules/bowser": { 2849 3114 "version": "2.13.1", 3115 + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", 3116 + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", 2850 3117 "license": "MIT" 2851 3118 }, 2852 3119 "node_modules/brace-expansion": { ··· 2858 3125 "dependencies": { 2859 3126 "balanced-match": "^1.0.0", 2860 3127 "concat-map": "0.0.1" 3128 + } 3129 + }, 3130 + "node_modules/buffer": { 3131 + "version": "5.6.0", 3132 + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", 3133 + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", 3134 + "license": "MIT", 3135 + "dependencies": { 3136 + "base64-js": "^1.0.2", 3137 + "ieee754": "^1.1.4" 2861 3138 } 2862 3139 }, 2863 3140 "node_modules/bun-types": { ··· 3073 3350 }, 3074 3351 "peerDependenciesMeta": { 3075 3352 "jiti": { 3353 + "optional": true 3354 + } 3355 + } 3356 + }, 3357 + "node_modules/eslint-config-prettier": { 3358 + "version": "10.1.8", 3359 + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", 3360 + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", 3361 + "dev": true, 3362 + "license": "MIT", 3363 + "peer": true, 3364 + "bin": { 3365 + "eslint-config-prettier": "bin/cli.js" 3366 + }, 3367 + "funding": { 3368 + "url": "https://opencollective.com/eslint-config-prettier" 3369 + }, 3370 + "peerDependencies": { 3371 + "eslint": ">=7.0.0" 3372 + } 3373 + }, 3374 + "node_modules/eslint-plugin-prettier": { 3375 + "version": "5.5.4", 3376 + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", 3377 + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", 3378 + "dev": true, 3379 + "license": "MIT", 3380 + "dependencies": { 3381 + "prettier-linter-helpers": "^1.0.0", 3382 + "synckit": "^0.11.7" 3383 + }, 3384 + "engines": { 3385 + "node": "^14.18.0 || >=16.0.0" 3386 + }, 3387 + "funding": { 3388 + "url": "https://opencollective.com/eslint-plugin-prettier" 3389 + }, 3390 + "peerDependencies": { 3391 + "@types/eslint": ">=8.0.0", 3392 + "eslint": ">=8.0.0", 3393 + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", 3394 + "prettier": ">=3.0.0" 3395 + }, 3396 + "peerDependenciesMeta": { 3397 + "@types/eslint": { 3398 + "optional": true 3399 + }, 3400 + "eslint-config-prettier": { 3076 3401 "optional": true 3077 3402 } 3078 3403 } ··· 3207 3532 "node": ">=0.10.0" 3208 3533 } 3209 3534 }, 3535 + "node_modules/events": { 3536 + "version": "3.3.0", 3537 + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 3538 + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 3539 + "license": "MIT", 3540 + "engines": { 3541 + "node": ">=0.8.x" 3542 + } 3543 + }, 3210 3544 "node_modules/expect-type": { 3211 3545 "version": "1.3.0", 3212 3546 "dev": true, ··· 3222 3556 "dev": true, 3223 3557 "license": "MIT" 3224 3558 }, 3559 + "node_modules/fast-diff": { 3560 + "version": "1.3.0", 3561 + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", 3562 + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", 3563 + "dev": true, 3564 + "license": "Apache-2.0" 3565 + }, 3225 3566 "node_modules/fast-json-stable-stringify": { 3226 3567 "version": "2.1.0", 3227 3568 "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", ··· 3236 3577 }, 3237 3578 "node_modules/fast-xml-parser": { 3238 3579 "version": "5.2.5", 3580 + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", 3581 + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", 3239 3582 "funding": [ 3240 3583 { 3241 3584 "type": "github", ··· 3377 3720 "node": ">=16.9.0" 3378 3721 } 3379 3722 }, 3723 + "node_modules/ieee754": { 3724 + "version": "1.2.1", 3725 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 3726 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 3727 + "funding": [ 3728 + { 3729 + "type": "github", 3730 + "url": "https://github.com/sponsors/feross" 3731 + }, 3732 + { 3733 + "type": "patreon", 3734 + "url": "https://www.patreon.com/feross" 3735 + }, 3736 + { 3737 + "type": "consulting", 3738 + "url": "https://feross.org/support" 3739 + } 3740 + ], 3741 + "license": "BSD-3-Clause" 3742 + }, 3380 3743 "node_modules/ignore": { 3381 3744 "version": "7.0.5", 3382 3745 "dev": true, ··· 3409 3772 "engines": { 3410 3773 "node": ">=0.8.19" 3411 3774 } 3775 + }, 3776 + "node_modules/inherits": { 3777 + "version": "2.0.4", 3778 + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 3779 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 3780 + "license": "ISC" 3412 3781 }, 3413 3782 "node_modules/is-extglob": { 3414 3783 "version": "2.1.1", ··· 3715 4084 "node": ">= 0.8.0" 3716 4085 } 3717 4086 }, 4087 + "node_modules/prettier": { 4088 + "version": "3.7.4", 4089 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", 4090 + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", 4091 + "dev": true, 4092 + "license": "MIT", 4093 + "peer": true, 4094 + "bin": { 4095 + "prettier": "bin/prettier.cjs" 4096 + }, 4097 + "engines": { 4098 + "node": ">=14" 4099 + }, 4100 + "funding": { 4101 + "url": "https://github.com/prettier/prettier?sponsor=1" 4102 + } 4103 + }, 4104 + "node_modules/prettier-linter-helpers": { 4105 + "version": "1.0.0", 4106 + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", 4107 + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", 4108 + "dev": true, 4109 + "license": "MIT", 4110 + "dependencies": { 4111 + "fast-diff": "^1.1.2" 4112 + }, 4113 + "engines": { 4114 + "node": ">=6.0.0" 4115 + } 4116 + }, 3718 4117 "node_modules/punycode": { 3719 4118 "version": "2.3.1", 3720 4119 "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", ··· 3723 4122 "license": "MIT", 3724 4123 "engines": { 3725 4124 "node": ">=6" 4125 + } 4126 + }, 4127 + "node_modules/readable-stream": { 4128 + "version": "3.6.2", 4129 + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 4130 + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 4131 + "license": "MIT", 4132 + "dependencies": { 4133 + "inherits": "^2.0.3", 4134 + "string_decoder": "^1.1.1", 4135 + "util-deprecate": "^1.0.1" 4136 + }, 4137 + "engines": { 4138 + "node": ">= 6" 3726 4139 } 3727 4140 }, 3728 4141 "node_modules/resolve-from": { ··· 3783 4196 "fsevents": "~2.3.2" 3784 4197 } 3785 4198 }, 4199 + "node_modules/safe-buffer": { 4200 + "version": "5.2.1", 4201 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 4202 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 4203 + "funding": [ 4204 + { 4205 + "type": "github", 4206 + "url": "https://github.com/sponsors/feross" 4207 + }, 4208 + { 4209 + "type": "patreon", 4210 + "url": "https://www.patreon.com/feross" 4211 + }, 4212 + { 4213 + "type": "consulting", 4214 + "url": "https://feross.org/support" 4215 + } 4216 + ], 4217 + "license": "MIT" 4218 + }, 3786 4219 "node_modules/semver": { 3787 4220 "version": "7.7.3", 4221 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", 4222 + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", 3788 4223 "dev": true, 3789 4224 "license": "ISC", 3790 4225 "bin": { ··· 3836 4271 "dev": true, 3837 4272 "license": "MIT" 3838 4273 }, 4274 + "node_modules/stream-browserify": { 4275 + "version": "3.0.0", 4276 + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", 4277 + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", 4278 + "license": "MIT", 4279 + "dependencies": { 4280 + "inherits": "~2.0.4", 4281 + "readable-stream": "^3.5.0" 4282 + } 4283 + }, 4284 + "node_modules/string_decoder": { 4285 + "version": "1.3.0", 4286 + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 4287 + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 4288 + "license": "MIT", 4289 + "dependencies": { 4290 + "safe-buffer": "~5.2.0" 4291 + } 4292 + }, 3839 4293 "node_modules/strip-json-comments": { 3840 4294 "version": "3.1.1", 3841 4295 "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", ··· 3850 4304 } 3851 4305 }, 3852 4306 "node_modules/strnum": { 3853 - "version": "2.1.1", 4307 + "version": "2.1.2", 4308 + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", 4309 + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", 3854 4310 "funding": [ 3855 4311 { 3856 4312 "type": "github", ··· 3870 4326 "node": ">=8" 3871 4327 } 3872 4328 }, 4329 + "node_modules/synckit": { 4330 + "version": "0.11.11", 4331 + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", 4332 + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", 4333 + "dev": true, 4334 + "license": "MIT", 4335 + "dependencies": { 4336 + "@pkgr/core": "^0.2.9" 4337 + }, 4338 + "engines": { 4339 + "node": "^14.18.0 || >=16.0.0" 4340 + }, 4341 + "funding": { 4342 + "url": "https://opencollective.com/synckit" 4343 + } 4344 + }, 3873 4345 "node_modules/tiny-lru": { 3874 4346 "version": "11.4.5", 3875 4347 "license": "BSD-3-Clause", ··· 3971 4443 "node": ">=14.17" 3972 4444 } 3973 4445 }, 4446 + "node_modules/typescript-eslint": { 4447 + "version": "8.50.0", 4448 + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", 4449 + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", 4450 + "dev": true, 4451 + "license": "MIT", 4452 + "dependencies": { 4453 + "@typescript-eslint/eslint-plugin": "8.50.0", 4454 + "@typescript-eslint/parser": "8.50.0", 4455 + "@typescript-eslint/typescript-estree": "8.50.0", 4456 + "@typescript-eslint/utils": "8.50.0" 4457 + }, 4458 + "engines": { 4459 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 4460 + }, 4461 + "funding": { 4462 + "type": "opencollective", 4463 + "url": "https://opencollective.com/typescript-eslint" 4464 + }, 4465 + "peerDependencies": { 4466 + "eslint": "^8.57.0 || ^9.0.0", 4467 + "typescript": ">=4.8.4 <6.0.0" 4468 + } 4469 + }, 3974 4470 "node_modules/undici-types": { 3975 4471 "version": "7.16.0", 3976 4472 "dev": true, ··· 3985 4481 "dependencies": { 3986 4482 "punycode": "^2.1.0" 3987 4483 } 4484 + }, 4485 + "node_modules/util-deprecate": { 4486 + "version": "1.0.2", 4487 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 4488 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 4489 + "license": "MIT" 3988 4490 }, 3989 4491 "node_modules/vite": { 3990 4492 "version": "7.2.7",
+11 -4
package.json
··· 1 1 { 2 2 "name": "tiered-storage", 3 - "version": "1.0.0", 3 + "version": "1.0.2", 4 4 "description": "Tiered storage library with S3, disk, and memory caching", 5 5 "main": "dist/index.js", 6 6 "types": "dist/index.d.ts", ··· 13 13 "serve": "tsx serve-example.ts", 14 14 "test": "vitest", 15 15 "test:watch": "vitest --watch", 16 - "lint": "eslint src --ext .ts", 17 - "lint:fix": "eslint src --ext .ts --fix", 18 - "typecheck": "tsc --noEmit" 16 + "lint": "eslint src test --ext .ts", 17 + "lint:fix": "eslint src test --ext .ts --fix", 18 + "typecheck": "tsc --noEmit", 19 + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 20 + "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"" 19 21 }, 20 22 "dependencies": { 21 23 "@aws-sdk/client-s3": "^3.500.0", 24 + "@aws-sdk/lib-storage": "^3.500.0", 22 25 "hono": "^4.10.7", 23 26 "mime-types": "^3.0.2", 24 27 "tiny-lru": "^11.0.0" ··· 30 33 "@typescript-eslint/eslint-plugin": "^8.48.1", 31 34 "@typescript-eslint/parser": "^8.48.1", 32 35 "eslint": "^9.39.1", 36 + "eslint-config-prettier": "^10.1.8", 37 + "eslint-plugin-prettier": "^5.5.4", 38 + "prettier": "^3.7.4", 33 39 "tsx": "^4.0.0", 34 40 "typescript": "^5.3.0", 41 + "typescript-eslint": "^8.50.0", 35 42 "vitest": "^4.0.15" 36 43 }, 37 44 "engines": {
+17 -10
serve-example.ts
··· 44 44 prefix: 'demo-sites/', 45 45 }), 46 46 }, 47 + placementRules: [ 48 + // index.html goes to all tiers for instant serving 49 + { pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] }, 50 + 51 + // everything else: warm + cold only 52 + { pattern: '**', tiers: ['warm', 'cold'] }, 53 + ], 47 54 compression: true, 48 55 defaultTTL: 14 * 24 * 60 * 60 * 1000, 49 56 promotionStrategy: 'lazy', ··· 62 69 console.log('\n๐Ÿ“ฆ Loading example site into tiered storage...\n'); 63 70 64 71 const files = [ 65 - { name: 'index.html', skipTiers: [], mimeType: 'text/html' }, 66 - { name: 'about.html', skipTiers: ['hot'], mimeType: 'text/html' }, 67 - { name: 'docs.html', skipTiers: ['hot'], mimeType: 'text/html' }, 68 - { name: 'style.css', skipTiers: ['hot'], mimeType: 'text/css' }, 69 - { name: 'script.js', skipTiers: ['hot'], mimeType: 'application/javascript' }, 72 + { name: 'index.html', mimeType: 'text/html' }, 73 + { name: 'about.html', mimeType: 'text/html' }, 74 + { name: 'docs.html', mimeType: 'text/html' }, 75 + { name: 'style.css', mimeType: 'text/css' }, 76 + { name: 'script.js', mimeType: 'application/javascript' }, 70 77 ]; 71 78 72 79 for (const file of files) { ··· 74 81 const key = `${siteId}/${siteName}/${file.name}`; 75 82 76 83 await storage.set(key, content, { 77 - skipTiers: file.skipTiers as ('hot' | 'warm')[], 78 84 metadata: { mimeType: file.mimeType }, 79 85 }); 80 86 81 - const tierInfo = 82 - file.skipTiers.length === 0 83 - ? '๐Ÿ”ฅ hot + ๐Ÿ’พ warm + โ˜๏ธ cold' 84 - : `๐Ÿ’พ warm + โ˜๏ธ cold (skipped hot)`; 87 + // Determine which tiers this file went to based on placement rules 88 + const isIndex = file.name === 'index.html'; 89 + const tierInfo = isIndex 90 + ? '๐Ÿ”ฅ hot + ๐Ÿ’พ warm + โ˜๏ธ cold' 91 + : '๐Ÿ’พ warm + โ˜๏ธ cold (skipped hot)'; 85 92 const sizeKB = (content.length / 1024).toFixed(2); 86 93 console.log(` โœ“ ${file.name.padEnd(15)} ${sizeKB.padStart(6)} KB โ†’ ${tierInfo}`); 87 94 }
+910 -572
src/TieredStorage.ts
··· 1 1 import type { 2 - TieredStorageConfig, 3 - SetOptions, 4 - StorageResult, 5 - SetResult, 6 - StorageMetadata, 7 - AllTierStats, 8 - StorageSnapshot, 9 - } from './types/index.js'; 10 - import { compress, decompress } from './utils/compression.js'; 2 + TieredStorageConfig, 3 + SetOptions, 4 + StorageResult, 5 + SetResult, 6 + StorageMetadata, 7 + StorageTier, 8 + AllTierStats, 9 + StorageSnapshot, 10 + StreamResult, 11 + StreamSetOptions, 12 + } from './types/index'; 13 + import { 14 + compress, 15 + decompress, 16 + createCompressStream, 17 + createDecompressStream, 18 + } from './utils/compression.js'; 11 19 import { defaultSerialize, defaultDeserialize } from './utils/serialization.js'; 12 20 import { calculateChecksum } from './utils/checksum.js'; 21 + import { matchGlob } from './utils/glob.js'; 22 + import { PassThrough, type Readable } from 'node:stream'; 23 + import { createHash } from 'node:crypto'; 13 24 14 25 /** 15 26 * Main orchestrator for tiered storage system. ··· 28 39 * @example 29 40 * ```typescript 30 41 * const storage = new TieredStorage({ 31 - * tiers: { 32 - * hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), // 100MB 33 - * warm: new DiskStorageTier({ directory: './cache' }), 34 - * cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }), 35 - * }, 36 - * compression: true, 37 - * defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days 38 - * promotionStrategy: 'lazy', 42 + * tiers: { 43 + * hot: new MemoryStorageTier({ maxSizeBytes: 100 * 1024 * 1024 }), // 100MB 44 + * warm: new DiskStorageTier({ directory: './cache' }), 45 + * cold: new S3StorageTier({ bucket: 'my-bucket', region: 'us-east-1' }), 46 + * }, 47 + * compression: true, 48 + * defaultTTL: 14 * 24 * 60 * 60 * 1000, // 14 days 49 + * promotionStrategy: 'lazy', 39 50 * }); 40 51 * 41 52 * // Store data (cascades to all tiers) ··· 49 60 * ``` 50 61 */ 51 62 export class TieredStorage<T = unknown> { 52 - private serialize: (data: unknown) => Promise<Uint8Array>; 53 - private deserialize: (data: Uint8Array) => Promise<unknown>; 63 + private serialize: (data: unknown) => Promise<Uint8Array>; 64 + private deserialize: (data: Uint8Array) => Promise<unknown>; 54 65 55 - constructor(private config: TieredStorageConfig) { 56 - if (!config.tiers.cold) { 57 - throw new Error('Cold tier is required'); 58 - } 66 + constructor(private config: TieredStorageConfig) { 67 + if (!config.tiers.cold) { 68 + throw new Error('Cold tier is required'); 69 + } 59 70 60 - this.serialize = config.serialization?.serialize ?? defaultSerialize; 61 - this.deserialize = config.serialization?.deserialize ?? defaultDeserialize; 62 - } 71 + this.serialize = config.serialization?.serialize ?? defaultSerialize; 72 + this.deserialize = config.serialization?.deserialize ?? defaultDeserialize; 73 + } 63 74 64 - /** 65 - * Retrieve data for a key. 66 - * 67 - * @param key - The key to retrieve 68 - * @returns The data, or null if not found or expired 69 - * 70 - * @remarks 71 - * Checks tiers in order: hot โ†’ warm โ†’ cold. 72 - * On cache miss, promotes data to upper tiers based on promotionStrategy. 73 - * Automatically handles decompression and deserialization. 74 - * Returns null if key doesn't exist or has expired (TTL). 75 - */ 76 - async get(key: string): Promise<T | null> { 77 - const result = await this.getWithMetadata(key); 78 - return result ? result.data : null; 79 - } 75 + /** 76 + * Retrieve data for a key. 77 + * 78 + * @param key - The key to retrieve 79 + * @returns The data, or null if not found or expired 80 + * 81 + * @remarks 82 + * Checks tiers in order: hot โ†’ warm โ†’ cold. 83 + * On cache miss, promotes data to upper tiers based on promotionStrategy. 84 + * Automatically handles decompression and deserialization. 85 + * Returns null if key doesn't exist or has expired (TTL). 86 + */ 87 + async get(key: string): Promise<T | null> { 88 + const result = await this.getWithMetadata(key); 89 + return result ? result.data : null; 90 + } 80 91 81 - /** 82 - * Retrieve data with metadata and source tier information. 83 - * 84 - * @param key - The key to retrieve 85 - * @returns The data, metadata, and source tier, or null if not found 86 - * 87 - * @remarks 88 - * Use this when you need to know: 89 - * - Which tier served the data (for observability) 90 - * - Metadata like access count, TTL, checksum 91 - * - When the data was created/last accessed 92 - */ 93 - async getWithMetadata(key: string): Promise<StorageResult<T> | null> { 94 - // 1. Check hot tier first 95 - if (this.config.tiers.hot) { 96 - const data = await this.config.tiers.hot.get(key); 97 - if (data) { 98 - const metadata = await this.config.tiers.hot.getMetadata(key); 99 - if (!metadata) { 100 - await this.delete(key); 101 - } else if (this.isExpired(metadata)) { 102 - await this.delete(key); 103 - return null; 104 - } else { 105 - await this.updateAccessStats(key, 'hot'); 106 - return { 107 - data: (await this.deserializeData(data)) as T, 108 - metadata, 109 - source: 'hot', 110 - }; 111 - } 112 - } 113 - } 92 + /** 93 + * Retrieve data with metadata and source tier information. 94 + * 95 + * @param key - The key to retrieve 96 + * @returns The data, metadata, and source tier, or null if not found 97 + * 98 + * @remarks 99 + * Use this when you need to know: 100 + * - Which tier served the data (for observability) 101 + * - Metadata like access count, TTL, checksum 102 + * - When the data was created/last accessed 103 + */ 104 + async getWithMetadata(key: string): Promise<StorageResult<T> | null> { 105 + // 1. Check hot tier first 106 + if (this.config.tiers.hot) { 107 + const result = await this.getFromTier(this.config.tiers.hot, key); 108 + if (result) { 109 + if (this.isExpired(result.metadata)) { 110 + await this.delete(key); 111 + return null; 112 + } 113 + // Fire-and-forget access stats update (non-critical) 114 + void this.updateAccessStats(key, 'hot'); 115 + return { 116 + data: (await this.deserializeData(result.data)) as T, 117 + metadata: result.metadata, 118 + source: 'hot', 119 + }; 120 + } 121 + } 114 122 115 - // 2. Check warm tier 116 - if (this.config.tiers.warm) { 117 - const data = await this.config.tiers.warm.get(key); 118 - if (data) { 119 - const metadata = await this.config.tiers.warm.getMetadata(key); 120 - if (!metadata) { 121 - await this.delete(key); 122 - } else if (this.isExpired(metadata)) { 123 - await this.delete(key); 124 - return null; 125 - } else { 126 - if (this.config.tiers.hot && this.config.promotionStrategy === 'eager') { 127 - await this.config.tiers.hot.set(key, data, metadata); 128 - } 123 + // 2. Check warm tier 124 + if (this.config.tiers.warm) { 125 + const result = await this.getFromTier(this.config.tiers.warm, key); 126 + if (result) { 127 + if (this.isExpired(result.metadata)) { 128 + await this.delete(key); 129 + return null; 130 + } 131 + // Eager promotion to hot tier (awaited - guaranteed to complete) 132 + if (this.config.tiers.hot && this.config.promotionStrategy === 'eager') { 133 + await this.config.tiers.hot.set(key, result.data, result.metadata); 134 + } 135 + // Fire-and-forget access stats update (non-critical) 136 + void this.updateAccessStats(key, 'warm'); 137 + return { 138 + data: (await this.deserializeData(result.data)) as T, 139 + metadata: result.metadata, 140 + source: 'warm', 141 + }; 142 + } 143 + } 129 144 130 - await this.updateAccessStats(key, 'warm'); 131 - return { 132 - data: (await this.deserializeData(data)) as T, 133 - metadata, 134 - source: 'warm', 135 - }; 136 - } 137 - } 138 - } 145 + // 3. Check cold tier (source of truth) 146 + const result = await this.getFromTier(this.config.tiers.cold, key); 147 + if (result) { 148 + if (this.isExpired(result.metadata)) { 149 + await this.delete(key); 150 + return null; 151 + } 139 152 140 - // 3. Check cold tier (source of truth) 141 - const data = await this.config.tiers.cold.get(key); 142 - if (data) { 143 - const metadata = await this.config.tiers.cold.getMetadata(key); 144 - if (!metadata) { 145 - await this.config.tiers.cold.delete(key); 146 - return null; 147 - } 153 + // Promote to warm and hot (if configured) 154 + // Eager promotion is awaited to guarantee completion 155 + if (this.config.promotionStrategy === 'eager') { 156 + const promotions: Promise<void>[] = []; 157 + if (this.config.tiers.warm) { 158 + promotions.push(this.config.tiers.warm.set(key, result.data, result.metadata)); 159 + } 160 + if (this.config.tiers.hot) { 161 + promotions.push(this.config.tiers.hot.set(key, result.data, result.metadata)); 162 + } 163 + await Promise.all(promotions); 164 + } 148 165 149 - if (this.isExpired(metadata)) { 150 - await this.delete(key); 151 - return null; 152 - } 166 + // Fire-and-forget access stats update (non-critical) 167 + void this.updateAccessStats(key, 'cold'); 168 + return { 169 + data: (await this.deserializeData(result.data)) as T, 170 + metadata: result.metadata, 171 + source: 'cold', 172 + }; 173 + } 153 174 154 - // Promote to warm and hot (if configured) 155 - if (this.config.promotionStrategy === 'eager') { 156 - if (this.config.tiers.warm) { 157 - await this.config.tiers.warm.set(key, data, metadata); 158 - } 159 - if (this.config.tiers.hot) { 160 - await this.config.tiers.hot.set(key, data, metadata); 161 - } 162 - } 175 + return null; 176 + } 163 177 164 - await this.updateAccessStats(key, 'cold'); 165 - return { 166 - data: (await this.deserializeData(data)) as T, 167 - metadata, 168 - source: 'cold', 169 - }; 170 - } 178 + /** 179 + * Get data and metadata from a tier using the most efficient method. 180 + * 181 + * @remarks 182 + * Uses the tier's getWithMetadata if available, otherwise falls back 183 + * to separate get() and getMetadata() calls. 184 + */ 185 + private async getFromTier( 186 + tier: StorageTier, 187 + key: string, 188 + ): Promise<{ data: Uint8Array; metadata: StorageMetadata } | null> { 189 + // Use optimized combined method if available 190 + if (tier.getWithMetadata) { 191 + return tier.getWithMetadata(key); 192 + } 171 193 172 - return null; 173 - } 194 + // Fallback: separate calls 195 + const data = await tier.get(key); 196 + if (!data) { 197 + return null; 198 + } 199 + const metadata = await tier.getMetadata(key); 200 + if (!metadata) { 201 + return null; 202 + } 203 + return { data, metadata }; 204 + } 174 205 175 - /** 176 - * Store data with optional configuration. 177 - * 178 - * @param key - The key to store under 179 - * @param data - The data to store 180 - * @param options - Optional configuration (TTL, metadata, tier skipping) 181 - * @returns Information about what was stored and where 182 - * 183 - * @remarks 184 - * Data cascades down through tiers: 185 - * - If written to hot, also written to warm and cold 186 - * - If written to warm (hot skipped), also written to cold 187 - * - Cold is always written (source of truth) 188 - * 189 - * Use `skipTiers` to control placement. For example: 190 - * - Large files: `skipTiers: ['hot']` to avoid memory bloat 191 - * - Critical small files: Write to all tiers for fastest access 192 - * 193 - * Automatically handles serialization and optional compression. 194 - */ 195 - async set(key: string, data: T, options?: SetOptions): Promise<SetResult> { 196 - // 1. Serialize data 197 - const serialized = await this.serialize(data); 206 + /** 207 + * Retrieve data as a readable stream with metadata. 208 + * 209 + * @param key - The key to retrieve 210 + * @returns A readable stream, metadata, and source tier, or null if not found 211 + * 212 + * @remarks 213 + * Use this for large files to avoid loading entire content into memory. 214 + * The stream must be consumed or destroyed by the caller. 215 + * 216 + * Checks tiers in order: hot โ†’ warm โ†’ cold. 217 + * On cache miss, does NOT promote data to upper tiers (streaming would 218 + * require buffering, defeating the purpose). 219 + * 220 + * Decompression is automatically handled if the data was stored with 221 + * compression enabled (metadata.compressed = true). 222 + * 223 + * @example 224 + * ```typescript 225 + * const result = await storage.getStream('large-file.mp4'); 226 + * if (result) { 227 + * result.stream.pipe(response); // Stream directly to HTTP response 228 + * } 229 + * ``` 230 + */ 231 + async getStream(key: string): Promise<StreamResult | null> { 232 + // 1. Check hot tier first 233 + if (this.config.tiers.hot?.getStream) { 234 + const result = await this.config.tiers.hot.getStream(key); 235 + if (result) { 236 + if (this.isExpired(result.metadata)) { 237 + (result.stream as Readable).destroy?.(); 238 + await this.delete(key); 239 + return null; 240 + } 241 + void this.updateAccessStats(key, 'hot'); 242 + return this.wrapStreamWithDecompression(result, 'hot'); 243 + } 244 + } 198 245 199 - // 2. Optionally compress 200 - const finalData = this.config.compression ? await compress(serialized) : serialized; 246 + // 2. Check warm tier 247 + if (this.config.tiers.warm?.getStream) { 248 + const result = await this.config.tiers.warm.getStream(key); 249 + if (result) { 250 + if (this.isExpired(result.metadata)) { 251 + (result.stream as Readable).destroy?.(); 252 + await this.delete(key); 253 + return null; 254 + } 255 + // NOTE: No promotion for streaming (would require buffering) 256 + void this.updateAccessStats(key, 'warm'); 257 + return this.wrapStreamWithDecompression(result, 'warm'); 258 + } 259 + } 201 260 202 - // 3. Create metadata 203 - const metadata = this.createMetadata(key, finalData, options); 261 + // 3. Check cold tier (source of truth) 262 + if (this.config.tiers.cold.getStream) { 263 + const result = await this.config.tiers.cold.getStream(key); 264 + if (result) { 265 + if (this.isExpired(result.metadata)) { 266 + (result.stream as Readable).destroy?.(); 267 + await this.delete(key); 268 + return null; 269 + } 270 + // NOTE: No promotion for streaming (would require buffering) 271 + void this.updateAccessStats(key, 'cold'); 272 + return this.wrapStreamWithDecompression(result, 'cold'); 273 + } 274 + } 204 275 205 - // 4. Write to all tiers (cascading down) 206 - const tiersWritten: ('hot' | 'warm' | 'cold')[] = []; 276 + return null; 277 + } 207 278 208 - // Write to hot (if configured and not skipped) 209 - if (this.config.tiers.hot && !options?.skipTiers?.includes('hot')) { 210 - await this.config.tiers.hot.set(key, finalData, metadata); 211 - tiersWritten.push('hot'); 279 + /** 280 + * Wrap a stream result with decompression if needed. 281 + */ 282 + private wrapStreamWithDecompression( 283 + result: { stream: NodeJS.ReadableStream; metadata: StorageMetadata }, 284 + source: 'hot' | 'warm' | 'cold', 285 + ): StreamResult { 286 + if (result.metadata.compressed) { 287 + // Pipe through decompression stream 288 + const decompressStream = createDecompressStream(); 289 + (result.stream as Readable).pipe(decompressStream); 290 + return { stream: decompressStream, metadata: result.metadata, source }; 291 + } 292 + return { ...result, source }; 293 + } 212 294 213 - // Hot writes cascade to warm 214 - if (this.config.tiers.warm && !options?.skipTiers?.includes('warm')) { 215 - await this.config.tiers.warm.set(key, finalData, metadata); 216 - tiersWritten.push('warm'); 217 - } 218 - } else if (this.config.tiers.warm && !options?.skipTiers?.includes('warm')) { 219 - // Write to warm (if hot skipped) 220 - await this.config.tiers.warm.set(key, finalData, metadata); 221 - tiersWritten.push('warm'); 222 - } 295 + /** 296 + * Store data from a readable stream. 297 + * 298 + * @param key - The key to store under 299 + * @param stream - Readable stream of data to store 300 + * @param options - Configuration including size (required), checksum, and tier options 301 + * @returns Information about what was stored and where 302 + * 303 + * @remarks 304 + * Use this for large files to avoid loading entire content into memory. 305 + * 306 + * **Important differences from set():** 307 + * - `options.size` is required (stream size cannot be determined upfront) 308 + * - Serialization is NOT applied (stream is stored as-is) 309 + * - If no checksum is provided, one will be computed during streaming 310 + * - Checksum is computed on the original (pre-compression) data 311 + * 312 + * **Compression:** 313 + * - If `config.compression` is true, the stream is compressed before storage 314 + * - Checksum is always computed on the original uncompressed data 315 + * 316 + * **Tier handling:** 317 + * - Only writes to tiers that support streaming (have setStream method) 318 + * - Hot tier is skipped by default for streaming (typically memory-based) 319 + * - Tees the stream to write to multiple tiers simultaneously 320 + * 321 + * @example 322 + * ```typescript 323 + * const fileStream = fs.createReadStream('large-file.mp4'); 324 + * const stat = fs.statSync('large-file.mp4'); 325 + * 326 + * await storage.setStream('videos/large.mp4', fileStream, { 327 + * size: stat.size, 328 + * mimeType: 'video/mp4', 329 + * }); 330 + * ``` 331 + */ 332 + async setStream( 333 + key: string, 334 + stream: NodeJS.ReadableStream, 335 + options: StreamSetOptions, 336 + ): Promise<SetResult> { 337 + const shouldCompress = this.config.compression ?? false; 223 338 224 - // Always write to cold (source of truth) 225 - await this.config.tiers.cold.set(key, finalData, metadata); 226 - tiersWritten.push('cold'); 339 + // Create metadata 340 + const now = new Date(); 341 + const ttl = options.ttl ?? this.config.defaultTTL; 227 342 228 - return { key, metadata, tiersWritten }; 229 - } 343 + const metadata: StorageMetadata = { 344 + key, 345 + size: options.size, // Original uncompressed size 346 + createdAt: now, 347 + lastAccessed: now, 348 + accessCount: 0, 349 + compressed: shouldCompress, 350 + checksum: options.checksum ?? '', // Will be computed if not provided 351 + ...(options.mimeType && { mimeType: options.mimeType }), 352 + }; 230 353 231 - /** 232 - * Delete data from all tiers. 233 - * 234 - * @param key - The key to delete 235 - * 236 - * @remarks 237 - * Deletes from all configured tiers in parallel. 238 - * Does not throw if the key doesn't exist. 239 - */ 240 - async delete(key: string): Promise<void> { 241 - await Promise.all([ 242 - this.config.tiers.hot?.delete(key), 243 - this.config.tiers.warm?.delete(key), 244 - this.config.tiers.cold.delete(key), 245 - ]); 246 - } 354 + if (ttl) { 355 + metadata.ttl = new Date(now.getTime() + ttl); 356 + } 247 357 248 - /** 249 - * Check if a key exists in any tier. 250 - * 251 - * @param key - The key to check 252 - * @returns true if the key exists and hasn't expired 253 - * 254 - * @remarks 255 - * Checks tiers in order: hot โ†’ warm โ†’ cold. 256 - * Returns false if key exists but has expired. 257 - */ 258 - async exists(key: string): Promise<boolean> { 259 - // Check hot first (fastest) 260 - if (this.config.tiers.hot && (await this.config.tiers.hot.exists(key))) { 261 - const metadata = await this.config.tiers.hot.getMetadata(key); 262 - if (metadata && !this.isExpired(metadata)) { 263 - return true; 264 - } 265 - } 358 + if (options.metadata) { 359 + metadata.customMetadata = options.metadata; 360 + } 266 361 267 - // Check warm 268 - if (this.config.tiers.warm && (await this.config.tiers.warm.exists(key))) { 269 - const metadata = await this.config.tiers.warm.getMetadata(key); 270 - if (metadata && !this.isExpired(metadata)) { 271 - return true; 272 - } 273 - } 362 + // Determine which tiers to write to 363 + // Default: skip hot tier for streaming (typically memory-based, defeats purpose) 364 + const allowedTiers = this.getTiersForKey(key, options.skipTiers ?? ['hot']); 274 365 275 - // Check cold (source of truth) 276 - if (await this.config.tiers.cold.exists(key)) { 277 - const metadata = await this.config.tiers.cold.getMetadata(key); 278 - if (metadata && !this.isExpired(metadata)) { 279 - return true; 280 - } 281 - } 366 + // Collect tiers that support streaming 367 + const streamingTiers: Array<{ name: 'hot' | 'warm' | 'cold'; tier: StorageTier }> = []; 282 368 283 - return false; 284 - } 369 + if (this.config.tiers.hot?.setStream && allowedTiers.includes('hot')) { 370 + streamingTiers.push({ name: 'hot', tier: this.config.tiers.hot }); 371 + } 285 372 286 - /** 287 - * Renew TTL for a key. 288 - * 289 - * @param key - The key to touch 290 - * @param ttlMs - Optional new TTL in milliseconds (uses default if not provided) 291 - * 292 - * @remarks 293 - * Updates the TTL and lastAccessed timestamp in all tiers. 294 - * Useful for implementing "keep alive" behavior for actively used keys. 295 - * Does nothing if no TTL is configured. 296 - */ 297 - async touch(key: string, ttlMs?: number): Promise<void> { 298 - const ttl = ttlMs ?? this.config.defaultTTL; 299 - if (!ttl) return; 373 + if (this.config.tiers.warm?.setStream && allowedTiers.includes('warm')) { 374 + streamingTiers.push({ name: 'warm', tier: this.config.tiers.warm }); 375 + } 300 376 301 - const newTTL = new Date(Date.now() + ttl); 377 + if (this.config.tiers.cold.setStream) { 378 + streamingTiers.push({ name: 'cold', tier: this.config.tiers.cold }); 379 + } 302 380 303 - for (const tier of [this.config.tiers.hot, this.config.tiers.warm, this.config.tiers.cold]) { 304 - if (!tier) continue; 381 + const tiersWritten: ('hot' | 'warm' | 'cold')[] = []; 305 382 306 - const metadata = await tier.getMetadata(key); 307 - if (metadata) { 308 - metadata.ttl = newTTL; 309 - metadata.lastAccessed = new Date(); 310 - await tier.setMetadata(key, metadata); 311 - } 312 - } 313 - } 383 + if (streamingTiers.length === 0) { 384 + throw new Error('No tiers support streaming. Use set() for buffered writes.'); 385 + } 314 386 315 - /** 316 - * Invalidate all keys matching a prefix. 317 - * 318 - * @param prefix - The prefix to match (e.g., 'user:' matches 'user:123', 'user:456') 319 - * @returns Number of keys deleted 320 - * 321 - * @remarks 322 - * Useful for bulk invalidation: 323 - * - Site invalidation: `invalidate('site:abc:')` 324 - * - User invalidation: `invalidate('user:123:')` 325 - * - Global invalidation: `invalidate('')` (deletes everything) 326 - * 327 - * Deletes from all tiers in parallel for efficiency. 328 - */ 329 - async invalidate(prefix: string): Promise<number> { 330 - const keysToDelete = new Set<string>(); 387 + // We always need to compute checksum on uncompressed data if not provided 388 + const needsChecksum = !options.checksum; 331 389 332 - // Collect all keys matching prefix from all tiers 333 - if (this.config.tiers.hot) { 334 - for await (const key of this.config.tiers.hot.listKeys(prefix)) { 335 - keysToDelete.add(key); 336 - } 337 - } 390 + // Create pass-through streams for each tier 391 + const passThroughs = streamingTiers.map(() => new PassThrough()); 392 + const hashStream = needsChecksum ? createHash('sha256') : null; 338 393 339 - if (this.config.tiers.warm) { 340 - for await (const key of this.config.tiers.warm.listKeys(prefix)) { 341 - keysToDelete.add(key); 342 - } 343 - } 394 + // Set up the stream pipeline: 395 + // source -> (hash) -> (compress) -> tee to all tier streams 396 + const sourceStream = stream as Readable; 344 397 345 - for await (const key of this.config.tiers.cold.listKeys(prefix)) { 346 - keysToDelete.add(key); 347 - } 398 + // If compression is enabled, we need to: 399 + // 1. Compute hash on original data 400 + // 2. Then compress 401 + // 3. Then tee to all tiers 402 + if (shouldCompress) { 403 + const compressStream = createCompressStream(); 348 404 349 - // Delete from all tiers in parallel 350 - const keys = Array.from(keysToDelete); 405 + // Hash the original uncompressed data 406 + sourceStream.on('data', (chunk: Buffer) => { 407 + if (hashStream) { 408 + hashStream.update(chunk); 409 + } 410 + }); 351 411 352 - await Promise.all([ 353 - this.config.tiers.hot?.deleteMany(keys), 354 - this.config.tiers.warm?.deleteMany(keys), 355 - this.config.tiers.cold.deleteMany(keys), 356 - ]); 412 + // Pipe source through compression 413 + sourceStream.pipe(compressStream); 357 414 358 - return keys.length; 359 - } 415 + // Tee compressed output to all tier streams 416 + compressStream.on('data', (chunk: Buffer) => { 417 + for (const pt of passThroughs) { 418 + pt.write(chunk); 419 + } 420 + }); 360 421 361 - /** 362 - * List all keys, optionally filtered by prefix. 363 - * 364 - * @param prefix - Optional prefix to filter keys 365 - * @returns Async iterator of keys 366 - * 367 - * @remarks 368 - * Returns keys from the cold tier (source of truth). 369 - * Memory-efficient - streams keys rather than loading all into memory. 370 - * 371 - * @example 372 - * ```typescript 373 - * for await (const key of storage.listKeys('user:')) { 374 - * console.log(key); 375 - * } 376 - * ``` 377 - */ 378 - async *listKeys(prefix?: string): AsyncIterableIterator<string> { 379 - // List from cold tier (source of truth) 380 - for await (const key of this.config.tiers.cold.listKeys(prefix)) { 381 - yield key; 382 - } 383 - } 422 + compressStream.on('end', () => { 423 + for (const pt of passThroughs) { 424 + pt.end(); 425 + } 426 + }); 384 427 385 - /** 386 - * Get aggregated statistics across all tiers. 387 - * 388 - * @returns Statistics including size, item count, hits, misses, hit rate 389 - * 390 - * @remarks 391 - * Useful for monitoring and capacity planning. 392 - * Hit rate is calculated as: hits / (hits + misses). 393 - */ 394 - async getStats(): Promise<AllTierStats> { 395 - const [hot, warm, cold] = await Promise.all([ 396 - this.config.tiers.hot?.getStats(), 397 - this.config.tiers.warm?.getStats(), 398 - this.config.tiers.cold.getStats(), 399 - ]); 428 + compressStream.on('error', (err) => { 429 + for (const pt of passThroughs) { 430 + pt.destroy(err); 431 + } 432 + }); 433 + } else { 434 + // No compression - hash and tee directly 435 + sourceStream.on('data', (chunk: Buffer) => { 436 + for (const pt of passThroughs) { 437 + pt.write(chunk); 438 + } 439 + if (hashStream) { 440 + hashStream.update(chunk); 441 + } 442 + }); 400 443 401 - const totalHits = (hot?.hits ?? 0) + (warm?.hits ?? 0) + (cold?.hits ?? 0); 402 - const totalMisses = (hot?.misses ?? 0) + (warm?.misses ?? 0) + (cold?.misses ?? 0); 403 - const hitRate = totalHits + totalMisses > 0 ? totalHits / (totalHits + totalMisses) : 0; 444 + sourceStream.on('end', () => { 445 + for (const pt of passThroughs) { 446 + pt.end(); 447 + } 448 + }); 404 449 405 - return { 406 - ...(hot && { hot }), 407 - ...(warm && { warm }), 408 - cold, 409 - totalHits, 410 - totalMisses, 411 - hitRate, 412 - }; 413 - } 450 + sourceStream.on('error', (err) => { 451 + for (const pt of passThroughs) { 452 + pt.destroy(err); 453 + } 454 + }); 455 + } 414 456 415 - /** 416 - * Clear all data from all tiers. 417 - * 418 - * @remarks 419 - * Use with extreme caution! This will delete all data in the entire storage system. 420 - * Cannot be undone. 421 - */ 422 - async clear(): Promise<void> { 423 - await Promise.all([ 424 - this.config.tiers.hot?.clear(), 425 - this.config.tiers.warm?.clear(), 426 - this.config.tiers.cold.clear(), 427 - ]); 428 - } 457 + // Wait for all tier writes 458 + const writePromises = streamingTiers.map(async ({ name, tier }, index) => { 459 + await tier.setStream!(key, passThroughs[index]!, metadata); 460 + tiersWritten.push(name); 461 + }); 429 462 430 - /** 431 - * Clear a specific tier. 432 - * 433 - * @param tier - Which tier to clear 434 - * 435 - * @remarks 436 - * Useful for: 437 - * - Clearing hot tier to test warm/cold performance 438 - * - Clearing warm tier to force rebuilding from cold 439 - * - Clearing cold tier to start fresh (โš ๏ธ loses source of truth!) 440 - */ 441 - async clearTier(tier: 'hot' | 'warm' | 'cold'): Promise<void> { 442 - switch (tier) { 443 - case 'hot': 444 - await this.config.tiers.hot?.clear(); 445 - break; 446 - case 'warm': 447 - await this.config.tiers.warm?.clear(); 448 - break; 449 - case 'cold': 450 - await this.config.tiers.cold.clear(); 451 - break; 452 - } 453 - } 463 + await Promise.all(writePromises); 464 + 465 + // Update checksum in metadata if computed 466 + if (hashStream) { 467 + metadata.checksum = hashStream.digest('hex'); 468 + // Update metadata in all tiers with the computed checksum 469 + await Promise.all(streamingTiers.map(({ tier }) => tier.setMetadata(key, metadata))); 470 + } 471 + 472 + return { key, metadata, tiersWritten }; 473 + } 474 + 475 + /** 476 + * Store data with optional configuration. 477 + * 478 + * @param key - The key to store under 479 + * @param data - The data to store 480 + * @param options - Optional configuration (TTL, metadata, tier skipping) 481 + * @returns Information about what was stored and where 482 + * 483 + * @remarks 484 + * Data cascades down through tiers: 485 + * - If written to hot, also written to warm and cold 486 + * - If written to warm (hot skipped), also written to cold 487 + * - Cold is always written (source of truth) 488 + * 489 + * Use `skipTiers` to control placement. For example: 490 + * - Large files: `skipTiers: ['hot']` to avoid memory bloat 491 + * - Critical small files: Write to all tiers for fastest access 492 + * 493 + * Automatically handles serialization and optional compression. 494 + */ 495 + async set(key: string, data: T, options?: SetOptions): Promise<SetResult> { 496 + // 1. Serialize data 497 + const serialized = await this.serialize(data); 498 + 499 + // 2. Optionally compress 500 + const finalData = this.config.compression ? await compress(serialized) : serialized; 501 + 502 + // 3. Create metadata 503 + const metadata = this.createMetadata(key, finalData, options); 504 + 505 + // 4. Determine which tiers to write to 506 + const allowedTiers = this.getTiersForKey(key, options?.skipTiers); 507 + 508 + // 5. Write to tiers 509 + const tiersWritten: ('hot' | 'warm' | 'cold')[] = []; 510 + 511 + if (this.config.tiers.hot && allowedTiers.includes('hot')) { 512 + await this.config.tiers.hot.set(key, finalData, metadata); 513 + tiersWritten.push('hot'); 514 + } 515 + 516 + if (this.config.tiers.warm && allowedTiers.includes('warm')) { 517 + await this.config.tiers.warm.set(key, finalData, metadata); 518 + tiersWritten.push('warm'); 519 + } 520 + 521 + // Always write to cold (source of truth) 522 + await this.config.tiers.cold.set(key, finalData, metadata); 523 + tiersWritten.push('cold'); 524 + 525 + return { key, metadata, tiersWritten }; 526 + } 527 + 528 + /** 529 + * Determine which tiers a key should be written to. 530 + * 531 + * @param key - The key being stored 532 + * @param skipTiers - Explicit tiers to skip (overrides placement rules) 533 + * @returns Array of tiers to write to 534 + * 535 + * @remarks 536 + * Priority: skipTiers option > placementRules > all configured tiers 537 + */ 538 + private getTiersForKey( 539 + key: string, 540 + skipTiers?: ('hot' | 'warm')[], 541 + ): ('hot' | 'warm' | 'cold')[] { 542 + // If explicit skipTiers provided, use that 543 + if (skipTiers && skipTiers.length > 0) { 544 + const allTiers: ('hot' | 'warm' | 'cold')[] = ['hot', 'warm', 'cold']; 545 + return allTiers.filter((t) => !skipTiers.includes(t as 'hot' | 'warm')); 546 + } 547 + 548 + // Check placement rules 549 + if (this.config.placementRules) { 550 + for (const rule of this.config.placementRules) { 551 + if (matchGlob(rule.pattern, key)) { 552 + // Ensure cold is always included 553 + if (!rule.tiers.includes('cold')) { 554 + return [...rule.tiers, 'cold']; 555 + } 556 + return rule.tiers; 557 + } 558 + } 559 + } 560 + 561 + // Default: write to all configured tiers 562 + return ['hot', 'warm', 'cold']; 563 + } 564 + 565 + /** 566 + * Delete data from all tiers. 567 + * 568 + * @param key - The key to delete 569 + * 570 + * @remarks 571 + * Deletes from all configured tiers in parallel. 572 + * Does not throw if the key doesn't exist. 573 + */ 574 + async delete(key: string): Promise<void> { 575 + await Promise.all([ 576 + this.config.tiers.hot?.delete(key), 577 + this.config.tiers.warm?.delete(key), 578 + this.config.tiers.cold.delete(key), 579 + ]); 580 + } 581 + 582 + /** 583 + * Check if a key exists in any tier. 584 + * 585 + * @param key - The key to check 586 + * @returns true if the key exists and hasn't expired 587 + * 588 + * @remarks 589 + * Checks tiers in order: hot โ†’ warm โ†’ cold. 590 + * Returns false if key exists but has expired. 591 + */ 592 + async exists(key: string): Promise<boolean> { 593 + // Check hot first (fastest) 594 + if (this.config.tiers.hot && (await this.config.tiers.hot.exists(key))) { 595 + const metadata = await this.config.tiers.hot.getMetadata(key); 596 + if (metadata && !this.isExpired(metadata)) { 597 + return true; 598 + } 599 + } 600 + 601 + // Check warm 602 + if (this.config.tiers.warm && (await this.config.tiers.warm.exists(key))) { 603 + const metadata = await this.config.tiers.warm.getMetadata(key); 604 + if (metadata && !this.isExpired(metadata)) { 605 + return true; 606 + } 607 + } 608 + 609 + // Check cold (source of truth) 610 + if (await this.config.tiers.cold.exists(key)) { 611 + const metadata = await this.config.tiers.cold.getMetadata(key); 612 + if (metadata && !this.isExpired(metadata)) { 613 + return true; 614 + } 615 + } 616 + 617 + return false; 618 + } 619 + 620 + /** 621 + * Renew TTL for a key. 622 + * 623 + * @param key - The key to touch 624 + * @param ttlMs - Optional new TTL in milliseconds (uses default if not provided) 625 + * 626 + * @remarks 627 + * Updates the TTL and lastAccessed timestamp in all tiers. 628 + * Useful for implementing "keep alive" behavior for actively used keys. 629 + * Does nothing if no TTL is configured. 630 + */ 631 + async touch(key: string, ttlMs?: number): Promise<void> { 632 + const ttl = ttlMs ?? this.config.defaultTTL; 633 + if (!ttl) return; 634 + 635 + const newTTL = new Date(Date.now() + ttl); 636 + 637 + for (const tier of [ 638 + this.config.tiers.hot, 639 + this.config.tiers.warm, 640 + this.config.tiers.cold, 641 + ]) { 642 + if (!tier) continue; 643 + 644 + const metadata = await tier.getMetadata(key); 645 + if (metadata) { 646 + metadata.ttl = newTTL; 647 + metadata.lastAccessed = new Date(); 648 + await tier.setMetadata(key, metadata); 649 + } 650 + } 651 + } 652 + 653 + /** 654 + * Invalidate all keys matching a prefix. 655 + * 656 + * @param prefix - The prefix to match (e.g., 'user:' matches 'user:123', 'user:456') 657 + * @returns Number of keys deleted 658 + * 659 + * @remarks 660 + * Useful for bulk invalidation: 661 + * - Site invalidation: `invalidate('site:abc:')` 662 + * - User invalidation: `invalidate('user:123:')` 663 + * - Global invalidation: `invalidate('')` (deletes everything) 664 + * 665 + * Deletes from all tiers in parallel for efficiency. 666 + */ 667 + async invalidate(prefix: string): Promise<number> { 668 + const keysToDelete = new Set<string>(); 669 + 670 + // Collect all keys matching prefix from all tiers 671 + if (this.config.tiers.hot) { 672 + for await (const key of this.config.tiers.hot.listKeys(prefix)) { 673 + keysToDelete.add(key); 674 + } 675 + } 676 + 677 + if (this.config.tiers.warm) { 678 + for await (const key of this.config.tiers.warm.listKeys(prefix)) { 679 + keysToDelete.add(key); 680 + } 681 + } 682 + 683 + for await (const key of this.config.tiers.cold.listKeys(prefix)) { 684 + keysToDelete.add(key); 685 + } 686 + 687 + // Delete from all tiers in parallel 688 + const keys = Array.from(keysToDelete); 689 + 690 + await Promise.all([ 691 + this.config.tiers.hot?.deleteMany(keys), 692 + this.config.tiers.warm?.deleteMany(keys), 693 + this.config.tiers.cold.deleteMany(keys), 694 + ]); 695 + 696 + return keys.length; 697 + } 698 + 699 + /** 700 + * List all keys, optionally filtered by prefix. 701 + * 702 + * @param prefix - Optional prefix to filter keys 703 + * @returns Async iterator of keys 704 + * 705 + * @remarks 706 + * Returns keys from the cold tier (source of truth). 707 + * Memory-efficient - streams keys rather than loading all into memory. 708 + * 709 + * @example 710 + * ```typescript 711 + * for await (const key of storage.listKeys('user:')) { 712 + * console.log(key); 713 + * } 714 + * ``` 715 + */ 716 + async *listKeys(prefix?: string): AsyncIterableIterator<string> { 717 + // List from cold tier (source of truth) 718 + for await (const key of this.config.tiers.cold.listKeys(prefix)) { 719 + yield key; 720 + } 721 + } 722 + 723 + /** 724 + * Get aggregated statistics across all tiers. 725 + * 726 + * @returns Statistics including size, item count, hits, misses, hit rate 727 + * 728 + * @remarks 729 + * Useful for monitoring and capacity planning. 730 + * Hit rate is calculated as: hits / (hits + misses). 731 + */ 732 + async getStats(): Promise<AllTierStats> { 733 + const [hot, warm, cold] = await Promise.all([ 734 + this.config.tiers.hot?.getStats(), 735 + this.config.tiers.warm?.getStats(), 736 + this.config.tiers.cold.getStats(), 737 + ]); 738 + 739 + const totalHits = (hot?.hits ?? 0) + (warm?.hits ?? 0) + (cold?.hits ?? 0); 740 + const totalMisses = (hot?.misses ?? 0) + (warm?.misses ?? 0) + (cold?.misses ?? 0); 741 + const hitRate = totalHits + totalMisses > 0 ? totalHits / (totalHits + totalMisses) : 0; 454 742 455 - /** 456 - * Export metadata snapshot for backup or migration. 457 - * 458 - * @returns Snapshot containing all keys, metadata, and statistics 459 - * 460 - * @remarks 461 - * The snapshot includes metadata but not the actual data (data remains in tiers). 462 - * Useful for: 463 - * - Backup and restore 464 - * - Migration between storage systems 465 - * - Auditing and compliance 466 - */ 467 - async export(): Promise<StorageSnapshot> { 468 - const keys: string[] = []; 469 - const metadata: Record<string, StorageMetadata> = {}; 743 + return { 744 + ...(hot && { hot }), 745 + ...(warm && { warm }), 746 + cold, 747 + totalHits, 748 + totalMisses, 749 + hitRate, 750 + }; 751 + } 752 + 753 + /** 754 + * Clear all data from all tiers. 755 + * 756 + * @remarks 757 + * Use with extreme caution! This will delete all data in the entire storage system. 758 + * Cannot be undone. 759 + */ 760 + async clear(): Promise<void> { 761 + await Promise.all([ 762 + this.config.tiers.hot?.clear(), 763 + this.config.tiers.warm?.clear(), 764 + this.config.tiers.cold.clear(), 765 + ]); 766 + } 767 + 768 + /** 769 + * Clear a specific tier. 770 + * 771 + * @param tier - Which tier to clear 772 + * 773 + * @remarks 774 + * Useful for: 775 + * - Clearing hot tier to test warm/cold performance 776 + * - Clearing warm tier to force rebuilding from cold 777 + * - Clearing cold tier to start fresh (โš ๏ธ loses source of truth!) 778 + */ 779 + async clearTier(tier: 'hot' | 'warm' | 'cold'): Promise<void> { 780 + switch (tier) { 781 + case 'hot': 782 + await this.config.tiers.hot?.clear(); 783 + break; 784 + case 'warm': 785 + await this.config.tiers.warm?.clear(); 786 + break; 787 + case 'cold': 788 + await this.config.tiers.cold.clear(); 789 + break; 790 + } 791 + } 792 + 793 + /** 794 + * Export metadata snapshot for backup or migration. 795 + * 796 + * @returns Snapshot containing all keys, metadata, and statistics 797 + * 798 + * @remarks 799 + * The snapshot includes metadata but not the actual data (data remains in tiers). 800 + * Useful for: 801 + * - Backup and restore 802 + * - Migration between storage systems 803 + * - Auditing and compliance 804 + */ 805 + async export(): Promise<StorageSnapshot> { 806 + const keys: string[] = []; 807 + const metadata: Record<string, StorageMetadata> = {}; 470 808 471 - // Export from cold tier (source of truth) 472 - for await (const key of this.config.tiers.cold.listKeys()) { 473 - keys.push(key); 474 - const meta = await this.config.tiers.cold.getMetadata(key); 475 - if (meta) { 476 - metadata[key] = meta; 477 - } 478 - } 809 + // Export from cold tier (source of truth) 810 + for await (const key of this.config.tiers.cold.listKeys()) { 811 + keys.push(key); 812 + const meta = await this.config.tiers.cold.getMetadata(key); 813 + if (meta) { 814 + metadata[key] = meta; 815 + } 816 + } 479 817 480 - const stats = await this.getStats(); 818 + const stats = await this.getStats(); 481 819 482 - return { 483 - version: 1, 484 - exportedAt: new Date(), 485 - keys, 486 - metadata, 487 - stats, 488 - }; 489 - } 820 + return { 821 + version: 1, 822 + exportedAt: new Date(), 823 + keys, 824 + metadata, 825 + stats, 826 + }; 827 + } 490 828 491 - /** 492 - * Import metadata snapshot. 493 - * 494 - * @param snapshot - Snapshot to import 495 - * 496 - * @remarks 497 - * Validates version compatibility before importing. 498 - * Only imports metadata - assumes data already exists in cold tier. 499 - */ 500 - async import(snapshot: StorageSnapshot): Promise<void> { 501 - if (snapshot.version !== 1) { 502 - throw new Error(`Unsupported snapshot version: ${snapshot.version}`); 503 - } 829 + /** 830 + * Import metadata snapshot. 831 + * 832 + * @param snapshot - Snapshot to import 833 + * 834 + * @remarks 835 + * Validates version compatibility before importing. 836 + * Only imports metadata - assumes data already exists in cold tier. 837 + */ 838 + async import(snapshot: StorageSnapshot): Promise<void> { 839 + if (snapshot.version !== 1) { 840 + throw new Error(`Unsupported snapshot version: ${snapshot.version}`); 841 + } 504 842 505 - // Import metadata into all configured tiers 506 - for (const key of snapshot.keys) { 507 - const metadata = snapshot.metadata[key]; 508 - if (!metadata) continue; 843 + // Import metadata into all configured tiers 844 + for (const key of snapshot.keys) { 845 + const metadata = snapshot.metadata[key]; 846 + if (!metadata) continue; 509 847 510 - if (this.config.tiers.hot) { 511 - await this.config.tiers.hot.setMetadata(key, metadata); 512 - } 848 + if (this.config.tiers.hot) { 849 + await this.config.tiers.hot.setMetadata(key, metadata); 850 + } 513 851 514 - if (this.config.tiers.warm) { 515 - await this.config.tiers.warm.setMetadata(key, metadata); 516 - } 852 + if (this.config.tiers.warm) { 853 + await this.config.tiers.warm.setMetadata(key, metadata); 854 + } 517 855 518 - await this.config.tiers.cold.setMetadata(key, metadata); 519 - } 520 - } 856 + await this.config.tiers.cold.setMetadata(key, metadata); 857 + } 858 + } 521 859 522 - /** 523 - * Bootstrap hot tier from warm tier. 524 - * 525 - * @param limit - Optional limit on number of items to load 526 - * @returns Number of items loaded 527 - * 528 - * @remarks 529 - * Loads the most frequently accessed items from warm into hot. 530 - * Useful for warming up the cache after a restart. 531 - * Items are sorted by: accessCount * lastAccessed timestamp (higher is better). 532 - */ 533 - async bootstrapHot(limit?: number): Promise<number> { 534 - if (!this.config.tiers.hot || !this.config.tiers.warm) { 535 - return 0; 536 - } 860 + /** 861 + * Bootstrap hot tier from warm tier. 862 + * 863 + * @param limit - Optional limit on number of items to load 864 + * @returns Number of items loaded 865 + * 866 + * @remarks 867 + * Loads the most frequently accessed items from warm into hot. 868 + * Useful for warming up the cache after a restart. 869 + * Items are sorted by: accessCount * lastAccessed timestamp (higher is better). 870 + */ 871 + async bootstrapHot(limit?: number): Promise<number> { 872 + if (!this.config.tiers.hot || !this.config.tiers.warm) { 873 + return 0; 874 + } 537 875 538 - let loaded = 0; 539 - const keyMetadata: Array<[string, StorageMetadata]> = []; 876 + let loaded = 0; 877 + const keyMetadata: Array<[string, StorageMetadata]> = []; 540 878 541 - // Load metadata for all keys 542 - for await (const key of this.config.tiers.warm.listKeys()) { 543 - const metadata = await this.config.tiers.warm.getMetadata(key); 544 - if (metadata) { 545 - keyMetadata.push([key, metadata]); 546 - } 547 - } 879 + // Load metadata for all keys 880 + for await (const key of this.config.tiers.warm.listKeys()) { 881 + const metadata = await this.config.tiers.warm.getMetadata(key); 882 + if (metadata) { 883 + keyMetadata.push([key, metadata]); 884 + } 885 + } 548 886 549 - // Sort by access count * recency (simple scoring) 550 - keyMetadata.sort((a, b) => { 551 - const scoreA = a[1].accessCount * a[1].lastAccessed.getTime(); 552 - const scoreB = b[1].accessCount * b[1].lastAccessed.getTime(); 553 - return scoreB - scoreA; 554 - }); 887 + // Sort by access count * recency (simple scoring) 888 + keyMetadata.sort((a, b) => { 889 + const scoreA = a[1].accessCount * a[1].lastAccessed.getTime(); 890 + const scoreB = b[1].accessCount * b[1].lastAccessed.getTime(); 891 + return scoreB - scoreA; 892 + }); 555 893 556 - // Load top N keys into hot tier 557 - const keysToLoad = limit ? keyMetadata.slice(0, limit) : keyMetadata; 894 + // Load top N keys into hot tier 895 + const keysToLoad = limit ? keyMetadata.slice(0, limit) : keyMetadata; 558 896 559 - for (const [key, metadata] of keysToLoad) { 560 - const data = await this.config.tiers.warm.get(key); 561 - if (data) { 562 - await this.config.tiers.hot.set(key, data, metadata); 563 - loaded++; 564 - } 565 - } 897 + for (const [key, metadata] of keysToLoad) { 898 + const data = await this.config.tiers.warm.get(key); 899 + if (data) { 900 + await this.config.tiers.hot.set(key, data, metadata); 901 + loaded++; 902 + } 903 + } 566 904 567 - return loaded; 568 - } 905 + return loaded; 906 + } 569 907 570 - /** 571 - * Bootstrap warm tier from cold tier. 572 - * 573 - * @param options - Optional limit and date filter 574 - * @returns Number of items loaded 575 - * 576 - * @remarks 577 - * Loads recent items from cold into warm. 578 - * Useful for: 579 - * - Initial cache population 580 - * - Recovering from warm tier failure 581 - * - Migrating to a new warm tier implementation 582 - */ 583 - async bootstrapWarm(options?: { limit?: number; sinceDate?: Date }): Promise<number> { 584 - if (!this.config.tiers.warm) { 585 - return 0; 586 - } 908 + /** 909 + * Bootstrap warm tier from cold tier. 910 + * 911 + * @param options - Optional limit and date filter 912 + * @returns Number of items loaded 913 + * 914 + * @remarks 915 + * Loads recent items from cold into warm. 916 + * Useful for: 917 + * - Initial cache population 918 + * - Recovering from warm tier failure 919 + * - Migrating to a new warm tier implementation 920 + */ 921 + async bootstrapWarm(options?: { limit?: number; sinceDate?: Date }): Promise<number> { 922 + if (!this.config.tiers.warm) { 923 + return 0; 924 + } 587 925 588 - let loaded = 0; 926 + let loaded = 0; 589 927 590 - for await (const key of this.config.tiers.cold.listKeys()) { 591 - const metadata = await this.config.tiers.cold.getMetadata(key); 592 - if (!metadata) continue; 928 + for await (const key of this.config.tiers.cold.listKeys()) { 929 + const metadata = await this.config.tiers.cold.getMetadata(key); 930 + if (!metadata) continue; 593 931 594 - // Skip if too old 595 - if (options?.sinceDate && metadata.lastAccessed < options.sinceDate) { 596 - continue; 597 - } 932 + // Skip if too old 933 + if (options?.sinceDate && metadata.lastAccessed < options.sinceDate) { 934 + continue; 935 + } 598 936 599 - const data = await this.config.tiers.cold.get(key); 600 - if (data) { 601 - await this.config.tiers.warm.set(key, data, metadata); 602 - loaded++; 937 + const data = await this.config.tiers.cold.get(key); 938 + if (data) { 939 + await this.config.tiers.warm.set(key, data, metadata); 940 + loaded++; 603 941 604 - if (options?.limit && loaded >= options.limit) { 605 - break; 606 - } 607 - } 608 - } 942 + if (options?.limit && loaded >= options.limit) { 943 + break; 944 + } 945 + } 946 + } 609 947 610 - return loaded; 611 - } 948 + return loaded; 949 + } 612 950 613 - /** 614 - * Check if data has expired based on TTL. 615 - */ 616 - private isExpired(metadata: StorageMetadata): boolean { 617 - if (!metadata.ttl) return false; 618 - return Date.now() > metadata.ttl.getTime(); 619 - } 951 + /** 952 + * Check if data has expired based on TTL. 953 + */ 954 + private isExpired(metadata: StorageMetadata): boolean { 955 + if (!metadata.ttl) return false; 956 + return Date.now() > metadata.ttl.getTime(); 957 + } 620 958 621 - /** 622 - * Update access statistics for a key. 623 - */ 624 - private async updateAccessStats(key: string, tier: 'hot' | 'warm' | 'cold'): Promise<void> { 625 - const tierObj = 626 - tier === 'hot' 627 - ? this.config.tiers.hot 628 - : tier === 'warm' 629 - ? this.config.tiers.warm 630 - : this.config.tiers.cold; 959 + /** 960 + * Update access statistics for a key. 961 + */ 962 + private async updateAccessStats(key: string, tier: 'hot' | 'warm' | 'cold'): Promise<void> { 963 + const tierObj = 964 + tier === 'hot' 965 + ? this.config.tiers.hot 966 + : tier === 'warm' 967 + ? this.config.tiers.warm 968 + : this.config.tiers.cold; 631 969 632 - if (!tierObj) return; 970 + if (!tierObj) return; 633 971 634 - const metadata = await tierObj.getMetadata(key); 635 - if (metadata) { 636 - metadata.lastAccessed = new Date(); 637 - metadata.accessCount++; 638 - await tierObj.setMetadata(key, metadata); 639 - } 640 - } 972 + const metadata = await tierObj.getMetadata(key); 973 + if (metadata) { 974 + metadata.lastAccessed = new Date(); 975 + metadata.accessCount++; 976 + await tierObj.setMetadata(key, metadata); 977 + } 978 + } 641 979 642 - /** 643 - * Create metadata for new data. 644 - */ 645 - private createMetadata(key: string, data: Uint8Array, options?: SetOptions): StorageMetadata { 646 - const now = new Date(); 647 - const ttl = options?.ttl ?? this.config.defaultTTL; 980 + /** 981 + * Create metadata for new data. 982 + */ 983 + private createMetadata(key: string, data: Uint8Array, options?: SetOptions): StorageMetadata { 984 + const now = new Date(); 985 + const ttl = options?.ttl ?? this.config.defaultTTL; 648 986 649 - const metadata: StorageMetadata = { 650 - key, 651 - size: data.byteLength, 652 - createdAt: now, 653 - lastAccessed: now, 654 - accessCount: 0, 655 - compressed: this.config.compression ?? false, 656 - checksum: calculateChecksum(data), 657 - }; 987 + const metadata: StorageMetadata = { 988 + key, 989 + size: data.byteLength, 990 + createdAt: now, 991 + lastAccessed: now, 992 + accessCount: 0, 993 + compressed: this.config.compression ?? false, 994 + checksum: calculateChecksum(data), 995 + }; 658 996 659 - if (ttl) { 660 - metadata.ttl = new Date(now.getTime() + ttl); 661 - } 997 + if (ttl) { 998 + metadata.ttl = new Date(now.getTime() + ttl); 999 + } 662 1000 663 - if (options?.metadata) { 664 - metadata.customMetadata = options.metadata; 665 - } 1001 + if (options?.metadata) { 1002 + metadata.customMetadata = options.metadata; 1003 + } 666 1004 667 - return metadata; 668 - } 1005 + return metadata; 1006 + } 669 1007 670 - /** 671 - * Deserialize data, handling compression automatically. 672 - */ 673 - private async deserializeData(data: Uint8Array): Promise<unknown> { 674 - // Decompress if needed (check for gzip magic bytes) 675 - const finalData = 676 - this.config.compression && data[0] === 0x1f && data[1] === 0x8b 677 - ? await decompress(data) 678 - : data; 1008 + /** 1009 + * Deserialize data, handling compression automatically. 1010 + */ 1011 + private async deserializeData(data: Uint8Array): Promise<unknown> { 1012 + // Decompress if needed (check for gzip magic bytes) 1013 + const finalData = 1014 + this.config.compression && data[0] === 0x1f && data[1] === 0x8b 1015 + ? await decompress(data) 1016 + : data; 679 1017 680 - return this.deserialize(finalData); 681 - } 1018 + return this.deserialize(finalData); 1019 + } 682 1020 }
+26 -11
src/index.ts
··· 12 12 13 13 // Built-in tier implementations 14 14 export { MemoryStorageTier, type MemoryStorageTierConfig } from './tiers/MemoryStorageTier.js'; 15 - export { DiskStorageTier, type DiskStorageTierConfig, type EvictionPolicy } from './tiers/DiskStorageTier.js'; 15 + export { 16 + DiskStorageTier, 17 + type DiskStorageTierConfig, 18 + type EvictionPolicy, 19 + } from './tiers/DiskStorageTier.js'; 16 20 export { S3StorageTier, type S3StorageTierConfig } from './tiers/S3StorageTier.js'; 17 21 18 22 // Types 19 23 export type { 20 - StorageTier, 21 - StorageMetadata, 22 - TierStats, 23 - AllTierStats, 24 - TieredStorageConfig, 25 - SetOptions, 26 - StorageResult, 27 - SetResult, 28 - StorageSnapshot, 24 + StorageTier, 25 + StorageMetadata, 26 + TierStats, 27 + TierGetResult, 28 + TierStreamResult, 29 + AllTierStats, 30 + TieredStorageConfig, 31 + PlacementRule, 32 + SetOptions, 33 + StreamSetOptions, 34 + StorageResult, 35 + StreamResult, 36 + SetResult, 37 + StorageSnapshot, 29 38 } from './types/index.js'; 30 39 31 40 // Utilities 32 - export { compress, decompress, isGzipped } from './utils/compression.js'; 41 + export { 42 + compress, 43 + decompress, 44 + isGzipped, 45 + createCompressStream, 46 + createDecompressStream, 47 + } from './utils/compression.js'; 33 48 export { defaultSerialize, defaultDeserialize } from './utils/serialization.js'; 34 49 export { calculateChecksum, verifyChecksum } from './utils/checksum.js'; 35 50 export { encodeKey, decodeKey } from './utils/path-encoding.js';
+485 -273
src/tiers/DiskStorageTier.ts
··· 1 1 import { readFile, writeFile, unlink, readdir, stat, mkdir, rm, rename } from 'node:fs/promises'; 2 - import { existsSync } from 'node:fs'; 2 + import { existsSync, createReadStream, createWriteStream } from 'node:fs'; 3 3 import { join, dirname } from 'node:path'; 4 - import type { StorageTier, StorageMetadata, TierStats } from '../types/index.js'; 5 - import { encodeKey } from '../utils/path-encoding.js'; 4 + import { pipeline } from 'node:stream/promises'; 5 + import type { 6 + StorageTier, 7 + StorageMetadata, 8 + TierStats, 9 + TierGetResult, 10 + TierStreamResult, 11 + } from '../types/index.js'; 12 + import { encodeKey, decodeKey } from '../utils/path-encoding.js'; 6 13 7 14 /** 8 15 * Eviction policy for disk tier when size limit is reached. ··· 13 20 * Configuration for DiskStorageTier. 14 21 */ 15 22 export interface DiskStorageTierConfig { 16 - /** 17 - * Directory path where files will be stored. 18 - * 19 - * @remarks 20 - * Created automatically if it doesn't exist. 21 - * Files are stored as: `{directory}/{encoded-key}` 22 - * Metadata is stored as: `{directory}/{encoded-key}.meta` 23 - */ 24 - directory: string; 23 + /** 24 + * Directory path where files will be stored. 25 + * 26 + * @remarks 27 + * Created automatically if it doesn't exist. 28 + * Files are stored as: `{directory}/{encoded-key}` 29 + * Metadata is stored as: `{directory}/{encoded-key}.meta` 30 + */ 31 + directory: string; 25 32 26 - /** 27 - * Optional maximum size in bytes. 28 - * 29 - * @remarks 30 - * When this limit is reached, files are evicted according to the eviction policy. 31 - * If not set, no size limit is enforced (grows unbounded). 32 - */ 33 - maxSizeBytes?: number; 33 + /** 34 + * Optional maximum size in bytes. 35 + * 36 + * @remarks 37 + * When this limit is reached, files are evicted according to the eviction policy. 38 + * If not set, no size limit is enforced (grows unbounded). 39 + */ 40 + maxSizeBytes?: number; 41 + 42 + /** 43 + * Eviction policy when maxSizeBytes is reached. 44 + * 45 + * @defaultValue 'lru' 46 + * 47 + * @remarks 48 + * - 'lru': Evict least-recently-accessed files (based on metadata.lastAccessed) 49 + * - 'fifo': Evict oldest files (based on metadata.createdAt) 50 + * - 'size': Evict largest files first 51 + */ 52 + evictionPolicy?: EvictionPolicy; 34 53 35 - /** 36 - * Eviction policy when maxSizeBytes is reached. 37 - * 38 - * @defaultValue 'lru' 39 - * 40 - * @remarks 41 - * - 'lru': Evict least-recently-accessed files (based on metadata.lastAccessed) 42 - * - 'fifo': Evict oldest files (based on metadata.createdAt) 43 - * - 'size': Evict largest files first 44 - */ 45 - evictionPolicy?: EvictionPolicy; 54 + /** 55 + * Whether to encode colons in keys as %3A. 56 + * 57 + * @defaultValue true on Windows, false on Unix/macOS 58 + * 59 + * @remarks 60 + * Colons are invalid in Windows filenames but allowed on Unix. 61 + * Set to false to preserve colons for human-readable paths on Unix systems. 62 + * Set to true on Windows or for cross-platform compatibility. 63 + * 64 + * @example 65 + * ```typescript 66 + * // Unix with readable paths 67 + * new DiskStorageTier({ directory: './cache', encodeColons: false }) 68 + * // Result: cache/did:plc:abc123/site/index.html 69 + * 70 + * // Windows or cross-platform 71 + * new DiskStorageTier({ directory: './cache', encodeColons: true }) 72 + * // Result: cache/did%3Aplc%3Aabc123/site/index.html 73 + * ``` 74 + */ 75 + encodeColons?: boolean; 46 76 } 47 77 48 78 /** ··· 58 88 * File structure: 59 89 * ``` 60 90 * cache/ 61 - * โ”œโ”€โ”€ user%3A123 # Data file (encoded key) 62 - * โ”œโ”€โ”€ user%3A123.meta # Metadata JSON 63 - * โ”œโ”€โ”€ site%3Aabc%2Findex.html 64 - * โ””โ”€โ”€ site%3Aabc%2Findex.html.meta 91 + * โ”œโ”€โ”€ user%3A123/ 92 + * โ”‚ โ”œโ”€โ”€ profile # Data file (encoded key) 93 + * โ”‚ โ””โ”€โ”€ profile.meta # Metadata JSON 94 + * โ””โ”€โ”€ did%3Aplc%3Aabc/ 95 + * โ””โ”€โ”€ site/ 96 + * โ”œโ”€โ”€ index.html 97 + * โ””โ”€โ”€ index.html.meta 65 98 * ``` 66 99 * 67 100 * @example 68 101 * ```typescript 69 102 * const tier = new DiskStorageTier({ 70 - * directory: './cache', 71 - * maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB 72 - * evictionPolicy: 'lru', 103 + * directory: './cache', 104 + * maxSizeBytes: 10 * 1024 * 1024 * 1024, // 10GB 105 + * evictionPolicy: 'lru', 73 106 * }); 74 107 * 75 108 * await tier.set('key', data, metadata); ··· 77 110 * ``` 78 111 */ 79 112 export class DiskStorageTier implements StorageTier { 80 - private metadataIndex = new Map< 81 - string, 82 - { size: number; createdAt: Date; lastAccessed: Date } 83 - >(); 84 - private currentSize = 0; 113 + private metadataIndex = new Map< 114 + string, 115 + { size: number; createdAt: Date; lastAccessed: Date } 116 + >(); 117 + private currentSize = 0; 118 + private readonly encodeColons: boolean; 85 119 86 - constructor(private config: DiskStorageTierConfig) { 87 - if (!config.directory) { 88 - throw new Error('directory is required'); 89 - } 90 - if (config.maxSizeBytes !== undefined && config.maxSizeBytes <= 0) { 91 - throw new Error('maxSizeBytes must be positive'); 92 - } 120 + constructor(private config: DiskStorageTierConfig) { 121 + if (!config.directory) { 122 + throw new Error('directory is required'); 123 + } 124 + if (config.maxSizeBytes !== undefined && config.maxSizeBytes <= 0) { 125 + throw new Error('maxSizeBytes must be positive'); 126 + } 93 127 94 - void this.ensureDirectory(); 95 - void this.rebuildIndex(); 96 - } 128 + // Default: encode colons on Windows, preserve on Unix/macOS 129 + const platform = process.platform; 130 + this.encodeColons = config.encodeColons ?? platform === 'win32'; 97 131 98 - private async rebuildIndex(): Promise<void> { 99 - if (!existsSync(this.config.directory)) { 100 - return; 101 - } 132 + void this.ensureDirectory(); 133 + void this.rebuildIndex(); 134 + } 102 135 103 - const files = await readdir(this.config.directory); 136 + private async rebuildIndex(): Promise<void> { 137 + if (!existsSync(this.config.directory)) { 138 + return; 139 + } 104 140 105 - for (const file of files) { 106 - if (file.endsWith('.meta')) { 107 - continue; 108 - } 141 + await this.rebuildIndexRecursive(this.config.directory); 142 + } 109 143 110 - try { 111 - const metaPath = join(this.config.directory, `${file}.meta`); 112 - const metaContent = await readFile(metaPath, 'utf-8'); 113 - const metadata = JSON.parse(metaContent) as StorageMetadata; 114 - const filePath = join(this.config.directory, file); 115 - const fileStats = await stat(filePath); 144 + /** 145 + * Recursively rebuild index from a directory and its subdirectories. 146 + */ 147 + private async rebuildIndexRecursive(dir: string): Promise<void> { 148 + const entries = await readdir(dir, { withFileTypes: true }); 116 149 117 - this.metadataIndex.set(metadata.key, { 118 - size: fileStats.size, 119 - createdAt: new Date(metadata.createdAt), 120 - lastAccessed: new Date(metadata.lastAccessed), 121 - }); 150 + for (const entry of entries) { 151 + const fullPath = join(dir, entry.name); 122 152 123 - this.currentSize += fileStats.size; 124 - } catch { 125 - continue; 126 - } 127 - } 128 - } 153 + if (entry.isDirectory()) { 154 + await this.rebuildIndexRecursive(fullPath); 155 + } else if (!entry.name.endsWith('.meta')) { 156 + try { 157 + const metaPath = `${fullPath}.meta`; 158 + const metaContent = await readFile(metaPath, 'utf-8'); 159 + const metadata = JSON.parse(metaContent) as StorageMetadata; 160 + const fileStats = await stat(fullPath); 129 161 130 - async get(key: string): Promise<Uint8Array | null> { 131 - const filePath = this.getFilePath(key); 162 + this.metadataIndex.set(metadata.key, { 163 + size: fileStats.size, 164 + createdAt: new Date(metadata.createdAt), 165 + lastAccessed: new Date(metadata.lastAccessed), 166 + }); 132 167 133 - try { 134 - const data = await readFile(filePath); 168 + this.currentSize += fileStats.size; 169 + } catch { 170 + continue; 171 + } 172 + } 173 + } 174 + } 135 175 136 - const metadata = await this.getMetadata(key); 137 - if (metadata) { 138 - metadata.lastAccessed = new Date(); 139 - metadata.accessCount++; 140 - await this.setMetadata(key, metadata); 176 + async get(key: string): Promise<Uint8Array | null> { 177 + const filePath = this.getFilePath(key); 141 178 142 - const entry = this.metadataIndex.get(key); 143 - if (entry) { 144 - entry.lastAccessed = metadata.lastAccessed; 145 - } 146 - } 179 + try { 180 + const data = await readFile(filePath); 181 + return new Uint8Array(data); 182 + } catch (error) { 183 + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 184 + return null; 185 + } 186 + throw error; 187 + } 188 + } 147 189 148 - return new Uint8Array(data); 149 - } catch (error) { 150 - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 151 - return null; 152 - } 153 - throw error; 154 - } 155 - } 190 + /** 191 + * Retrieve data and metadata together in a single operation. 192 + * 193 + * @param key - The key to retrieve 194 + * @returns The data and metadata, or null if not found 195 + * 196 + * @remarks 197 + * Reads data and metadata files in parallel for better performance. 198 + */ 199 + async getWithMetadata(key: string): Promise<TierGetResult | null> { 200 + const filePath = this.getFilePath(key); 201 + const metaPath = this.getMetaPath(key); 156 202 157 - async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 158 - const filePath = this.getFilePath(key); 159 - const metaPath = this.getMetaPath(key); 203 + try { 204 + // Read data and metadata in parallel 205 + const [dataBuffer, metaContent] = await Promise.all([ 206 + readFile(filePath), 207 + readFile(metaPath, 'utf-8'), 208 + ]); 160 209 161 - const dir = dirname(filePath); 162 - if (!existsSync(dir)) { 163 - await mkdir(dir, { recursive: true }); 164 - } 210 + const metadata = JSON.parse(metaContent) as StorageMetadata; 165 211 166 - const existingEntry = this.metadataIndex.get(key); 167 - if (existingEntry) { 168 - this.currentSize -= existingEntry.size; 169 - } 212 + // Convert date strings back to Date objects 213 + metadata.createdAt = new Date(metadata.createdAt); 214 + metadata.lastAccessed = new Date(metadata.lastAccessed); 215 + if (metadata.ttl) { 216 + metadata.ttl = new Date(metadata.ttl); 217 + } 170 218 171 - if (this.config.maxSizeBytes) { 172 - await this.evictIfNeeded(data.byteLength); 173 - } 219 + return { data: new Uint8Array(dataBuffer), metadata }; 220 + } catch (error) { 221 + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 222 + return null; 223 + } 224 + throw error; 225 + } 226 + } 174 227 175 - const tempMetaPath = `${metaPath}.tmp`; 176 - await writeFile(tempMetaPath, JSON.stringify(metadata, null, 2)); 177 - await writeFile(filePath, data); 178 - await rename(tempMetaPath, metaPath); 228 + /** 229 + * Retrieve data as a readable stream with metadata. 230 + * 231 + * @param key - The key to retrieve 232 + * @returns A readable stream and metadata, or null if not found 233 + * 234 + * @remarks 235 + * Use this for large files to avoid loading entire content into memory. 236 + * The stream must be consumed or destroyed by the caller. 237 + */ 238 + async getStream(key: string): Promise<TierStreamResult | null> { 239 + const filePath = this.getFilePath(key); 240 + const metaPath = this.getMetaPath(key); 179 241 180 - this.metadataIndex.set(key, { 181 - size: data.byteLength, 182 - createdAt: metadata.createdAt, 183 - lastAccessed: metadata.lastAccessed, 184 - }); 185 - this.currentSize += data.byteLength; 186 - } 242 + try { 243 + // Read metadata first to verify file exists 244 + const metaContent = await readFile(metaPath, 'utf-8'); 245 + const metadata = JSON.parse(metaContent) as StorageMetadata; 187 246 188 - async delete(key: string): Promise<void> { 189 - const filePath = this.getFilePath(key); 190 - const metaPath = this.getMetaPath(key); 247 + // Convert date strings back to Date objects 248 + metadata.createdAt = new Date(metadata.createdAt); 249 + metadata.lastAccessed = new Date(metadata.lastAccessed); 250 + if (metadata.ttl) { 251 + metadata.ttl = new Date(metadata.ttl); 252 + } 191 253 192 - const entry = this.metadataIndex.get(key); 193 - if (entry) { 194 - this.currentSize -= entry.size; 195 - this.metadataIndex.delete(key); 196 - } 254 + // Create stream - will throw if file doesn't exist 255 + const stream = createReadStream(filePath); 197 256 198 - await Promise.all([ 199 - unlink(filePath).catch(() => {}), 200 - unlink(metaPath).catch(() => {}), 201 - ]); 202 - } 257 + return { stream, metadata }; 258 + } catch (error) { 259 + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 260 + return null; 261 + } 262 + throw error; 263 + } 264 + } 203 265 204 - async exists(key: string): Promise<boolean> { 205 - const filePath = this.getFilePath(key); 206 - return existsSync(filePath); 207 - } 266 + /** 267 + * Store data from a readable stream. 268 + * 269 + * @param key - The key to store under 270 + * @param stream - Readable stream of data to store 271 + * @param metadata - Metadata to store alongside the data 272 + * 273 + * @remarks 274 + * Use this for large files to avoid loading entire content into memory. 275 + * The stream will be fully consumed by this operation. 276 + */ 277 + async setStream( 278 + key: string, 279 + stream: NodeJS.ReadableStream, 280 + metadata: StorageMetadata, 281 + ): Promise<void> { 282 + const filePath = this.getFilePath(key); 283 + const metaPath = this.getMetaPath(key); 208 284 209 - async *listKeys(prefix?: string): AsyncIterableIterator<string> { 210 - if (!existsSync(this.config.directory)) { 211 - return; 212 - } 285 + const dir = dirname(filePath); 286 + if (!existsSync(dir)) { 287 + await mkdir(dir, { recursive: true }); 288 + } 213 289 214 - const files = await readdir(this.config.directory); 290 + const existingEntry = this.metadataIndex.get(key); 291 + if (existingEntry) { 292 + this.currentSize -= existingEntry.size; 293 + } 215 294 216 - for (const file of files) { 217 - // Skip metadata files 218 - if (file.endsWith('.meta')) { 219 - continue; 220 - } 295 + if (this.config.maxSizeBytes) { 296 + await this.evictIfNeeded(metadata.size); 297 + } 221 298 222 - // The file name is the encoded key 223 - // We need to read metadata to get the original key for prefix matching 224 - const metaPath = join(this.config.directory, `${file}.meta`); 225 - try { 226 - const metaContent = await readFile(metaPath, 'utf-8'); 227 - const metadata = JSON.parse(metaContent) as StorageMetadata; 228 - const originalKey = metadata.key; 299 + // Write metadata first (atomic via temp file) 300 + const tempMetaPath = `${metaPath}.tmp`; 301 + await writeFile(tempMetaPath, JSON.stringify(metadata, null, 2)); 229 302 230 - if (!prefix || originalKey.startsWith(prefix)) { 231 - yield originalKey; 232 - } 233 - } catch { 234 - // If metadata is missing or invalid, skip this file 235 - continue; 236 - } 237 - } 238 - } 303 + // Stream data to file 304 + const writeStream = createWriteStream(filePath); 305 + await pipeline(stream, writeStream); 306 + 307 + // Commit metadata 308 + await rename(tempMetaPath, metaPath); 309 + 310 + this.metadataIndex.set(key, { 311 + size: metadata.size, 312 + createdAt: metadata.createdAt, 313 + lastAccessed: metadata.lastAccessed, 314 + }); 315 + this.currentSize += metadata.size; 316 + } 317 + 318 + async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 319 + const filePath = this.getFilePath(key); 320 + const metaPath = this.getMetaPath(key); 321 + 322 + const dir = dirname(filePath); 323 + if (!existsSync(dir)) { 324 + await mkdir(dir, { recursive: true }); 325 + } 326 + 327 + const existingEntry = this.metadataIndex.get(key); 328 + if (existingEntry) { 329 + this.currentSize -= existingEntry.size; 330 + } 239 331 240 - async deleteMany(keys: string[]): Promise<void> { 241 - await Promise.all(keys.map((key) => this.delete(key))); 242 - } 332 + if (this.config.maxSizeBytes) { 333 + await this.evictIfNeeded(data.byteLength); 334 + } 243 335 244 - async getMetadata(key: string): Promise<StorageMetadata | null> { 245 - const metaPath = this.getMetaPath(key); 336 + const tempMetaPath = `${metaPath}.tmp`; 337 + await writeFile(tempMetaPath, JSON.stringify(metadata, null, 2)); 338 + await writeFile(filePath, data); 339 + await rename(tempMetaPath, metaPath); 246 340 247 - try { 248 - const content = await readFile(metaPath, 'utf-8'); 249 - const metadata = JSON.parse(content) as StorageMetadata; 341 + this.metadataIndex.set(key, { 342 + size: data.byteLength, 343 + createdAt: metadata.createdAt, 344 + lastAccessed: metadata.lastAccessed, 345 + }); 346 + this.currentSize += data.byteLength; 347 + } 250 348 251 - // Convert date strings back to Date objects 252 - metadata.createdAt = new Date(metadata.createdAt); 253 - metadata.lastAccessed = new Date(metadata.lastAccessed); 254 - if (metadata.ttl) { 255 - metadata.ttl = new Date(metadata.ttl); 256 - } 349 + async delete(key: string): Promise<void> { 350 + const filePath = this.getFilePath(key); 351 + const metaPath = this.getMetaPath(key); 257 352 258 - return metadata; 259 - } catch (error) { 260 - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 261 - return null; 262 - } 263 - throw error; 264 - } 265 - } 353 + const entry = this.metadataIndex.get(key); 354 + if (entry) { 355 + this.currentSize -= entry.size; 356 + this.metadataIndex.delete(key); 357 + } 266 358 267 - async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 268 - const metaPath = this.getMetaPath(key); 359 + await Promise.all([unlink(filePath).catch(() => {}), unlink(metaPath).catch(() => {})]); 360 + 361 + // Clean up empty parent directories 362 + await this.cleanupEmptyDirectories(dirname(filePath)); 363 + } 364 + 365 + async exists(key: string): Promise<boolean> { 366 + const filePath = this.getFilePath(key); 367 + return existsSync(filePath); 368 + } 369 + 370 + async *listKeys(prefix?: string): AsyncIterableIterator<string> { 371 + if (!existsSync(this.config.directory)) { 372 + return; 373 + } 374 + 375 + // Recursively list all files in directory tree 376 + for await (const key of this.listKeysRecursive(this.config.directory, prefix)) { 377 + yield key; 378 + } 379 + } 380 + 381 + /** 382 + * Recursively list keys from a directory and its subdirectories. 383 + */ 384 + private async *listKeysRecursive(dir: string, prefix?: string): AsyncIterableIterator<string> { 385 + const entries = await readdir(dir, { withFileTypes: true }); 386 + 387 + for (const entry of entries) { 388 + const fullPath = join(dir, entry.name); 389 + 390 + if (entry.isDirectory()) { 391 + // Recurse into subdirectory 392 + for await (const key of this.listKeysRecursive(fullPath, prefix)) { 393 + yield key; 394 + } 395 + } else if (!entry.name.endsWith('.meta')) { 396 + // Data file - read metadata to get original key 397 + const metaPath = `${fullPath}.meta`; 398 + try { 399 + const metaContent = await readFile(metaPath, 'utf-8'); 400 + const metadata = JSON.parse(metaContent) as StorageMetadata; 401 + const originalKey = metadata.key; 402 + 403 + if (!prefix || originalKey.startsWith(prefix)) { 404 + yield originalKey; 405 + } 406 + } catch { 407 + // If metadata is missing or invalid, skip this file 408 + continue; 409 + } 410 + } 411 + } 412 + } 413 + 414 + async deleteMany(keys: string[]): Promise<void> { 415 + await Promise.all(keys.map((key) => this.delete(key))); 416 + } 417 + 418 + async getMetadata(key: string): Promise<StorageMetadata | null> { 419 + const metaPath = this.getMetaPath(key); 420 + 421 + try { 422 + const content = await readFile(metaPath, 'utf-8'); 423 + const metadata = JSON.parse(content) as StorageMetadata; 424 + 425 + // Convert date strings back to Date objects 426 + metadata.createdAt = new Date(metadata.createdAt); 427 + metadata.lastAccessed = new Date(metadata.lastAccessed); 428 + if (metadata.ttl) { 429 + metadata.ttl = new Date(metadata.ttl); 430 + } 431 + 432 + return metadata; 433 + } catch (error) { 434 + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { 435 + return null; 436 + } 437 + throw error; 438 + } 439 + } 440 + 441 + async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 442 + const metaPath = this.getMetaPath(key); 443 + 444 + // Ensure parent directory exists 445 + const dir = dirname(metaPath); 446 + if (!existsSync(dir)) { 447 + await mkdir(dir, { recursive: true }); 448 + } 449 + 450 + await writeFile(metaPath, JSON.stringify(metadata, null, 2)); 451 + } 452 + 453 + async getStats(): Promise<TierStats> { 454 + if (!existsSync(this.config.directory)) { 455 + return { bytes: 0, items: 0 }; 456 + } 269 457 270 - // Ensure parent directory exists 271 - const dir = dirname(metaPath); 272 - if (!existsSync(dir)) { 273 - await mkdir(dir, { recursive: true }); 274 - } 458 + return this.getStatsRecursive(this.config.directory); 459 + } 275 460 276 - await writeFile(metaPath, JSON.stringify(metadata, null, 2)); 277 - } 461 + /** 462 + * Recursively collect stats from a directory and its subdirectories. 463 + */ 464 + private async getStatsRecursive(dir: string): Promise<TierStats> { 465 + let bytes = 0; 466 + let items = 0; 278 467 279 - async getStats(): Promise<TierStats> { 280 - let bytes = 0; 281 - let items = 0; 468 + const entries = await readdir(dir, { withFileTypes: true }); 282 469 283 - if (!existsSync(this.config.directory)) { 284 - return { bytes: 0, items: 0 }; 285 - } 470 + for (const entry of entries) { 471 + const fullPath = join(dir, entry.name); 286 472 287 - const files = await readdir(this.config.directory); 473 + if (entry.isDirectory()) { 474 + const subStats = await this.getStatsRecursive(fullPath); 475 + bytes += subStats.bytes; 476 + items += subStats.items; 477 + } else if (!entry.name.endsWith('.meta')) { 478 + const fileStats = await stat(fullPath); 479 + bytes += fileStats.size; 480 + items++; 481 + } 482 + } 288 483 289 - for (const file of files) { 290 - if (file.endsWith('.meta')) { 291 - continue; 292 - } 484 + return { bytes, items }; 485 + } 293 486 294 - const filePath = join(this.config.directory, file); 295 - const stats = await stat(filePath); 296 - bytes += stats.size; 297 - items++; 298 - } 487 + async clear(): Promise<void> { 488 + if (existsSync(this.config.directory)) { 489 + await rm(this.config.directory, { recursive: true, force: true }); 490 + await this.ensureDirectory(); 491 + this.metadataIndex.clear(); 492 + this.currentSize = 0; 493 + } 494 + } 299 495 300 - return { bytes, items }; 301 - } 496 + /** 497 + * Clean up empty parent directories after file deletion. 498 + * 499 + * @param dirPath - Directory path to start cleanup from 500 + * 501 + * @remarks 502 + * Recursively removes empty directories up to (but not including) the base directory. 503 + * This prevents directory bloat when files with nested paths are deleted. 504 + */ 505 + private async cleanupEmptyDirectories(dirPath: string): Promise<void> { 506 + // Don't remove the base directory 507 + if (dirPath === this.config.directory || !dirPath.startsWith(this.config.directory)) { 508 + return; 509 + } 302 510 303 - async clear(): Promise<void> { 304 - if (existsSync(this.config.directory)) { 305 - await rm(this.config.directory, { recursive: true, force: true }); 306 - await this.ensureDirectory(); 307 - this.metadataIndex.clear(); 308 - this.currentSize = 0; 309 - } 310 - } 511 + try { 512 + const entries = await readdir(dirPath); 513 + // If directory is empty, remove it and recurse to parent 514 + if (entries.length === 0) { 515 + await rm(dirPath, { recursive: false }); 516 + await this.cleanupEmptyDirectories(dirname(dirPath)); 517 + } 518 + } catch { 519 + // Directory doesn't exist or can't be read - that's fine 520 + return; 521 + } 522 + } 311 523 312 - /** 313 - * Get the filesystem path for a key's data file. 314 - */ 315 - private getFilePath(key: string): string { 316 - const encoded = encodeKey(key); 317 - return join(this.config.directory, encoded); 318 - } 524 + /** 525 + * Get the filesystem path for a key's data file. 526 + */ 527 + private getFilePath(key: string): string { 528 + const encoded = encodeKey(key, this.encodeColons); 529 + return join(this.config.directory, encoded); 530 + } 319 531 320 - /** 321 - * Get the filesystem path for a key's metadata file. 322 - */ 323 - private getMetaPath(key: string): string { 324 - return `${this.getFilePath(key)}.meta`; 325 - } 532 + /** 533 + * Get the filesystem path for a key's metadata file. 534 + */ 535 + private getMetaPath(key: string): string { 536 + return `${this.getFilePath(key)}.meta`; 537 + } 326 538 327 - private async ensureDirectory(): Promise<void> { 328 - await mkdir(this.config.directory, { recursive: true }).catch(() => {}); 329 - } 539 + private async ensureDirectory(): Promise<void> { 540 + await mkdir(this.config.directory, { recursive: true }).catch(() => {}); 541 + } 330 542 331 - private async evictIfNeeded(incomingSize: number): Promise<void> { 332 - if (!this.config.maxSizeBytes) { 333 - return; 334 - } 543 + private async evictIfNeeded(incomingSize: number): Promise<void> { 544 + if (!this.config.maxSizeBytes) { 545 + return; 546 + } 335 547 336 - if (this.currentSize + incomingSize <= this.config.maxSizeBytes) { 337 - return; 338 - } 548 + if (this.currentSize + incomingSize <= this.config.maxSizeBytes) { 549 + return; 550 + } 339 551 340 - const entries = Array.from(this.metadataIndex.entries()).map(([key, info]) => ({ 341 - key, 342 - ...info, 343 - })); 552 + const entries = Array.from(this.metadataIndex.entries()).map(([key, info]) => ({ 553 + key, 554 + ...info, 555 + })); 344 556 345 - const policy = this.config.evictionPolicy ?? 'lru'; 346 - entries.sort((a, b) => { 347 - switch (policy) { 348 - case 'lru': 349 - return a.lastAccessed.getTime() - b.lastAccessed.getTime(); 350 - case 'fifo': 351 - return a.createdAt.getTime() - b.createdAt.getTime(); 352 - case 'size': 353 - return b.size - a.size; 354 - default: 355 - return 0; 356 - } 357 - }); 557 + const policy = this.config.evictionPolicy ?? 'lru'; 558 + entries.sort((a, b) => { 559 + switch (policy) { 560 + case 'lru': 561 + return a.lastAccessed.getTime() - b.lastAccessed.getTime(); 562 + case 'fifo': 563 + return a.createdAt.getTime() - b.createdAt.getTime(); 564 + case 'size': 565 + return b.size - a.size; 566 + default: 567 + return 0; 568 + } 569 + }); 358 570 359 - for (const entry of entries) { 360 - if (this.currentSize + incomingSize <= this.config.maxSizeBytes) { 361 - break; 362 - } 571 + for (const entry of entries) { 572 + if (this.currentSize + incomingSize <= this.config.maxSizeBytes) { 573 + break; 574 + } 363 575 364 - await this.delete(entry.key); 365 - } 366 - } 576 + await this.delete(entry.key); 577 + } 578 + } 367 579 }
+233 -142
src/tiers/MemoryStorageTier.ts
··· 1 - import { lru } from 'tiny-lru'; 2 - import type { StorageTier, StorageMetadata, TierStats } from '../types/index.js'; 1 + import { lru, type LRU } from 'tiny-lru'; 2 + import { Readable } from 'node:stream'; 3 + import type { 4 + StorageTier, 5 + StorageMetadata, 6 + TierStats, 7 + TierGetResult, 8 + TierStreamResult, 9 + } from '../types/index.js'; 3 10 4 11 interface CacheEntry { 5 - data: Uint8Array; 6 - metadata: StorageMetadata; 7 - size: number; 12 + data: Uint8Array; 13 + metadata: StorageMetadata; 14 + size: number; 8 15 } 9 16 10 17 /** 11 18 * Configuration for MemoryStorageTier. 12 19 */ 13 20 export interface MemoryStorageTierConfig { 14 - /** 15 - * Maximum total size in bytes. 16 - * 17 - * @remarks 18 - * When this limit is reached, least-recently-used entries are evicted. 19 - */ 20 - maxSizeBytes: number; 21 + /** 22 + * Maximum total size in bytes. 23 + * 24 + * @remarks 25 + * When this limit is reached, least-recently-used entries are evicted. 26 + */ 27 + maxSizeBytes: number; 21 28 22 - /** 23 - * Maximum number of items. 24 - * 25 - * @remarks 26 - * When this limit is reached, least-recently-used entries are evicted. 27 - * Useful for limiting memory usage when items have variable sizes. 28 - */ 29 - maxItems?: number; 29 + /** 30 + * Maximum number of items. 31 + * 32 + * @remarks 33 + * When this limit is reached, least-recently-used entries are evicted. 34 + * Useful for limiting memory usage when items have variable sizes. 35 + */ 36 + maxItems?: number; 30 37 } 31 38 32 39 /** ··· 42 49 * @example 43 50 * ```typescript 44 51 * const tier = new MemoryStorageTier({ 45 - * maxSizeBytes: 100 * 1024 * 1024, // 100MB 46 - * maxItems: 1000, 52 + * maxSizeBytes: 100 * 1024 * 1024, // 100MB 53 + * maxItems: 1000, 47 54 * }); 48 55 * 49 56 * await tier.set('key', data, metadata); ··· 51 58 * ``` 52 59 */ 53 60 export class MemoryStorageTier implements StorageTier { 54 - private cache: ReturnType<typeof lru<CacheEntry>>; 55 - private currentSize = 0; 56 - private stats = { 57 - hits: 0, 58 - misses: 0, 59 - evictions: 0, 60 - }; 61 + private cache: LRU<CacheEntry>; 62 + private currentSize = 0; 63 + private stats = { 64 + hits: 0, 65 + misses: 0, 66 + evictions: 0, 67 + }; 61 68 62 - constructor(private config: MemoryStorageTierConfig) { 63 - if (config.maxSizeBytes <= 0) { 64 - throw new Error('maxSizeBytes must be positive'); 65 - } 66 - if (config.maxItems !== undefined && config.maxItems <= 0) { 67 - throw new Error('maxItems must be positive'); 68 - } 69 + constructor(private config: MemoryStorageTierConfig) { 70 + if (config.maxSizeBytes <= 0) { 71 + throw new Error('maxSizeBytes must be positive'); 72 + } 73 + if (config.maxItems !== undefined && config.maxItems <= 0) { 74 + throw new Error('maxItems must be positive'); 75 + } 69 76 70 - // Initialize TinyLRU with max items (we'll handle size limits separately) 71 - const maxItems = config.maxItems ?? 10000; // Default to 10k items if not specified 72 - this.cache = lru<CacheEntry>(maxItems); 73 - } 77 + // Initialize TinyLRU with max items (we'll handle size limits separately) 78 + const maxItems = config.maxItems ?? 10000; // Default to 10k items if not specified 79 + this.cache = lru<CacheEntry>(maxItems); 80 + } 74 81 75 - async get(key: string): Promise<Uint8Array | null> { 76 - const entry = this.cache.get(key); 82 + async get(key: string): Promise<Uint8Array | null> { 83 + const entry = this.cache.get(key); 77 84 78 - if (!entry) { 79 - this.stats.misses++; 80 - return null; 81 - } 85 + if (!entry) { 86 + this.stats.misses++; 87 + return null; 88 + } 89 + 90 + this.stats.hits++; 91 + return entry.data; 92 + } 93 + 94 + /** 95 + * Retrieve data and metadata together in a single cache lookup. 96 + * 97 + * @param key - The key to retrieve 98 + * @returns The data and metadata, or null if not found 99 + */ 100 + async getWithMetadata(key: string): Promise<TierGetResult | null> { 101 + const entry = this.cache.get(key); 102 + 103 + if (!entry) { 104 + this.stats.misses++; 105 + return null; 106 + } 107 + 108 + this.stats.hits++; 109 + return { data: entry.data, metadata: entry.metadata }; 110 + } 111 + 112 + /** 113 + * Retrieve data as a readable stream with metadata. 114 + * 115 + * @param key - The key to retrieve 116 + * @returns A readable stream and metadata, or null if not found 117 + * 118 + * @remarks 119 + * Creates a readable stream from the in-memory data. 120 + * Note that for memory tier, data is already in memory, so this 121 + * provides API consistency rather than memory savings. 122 + */ 123 + async getStream(key: string): Promise<TierStreamResult | null> { 124 + const entry = this.cache.get(key); 125 + 126 + if (!entry) { 127 + this.stats.misses++; 128 + return null; 129 + } 82 130 83 - this.stats.hits++; 84 - return entry.data; 85 - } 131 + this.stats.hits++; 132 + 133 + // Create a readable stream from the buffer 134 + const stream = Readable.from([entry.data]); 135 + 136 + return { stream, metadata: entry.metadata }; 137 + } 138 + 139 + /** 140 + * Store data from a readable stream. 141 + * 142 + * @param key - The key to store under 143 + * @param stream - Readable stream of data to store 144 + * @param metadata - Metadata to store alongside the data 145 + * 146 + * @remarks 147 + * Buffers the stream into memory. For memory tier, this is unavoidable 148 + * since the tier stores data in memory. Use disk or S3 tiers for 149 + * truly streaming large file handling. 150 + */ 151 + async setStream( 152 + key: string, 153 + stream: NodeJS.ReadableStream, 154 + metadata: StorageMetadata, 155 + ): Promise<void> { 156 + const chunks: Uint8Array[] = []; 157 + 158 + for await (const chunk of stream) { 159 + if (Buffer.isBuffer(chunk)) { 160 + chunks.push(new Uint8Array(chunk)); 161 + } else if (ArrayBuffer.isView(chunk)) { 162 + chunks.push(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)); 163 + } else if (typeof chunk === 'string') { 164 + chunks.push(new TextEncoder().encode(chunk)); 165 + } 166 + } 167 + 168 + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); 169 + const data = new Uint8Array(totalLength); 170 + let offset = 0; 171 + for (const chunk of chunks) { 172 + data.set(chunk, offset); 173 + offset += chunk.length; 174 + } 175 + 176 + await this.set(key, data, metadata); 177 + } 86 178 87 - async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 88 - const size = data.byteLength; 179 + async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 180 + const size = data.byteLength; 89 181 90 - // Check existing entry for size accounting 91 - const existing = this.cache.get(key); 92 - if (existing) { 93 - this.currentSize -= existing.size; 94 - } 182 + // Check existing entry for size accounting 183 + const existing = this.cache.get(key); 184 + if (existing) { 185 + this.currentSize -= existing.size; 186 + } 95 187 96 - // Evict entries until we have space for the new entry 97 - await this.evictIfNeeded(size); 188 + // Evict entries until we have space for the new entry 189 + await this.evictIfNeeded(size); 98 190 99 - // Add new entry 100 - const entry: CacheEntry = { data, metadata, size }; 101 - this.cache.set(key, entry); 102 - this.currentSize += size; 103 - } 191 + // Add new entry 192 + const entry: CacheEntry = { data, metadata, size }; 193 + this.cache.set(key, entry); 194 + this.currentSize += size; 195 + } 104 196 105 - async delete(key: string): Promise<void> { 106 - const entry = this.cache.get(key); 107 - if (entry) { 108 - this.cache.delete(key); 109 - this.currentSize -= entry.size; 110 - } 111 - } 197 + async delete(key: string): Promise<void> { 198 + const entry = this.cache.get(key); 199 + if (entry) { 200 + this.cache.delete(key); 201 + this.currentSize -= entry.size; 202 + } 203 + } 112 204 113 - async exists(key: string): Promise<boolean> { 114 - return this.cache.has(key); 115 - } 205 + async exists(key: string): Promise<boolean> { 206 + return this.cache.has(key); 207 + } 116 208 117 - async *listKeys(prefix?: string): AsyncIterableIterator<string> { 118 - // TinyLRU doesn't expose keys(), so we need to track them separately 119 - // For now, we'll use the cache's internal structure 120 - const keys = this.cache.keys(); 121 - for (const key of keys) { 122 - if (!prefix || key.startsWith(prefix)) { 123 - yield key; 124 - } 125 - } 126 - } 209 + async *listKeys(prefix?: string): AsyncIterableIterator<string> { 210 + // TinyLRU returns keys as any[] but they are strings in our usage 211 + const keys = this.cache.keys() as string[]; 212 + for (const key of keys) { 213 + if (!prefix || key.startsWith(prefix)) { 214 + yield key; 215 + } 216 + } 217 + } 127 218 128 - async deleteMany(keys: string[]): Promise<void> { 129 - for (const key of keys) { 130 - await this.delete(key); 131 - } 132 - } 219 + async deleteMany(keys: string[]): Promise<void> { 220 + for (const key of keys) { 221 + await this.delete(key); 222 + } 223 + } 133 224 134 - async getMetadata(key: string): Promise<StorageMetadata | null> { 135 - const entry = this.cache.get(key); 136 - return entry ? entry.metadata : null; 137 - } 225 + async getMetadata(key: string): Promise<StorageMetadata | null> { 226 + const entry = this.cache.get(key); 227 + return entry ? entry.metadata : null; 228 + } 138 229 139 - async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 140 - const entry = this.cache.get(key); 141 - if (entry) { 142 - // Update metadata in place 143 - entry.metadata = metadata; 144 - // Re-set to mark as recently used 145 - this.cache.set(key, entry); 146 - } 147 - } 230 + async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 231 + const entry = this.cache.get(key); 232 + if (entry) { 233 + // Update metadata in place 234 + entry.metadata = metadata; 235 + // Re-set to mark as recently used 236 + this.cache.set(key, entry); 237 + } 238 + } 148 239 149 - async getStats(): Promise<TierStats> { 150 - return { 151 - bytes: this.currentSize, 152 - items: this.cache.size, 153 - hits: this.stats.hits, 154 - misses: this.stats.misses, 155 - evictions: this.stats.evictions, 156 - }; 157 - } 240 + async getStats(): Promise<TierStats> { 241 + return { 242 + bytes: this.currentSize, 243 + items: this.cache.size, 244 + hits: this.stats.hits, 245 + misses: this.stats.misses, 246 + evictions: this.stats.evictions, 247 + }; 248 + } 158 249 159 - async clear(): Promise<void> { 160 - this.cache.clear(); 161 - this.currentSize = 0; 162 - } 250 + async clear(): Promise<void> { 251 + this.cache.clear(); 252 + this.currentSize = 0; 253 + } 163 254 164 - /** 165 - * Evict least-recently-used entries until there's space for new data. 166 - * 167 - * @param incomingSize - Size of data being added 168 - * 169 - * @remarks 170 - * TinyLRU handles count-based eviction automatically. 171 - * This method handles size-based eviction by using TinyLRU's built-in evict() method, 172 - * which properly removes the LRU item without updating access order. 173 - */ 174 - private async evictIfNeeded(incomingSize: number): Promise<void> { 175 - // Keep evicting until we have enough space 176 - while (this.currentSize + incomingSize > this.config.maxSizeBytes && this.cache.size > 0) { 177 - // Get the LRU key (first in the list) without accessing it 178 - const keys = this.cache.keys(); 179 - if (keys.length === 0) break; 255 + /** 256 + * Evict least-recently-used entries until there's space for new data. 257 + * 258 + * @param incomingSize - Size of data being added 259 + * 260 + * @remarks 261 + * TinyLRU handles count-based eviction automatically. 262 + * This method handles size-based eviction by using TinyLRU's built-in evict() method, 263 + * which properly removes the LRU item without updating access order. 264 + */ 265 + private async evictIfNeeded(incomingSize: number): Promise<void> { 266 + // Keep evicting until we have enough space 267 + while (this.currentSize + incomingSize > this.config.maxSizeBytes && this.cache.size > 0) { 268 + // Get the LRU key (first in the list) without accessing it 269 + const keys = this.cache.keys() as string[]; 270 + if (keys.length === 0) break; 180 271 181 - const lruKey = keys[0]; 182 - if (!lruKey) break; 272 + const lruKey = keys[0]; 273 + if (!lruKey) break; 183 274 184 - // Access the entry directly from internal items without triggering LRU update 185 - // TinyLRU exposes items as a public property for this purpose 186 - const entry = (this.cache as any).items[lruKey] as CacheEntry | undefined; 187 - if (!entry) break; 275 + // Access the entry directly from internal items without triggering LRU update 276 + // items is a public property in LRU interface for this purpose 277 + const entry = this.cache.items[lruKey]?.value; 278 + if (!entry) break; 188 279 189 - // Use TinyLRU's built-in evict() which properly removes the LRU item 190 - this.cache.evict(); 191 - this.currentSize -= entry.size; 192 - this.stats.evictions++; 193 - } 194 - } 280 + // Use TinyLRU's built-in evict() which properly removes the LRU item 281 + this.cache.evict(); 282 + this.currentSize -= entry.size; 283 + this.stats.evictions++; 284 + } 285 + } 195 286 }
+670 -456
src/tiers/S3StorageTier.ts
··· 1 1 import { 2 - S3Client, 3 - PutObjectCommand, 4 - GetObjectCommand, 5 - DeleteObjectCommand, 6 - HeadObjectCommand, 7 - ListObjectsV2Command, 8 - DeleteObjectsCommand, 9 - CopyObjectCommand, 10 - type S3ClientConfig, 2 + S3Client, 3 + PutObjectCommand, 4 + GetObjectCommand, 5 + DeleteObjectCommand, 6 + HeadObjectCommand, 7 + ListObjectsV2Command, 8 + DeleteObjectsCommand, 9 + CopyObjectCommand, 10 + type S3ClientConfig, 11 11 } from '@aws-sdk/client-s3'; 12 + import { Upload } from '@aws-sdk/lib-storage'; 12 13 import type { Readable } from 'node:stream'; 13 - import type { StorageTier, StorageMetadata, TierStats } from '../types/index.js'; 14 + import type { 15 + StorageTier, 16 + StorageMetadata, 17 + TierStats, 18 + TierGetResult, 19 + TierStreamResult, 20 + } from '../types/index.js'; 14 21 15 22 /** 16 23 * Configuration for S3StorageTier. 17 24 */ 18 25 export interface S3StorageTierConfig { 19 - /** 20 - * S3 bucket name. 21 - */ 22 - bucket: string; 26 + /** 27 + * S3 bucket name. 28 + */ 29 + bucket: string; 23 30 24 - /** 25 - * AWS region. 26 - */ 27 - region: string; 31 + /** 32 + * AWS region. 33 + */ 34 + region: string; 28 35 29 - /** 30 - * Optional S3-compatible endpoint (for R2, Minio, etc.). 31 - * 32 - * @example 'https://s3.us-east-1.amazonaws.com' 33 - * @example 'https://account-id.r2.cloudflarestorage.com' 34 - */ 35 - endpoint?: string; 36 + /** 37 + * Optional S3-compatible endpoint (for R2, Minio, etc.). 38 + * 39 + * @example 'https://s3.us-east-1.amazonaws.com' 40 + * @example 'https://account-id.r2.cloudflarestorage.com' 41 + */ 42 + endpoint?: string; 36 43 37 - /** 38 - * Optional AWS credentials. 39 - * 40 - * @remarks 41 - * If not provided, uses the default AWS credential chain 42 - * (environment variables, ~/.aws/credentials, IAM roles, etc.) 43 - */ 44 - credentials?: { 45 - accessKeyId: string; 46 - secretAccessKey: string; 47 - }; 44 + /** 45 + * Optional AWS credentials. 46 + * 47 + * @remarks 48 + * If not provided, uses the default AWS credential chain 49 + * (environment variables, ~/.aws/credentials, IAM roles, etc.) 50 + */ 51 + credentials?: { 52 + accessKeyId: string; 53 + secretAccessKey: string; 54 + }; 48 55 49 - /** 50 - * Optional key prefix for namespacing. 51 - * 52 - * @remarks 53 - * All keys will be prefixed with this value. 54 - * Useful for multi-tenant scenarios or organizing data. 55 - * 56 - * @example 'tiered-storage/' 57 - */ 58 - prefix?: string; 56 + /** 57 + * Optional key prefix for namespacing. 58 + * 59 + * @remarks 60 + * All keys will be prefixed with this value. 61 + * Useful for multi-tenant scenarios or organizing data. 62 + * 63 + * @example 'tiered-storage/' 64 + */ 65 + prefix?: string; 59 66 60 - /** 61 - * Force path-style addressing for S3-compatible services. 62 - * 63 - * @defaultValue true 64 - * 65 - * @remarks 66 - * Most S3-compatible services (MinIO, R2, etc.) require path-style URLs. 67 - * AWS S3 uses virtual-hosted-style by default, but path-style also works. 68 - * 69 - * - true: `https://endpoint.com/bucket/key` (path-style) 70 - * - false: `https://bucket.endpoint.com/key` (virtual-hosted-style) 71 - */ 72 - forcePathStyle?: boolean; 67 + /** 68 + * Force path-style addressing for S3-compatible services. 69 + * 70 + * @defaultValue true 71 + * 72 + * @remarks 73 + * Most S3-compatible services (MinIO, R2, etc.) require path-style URLs. 74 + * AWS S3 uses virtual-hosted-style by default, but path-style also works. 75 + * 76 + * - true: `https://endpoint.com/bucket/key` (path-style) 77 + * - false: `https://bucket.endpoint.com/key` (virtual-hosted-style) 78 + */ 79 + forcePathStyle?: boolean; 73 80 74 - /** 75 - * Optional separate bucket for storing metadata. 76 - * 77 - * @remarks 78 - * **RECOMMENDED for production use!** 79 - * 80 - * By default, metadata is stored in S3 object metadata fields. However, updating 81 - * metadata requires copying the entire object, which is slow and expensive for large files. 82 - * 83 - * When `metadataBucket` is specified, metadata is stored as separate JSON objects 84 - * in this bucket. This allows fast, cheap metadata updates without copying data. 85 - * 86 - * **Benefits:** 87 - * - Fast metadata updates (no object copying) 88 - * - Much cheaper for large objects 89 - * - No impact on data object performance 90 - * 91 - * **Trade-offs:** 92 - * - Requires managing two buckets 93 - * - Metadata and data could become out of sync if not handled carefully 94 - * - Additional S3 API calls for metadata operations 95 - * 96 - * @example 97 - * ```typescript 98 - * const tier = new S3StorageTier({ 99 - * bucket: 'my-data-bucket', 100 - * metadataBucket: 'my-metadata-bucket', // Separate bucket for metadata 101 - * region: 'us-east-1', 102 - * }); 103 - * ``` 104 - */ 105 - metadataBucket?: string; 81 + /** 82 + * Optional separate bucket for storing metadata. 83 + * 84 + * @remarks 85 + * **RECOMMENDED for production use!** 86 + * 87 + * By default, metadata is stored in S3 object metadata fields. However, updating 88 + * metadata requires copying the entire object, which is slow and expensive for large files. 89 + * 90 + * When `metadataBucket` is specified, metadata is stored as separate JSON objects 91 + * in this bucket. This allows fast, cheap metadata updates without copying data. 92 + * 93 + * **Benefits:** 94 + * - Fast metadata updates (no object copying) 95 + * - Much cheaper for large objects 96 + * - No impact on data object performance 97 + * 98 + * **Trade-offs:** 99 + * - Requires managing two buckets 100 + * - Metadata and data could become out of sync if not handled carefully 101 + * - Additional S3 API calls for metadata operations 102 + * 103 + * @example 104 + * ```typescript 105 + * const tier = new S3StorageTier({ 106 + * bucket: 'my-data-bucket', 107 + * metadataBucket: 'my-metadata-bucket', // Separate bucket for metadata 108 + * region: 'us-east-1', 109 + * }); 110 + * ``` 111 + */ 112 + metadataBucket?: string; 106 113 } 107 114 108 115 /** ··· 122 129 * @example 123 130 * ```typescript 124 131 * const tier = new S3StorageTier({ 125 - * bucket: 'my-bucket', 126 - * region: 'us-east-1', 127 - * credentials: { 128 - * accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 129 - * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 130 - * }, 131 - * prefix: 'cache/', 132 + * bucket: 'my-bucket', 133 + * region: 'us-east-1', 134 + * credentials: { 135 + * accessKeyId: process.env.AWS_ACCESS_KEY_ID!, 136 + * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, 137 + * }, 138 + * prefix: 'cache/', 132 139 * }); 133 140 * ``` 134 141 * 135 142 * @example Cloudflare R2 136 143 * ```typescript 137 144 * const tier = new S3StorageTier({ 138 - * bucket: 'my-bucket', 139 - * region: 'auto', 140 - * endpoint: 'https://account-id.r2.cloudflarestorage.com', 141 - * credentials: { 142 - * accessKeyId: process.env.R2_ACCESS_KEY_ID!, 143 - * secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, 144 - * }, 145 + * bucket: 'my-bucket', 146 + * region: 'auto', 147 + * endpoint: 'https://account-id.r2.cloudflarestorage.com', 148 + * credentials: { 149 + * accessKeyId: process.env.R2_ACCESS_KEY_ID!, 150 + * secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, 151 + * }, 145 152 * }); 146 153 * ``` 147 154 */ 148 155 export class S3StorageTier implements StorageTier { 149 - private client: S3Client; 150 - private prefix: string; 151 - private metadataBucket?: string; 156 + private client: S3Client; 157 + private prefix: string; 158 + private metadataBucket?: string; 152 159 153 - constructor(private config: S3StorageTierConfig) { 154 - const clientConfig: S3ClientConfig = { 155 - region: config.region, 156 - // Most S3-compatible services need path-style URLs 157 - forcePathStyle: config.forcePathStyle ?? true, 158 - ...(config.endpoint && { endpoint: config.endpoint }), 159 - ...(config.credentials && { credentials: config.credentials }), 160 - }; 160 + constructor(private config: S3StorageTierConfig) { 161 + const clientConfig: S3ClientConfig = { 162 + region: config.region, 163 + // Most S3-compatible services need path-style URLs 164 + forcePathStyle: config.forcePathStyle ?? true, 165 + ...(config.endpoint && { endpoint: config.endpoint }), 166 + ...(config.credentials && { credentials: config.credentials }), 167 + }; 161 168 162 - this.client = new S3Client(clientConfig); 163 - this.prefix = config.prefix ?? ''; 164 - if (config.metadataBucket) { 165 - this.metadataBucket = config.metadataBucket; 166 - } 167 - } 169 + this.client = new S3Client(clientConfig); 170 + this.prefix = config.prefix ?? ''; 171 + if (config.metadataBucket) { 172 + this.metadataBucket = config.metadataBucket; 173 + } 174 + } 168 175 169 - async get(key: string): Promise<Uint8Array | null> { 170 - try { 171 - const command = new GetObjectCommand({ 172 - Bucket: this.config.bucket, 173 - Key: this.getS3Key(key), 174 - }); 176 + async get(key: string): Promise<Uint8Array | null> { 177 + try { 178 + const command = new GetObjectCommand({ 179 + Bucket: this.config.bucket, 180 + Key: this.getS3Key(key), 181 + }); 175 182 176 - const response = await this.client.send(command); 183 + const response = await this.client.send(command); 177 184 178 - if (!response.Body) { 179 - return null; 180 - } 185 + if (!response.Body) { 186 + return null; 187 + } 181 188 182 - return await this.streamToUint8Array(response.Body as Readable); 183 - } catch (error) { 184 - if (this.isNoSuchKeyError(error)) { 185 - return null; 186 - } 187 - throw error; 188 - } 189 - } 189 + return await this.streamToUint8Array(response.Body as Readable); 190 + } catch (error) { 191 + if (this.isNoSuchKeyError(error)) { 192 + return null; 193 + } 194 + throw error; 195 + } 196 + } 190 197 191 - private async streamToUint8Array(stream: Readable): Promise<Uint8Array> { 192 - const chunks: Uint8Array[] = []; 198 + /** 199 + * Retrieve data and metadata together in a single operation. 200 + * 201 + * @param key - The key to retrieve 202 + * @returns The data and metadata, or null if not found 203 + * 204 + * @remarks 205 + * When using a separate metadata bucket, fetches data and metadata in parallel. 206 + * Otherwise, uses the data object's embedded metadata. 207 + */ 208 + async getWithMetadata(key: string): Promise<TierGetResult | null> { 209 + const s3Key = this.getS3Key(key); 193 210 194 - for await (const chunk of stream) { 195 - if (Buffer.isBuffer(chunk)) { 196 - chunks.push(new Uint8Array(chunk)); 197 - } else if (chunk instanceof Uint8Array) { 198 - chunks.push(chunk); 199 - } else { 200 - throw new Error('Unexpected chunk type in S3 stream'); 201 - } 202 - } 211 + try { 212 + if (this.metadataBucket) { 213 + // Fetch data and metadata in parallel 214 + const [dataResponse, metadataResponse] = await Promise.all([ 215 + this.client.send( 216 + new GetObjectCommand({ 217 + Bucket: this.config.bucket, 218 + Key: s3Key, 219 + }), 220 + ), 221 + this.client.send( 222 + new GetObjectCommand({ 223 + Bucket: this.metadataBucket, 224 + Key: s3Key + '.meta', 225 + }), 226 + ), 227 + ]); 203 228 204 - const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); 205 - const result = new Uint8Array(totalLength); 206 - let offset = 0; 207 - for (const chunk of chunks) { 208 - result.set(chunk, offset); 209 - offset += chunk.length; 210 - } 229 + if (!dataResponse.Body || !metadataResponse.Body) { 230 + return null; 231 + } 211 232 212 - return result; 213 - } 233 + const [data, metaBuffer] = await Promise.all([ 234 + this.streamToUint8Array(dataResponse.Body as Readable), 235 + this.streamToUint8Array(metadataResponse.Body as Readable), 236 + ]); 214 237 215 - private isNoSuchKeyError(error: unknown): boolean { 216 - return ( 217 - typeof error === 'object' && 218 - error !== null && 219 - 'name' in error && 220 - (error.name === 'NoSuchKey' || error.name === 'NotFound') 221 - ); 222 - } 238 + const json = new TextDecoder().decode(metaBuffer); 239 + const metadata = JSON.parse(json) as StorageMetadata; 240 + metadata.createdAt = new Date(metadata.createdAt); 241 + metadata.lastAccessed = new Date(metadata.lastAccessed); 242 + if (metadata.ttl) { 243 + metadata.ttl = new Date(metadata.ttl); 244 + } 223 245 224 - async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 225 - const s3Key = this.getS3Key(key); 246 + return { data, metadata }; 247 + } else { 248 + // Get data with embedded metadata from response headers 249 + const response = await this.client.send( 250 + new GetObjectCommand({ 251 + Bucket: this.config.bucket, 252 + Key: s3Key, 253 + }), 254 + ); 226 255 227 - if (this.metadataBucket) { 228 - const dataCommand = new PutObjectCommand({ 229 - Bucket: this.config.bucket, 230 - Key: s3Key, 231 - Body: data, 232 - ContentLength: data.byteLength, 233 - }); 256 + if (!response.Body || !response.Metadata) { 257 + return null; 258 + } 234 259 235 - const metadataJson = JSON.stringify(metadata); 236 - const metadataBuffer = new TextEncoder().encode(metadataJson); 237 - const metadataCommand = new PutObjectCommand({ 238 - Bucket: this.metadataBucket, 239 - Key: s3Key + '.meta', 240 - Body: metadataBuffer, 241 - ContentType: 'application/json', 242 - }); 260 + const data = await this.streamToUint8Array(response.Body as Readable); 261 + const metadata = this.s3ToMetadata(response.Metadata); 243 262 244 - await Promise.all([ 245 - this.client.send(dataCommand), 246 - this.client.send(metadataCommand), 247 - ]); 248 - } else { 249 - const command = new PutObjectCommand({ 250 - Bucket: this.config.bucket, 251 - Key: s3Key, 252 - Body: data, 253 - ContentLength: data.byteLength, 254 - Metadata: this.metadataToS3(metadata), 255 - }); 263 + return { data, metadata }; 264 + } 265 + } catch (error) { 266 + if (this.isNoSuchKeyError(error)) { 267 + return null; 268 + } 269 + throw error; 270 + } 271 + } 256 272 257 - await this.client.send(command); 258 - } 259 - } 273 + /** 274 + * Retrieve data as a readable stream with metadata. 275 + * 276 + * @param key - The key to retrieve 277 + * @returns A readable stream and metadata, or null if not found 278 + * 279 + * @remarks 280 + * Use this for large files to avoid loading entire content into memory. 281 + * The stream must be consumed or destroyed by the caller. 282 + */ 283 + async getStream(key: string): Promise<TierStreamResult | null> { 284 + const s3Key = this.getS3Key(key); 260 285 261 - async delete(key: string): Promise<void> { 262 - const s3Key = this.getS3Key(key); 286 + try { 287 + if (this.metadataBucket) { 288 + // Fetch data stream and metadata in parallel 289 + const [dataResponse, metadataResponse] = await Promise.all([ 290 + this.client.send( 291 + new GetObjectCommand({ 292 + Bucket: this.config.bucket, 293 + Key: s3Key, 294 + }), 295 + ), 296 + this.client.send( 297 + new GetObjectCommand({ 298 + Bucket: this.metadataBucket, 299 + Key: s3Key + '.meta', 300 + }), 301 + ), 302 + ]); 263 303 264 - try { 265 - const dataCommand = new DeleteObjectCommand({ 266 - Bucket: this.config.bucket, 267 - Key: s3Key, 268 - }); 304 + if (!dataResponse.Body || !metadataResponse.Body) { 305 + return null; 306 + } 269 307 270 - if (this.metadataBucket) { 271 - const metadataCommand = new DeleteObjectCommand({ 272 - Bucket: this.metadataBucket, 273 - Key: s3Key + '.meta', 274 - }); 308 + // Only buffer the small metadata, stream the data 309 + const metaBuffer = await this.streamToUint8Array(metadataResponse.Body as Readable); 310 + const json = new TextDecoder().decode(metaBuffer); 311 + const metadata = JSON.parse(json) as StorageMetadata; 312 + metadata.createdAt = new Date(metadata.createdAt); 313 + metadata.lastAccessed = new Date(metadata.lastAccessed); 314 + if (metadata.ttl) { 315 + metadata.ttl = new Date(metadata.ttl); 316 + } 275 317 276 - await Promise.all([ 277 - this.client.send(dataCommand), 278 - this.client.send(metadataCommand).catch((error) => { 279 - if (!this.isNoSuchKeyError(error)) throw error; 280 - }), 281 - ]); 282 - } else { 283 - await this.client.send(dataCommand); 284 - } 285 - } catch (error) { 286 - if (!this.isNoSuchKeyError(error)) { 287 - throw error; 288 - } 289 - } 290 - } 318 + return { stream: dataResponse.Body as Readable, metadata }; 319 + } else { 320 + // Get data stream with embedded metadata from response headers 321 + const response = await this.client.send( 322 + new GetObjectCommand({ 323 + Bucket: this.config.bucket, 324 + Key: s3Key, 325 + }), 326 + ); 291 327 292 - async exists(key: string): Promise<boolean> { 293 - try { 294 - const command = new HeadObjectCommand({ 295 - Bucket: this.config.bucket, 296 - Key: this.getS3Key(key), 297 - }); 328 + if (!response.Body || !response.Metadata) { 329 + return null; 330 + } 298 331 299 - await this.client.send(command); 300 - return true; 301 - } catch (error) { 302 - if (this.isNoSuchKeyError(error)) { 303 - return false; 304 - } 305 - throw error; 306 - } 307 - } 332 + const metadata = this.s3ToMetadata(response.Metadata); 308 333 309 - async *listKeys(prefix?: string): AsyncIterableIterator<string> { 310 - const s3Prefix = prefix ? this.getS3Key(prefix) : this.prefix; 311 - let continuationToken: string | undefined; 334 + return { stream: response.Body as Readable, metadata }; 335 + } 336 + } catch (error) { 337 + if (this.isNoSuchKeyError(error)) { 338 + return null; 339 + } 340 + throw error; 341 + } 342 + } 312 343 313 - do { 314 - const command = new ListObjectsV2Command({ 315 - Bucket: this.config.bucket, 316 - Prefix: s3Prefix, 317 - ContinuationToken: continuationToken, 318 - }); 344 + /** 345 + * Store data from a readable stream. 346 + * 347 + * @param key - The key to store under 348 + * @param stream - Readable stream of data to store 349 + * @param metadata - Metadata to store alongside the data 350 + * 351 + * @remarks 352 + * Uses multipart upload for efficient streaming of large files. 353 + * The stream will be fully consumed by this operation. 354 + */ 355 + async setStream( 356 + key: string, 357 + stream: NodeJS.ReadableStream, 358 + metadata: StorageMetadata, 359 + ): Promise<void> { 360 + const s3Key = this.getS3Key(key); 319 361 320 - const response = await this.client.send(command); 362 + if (this.metadataBucket) { 363 + // Use multipart upload for streaming data 364 + const upload = new Upload({ 365 + client: this.client, 366 + params: { 367 + Bucket: this.config.bucket, 368 + Key: s3Key, 369 + Body: stream as Readable, 370 + }, 371 + }); 321 372 322 - if (response.Contents) { 323 - for (const object of response.Contents) { 324 - if (object.Key) { 325 - // Remove prefix to get original key 326 - const key = this.removePrefix(object.Key); 327 - yield key; 328 - } 329 - } 330 - } 373 + const metadataJson = JSON.stringify(metadata); 374 + const metadataBuffer = new TextEncoder().encode(metadataJson); 375 + const metadataCommand = new PutObjectCommand({ 376 + Bucket: this.metadataBucket, 377 + Key: s3Key + '.meta', 378 + Body: metadataBuffer, 379 + ContentType: 'application/json', 380 + }); 331 381 332 - continuationToken = response.NextContinuationToken; 333 - } while (continuationToken); 334 - } 382 + await Promise.all([upload.done(), this.client.send(metadataCommand)]); 383 + } else { 384 + // Use multipart upload with embedded metadata 385 + const upload = new Upload({ 386 + client: this.client, 387 + params: { 388 + Bucket: this.config.bucket, 389 + Key: s3Key, 390 + Body: stream as Readable, 391 + Metadata: this.metadataToS3(metadata), 392 + }, 393 + }); 335 394 336 - async deleteMany(keys: string[]): Promise<void> { 337 - if (keys.length === 0) return; 395 + await upload.done(); 396 + } 397 + } 338 398 339 - const batchSize = 1000; 399 + private async streamToUint8Array(stream: Readable): Promise<Uint8Array> { 400 + const chunks: Uint8Array[] = []; 340 401 341 - for (let i = 0; i < keys.length; i += batchSize) { 342 - const batch = keys.slice(i, i + batchSize); 402 + for await (const chunk of stream) { 403 + if (Buffer.isBuffer(chunk)) { 404 + chunks.push(new Uint8Array(chunk)); 405 + } else if (chunk instanceof Uint8Array) { 406 + chunks.push(chunk); 407 + } else { 408 + throw new Error('Unexpected chunk type in S3 stream'); 409 + } 410 + } 343 411 344 - const dataCommand = new DeleteObjectsCommand({ 345 - Bucket: this.config.bucket, 346 - Delete: { 347 - Objects: batch.map((key) => ({ Key: this.getS3Key(key) })), 348 - }, 349 - }); 412 + const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); 413 + const result = new Uint8Array(totalLength); 414 + let offset = 0; 415 + for (const chunk of chunks) { 416 + result.set(chunk, offset); 417 + offset += chunk.length; 418 + } 350 419 351 - if (this.metadataBucket) { 352 - const metadataCommand = new DeleteObjectsCommand({ 353 - Bucket: this.metadataBucket, 354 - Delete: { 355 - Objects: batch.map((key) => ({ Key: this.getS3Key(key) + '.meta' })), 356 - }, 357 - }); 420 + return result; 421 + } 358 422 359 - await Promise.all([ 360 - this.client.send(dataCommand), 361 - this.client.send(metadataCommand).catch(() => {}), 362 - ]); 363 - } else { 364 - await this.client.send(dataCommand); 365 - } 366 - } 367 - } 423 + private isNoSuchKeyError(error: unknown): boolean { 424 + return ( 425 + typeof error === 'object' && 426 + error !== null && 427 + 'name' in error && 428 + (error.name === 'NoSuchKey' || error.name === 'NotFound') 429 + ); 430 + } 368 431 369 - async getMetadata(key: string): Promise<StorageMetadata | null> { 370 - if (this.metadataBucket) { 371 - try { 372 - const command = new GetObjectCommand({ 373 - Bucket: this.metadataBucket, 374 - Key: this.getS3Key(key) + '.meta', 375 - }); 432 + async set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void> { 433 + const s3Key = this.getS3Key(key); 376 434 377 - const response = await this.client.send(command); 435 + if (this.metadataBucket) { 436 + const dataCommand = new PutObjectCommand({ 437 + Bucket: this.config.bucket, 438 + Key: s3Key, 439 + Body: data, 440 + ContentLength: data.byteLength, 441 + }); 378 442 379 - if (!response.Body) { 380 - return null; 381 - } 443 + const metadataJson = JSON.stringify(metadata); 444 + const metadataBuffer = new TextEncoder().encode(metadataJson); 445 + const metadataCommand = new PutObjectCommand({ 446 + Bucket: this.metadataBucket, 447 + Key: s3Key + '.meta', 448 + Body: metadataBuffer, 449 + ContentType: 'application/json', 450 + }); 382 451 383 - const buffer = await this.streamToUint8Array(response.Body as Readable); 384 - const json = new TextDecoder().decode(buffer); 385 - const metadata = JSON.parse(json) as StorageMetadata; 452 + await Promise.all([this.client.send(dataCommand), this.client.send(metadataCommand)]); 453 + } else { 454 + const command = new PutObjectCommand({ 455 + Bucket: this.config.bucket, 456 + Key: s3Key, 457 + Body: data, 458 + ContentLength: data.byteLength, 459 + Metadata: this.metadataToS3(metadata), 460 + }); 386 461 387 - metadata.createdAt = new Date(metadata.createdAt); 388 - metadata.lastAccessed = new Date(metadata.lastAccessed); 389 - if (metadata.ttl) { 390 - metadata.ttl = new Date(metadata.ttl); 391 - } 462 + await this.client.send(command); 463 + } 464 + } 392 465 393 - return metadata; 394 - } catch (error) { 395 - if (this.isNoSuchKeyError(error)) { 396 - return null; 397 - } 398 - throw error; 399 - } 400 - } 466 + async delete(key: string): Promise<void> { 467 + const s3Key = this.getS3Key(key); 401 468 402 - try { 403 - const command = new HeadObjectCommand({ 404 - Bucket: this.config.bucket, 405 - Key: this.getS3Key(key), 406 - }); 469 + try { 470 + const dataCommand = new DeleteObjectCommand({ 471 + Bucket: this.config.bucket, 472 + Key: s3Key, 473 + }); 474 + 475 + if (this.metadataBucket) { 476 + const metadataCommand = new DeleteObjectCommand({ 477 + Bucket: this.metadataBucket, 478 + Key: s3Key + '.meta', 479 + }); 480 + 481 + await Promise.all([ 482 + this.client.send(dataCommand), 483 + this.client.send(metadataCommand).catch((error) => { 484 + if (!this.isNoSuchKeyError(error)) throw error; 485 + }), 486 + ]); 487 + } else { 488 + await this.client.send(dataCommand); 489 + } 490 + } catch (error) { 491 + if (!this.isNoSuchKeyError(error)) { 492 + throw error; 493 + } 494 + } 495 + } 496 + 497 + async exists(key: string): Promise<boolean> { 498 + try { 499 + const command = new HeadObjectCommand({ 500 + Bucket: this.config.bucket, 501 + Key: this.getS3Key(key), 502 + }); 503 + 504 + await this.client.send(command); 505 + return true; 506 + } catch (error) { 507 + if (this.isNoSuchKeyError(error)) { 508 + return false; 509 + } 510 + throw error; 511 + } 512 + } 513 + 514 + async *listKeys(prefix?: string): AsyncIterableIterator<string> { 515 + const s3Prefix = prefix ? this.getS3Key(prefix) : this.prefix; 516 + let continuationToken: string | undefined; 517 + 518 + do { 519 + const command = new ListObjectsV2Command({ 520 + Bucket: this.config.bucket, 521 + Prefix: s3Prefix, 522 + ContinuationToken: continuationToken, 523 + }); 524 + 525 + const response = await this.client.send(command); 526 + 527 + if (response.Contents) { 528 + for (const object of response.Contents) { 529 + if (object.Key) { 530 + // Remove prefix to get original key 531 + const key = this.removePrefix(object.Key); 532 + yield key; 533 + } 534 + } 535 + } 536 + 537 + continuationToken = response.NextContinuationToken; 538 + } while (continuationToken); 539 + } 540 + 541 + async deleteMany(keys: string[]): Promise<void> { 542 + if (keys.length === 0) return; 543 + 544 + const batchSize = 1000; 545 + 546 + for (let i = 0; i < keys.length; i += batchSize) { 547 + const batch = keys.slice(i, i + batchSize); 548 + 549 + const dataCommand = new DeleteObjectsCommand({ 550 + Bucket: this.config.bucket, 551 + Delete: { 552 + Objects: batch.map((key) => ({ Key: this.getS3Key(key) })), 553 + }, 554 + }); 555 + 556 + if (this.metadataBucket) { 557 + const metadataCommand = new DeleteObjectsCommand({ 558 + Bucket: this.metadataBucket, 559 + Delete: { 560 + Objects: batch.map((key) => ({ Key: this.getS3Key(key) + '.meta' })), 561 + }, 562 + }); 563 + 564 + await Promise.all([ 565 + this.client.send(dataCommand), 566 + this.client.send(metadataCommand).catch(() => {}), 567 + ]); 568 + } else { 569 + await this.client.send(dataCommand); 570 + } 571 + } 572 + } 573 + 574 + async getMetadata(key: string): Promise<StorageMetadata | null> { 575 + if (this.metadataBucket) { 576 + try { 577 + const command = new GetObjectCommand({ 578 + Bucket: this.metadataBucket, 579 + Key: this.getS3Key(key) + '.meta', 580 + }); 581 + 582 + const response = await this.client.send(command); 583 + 584 + if (!response.Body) { 585 + return null; 586 + } 587 + 588 + const buffer = await this.streamToUint8Array(response.Body as Readable); 589 + const json = new TextDecoder().decode(buffer); 590 + const metadata = JSON.parse(json) as StorageMetadata; 591 + 592 + metadata.createdAt = new Date(metadata.createdAt); 593 + metadata.lastAccessed = new Date(metadata.lastAccessed); 594 + if (metadata.ttl) { 595 + metadata.ttl = new Date(metadata.ttl); 596 + } 597 + 598 + return metadata; 599 + } catch (error) { 600 + if (this.isNoSuchKeyError(error)) { 601 + return null; 602 + } 603 + throw error; 604 + } 605 + } 606 + 607 + try { 608 + const command = new HeadObjectCommand({ 609 + Bucket: this.config.bucket, 610 + Key: this.getS3Key(key), 611 + }); 407 612 408 - const response = await this.client.send(command); 613 + const response = await this.client.send(command); 409 614 410 - if (!response.Metadata) { 411 - return null; 412 - } 615 + if (!response.Metadata) { 616 + return null; 617 + } 413 618 414 - return this.s3ToMetadata(response.Metadata); 415 - } catch (error) { 416 - if (this.isNoSuchKeyError(error)) { 417 - return null; 418 - } 419 - throw error; 420 - } 421 - } 619 + return this.s3ToMetadata(response.Metadata); 620 + } catch (error) { 621 + if (this.isNoSuchKeyError(error)) { 622 + return null; 623 + } 624 + throw error; 625 + } 626 + } 422 627 423 - async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 424 - if (this.metadataBucket) { 425 - const metadataJson = JSON.stringify(metadata); 426 - const buffer = new TextEncoder().encode(metadataJson); 628 + async setMetadata(key: string, metadata: StorageMetadata): Promise<void> { 629 + if (this.metadataBucket) { 630 + const metadataJson = JSON.stringify(metadata); 631 + const buffer = new TextEncoder().encode(metadataJson); 427 632 428 - const command = new PutObjectCommand({ 429 - Bucket: this.metadataBucket, 430 - Key: this.getS3Key(key) + '.meta', 431 - Body: buffer, 432 - ContentType: 'application/json', 433 - }); 633 + const command = new PutObjectCommand({ 634 + Bucket: this.metadataBucket, 635 + Key: this.getS3Key(key) + '.meta', 636 + Body: buffer, 637 + ContentType: 'application/json', 638 + }); 434 639 435 - await this.client.send(command); 436 - return; 437 - } 640 + await this.client.send(command); 641 + return; 642 + } 438 643 439 - const s3Key = this.getS3Key(key); 440 - const command = new CopyObjectCommand({ 441 - Bucket: this.config.bucket, 442 - Key: s3Key, 443 - CopySource: `${this.config.bucket}/${s3Key}`, 444 - Metadata: this.metadataToS3(metadata), 445 - MetadataDirective: 'REPLACE', 446 - }); 644 + const s3Key = this.getS3Key(key); 645 + const command = new CopyObjectCommand({ 646 + Bucket: this.config.bucket, 647 + Key: s3Key, 648 + CopySource: `${this.config.bucket}/${s3Key}`, 649 + Metadata: this.metadataToS3(metadata), 650 + MetadataDirective: 'REPLACE', 651 + }); 447 652 448 - await this.client.send(command); 449 - } 653 + await this.client.send(command); 654 + } 450 655 451 - async getStats(): Promise<TierStats> { 452 - let bytes = 0; 453 - let items = 0; 656 + async getStats(): Promise<TierStats> { 657 + let bytes = 0; 658 + let items = 0; 454 659 455 - // List all objects and sum up sizes 456 - let continuationToken: string | undefined; 660 + // List all objects and sum up sizes 661 + let continuationToken: string | undefined; 457 662 458 - do { 459 - const command = new ListObjectsV2Command({ 460 - Bucket: this.config.bucket, 461 - Prefix: this.prefix, 462 - ContinuationToken: continuationToken, 463 - }); 663 + do { 664 + const command = new ListObjectsV2Command({ 665 + Bucket: this.config.bucket, 666 + Prefix: this.prefix, 667 + ContinuationToken: continuationToken, 668 + }); 464 669 465 - const response = await this.client.send(command); 670 + const response = await this.client.send(command); 466 671 467 - if (response.Contents) { 468 - for (const object of response.Contents) { 469 - items++; 470 - bytes += object.Size ?? 0; 471 - } 472 - } 672 + if (response.Contents) { 673 + for (const object of response.Contents) { 674 + items++; 675 + bytes += object.Size ?? 0; 676 + } 677 + } 473 678 474 - continuationToken = response.NextContinuationToken; 475 - } while (continuationToken); 679 + continuationToken = response.NextContinuationToken; 680 + } while (continuationToken); 476 681 477 - return { bytes, items }; 478 - } 682 + return { bytes, items }; 683 + } 479 684 480 - async clear(): Promise<void> { 481 - // List and delete all objects with the prefix 482 - const keys: string[] = []; 685 + async clear(): Promise<void> { 686 + // List and delete all objects with the prefix 687 + const keys: string[] = []; 483 688 484 - for await (const key of this.listKeys()) { 485 - keys.push(key); 486 - } 689 + for await (const key of this.listKeys()) { 690 + keys.push(key); 691 + } 487 692 488 - await this.deleteMany(keys); 489 - } 693 + await this.deleteMany(keys); 694 + } 490 695 491 - /** 492 - * Get the full S3 key including prefix. 493 - */ 494 - private getS3Key(key: string): string { 495 - return this.prefix + key; 496 - } 696 + /** 697 + * Get the full S3 key including prefix. 698 + */ 699 + private getS3Key(key: string): string { 700 + return this.prefix + key; 701 + } 497 702 498 - /** 499 - * Remove the prefix from an S3 key to get the original key. 500 - */ 501 - private removePrefix(s3Key: string): string { 502 - if (this.prefix && s3Key.startsWith(this.prefix)) { 503 - return s3Key.slice(this.prefix.length); 504 - } 505 - return s3Key; 506 - } 703 + /** 704 + * Remove the prefix from an S3 key to get the original key. 705 + */ 706 + private removePrefix(s3Key: string): string { 707 + if (this.prefix && s3Key.startsWith(this.prefix)) { 708 + return s3Key.slice(this.prefix.length); 709 + } 710 + return s3Key; 711 + } 507 712 508 - /** 509 - * Convert StorageMetadata to S3 metadata format. 510 - * 511 - * @remarks 512 - * S3 metadata keys must be lowercase and values must be strings. 513 - * We serialize complex values as JSON. 514 - */ 515 - private metadataToS3(metadata: StorageMetadata): Record<string, string> { 516 - return { 517 - key: metadata.key, 518 - size: metadata.size.toString(), 519 - createdat: metadata.createdAt.toISOString(), 520 - lastaccessed: metadata.lastAccessed.toISOString(), 521 - accesscount: metadata.accessCount.toString(), 522 - compressed: metadata.compressed.toString(), 523 - checksum: metadata.checksum, 524 - ...(metadata.ttl && { ttl: metadata.ttl.toISOString() }), 525 - ...(metadata.mimeType && { mimetype: metadata.mimeType }), 526 - ...(metadata.encoding && { encoding: metadata.encoding }), 527 - ...(metadata.customMetadata && { custom: JSON.stringify(metadata.customMetadata) }), 528 - }; 529 - } 713 + /** 714 + * Convert StorageMetadata to S3 metadata format. 715 + * 716 + * @remarks 717 + * S3 metadata keys must be lowercase and values must be strings. 718 + * We serialize complex values as JSON. 719 + */ 720 + private metadataToS3(metadata: StorageMetadata): Record<string, string> { 721 + return { 722 + key: metadata.key, 723 + size: metadata.size.toString(), 724 + createdat: metadata.createdAt.toISOString(), 725 + lastaccessed: metadata.lastAccessed.toISOString(), 726 + accesscount: metadata.accessCount.toString(), 727 + compressed: metadata.compressed.toString(), 728 + checksum: metadata.checksum, 729 + ...(metadata.ttl && { ttl: metadata.ttl.toISOString() }), 730 + ...(metadata.mimeType && { mimetype: metadata.mimeType }), 731 + ...(metadata.encoding && { encoding: metadata.encoding }), 732 + ...(metadata.customMetadata && { custom: JSON.stringify(metadata.customMetadata) }), 733 + }; 734 + } 530 735 531 - /** 532 - * Convert S3 metadata to StorageMetadata format. 533 - */ 534 - private s3ToMetadata(s3Metadata: Record<string, string>): StorageMetadata { 535 - const metadata: StorageMetadata = { 536 - key: s3Metadata.key ?? '', 537 - size: parseInt(s3Metadata.size ?? '0', 10), 538 - createdAt: new Date(s3Metadata.createdat ?? Date.now()), 539 - lastAccessed: new Date(s3Metadata.lastaccessed ?? Date.now()), 540 - accessCount: parseInt(s3Metadata.accesscount ?? '0', 10), 541 - compressed: s3Metadata.compressed === 'true', 542 - checksum: s3Metadata.checksum ?? '', 543 - }; 736 + /** 737 + * Convert S3 metadata to StorageMetadata format. 738 + */ 739 + private s3ToMetadata(s3Metadata: Record<string, string>): StorageMetadata { 740 + const metadata: StorageMetadata = { 741 + key: s3Metadata.key ?? '', 742 + size: parseInt(s3Metadata.size ?? '0', 10), 743 + createdAt: new Date(s3Metadata.createdat ?? Date.now()), 744 + lastAccessed: new Date(s3Metadata.lastaccessed ?? Date.now()), 745 + accessCount: parseInt(s3Metadata.accesscount ?? '0', 10), 746 + compressed: s3Metadata.compressed === 'true', 747 + checksum: s3Metadata.checksum ?? '', 748 + }; 544 749 545 - if (s3Metadata.ttl) { 546 - metadata.ttl = new Date(s3Metadata.ttl); 547 - } 750 + if (s3Metadata.ttl) { 751 + metadata.ttl = new Date(s3Metadata.ttl); 752 + } 548 753 549 - if (s3Metadata.mimetype) { 550 - metadata.mimeType = s3Metadata.mimetype; 551 - } 754 + if (s3Metadata.mimetype) { 755 + metadata.mimeType = s3Metadata.mimetype; 756 + } 552 757 553 - if (s3Metadata.encoding) { 554 - metadata.encoding = s3Metadata.encoding; 555 - } 758 + if (s3Metadata.encoding) { 759 + metadata.encoding = s3Metadata.encoding; 760 + } 556 761 557 - if (s3Metadata.custom) { 558 - try { 559 - metadata.customMetadata = JSON.parse(s3Metadata.custom); 560 - } catch { 561 - // Ignore invalid JSON 562 - } 563 - } 762 + if (s3Metadata.custom) { 763 + try { 764 + const parsed: unknown = JSON.parse(s3Metadata.custom); 765 + // Validate it's a Record<string, string> 766 + if ( 767 + parsed && 768 + typeof parsed === 'object' && 769 + !Array.isArray(parsed) && 770 + Object.values(parsed).every((v) => typeof v === 'string') 771 + ) { 772 + metadata.customMetadata = parsed as Record<string, string>; 773 + } 774 + } catch { 775 + // Ignore invalid JSON 776 + } 777 + } 564 778 565 - return metadata; 566 - } 779 + return metadata; 780 + } 567 781 }
+410 -258
src/types/index.ts
··· 9 9 * - Content type information for HTTP serving 10 10 */ 11 11 export interface StorageMetadata { 12 - /** Original key used to store the data (human-readable) */ 13 - key: string; 12 + /** Original key used to store the data (human-readable) */ 13 + key: string; 14 14 15 - /** Size of the data in bytes (uncompressed size) */ 16 - size: number; 15 + /** Size of the data in bytes (uncompressed size) */ 16 + size: number; 17 17 18 - /** Timestamp when the data was first created */ 19 - createdAt: Date; 18 + /** Timestamp when the data was first created */ 19 + createdAt: Date; 20 20 21 - /** Timestamp when the data was last accessed */ 22 - lastAccessed: Date; 21 + /** Timestamp when the data was last accessed */ 22 + lastAccessed: Date; 23 23 24 - /** Number of times this data has been accessed */ 25 - accessCount: number; 24 + /** Number of times this data has been accessed */ 25 + accessCount: number; 26 26 27 - /** Optional expiration timestamp. Data expires when current time > ttl */ 28 - ttl?: Date; 27 + /** Optional expiration timestamp. Data expires when current time > ttl */ 28 + ttl?: Date; 29 29 30 - /** Whether the data is compressed (e.g., with gzip) */ 31 - compressed: boolean; 30 + /** Whether the data is compressed (e.g., with gzip) */ 31 + compressed: boolean; 32 32 33 - /** SHA256 checksum of the data for integrity verification */ 34 - checksum: string; 33 + /** SHA256 checksum of the data for integrity verification */ 34 + checksum: string; 35 35 36 - /** Optional MIME type (e.g., 'text/html', 'application/json') */ 37 - mimeType?: string; 36 + /** Optional MIME type (e.g., 'text/html', 'application/json') */ 37 + mimeType?: string; 38 38 39 - /** Optional encoding (e.g., 'gzip', 'base64') */ 40 - encoding?: string; 39 + /** Optional encoding (e.g., 'gzip', 'base64') */ 40 + encoding?: string; 41 41 42 - /** User-defined metadata fields */ 43 - customMetadata?: Record<string, string>; 42 + /** User-defined metadata fields */ 43 + customMetadata?: Record<string, string>; 44 44 } 45 45 46 46 /** ··· 50 50 * Used for monitoring cache performance and capacity planning. 51 51 */ 52 52 export interface TierStats { 53 - /** Total bytes stored in this tier */ 54 - bytes: number; 53 + /** Total bytes stored in this tier */ 54 + bytes: number; 55 55 56 - /** Total number of items stored in this tier */ 57 - items: number; 56 + /** Total number of items stored in this tier */ 57 + items: number; 58 58 59 - /** Number of cache hits (only tracked if tier implements hit tracking) */ 60 - hits?: number; 59 + /** Number of cache hits (only tracked if tier implements hit tracking) */ 60 + hits?: number; 61 61 62 - /** Number of cache misses (only tracked if tier implements miss tracking) */ 63 - misses?: number; 62 + /** Number of cache misses (only tracked if tier implements miss tracking) */ 63 + misses?: number; 64 64 65 - /** Number of evictions due to size/count limits (only tracked if tier implements eviction) */ 66 - evictions?: number; 65 + /** Number of evictions due to size/count limits (only tracked if tier implements eviction) */ 66 + evictions?: number; 67 67 } 68 68 69 69 /** ··· 73 73 * Provides a complete view of cache performance across the entire storage hierarchy. 74 74 */ 75 75 export interface AllTierStats { 76 - /** Statistics for hot tier (if configured) */ 77 - hot?: TierStats; 76 + /** Statistics for hot tier (if configured) */ 77 + hot?: TierStats; 78 78 79 - /** Statistics for warm tier (if configured) */ 80 - warm?: TierStats; 79 + /** Statistics for warm tier (if configured) */ 80 + warm?: TierStats; 81 81 82 - /** Statistics for cold tier (always present) */ 83 - cold: TierStats; 82 + /** Statistics for cold tier (always present) */ 83 + cold: TierStats; 84 84 85 - /** Total hits across all tiers */ 86 - totalHits: number; 85 + /** Total hits across all tiers */ 86 + totalHits: number; 87 87 88 - /** Total misses across all tiers */ 89 - totalMisses: number; 88 + /** Total misses across all tiers */ 89 + totalMisses: number; 90 90 91 - /** Hit rate as a percentage (0-1) */ 92 - hitRate: number; 91 + /** Hit rate as a percentage (0-1) */ 92 + hitRate: number; 93 93 } 94 94 95 95 /** ··· 103 103 * @example 104 104 * ```typescript 105 105 * class RedisStorageTier implements StorageTier { 106 - * constructor(private client: RedisClient) {} 106 + * constructor(private client: RedisClient) {} 107 107 * 108 - * async get(key: string): Promise<Uint8Array | null> { 109 - * const buffer = await this.client.getBuffer(key); 110 - * return buffer ? new Uint8Array(buffer) : null; 111 - * } 108 + * async get(key: string): Promise<Uint8Array | null> { 109 + * const buffer = await this.client.getBuffer(key); 110 + * return buffer ? new Uint8Array(buffer) : null; 111 + * } 112 112 * 113 - * // ... implement other methods 113 + * // ... implement other methods 114 114 * } 115 115 * ``` 116 116 */ 117 + /** 118 + * Result from a combined get+metadata operation on a tier. 119 + */ 120 + export interface TierGetResult { 121 + /** The retrieved data */ 122 + data: Uint8Array; 123 + /** Metadata associated with the data */ 124 + metadata: StorageMetadata; 125 + } 126 + 127 + /** 128 + * Result from a streaming get operation on a tier. 129 + */ 130 + export interface TierStreamResult { 131 + /** Readable stream of the data */ 132 + stream: NodeJS.ReadableStream; 133 + /** Metadata associated with the data */ 134 + metadata: StorageMetadata; 135 + } 136 + 137 + /** 138 + * Result from a streaming get operation on TieredStorage. 139 + * 140 + * @remarks 141 + * Includes the source tier for observability. 142 + */ 143 + export interface StreamResult { 144 + /** Readable stream of the data */ 145 + stream: NodeJS.ReadableStream; 146 + /** Metadata associated with the data */ 147 + metadata: StorageMetadata; 148 + /** Which tier the data was served from */ 149 + source: 'hot' | 'warm' | 'cold'; 150 + } 151 + 152 + /** 153 + * Options for streaming set operations. 154 + */ 155 + export interface StreamSetOptions extends SetOptions { 156 + /** 157 + * Size of the data being streamed in bytes. 158 + * 159 + * @remarks 160 + * Required for streaming writes because the size cannot be determined 161 + * until the stream is fully consumed. This is used for: 162 + * - Metadata creation before streaming starts 163 + * - Capacity checks and eviction in tiers with size limits 164 + */ 165 + size: number; 166 + 167 + /** 168 + * Pre-computed checksum of the data. 169 + * 170 + * @remarks 171 + * If not provided, checksum will be computed during streaming. 172 + * Providing it upfront is useful when the checksum is already known 173 + * (e.g., from a previous upload or external source). 174 + */ 175 + checksum?: string; 176 + 177 + /** 178 + * MIME type of the content. 179 + */ 180 + mimeType?: string; 181 + } 182 + 117 183 export interface StorageTier { 118 - /** 119 - * Retrieve data for a key. 120 - * 121 - * @param key - The key to retrieve 122 - * @returns The data as a Uint8Array, or null if not found 123 - */ 124 - get(key: string): Promise<Uint8Array | null>; 184 + /** 185 + * Retrieve data for a key. 186 + * 187 + * @param key - The key to retrieve 188 + * @returns The data as a Uint8Array, or null if not found 189 + */ 190 + get(key: string): Promise<Uint8Array | null>; 125 191 126 - /** 127 - * Store data with associated metadata. 128 - * 129 - * @param key - The key to store under 130 - * @param data - The data to store (as Uint8Array) 131 - * @param metadata - Metadata to store alongside the data 132 - * 133 - * @remarks 134 - * If the key already exists, it should be overwritten. 135 - */ 136 - set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void>; 192 + /** 193 + * Retrieve data and metadata together in a single operation. 194 + * 195 + * @param key - The key to retrieve 196 + * @returns The data and metadata, or null if not found 197 + * 198 + * @remarks 199 + * This is more efficient than calling get() and getMetadata() separately, 200 + * especially for disk and network-based tiers. 201 + */ 202 + getWithMetadata?(key: string): Promise<TierGetResult | null>; 137 203 138 - /** 139 - * Delete data for a key. 140 - * 141 - * @param key - The key to delete 142 - * 143 - * @remarks 144 - * Should not throw if the key doesn't exist. 145 - */ 146 - delete(key: string): Promise<void>; 204 + /** 205 + * Retrieve data as a readable stream with metadata. 206 + * 207 + * @param key - The key to retrieve 208 + * @returns A readable stream and metadata, or null if not found 209 + * 210 + * @remarks 211 + * Use this for large files to avoid loading entire content into memory. 212 + * The stream must be consumed or destroyed by the caller. 213 + */ 214 + getStream?(key: string): Promise<TierStreamResult | null>; 147 215 148 - /** 149 - * Check if a key exists in this tier. 150 - * 151 - * @param key - The key to check 152 - * @returns true if the key exists, false otherwise 153 - */ 154 - exists(key: string): Promise<boolean>; 216 + /** 217 + * Store data from a readable stream. 218 + * 219 + * @param key - The key to store under 220 + * @param stream - Readable stream of data to store 221 + * @param metadata - Metadata to store alongside the data 222 + * 223 + * @remarks 224 + * Use this for large files to avoid loading entire content into memory. 225 + * The stream will be fully consumed by this operation. 226 + */ 227 + setStream?( 228 + key: string, 229 + stream: NodeJS.ReadableStream, 230 + metadata: StorageMetadata, 231 + ): Promise<void>; 155 232 156 - /** 157 - * List all keys in this tier, optionally filtered by prefix. 158 - * 159 - * @param prefix - Optional prefix to filter keys (e.g., 'user:' matches 'user:123', 'user:456') 160 - * @returns An async iterator of keys 161 - * 162 - * @remarks 163 - * This should be memory-efficient and stream keys rather than loading all into memory. 164 - * Useful for prefix-based invalidation and cache warming. 165 - * 166 - * @example 167 - * ```typescript 168 - * for await (const key of tier.listKeys('site:')) { 169 - * console.log(key); // 'site:abc', 'site:xyz', etc. 170 - * } 171 - * ``` 172 - */ 173 - listKeys(prefix?: string): AsyncIterableIterator<string>; 233 + /** 234 + * Store data with associated metadata. 235 + * 236 + * @param key - The key to store under 237 + * @param data - The data to store (as Uint8Array) 238 + * @param metadata - Metadata to store alongside the data 239 + * 240 + * @remarks 241 + * If the key already exists, it should be overwritten. 242 + */ 243 + set(key: string, data: Uint8Array, metadata: StorageMetadata): Promise<void>; 174 244 175 - /** 176 - * Delete multiple keys in a single operation. 177 - * 178 - * @param keys - Array of keys to delete 179 - * 180 - * @remarks 181 - * This is more efficient than calling delete() in a loop. 182 - * Implementations should batch deletions where possible. 183 - */ 184 - deleteMany(keys: string[]): Promise<void>; 245 + /** 246 + * Delete data for a key. 247 + * 248 + * @param key - The key to delete 249 + * 250 + * @remarks 251 + * Should not throw if the key doesn't exist. 252 + */ 253 + delete(key: string): Promise<void>; 185 254 186 - /** 187 - * Retrieve metadata for a key without fetching the data. 188 - * 189 - * @param key - The key to get metadata for 190 - * @returns The metadata, or null if not found 191 - * 192 - * @remarks 193 - * This is useful for checking TTL, access counts, etc. without loading large data. 194 - */ 195 - getMetadata(key: string): Promise<StorageMetadata | null>; 255 + /** 256 + * Check if a key exists in this tier. 257 + * 258 + * @param key - The key to check 259 + * @returns true if the key exists, false otherwise 260 + */ 261 + exists(key: string): Promise<boolean>; 196 262 197 - /** 198 - * Update metadata for a key without modifying the data. 199 - * 200 - * @param key - The key to update metadata for 201 - * @param metadata - The new metadata 202 - * 203 - * @remarks 204 - * Useful for updating TTL (via touch()) or access counts. 205 - */ 206 - setMetadata(key: string, metadata: StorageMetadata): Promise<void>; 263 + /** 264 + * List all keys in this tier, optionally filtered by prefix. 265 + * 266 + * @param prefix - Optional prefix to filter keys (e.g., 'user:' matches 'user:123', 'user:456') 267 + * @returns An async iterator of keys 268 + * 269 + * @remarks 270 + * This should be memory-efficient and stream keys rather than loading all into memory. 271 + * Useful for prefix-based invalidation and cache warming. 272 + * 273 + * @example 274 + * ```typescript 275 + * for await (const key of tier.listKeys('site:')) { 276 + * console.log(key); // 'site:abc', 'site:xyz', etc. 277 + * } 278 + * ``` 279 + */ 280 + listKeys(prefix?: string): AsyncIterableIterator<string>; 207 281 208 - /** 209 - * Get statistics about this tier. 210 - * 211 - * @returns Statistics including size, item count, hits, misses, etc. 212 - */ 213 - getStats(): Promise<TierStats>; 282 + /** 283 + * Delete multiple keys in a single operation. 284 + * 285 + * @param keys - Array of keys to delete 286 + * 287 + * @remarks 288 + * This is more efficient than calling delete() in a loop. 289 + * Implementations should batch deletions where possible. 290 + */ 291 + deleteMany(keys: string[]): Promise<void>; 292 + 293 + /** 294 + * Retrieve metadata for a key without fetching the data. 295 + * 296 + * @param key - The key to get metadata for 297 + * @returns The metadata, or null if not found 298 + * 299 + * @remarks 300 + * This is useful for checking TTL, access counts, etc. without loading large data. 301 + */ 302 + getMetadata(key: string): Promise<StorageMetadata | null>; 303 + 304 + /** 305 + * Update metadata for a key without modifying the data. 306 + * 307 + * @param key - The key to update metadata for 308 + * @param metadata - The new metadata 309 + * 310 + * @remarks 311 + * Useful for updating TTL (via touch()) or access counts. 312 + */ 313 + setMetadata(key: string, metadata: StorageMetadata): Promise<void>; 314 + 315 + /** 316 + * Get statistics about this tier. 317 + * 318 + * @returns Statistics including size, item count, hits, misses, etc. 319 + */ 320 + getStats(): Promise<TierStats>; 321 + 322 + /** 323 + * Clear all data from this tier. 324 + * 325 + * @remarks 326 + * Use with caution! This will delete all data in the tier. 327 + */ 328 + clear(): Promise<void>; 329 + } 330 + 331 + /** 332 + * Rule for automatic tier placement based on key patterns. 333 + * 334 + * @remarks 335 + * Rules are evaluated in order. First matching rule wins. 336 + * Use this to define which keys go to which tiers without 337 + * specifying skipTiers on every set() call. 338 + * 339 + * @example 340 + * ```typescript 341 + * placementRules: [ 342 + * { pattern: 'index.html', tiers: ['hot', 'warm', 'cold'] }, 343 + * { pattern: '*.html', tiers: ['warm', 'cold'] }, 344 + * { pattern: 'assets/**', tiers: ['warm', 'cold'] }, 345 + * { pattern: '**', tiers: ['warm', 'cold'] }, // default 346 + * ] 347 + * ``` 348 + */ 349 + export interface PlacementRule { 350 + /** 351 + * Glob pattern to match against keys. 352 + * 353 + * @remarks 354 + * Supports basic globs: 355 + * - `*` matches any characters except `/` 356 + * - `**` matches any characters including `/` 357 + * - Exact matches work too: `index.html` 358 + */ 359 + pattern: string; 214 360 215 - /** 216 - * Clear all data from this tier. 217 - * 218 - * @remarks 219 - * Use with caution! This will delete all data in the tier. 220 - */ 221 - clear(): Promise<void>; 361 + /** 362 + * Which tiers to write to for matching keys. 363 + * 364 + * @remarks 365 + * Cold is always included (source of truth). 366 + * Use `['hot', 'warm', 'cold']` for critical files. 367 + * Use `['warm', 'cold']` for large files. 368 + * Use `['cold']` for archival only. 369 + */ 370 + tiers: ('hot' | 'warm' | 'cold')[]; 222 371 } 223 372 224 373 /** ··· 235 384 * Data flows down on writes (hot โ†’ warm โ†’ cold) and bubbles up on reads (cold โ†’ warm โ†’ hot). 236 385 */ 237 386 export interface TieredStorageConfig { 238 - /** Storage tier configuration */ 239 - tiers: { 240 - /** Optional hot tier - fastest, smallest capacity (e.g., in-memory, Redis) */ 241 - hot?: StorageTier; 387 + /** Storage tier configuration */ 388 + tiers: { 389 + /** Optional hot tier - fastest, smallest capacity (e.g., in-memory, Redis) */ 390 + hot?: StorageTier; 242 391 243 - /** Optional warm tier - medium speed, medium capacity (e.g., disk, SQLite, Postgres) */ 244 - warm?: StorageTier; 392 + /** Optional warm tier - medium speed, medium capacity (e.g., disk, SQLite, Postgres) */ 393 + warm?: StorageTier; 245 394 246 - /** Required cold tier - slowest, largest capacity (e.g., S3, R2, object storage) */ 247 - cold: StorageTier; 248 - }; 395 + /** Required cold tier - slowest, largest capacity (e.g., S3, R2, object storage) */ 396 + cold: StorageTier; 397 + }; 249 398 250 - /** 251 - * Whether to automatically compress data before storing. 252 - * 253 - * @defaultValue false 254 - * 255 - * @remarks 256 - * Uses gzip compression. Compression is transparent - data is automatically 257 - * decompressed on retrieval. The `compressed` flag in metadata indicates compression state. 258 - */ 259 - compression?: boolean; 399 + /** Rules for automatic tier placement based on key patterns. First match wins. */ 400 + placementRules?: PlacementRule[]; 260 401 261 - /** 262 - * Default TTL (time-to-live) in milliseconds. 263 - * 264 - * @remarks 265 - * Data will expire after this duration. Can be overridden per-key via SetOptions. 266 - * If not set, data never expires. 267 - */ 268 - defaultTTL?: number; 402 + /** 403 + * Whether to automatically compress data before storing. 404 + * 405 + * @defaultValue false 406 + * 407 + * @remarks 408 + * Uses gzip compression. Compression is transparent - data is automatically 409 + * decompressed on retrieval. The `compressed` flag in metadata indicates compression state. 410 + */ 411 + compression?: boolean; 269 412 270 - /** 271 - * Strategy for promoting data to upper tiers on cache miss. 272 - * 273 - * @defaultValue 'lazy' 274 - * 275 - * @remarks 276 - * - 'eager': Immediately promote data to all upper tiers on read 277 - * - 'lazy': Don't automatically promote; rely on explicit promotion or next write 278 - * 279 - * Eager promotion increases hot tier hit rate but adds write overhead. 280 - * Lazy promotion reduces writes but may serve from lower tiers more often. 281 - */ 282 - promotionStrategy?: 'eager' | 'lazy'; 413 + /** 414 + * Default TTL (time-to-live) in milliseconds. 415 + * 416 + * @remarks 417 + * Data will expire after this duration. Can be overridden per-key via SetOptions. 418 + * If not set, data never expires. 419 + */ 420 + defaultTTL?: number; 283 421 284 - /** 285 - * Custom serialization/deserialization functions. 286 - * 287 - * @remarks 288 - * By default, JSON serialization is used. Provide custom functions for: 289 - * - Non-JSON types (e.g., Buffer, custom classes) 290 - * - Performance optimization (e.g., msgpack, protobuf) 291 - * - Encryption (serialize includes encryption, deserialize includes decryption) 292 - */ 293 - serialization?: { 294 - /** Convert data to Uint8Array for storage */ 295 - serialize: (data: unknown) => Promise<Uint8Array>; 422 + /** 423 + * Strategy for promoting data to upper tiers on cache miss. 424 + * 425 + * @defaultValue 'lazy' 426 + * 427 + * @remarks 428 + * - 'eager': Immediately promote data to all upper tiers on read 429 + * - 'lazy': Don't automatically promote; rely on explicit promotion or next write 430 + * 431 + * Eager promotion increases hot tier hit rate but adds write overhead. 432 + * Lazy promotion reduces writes but may serve from lower tiers more often. 433 + */ 434 + promotionStrategy?: 'eager' | 'lazy'; 296 435 297 - /** Convert Uint8Array back to original data */ 298 - deserialize: (data: Uint8Array) => Promise<unknown>; 299 - }; 436 + /** 437 + * Custom serialization/deserialization functions. 438 + * 439 + * @remarks 440 + * By default, JSON serialization is used. Provide custom functions for: 441 + * - Non-JSON types (e.g., Buffer, custom classes) 442 + * - Performance optimization (e.g., msgpack, protobuf) 443 + * - Encryption (serialize includes encryption, deserialize includes decryption) 444 + */ 445 + serialization?: { 446 + /** Convert data to Uint8Array for storage */ 447 + serialize: (data: unknown) => Promise<Uint8Array>; 448 + 449 + /** Convert Uint8Array back to original data */ 450 + deserialize: (data: Uint8Array) => Promise<unknown>; 451 + }; 300 452 } 301 453 302 454 /** ··· 306 458 * These options allow fine-grained control over where and how data is stored. 307 459 */ 308 460 export interface SetOptions { 309 - /** 310 - * Custom TTL in milliseconds for this specific key. 311 - * 312 - * @remarks 313 - * Overrides the default TTL from TieredStorageConfig. 314 - * Data will expire after this duration from the current time. 315 - */ 316 - ttl?: number; 461 + /** 462 + * Custom TTL in milliseconds for this specific key. 463 + * 464 + * @remarks 465 + * Overrides the default TTL from TieredStorageConfig. 466 + * Data will expire after this duration from the current time. 467 + */ 468 + ttl?: number; 317 469 318 - /** 319 - * Custom metadata to attach to this key. 320 - * 321 - * @remarks 322 - * Merged with system-generated metadata (size, checksum, timestamps). 323 - * Useful for storing application-specific information like content-type, encoding, etc. 324 - */ 325 - metadata?: Record<string, string>; 470 + /** 471 + * Custom metadata to attach to this key. 472 + * 473 + * @remarks 474 + * Merged with system-generated metadata (size, checksum, timestamps). 475 + * Useful for storing application-specific information like content-type, encoding, etc. 476 + */ 477 + metadata?: Record<string, string>; 326 478 327 - /** 328 - * Skip writing to specific tiers. 329 - * 330 - * @remarks 331 - * Useful for controlling which tiers receive data. For example: 332 - * - Large files: `skipTiers: ['hot']` to avoid filling memory 333 - * - Small critical files: Write to hot only for fastest access 334 - * 335 - * Note: Cold tier can never be skipped (it's the source of truth). 336 - * 337 - * @example 338 - * ```typescript 339 - * // Store large file only in warm and cold (skip memory) 340 - * await storage.set('large-video.mp4', videoData, { skipTiers: ['hot'] }); 341 - * 342 - * // Store index.html in all tiers for fast access 343 - * await storage.set('index.html', htmlData); // No skipping 344 - * ``` 345 - */ 346 - skipTiers?: ('hot' | 'warm')[]; 479 + /** 480 + * Skip writing to specific tiers. 481 + * 482 + * @remarks 483 + * Useful for controlling which tiers receive data. For example: 484 + * - Large files: `skipTiers: ['hot']` to avoid filling memory 485 + * - Small critical files: Write to hot only for fastest access 486 + * 487 + * Note: Cold tier can never be skipped (it's the source of truth). 488 + * 489 + * @example 490 + * ```typescript 491 + * // Store large file only in warm and cold (skip memory) 492 + * await storage.set('large-video.mp4', videoData, { skipTiers: ['hot'] }); 493 + * 494 + * // Store index.html in all tiers for fast access 495 + * await storage.set('index.html', htmlData); // No skipping 496 + * ``` 497 + */ 498 + skipTiers?: ('hot' | 'warm')[]; 347 499 } 348 500 349 501 /** ··· 355 507 * Includes both the data and information about where it was served from. 356 508 */ 357 509 export interface StorageResult<T> { 358 - /** The retrieved data */ 359 - data: T; 510 + /** The retrieved data */ 511 + data: T; 360 512 361 - /** Metadata associated with the data */ 362 - metadata: StorageMetadata; 513 + /** Metadata associated with the data */ 514 + metadata: StorageMetadata; 363 515 364 - /** Which tier the data was served from */ 365 - source: 'hot' | 'warm' | 'cold'; 516 + /** Which tier the data was served from */ 517 + source: 'hot' | 'warm' | 'cold'; 366 518 } 367 519 368 520 /** ··· 372 524 * Indicates which tiers successfully received the data. 373 525 */ 374 526 export interface SetResult { 375 - /** The key that was set */ 376 - key: string; 527 + /** The key that was set */ 528 + key: string; 377 529 378 - /** Metadata that was stored with the data */ 379 - metadata: StorageMetadata; 530 + /** Metadata that was stored with the data */ 531 + metadata: StorageMetadata; 380 532 381 - /** Which tiers received the data */ 382 - tiersWritten: ('hot' | 'warm' | 'cold')[]; 533 + /** Which tiers received the data */ 534 + tiersWritten: ('hot' | 'warm' | 'cold')[]; 383 535 } 384 536 385 537 /** ··· 390 542 * The snapshot includes metadata but not the actual data (data remains in tiers). 391 543 */ 392 544 export interface StorageSnapshot { 393 - /** Snapshot format version (for compatibility) */ 394 - version: number; 545 + /** Snapshot format version (for compatibility) */ 546 + version: number; 395 547 396 - /** When this snapshot was created */ 397 - exportedAt: Date; 548 + /** When this snapshot was created */ 549 + exportedAt: Date; 398 550 399 - /** All keys present in cold tier (source of truth) */ 400 - keys: string[]; 551 + /** All keys present in cold tier (source of truth) */ 552 + keys: string[]; 401 553 402 - /** Metadata for each key */ 403 - metadata: Record<string, StorageMetadata>; 554 + /** Metadata for each key */ 555 + metadata: Record<string, StorageMetadata>; 404 556 405 - /** Statistics at time of export */ 406 - stats: AllTierStats; 557 + /** Statistics at time of export */ 558 + stats: AllTierStats; 407 559 }
+15 -15
src/utils/checksum.ts
··· 18 18 * ``` 19 19 */ 20 20 export function calculateChecksum(data: Uint8Array): string { 21 - const hash = createHash('sha256'); 22 - hash.update(data); 23 - return hash.digest('hex'); 21 + const hash = createHash('sha256'); 22 + hash.update(data); 23 + return hash.digest('hex'); 24 24 } 25 25 26 26 /** ··· 37 37 * ```typescript 38 38 * const isValid = verifyChecksum(data, metadata.checksum); 39 39 * if (!isValid) { 40 - * throw new Error('Data corruption detected'); 40 + * throw new Error('Data corruption detected'); 41 41 * } 42 42 * ``` 43 43 */ 44 44 export function verifyChecksum(data: Uint8Array, expectedChecksum: string): boolean { 45 - const actualChecksum = calculateChecksum(data); 45 + const actualChecksum = calculateChecksum(data); 46 46 47 - // Use constant-time comparison to prevent timing attacks 48 - try { 49 - return timingSafeEqual( 50 - Buffer.from(actualChecksum, 'hex'), 51 - Buffer.from(expectedChecksum, 'hex') 52 - ); 53 - } catch { 54 - // If checksums have different lengths, timingSafeEqual throws 55 - return false; 56 - } 47 + // Use constant-time comparison to prevent timing attacks 48 + try { 49 + return timingSafeEqual( 50 + Buffer.from(actualChecksum, 'hex'), 51 + Buffer.from(expectedChecksum, 'hex'), 52 + ); 53 + } catch { 54 + // If checksums have different lengths, timingSafeEqual throws 55 + return false; 56 + } 57 57 }
+53 -14
src/utils/compression.ts
··· 1 - import { gzip, gunzip } from 'node:zlib'; 1 + import { gzip, gunzip, createGzip, createGunzip } from 'node:zlib'; 2 2 import { promisify } from 'node:util'; 3 + import type { Transform } from 'node:stream'; 3 4 4 5 const gzipAsync = promisify(gzip); 5 6 const gunzipAsync = promisify(gunzip); ··· 22 23 * ``` 23 24 */ 24 25 export async function compress(data: Uint8Array): Promise<Uint8Array> { 25 - const buffer = Buffer.from(data); 26 - const compressed = await gzipAsync(buffer); 27 - return new Uint8Array(compressed); 26 + const buffer = Buffer.from(data); 27 + const compressed = await gzipAsync(buffer); 28 + return new Uint8Array(compressed); 28 29 } 29 30 30 31 /** ··· 44 45 * ``` 45 46 */ 46 47 export async function decompress(data: Uint8Array): Promise<Uint8Array> { 47 - // Validate gzip magic bytes 48 - if (data.length < 2 || data[0] !== 0x1f || data[1] !== 0x8b) { 49 - throw new Error('Invalid gzip data: missing magic bytes'); 50 - } 48 + // Validate gzip magic bytes 49 + if (data.length < 2 || data[0] !== 0x1f || data[1] !== 0x8b) { 50 + throw new Error('Invalid gzip data: missing magic bytes'); 51 + } 51 52 52 - const buffer = Buffer.from(data); 53 - const decompressed = await gunzipAsync(buffer); 54 - return new Uint8Array(decompressed); 53 + const buffer = Buffer.from(data); 54 + const decompressed = await gunzipAsync(buffer); 55 + return new Uint8Array(decompressed); 55 56 } 56 57 57 58 /** ··· 67 68 * @example 68 69 * ```typescript 69 70 * if (isGzipped(data)) { 70 - * console.log('Already compressed, skipping compression'); 71 + * console.log('Already compressed, skipping compression'); 71 72 * } else { 72 - * data = await compress(data); 73 + * data = await compress(data); 73 74 * } 74 75 * ``` 75 76 */ 76 77 export function isGzipped(data: Uint8Array): boolean { 77 - return data.length >= 2 && data[0] === 0x1f && data[1] === 0x8b; 78 + return data.length >= 2 && data[0] === 0x1f && data[1] === 0x8b; 79 + } 80 + 81 + /** 82 + * Create a gzip compression transform stream. 83 + * 84 + * @returns A transform stream that compresses data passing through it 85 + * 86 + * @remarks 87 + * Use this for streaming compression of large files. 88 + * Pipe data through this stream to compress it on-the-fly. 89 + * 90 + * @example 91 + * ```typescript 92 + * const compressStream = createCompressStream(); 93 + * sourceStream.pipe(compressStream).pipe(destinationStream); 94 + * ``` 95 + */ 96 + export function createCompressStream(): Transform { 97 + return createGzip(); 98 + } 99 + 100 + /** 101 + * Create a gzip decompression transform stream. 102 + * 103 + * @returns A transform stream that decompresses data passing through it 104 + * 105 + * @remarks 106 + * Use this for streaming decompression of large files. 107 + * Pipe compressed data through this stream to decompress it on-the-fly. 108 + * 109 + * @example 110 + * ```typescript 111 + * const decompressStream = createDecompressStream(); 112 + * compressedStream.pipe(decompressStream).pipe(destinationStream); 113 + * ``` 114 + */ 115 + export function createDecompressStream(): Transform { 116 + return createGunzip(); 78 117 }
+43
src/utils/glob.ts
··· 1 + /** 2 + * Simple glob pattern matching for key placement rules. 3 + * 4 + * Supports: 5 + * - `*` matches any characters except `/` 6 + * - `**` matches any characters including `/` (including empty string) 7 + * - `{a,b,c}` matches any of the alternatives 8 + * - Exact strings match exactly 9 + */ 10 + export function matchGlob(pattern: string, key: string): boolean { 11 + // Handle exact match 12 + if (!pattern.includes('*') && !pattern.includes('{')) { 13 + return pattern === key; 14 + } 15 + 16 + // Escape regex special chars (except * and {}) 17 + let regex = pattern.replace(/[.+^$|\\()[\]]/g, '\\$&'); 18 + 19 + // Handle {a,b,c} alternation 20 + regex = regex.replace( 21 + /\{([^}]+)\}/g, 22 + (_match: string, alts: string) => `(${alts.split(',').join('|')})`, 23 + ); 24 + 25 + // Use placeholder to avoid double-processing 26 + const DOUBLE = '\x00DOUBLE\x00'; 27 + const SINGLE = '\x00SINGLE\x00'; 28 + 29 + // Mark ** and * with placeholders 30 + regex = regex.replace(/\*\*/g, DOUBLE); 31 + regex = regex.replace(/\*/g, SINGLE); 32 + 33 + // Replace placeholders with regex patterns 34 + // ** matches anything (including /) 35 + // When followed by /, it's optional (matches zero or more path segments) 36 + regex = regex 37 + .replace(new RegExp(`${DOUBLE}/`, 'g'), '(?:.*/)?') // **/ -> optional path prefix 38 + .replace(new RegExp(`/${DOUBLE}`, 'g'), '(?:/.*)?') // /** -> optional path suffix 39 + .replace(new RegExp(DOUBLE, 'g'), '.*') // ** alone -> match anything 40 + .replace(new RegExp(SINGLE, 'g'), '[^/]*'); // * -> match non-slash 41 + 42 + return new RegExp(`^${regex}$`).test(key); 43 + }
+57 -34
src/utils/path-encoding.ts
··· 2 2 * Encode a key to be safe for use as a filesystem path. 3 3 * 4 4 * @param key - The key to encode 5 + * @param encodeColons - Whether to encode colons as %3A (default: false) 5 6 * @returns Filesystem-safe encoded key 6 7 * 7 8 * @remarks 9 + * Preserves forward slashes to create directory structure. 8 10 * Encodes characters that are problematic in filenames: 9 - * - Forward slash (/) โ†’ %2F 10 11 * - Backslash (\) โ†’ %5C 11 - * - Colon (:) โ†’ %3A 12 + * - Colon (:) โ†’ %3A (when encodeColons is true, invalid on Windows) 12 13 * - Asterisk (*) โ†’ %2A 13 14 * - Question mark (?) โ†’ %3F 14 15 * - Quote (") โ†’ %22 ··· 20 21 * 21 22 * @example 22 23 * ```typescript 23 - * const key = 'user:123/profile.json'; 24 - * const encoded = encodeKey(key); 25 - * // Result: 'user%3A123%2Fprofile.json' 24 + * const key = 'did:plc:abc123/site/index.html'; 25 + * 26 + * // Encode colons (Windows/cross-platform) 27 + * const encoded = encodeKey(key, true); 28 + * // Result: 'did%3Aplc%3Aabc123/site/index.html' 29 + * // Creates: cache/did%3Aplc%3Aabc123/site/index.html 30 + * 31 + * // Preserve colons (Unix/macOS with readable paths) 32 + * const readable = encodeKey(key, false); 33 + * // Result: 'did:plc:abc123/site/index.html' 34 + * // Creates: cache/did:plc:abc123/site/index.html 26 35 * ``` 27 36 */ 28 - export function encodeKey(key: string): string { 29 - return key 30 - .replace(/%/g, '%25') // Must be first! 31 - .replace(/\//g, '%2F') 32 - .replace(/\\/g, '%5C') 33 - .replace(/:/g, '%3A') 34 - .replace(/\*/g, '%2A') 35 - .replace(/\?/g, '%3F') 36 - .replace(/"/g, '%22') 37 - .replace(/</g, '%3C') 38 - .replace(/>/g, '%3E') 39 - .replace(/\|/g, '%7C') 40 - .replace(/\0/g, '%00'); 37 + export function encodeKey(key: string, encodeColons = false): string { 38 + let result = key 39 + .replace(/%/g, '%25') // Must be first! 40 + .replace(/\\/g, '%5C'); 41 + 42 + if (encodeColons) { 43 + result = result.replace(/:/g, '%3A'); 44 + } 45 + 46 + return result 47 + .replace(/\*/g, '%2A') 48 + .replace(/\?/g, '%3F') 49 + .replace(/"/g, '%22') 50 + .replace(/</g, '%3C') 51 + .replace(/>/g, '%3E') 52 + .replace(/\|/g, '%7C') 53 + .replace(/\0/g, '%00'); 41 54 } 42 55 43 56 /** 44 57 * Decode a filesystem-safe key back to original form. 45 58 * 46 59 * @param encoded - The encoded key 60 + * @param decodeColons - Whether to decode %3A to : (default: false) 47 61 * @returns Original key 48 62 * 49 63 * @example 50 64 * ```typescript 51 - * const encoded = 'user%3A123%2Fprofile.json'; 52 - * const key = decodeKey(encoded); 53 - * // Result: 'user:123/profile.json' 65 + * const encoded = 'did%3Aplc%3Aabc123/site/index.html'; 66 + * 67 + * // Decode with colons 68 + * const key = decodeKey(encoded, true); 69 + * // Result: 'did:plc:abc123/site/index.html' 70 + * 71 + * // Decode without colons (already readable) 72 + * const readable = decodeKey('did:plc:abc123/site/index.html', false); 73 + * // Result: 'did:plc:abc123/site/index.html' 54 74 * ``` 55 75 */ 56 - export function decodeKey(encoded: string): string { 57 - return encoded 58 - .replace(/%2F/g, '/') 59 - .replace(/%5C/g, '\\') 60 - .replace(/%3A/g, ':') 61 - .replace(/%2A/g, '*') 62 - .replace(/%3F/g, '?') 63 - .replace(/%22/g, '"') 64 - .replace(/%3C/g, '<') 65 - .replace(/%3E/g, '>') 66 - .replace(/%7C/g, '|') 67 - .replace(/%00/g, '\0') 68 - .replace(/%25/g, '%'); // Must be last! 76 + export function decodeKey(encoded: string, decodeColons = false): string { 77 + let result = encoded 78 + .replace(/%5C/g, '\\') 79 + .replace(/%2A/g, '*') 80 + .replace(/%3F/g, '?') 81 + .replace(/%22/g, '"') 82 + .replace(/%3C/g, '<') 83 + .replace(/%3E/g, '>') 84 + .replace(/%7C/g, '|') 85 + .replace(/%00/g, '\0'); 86 + 87 + if (decodeColons) { 88 + result = result.replace(/%3A/g, ':'); 89 + } 90 + 91 + return result.replace(/%25/g, '%'); // Must be last! 69 92 }
+4 -4
src/utils/serialization.ts
··· 19 19 * ``` 20 20 */ 21 21 export async function defaultSerialize(data: unknown): Promise<Uint8Array> { 22 - const json = JSON.stringify(data); 23 - return new TextEncoder().encode(json); 22 + const json = JSON.stringify(data); 23 + return new TextEncoder().encode(json); 24 24 } 25 25 26 26 /** ··· 41 41 * ``` 42 42 */ 43 43 export async function defaultDeserialize(data: Uint8Array): Promise<unknown> { 44 - const json = new TextDecoder().decode(data); 45 - return JSON.parse(json); 44 + const json = new TextDecoder().decode(data); 45 + return JSON.parse(json); 46 46 }
+387
test/DiskStorageTier.test.ts
··· 1 + import { describe, it, expect, afterEach } from 'vitest'; 2 + import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js'; 3 + import { rm, readdir } from 'node:fs/promises'; 4 + import { existsSync } from 'node:fs'; 5 + import { join } from 'node:path'; 6 + 7 + describe('DiskStorageTier - Recursive Directory Support', () => { 8 + const testDir = './test-disk-cache'; 9 + 10 + afterEach(async () => { 11 + await rm(testDir, { recursive: true, force: true }); 12 + }); 13 + 14 + describe('Nested Directory Creation', () => { 15 + it('should create nested directories for keys with slashes', async () => { 16 + const tier = new DiskStorageTier({ directory: testDir }); 17 + 18 + const data = new TextEncoder().encode('test data'); 19 + const metadata = { 20 + key: 'did:plc:abc/site/pages/index.html', 21 + size: data.byteLength, 22 + createdAt: new Date(), 23 + lastAccessed: new Date(), 24 + accessCount: 0, 25 + compressed: false, 26 + checksum: 'abc123', 27 + }; 28 + 29 + await tier.set('did:plc:abc/site/pages/index.html', data, metadata); 30 + 31 + // Verify directory structure was created 32 + expect(existsSync(join(testDir, 'did%3Aplc%3Aabc'))).toBe(true); 33 + expect(existsSync(join(testDir, 'did%3Aplc%3Aabc/site'))).toBe(true); 34 + expect(existsSync(join(testDir, 'did%3Aplc%3Aabc/site/pages'))).toBe(true); 35 + expect(existsSync(join(testDir, 'did%3Aplc%3Aabc/site/pages/index.html'))).toBe(true); 36 + expect(existsSync(join(testDir, 'did%3Aplc%3Aabc/site/pages/index.html.meta'))).toBe( 37 + true, 38 + ); 39 + }); 40 + 41 + it('should handle multiple files in different nested directories', async () => { 42 + const tier = new DiskStorageTier({ directory: testDir }); 43 + 44 + const data = new TextEncoder().encode('test'); 45 + const createMetadata = (key: string) => ({ 46 + key, 47 + size: data.byteLength, 48 + createdAt: new Date(), 49 + lastAccessed: new Date(), 50 + accessCount: 0, 51 + compressed: false, 52 + checksum: 'abc', 53 + }); 54 + 55 + await tier.set( 56 + 'site:a/images/logo.png', 57 + data, 58 + createMetadata('site:a/images/logo.png'), 59 + ); 60 + await tier.set('site:a/css/style.css', data, createMetadata('site:a/css/style.css')); 61 + await tier.set('site:b/index.html', data, createMetadata('site:b/index.html')); 62 + 63 + expect(await tier.exists('site:a/images/logo.png')).toBe(true); 64 + expect(await tier.exists('site:a/css/style.css')).toBe(true); 65 + expect(await tier.exists('site:b/index.html')).toBe(true); 66 + }); 67 + }); 68 + 69 + describe('Recursive Listing', () => { 70 + it('should list all keys across nested directories', async () => { 71 + const tier = new DiskStorageTier({ directory: testDir }); 72 + 73 + const data = new TextEncoder().encode('test'); 74 + const createMetadata = (key: string) => ({ 75 + key, 76 + size: data.byteLength, 77 + createdAt: new Date(), 78 + lastAccessed: new Date(), 79 + accessCount: 0, 80 + compressed: false, 81 + checksum: 'abc', 82 + }); 83 + 84 + const keys = [ 85 + 'site:a/index.html', 86 + 'site:a/about.html', 87 + 'site:a/assets/logo.png', 88 + 'site:b/index.html', 89 + 'site:b/nested/deep/file.txt', 90 + ]; 91 + 92 + for (const key of keys) { 93 + await tier.set(key, data, createMetadata(key)); 94 + } 95 + 96 + const listedKeys: string[] = []; 97 + for await (const key of tier.listKeys()) { 98 + listedKeys.push(key); 99 + } 100 + 101 + expect(listedKeys.sort()).toEqual(keys.sort()); 102 + }); 103 + 104 + it('should list keys with prefix filter across directories', async () => { 105 + const tier = new DiskStorageTier({ directory: testDir }); 106 + 107 + const data = new TextEncoder().encode('test'); 108 + const createMetadata = (key: string) => ({ 109 + key, 110 + size: data.byteLength, 111 + createdAt: new Date(), 112 + lastAccessed: new Date(), 113 + accessCount: 0, 114 + compressed: false, 115 + checksum: 'abc', 116 + }); 117 + 118 + await tier.set('site:a/index.html', data, createMetadata('site:a/index.html')); 119 + await tier.set('site:a/about.html', data, createMetadata('site:a/about.html')); 120 + await tier.set('site:b/index.html', data, createMetadata('site:b/index.html')); 121 + await tier.set('user:123/profile.json', data, createMetadata('user:123/profile.json')); 122 + 123 + const siteKeys: string[] = []; 124 + for await (const key of tier.listKeys('site:')) { 125 + siteKeys.push(key); 126 + } 127 + 128 + expect(siteKeys.sort()).toEqual([ 129 + 'site:a/about.html', 130 + 'site:a/index.html', 131 + 'site:b/index.html', 132 + ]); 133 + }); 134 + 135 + it('should handle empty directories gracefully', async () => { 136 + const tier = new DiskStorageTier({ directory: testDir }); 137 + 138 + const keys: string[] = []; 139 + for await (const key of tier.listKeys()) { 140 + keys.push(key); 141 + } 142 + 143 + expect(keys).toEqual([]); 144 + }); 145 + }); 146 + 147 + describe('Recursive Stats Collection', () => { 148 + it('should calculate stats across all nested directories', async () => { 149 + const tier = new DiskStorageTier({ directory: testDir }); 150 + 151 + const data1 = new TextEncoder().encode('small'); 152 + const data2 = new TextEncoder().encode('medium content here'); 153 + const data3 = new TextEncoder().encode('x'.repeat(1000)); 154 + 155 + const createMetadata = (key: string, size: number) => ({ 156 + key, 157 + size, 158 + createdAt: new Date(), 159 + lastAccessed: new Date(), 160 + accessCount: 0, 161 + compressed: false, 162 + checksum: 'abc', 163 + }); 164 + 165 + await tier.set('a/file1.txt', data1, createMetadata('a/file1.txt', data1.byteLength)); 166 + await tier.set( 167 + 'a/b/file2.txt', 168 + data2, 169 + createMetadata('a/b/file2.txt', data2.byteLength), 170 + ); 171 + await tier.set( 172 + 'a/b/c/file3.txt', 173 + data3, 174 + createMetadata('a/b/c/file3.txt', data3.byteLength), 175 + ); 176 + 177 + const stats = await tier.getStats(); 178 + 179 + expect(stats.items).toBe(3); 180 + expect(stats.bytes).toBe(data1.byteLength + data2.byteLength + data3.byteLength); 181 + }); 182 + 183 + it('should return zero stats for empty directory', async () => { 184 + const tier = new DiskStorageTier({ directory: testDir }); 185 + 186 + const stats = await tier.getStats(); 187 + 188 + expect(stats.items).toBe(0); 189 + expect(stats.bytes).toBe(0); 190 + }); 191 + }); 192 + 193 + describe('Index Rebuilding', () => { 194 + it('should rebuild index from nested directory structure on init', async () => { 195 + const data = new TextEncoder().encode('test data'); 196 + const createMetadata = (key: string) => ({ 197 + key, 198 + size: data.byteLength, 199 + createdAt: new Date(), 200 + lastAccessed: new Date(), 201 + accessCount: 0, 202 + compressed: false, 203 + checksum: 'abc', 204 + }); 205 + 206 + // Create tier and add nested data 207 + const tier1 = new DiskStorageTier({ directory: testDir }); 208 + await tier1.set('site:a/index.html', data, createMetadata('site:a/index.html')); 209 + await tier1.set( 210 + 'site:a/nested/deep/file.txt', 211 + data, 212 + createMetadata('site:a/nested/deep/file.txt'), 213 + ); 214 + await tier1.set('site:b/page.html', data, createMetadata('site:b/page.html')); 215 + 216 + // Create new tier instance (should rebuild index from disk) 217 + const tier2 = new DiskStorageTier({ directory: testDir }); 218 + 219 + // Give it a moment to rebuild 220 + await new Promise((resolve) => setTimeout(resolve, 100)); 221 + 222 + // Verify all keys are accessible 223 + expect(await tier2.exists('site:a/index.html')).toBe(true); 224 + expect(await tier2.exists('site:a/nested/deep/file.txt')).toBe(true); 225 + expect(await tier2.exists('site:b/page.html')).toBe(true); 226 + 227 + // Verify stats are correct 228 + const stats = await tier2.getStats(); 229 + expect(stats.items).toBe(3); 230 + }); 231 + 232 + it('should handle corrupted metadata files during rebuild', async () => { 233 + const tier = new DiskStorageTier({ directory: testDir }); 234 + 235 + const data = new TextEncoder().encode('test'); 236 + const metadata = { 237 + key: 'test/key.txt', 238 + size: data.byteLength, 239 + createdAt: new Date(), 240 + lastAccessed: new Date(), 241 + accessCount: 0, 242 + compressed: false, 243 + checksum: 'abc', 244 + }; 245 + 246 + await tier.set('test/key.txt', data, metadata); 247 + 248 + // Verify directory structure 249 + const entries = await readdir(testDir, { withFileTypes: true }); 250 + expect(entries.length).toBeGreaterThan(0); 251 + 252 + // New tier instance should handle any issues gracefully 253 + const tier2 = new DiskStorageTier({ directory: testDir }); 254 + await new Promise((resolve) => setTimeout(resolve, 100)); 255 + 256 + // Should still work 257 + const stats = await tier2.getStats(); 258 + expect(stats.items).toBeGreaterThanOrEqual(0); 259 + }); 260 + }); 261 + 262 + describe('getWithMetadata Optimization', () => { 263 + it('should retrieve data and metadata from nested directories in parallel', async () => { 264 + const tier = new DiskStorageTier({ directory: testDir }); 265 + 266 + const data = new TextEncoder().encode('test data content'); 267 + const metadata = { 268 + key: 'deep/nested/path/file.json', 269 + size: data.byteLength, 270 + createdAt: new Date(), 271 + lastAccessed: new Date(), 272 + accessCount: 5, 273 + compressed: false, 274 + checksum: 'abc123', 275 + }; 276 + 277 + await tier.set('deep/nested/path/file.json', data, metadata); 278 + 279 + const result = await tier.getWithMetadata('deep/nested/path/file.json'); 280 + 281 + expect(result).not.toBeNull(); 282 + expect(result?.data).toEqual(data); 283 + expect(result?.metadata.key).toBe('deep/nested/path/file.json'); 284 + expect(result?.metadata.accessCount).toBe(5); 285 + }); 286 + }); 287 + 288 + describe('Deletion from Nested Directories', () => { 289 + it('should delete files from nested directories', async () => { 290 + const tier = new DiskStorageTier({ directory: testDir }); 291 + 292 + const data = new TextEncoder().encode('test'); 293 + const createMetadata = (key: string) => ({ 294 + key, 295 + size: data.byteLength, 296 + createdAt: new Date(), 297 + lastAccessed: new Date(), 298 + accessCount: 0, 299 + compressed: false, 300 + checksum: 'abc', 301 + }); 302 + 303 + await tier.set('a/b/c/file1.txt', data, createMetadata('a/b/c/file1.txt')); 304 + await tier.set('a/b/file2.txt', data, createMetadata('a/b/file2.txt')); 305 + 306 + expect(await tier.exists('a/b/c/file1.txt')).toBe(true); 307 + 308 + await tier.delete('a/b/c/file1.txt'); 309 + 310 + expect(await tier.exists('a/b/c/file1.txt')).toBe(false); 311 + expect(await tier.exists('a/b/file2.txt')).toBe(true); 312 + }); 313 + 314 + it('should delete multiple files across nested directories', async () => { 315 + const tier = new DiskStorageTier({ directory: testDir }); 316 + 317 + const data = new TextEncoder().encode('test'); 318 + const createMetadata = (key: string) => ({ 319 + key, 320 + size: data.byteLength, 321 + createdAt: new Date(), 322 + lastAccessed: new Date(), 323 + accessCount: 0, 324 + compressed: false, 325 + checksum: 'abc', 326 + }); 327 + 328 + const keys = ['site:a/index.html', 'site:a/nested/page.html', 'site:b/index.html']; 329 + 330 + for (const key of keys) { 331 + await tier.set(key, data, createMetadata(key)); 332 + } 333 + 334 + await tier.deleteMany(keys); 335 + 336 + for (const key of keys) { 337 + expect(await tier.exists(key)).toBe(false); 338 + } 339 + }); 340 + }); 341 + 342 + describe('Edge Cases', () => { 343 + it('should handle keys with many nested levels', async () => { 344 + const tier = new DiskStorageTier({ directory: testDir }); 345 + 346 + const data = new TextEncoder().encode('deep'); 347 + const deepKey = 'a/b/c/d/e/f/g/h/i/j/k/file.txt'; 348 + const metadata = { 349 + key: deepKey, 350 + size: data.byteLength, 351 + createdAt: new Date(), 352 + lastAccessed: new Date(), 353 + accessCount: 0, 354 + compressed: false, 355 + checksum: 'abc', 356 + }; 357 + 358 + await tier.set(deepKey, data, metadata); 359 + 360 + expect(await tier.exists(deepKey)).toBe(true); 361 + 362 + const retrieved = await tier.get(deepKey); 363 + expect(retrieved).toEqual(data); 364 + }); 365 + 366 + it('should handle keys with special characters', async () => { 367 + const tier = new DiskStorageTier({ directory: testDir }); 368 + 369 + const data = new TextEncoder().encode('test'); 370 + const metadata = { 371 + key: 'site:abc/file[1].txt', 372 + size: data.byteLength, 373 + createdAt: new Date(), 374 + lastAccessed: new Date(), 375 + accessCount: 0, 376 + compressed: false, 377 + checksum: 'abc', 378 + }; 379 + 380 + await tier.set('site:abc/file[1].txt', data, metadata); 381 + 382 + expect(await tier.exists('site:abc/file[1].txt')).toBe(true); 383 + const retrieved = await tier.get('site:abc/file[1].txt'); 384 + expect(retrieved).toEqual(data); 385 + }); 386 + }); 387 + });
+473 -318
test/TieredStorage.test.ts
··· 1 - import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 1 + import { describe, it, expect, afterEach } from 'vitest'; 2 2 import { TieredStorage } from '../src/TieredStorage.js'; 3 3 import { MemoryStorageTier } from '../src/tiers/MemoryStorageTier.js'; 4 4 import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js'; 5 5 import { rm } from 'node:fs/promises'; 6 6 7 7 describe('TieredStorage', () => { 8 - const testDir = './test-cache'; 8 + const testDir = './test-cache'; 9 9 10 - afterEach(async () => { 11 - await rm(testDir, { recursive: true, force: true }); 12 - }); 10 + afterEach(async () => { 11 + await rm(testDir, { recursive: true, force: true }); 12 + }); 13 13 14 - describe('Basic Operations', () => { 15 - it('should store and retrieve data', async () => { 16 - const storage = new TieredStorage({ 17 - tiers: { 18 - hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 19 - warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 20 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 21 - }, 22 - }); 14 + describe('Basic Operations', () => { 15 + it('should store and retrieve data', async () => { 16 + const storage = new TieredStorage({ 17 + tiers: { 18 + hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 19 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 20 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 21 + }, 22 + }); 23 23 24 - await storage.set('test-key', { message: 'Hello, world!' }); 25 - const result = await storage.get('test-key'); 24 + await storage.set('test-key', { message: 'Hello, world!' }); 25 + const result = await storage.get('test-key'); 26 26 27 - expect(result).toEqual({ message: 'Hello, world!' }); 28 - }); 27 + expect(result).toEqual({ message: 'Hello, world!' }); 28 + }); 29 29 30 - it('should return null for non-existent key', async () => { 31 - const storage = new TieredStorage({ 32 - tiers: { 33 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 34 - }, 35 - }); 30 + it('should return null for non-existent key', async () => { 31 + const storage = new TieredStorage({ 32 + tiers: { 33 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 34 + }, 35 + }); 36 36 37 - const result = await storage.get('non-existent'); 38 - expect(result).toBeNull(); 39 - }); 37 + const result = await storage.get('non-existent'); 38 + expect(result).toBeNull(); 39 + }); 40 40 41 - it('should delete data from all tiers', async () => { 42 - const storage = new TieredStorage({ 43 - tiers: { 44 - hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 45 - warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 46 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 47 - }, 48 - }); 41 + it('should delete data from all tiers', async () => { 42 + const storage = new TieredStorage({ 43 + tiers: { 44 + hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 45 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 46 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 47 + }, 48 + }); 49 49 50 - await storage.set('test-key', { data: 'test' }); 51 - await storage.delete('test-key'); 52 - const result = await storage.get('test-key'); 50 + await storage.set('test-key', { data: 'test' }); 51 + await storage.delete('test-key'); 52 + const result = await storage.get('test-key'); 53 53 54 - expect(result).toBeNull(); 55 - }); 54 + expect(result).toBeNull(); 55 + }); 56 56 57 - it('should check if key exists', async () => { 58 - const storage = new TieredStorage({ 59 - tiers: { 60 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 61 - }, 62 - }); 57 + it('should check if key exists', async () => { 58 + const storage = new TieredStorage({ 59 + tiers: { 60 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 61 + }, 62 + }); 63 63 64 - await storage.set('test-key', { data: 'test' }); 64 + await storage.set('test-key', { data: 'test' }); 65 65 66 - expect(await storage.exists('test-key')).toBe(true); 67 - expect(await storage.exists('non-existent')).toBe(false); 68 - }); 69 - }); 66 + expect(await storage.exists('test-key')).toBe(true); 67 + expect(await storage.exists('non-existent')).toBe(false); 68 + }); 69 + }); 70 70 71 - describe('Cascading Write', () => { 72 - it('should write to all configured tiers', async () => { 73 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 74 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 75 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 71 + describe('Cascading Write', () => { 72 + it('should write to all configured tiers', async () => { 73 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 74 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 75 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 76 76 77 - const storage = new TieredStorage({ 78 - tiers: { hot, warm, cold }, 79 - }); 77 + const storage = new TieredStorage({ 78 + tiers: { hot, warm, cold }, 79 + }); 80 80 81 - await storage.set('test-key', { data: 'test' }); 81 + await storage.set('test-key', { data: 'test' }); 82 82 83 - // Verify data exists in all tiers 84 - expect(await hot.exists('test-key')).toBe(true); 85 - expect(await warm.exists('test-key')).toBe(true); 86 - expect(await cold.exists('test-key')).toBe(true); 87 - }); 83 + // Verify data exists in all tiers 84 + expect(await hot.exists('test-key')).toBe(true); 85 + expect(await warm.exists('test-key')).toBe(true); 86 + expect(await cold.exists('test-key')).toBe(true); 87 + }); 88 88 89 - it('should skip tiers when specified', async () => { 90 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 91 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 92 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 89 + it('should skip tiers when specified', async () => { 90 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 91 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 92 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 93 93 94 - const storage = new TieredStorage({ 95 - tiers: { hot, warm, cold }, 96 - }); 94 + const storage = new TieredStorage({ 95 + tiers: { hot, warm, cold }, 96 + }); 97 97 98 - // Skip hot tier 99 - await storage.set('test-key', { data: 'test' }, { skipTiers: ['hot'] }); 98 + // Skip hot tier 99 + await storage.set('test-key', { data: 'test' }, { skipTiers: ['hot'] }); 100 100 101 - expect(await hot.exists('test-key')).toBe(false); 102 - expect(await warm.exists('test-key')).toBe(true); 103 - expect(await cold.exists('test-key')).toBe(true); 104 - }); 105 - }); 101 + expect(await hot.exists('test-key')).toBe(false); 102 + expect(await warm.exists('test-key')).toBe(true); 103 + expect(await cold.exists('test-key')).toBe(true); 104 + }); 105 + }); 106 106 107 - describe('Bubbling Read', () => { 108 - it('should read from hot tier first', async () => { 109 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 110 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 111 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 107 + describe('Bubbling Read', () => { 108 + it('should read from hot tier first', async () => { 109 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 110 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 111 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 112 112 113 - const storage = new TieredStorage({ 114 - tiers: { hot, warm, cold }, 115 - }); 113 + const storage = new TieredStorage({ 114 + tiers: { hot, warm, cold }, 115 + }); 116 116 117 - await storage.set('test-key', { data: 'test' }); 118 - const result = await storage.getWithMetadata('test-key'); 117 + await storage.set('test-key', { data: 'test' }); 118 + const result = await storage.getWithMetadata('test-key'); 119 119 120 - expect(result?.source).toBe('hot'); 121 - expect(result?.data).toEqual({ data: 'test' }); 122 - }); 120 + expect(result?.source).toBe('hot'); 121 + expect(result?.data).toEqual({ data: 'test' }); 122 + }); 123 123 124 - it('should fall back to warm tier on hot miss', async () => { 125 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 126 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 127 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 124 + it('should fall back to warm tier on hot miss', async () => { 125 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 126 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 127 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 128 128 129 - const storage = new TieredStorage({ 130 - tiers: { hot, warm, cold }, 131 - }); 129 + const storage = new TieredStorage({ 130 + tiers: { hot, warm, cold }, 131 + }); 132 132 133 - // Write to warm and cold, skip hot 134 - await storage.set('test-key', { data: 'test' }, { skipTiers: ['hot'] }); 133 + // Write to warm and cold, skip hot 134 + await storage.set('test-key', { data: 'test' }, { skipTiers: ['hot'] }); 135 + 136 + const result = await storage.getWithMetadata('test-key'); 137 + 138 + expect(result?.source).toBe('warm'); 139 + expect(result?.data).toEqual({ data: 'test' }); 140 + }); 141 + 142 + it('should fall back to cold tier on hot and warm miss', async () => { 143 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 144 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 145 + 146 + const storage = new TieredStorage({ 147 + tiers: { hot, cold }, 148 + }); 149 + 150 + // Write only to cold 151 + await cold.set('test-key', new TextEncoder().encode(JSON.stringify({ data: 'test' })), { 152 + key: 'test-key', 153 + size: 100, 154 + createdAt: new Date(), 155 + lastAccessed: new Date(), 156 + accessCount: 0, 157 + compressed: false, 158 + checksum: 'abc123', 159 + }); 160 + 161 + const result = await storage.getWithMetadata('test-key'); 162 + 163 + expect(result?.source).toBe('cold'); 164 + expect(result?.data).toEqual({ data: 'test' }); 165 + }); 166 + }); 167 + 168 + describe('Promotion Strategy', () => { 169 + it('should eagerly promote data to upper tiers', async () => { 170 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 171 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 172 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 173 + 174 + const storage = new TieredStorage({ 175 + tiers: { hot, warm, cold }, 176 + promotionStrategy: 'eager', 177 + }); 178 + 179 + // Write only to cold 180 + await cold.set('test-key', new TextEncoder().encode(JSON.stringify({ data: 'test' })), { 181 + key: 'test-key', 182 + size: 100, 183 + createdAt: new Date(), 184 + lastAccessed: new Date(), 185 + accessCount: 0, 186 + compressed: false, 187 + checksum: 'abc123', 188 + }); 189 + 190 + // Read should promote to hot and warm 191 + await storage.get('test-key'); 192 + 193 + expect(await hot.exists('test-key')).toBe(true); 194 + expect(await warm.exists('test-key')).toBe(true); 195 + }); 135 196 136 - const result = await storage.getWithMetadata('test-key'); 197 + it('should lazily promote data (not automatic)', async () => { 198 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 199 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 200 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 201 + 202 + const storage = new TieredStorage({ 203 + tiers: { hot, warm, cold }, 204 + promotionStrategy: 'lazy', 205 + }); 137 206 138 - expect(result?.source).toBe('warm'); 139 - expect(result?.data).toEqual({ data: 'test' }); 140 - }); 207 + // Write only to cold 208 + await cold.set('test-key', new TextEncoder().encode(JSON.stringify({ data: 'test' })), { 209 + key: 'test-key', 210 + size: 100, 211 + createdAt: new Date(), 212 + lastAccessed: new Date(), 213 + accessCount: 0, 214 + compressed: false, 215 + checksum: 'abc123', 216 + }); 141 217 142 - it('should fall back to cold tier on hot and warm miss', async () => { 143 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 144 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 218 + // Read should NOT promote to hot and warm 219 + await storage.get('test-key'); 145 220 146 - const storage = new TieredStorage({ 147 - tiers: { hot, cold }, 148 - }); 221 + expect(await hot.exists('test-key')).toBe(false); 222 + expect(await warm.exists('test-key')).toBe(false); 223 + }); 224 + }); 225 + 226 + describe('TTL Management', () => { 227 + it('should expire data after TTL', async () => { 228 + const storage = new TieredStorage({ 229 + tiers: { 230 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 231 + }, 232 + }); 233 + 234 + // Set with 100ms TTL 235 + await storage.set('test-key', { data: 'test' }, { ttl: 100 }); 236 + 237 + // Should exist immediately 238 + expect(await storage.get('test-key')).toEqual({ data: 'test' }); 239 + 240 + // Wait for expiration 241 + await new Promise((resolve) => setTimeout(resolve, 150)); 242 + 243 + // Should be null after expiration 244 + expect(await storage.get('test-key')).toBeNull(); 245 + }); 246 + 247 + it('should renew TTL with touch', async () => { 248 + const storage = new TieredStorage({ 249 + tiers: { 250 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 251 + }, 252 + defaultTTL: 100, 253 + }); 254 + 255 + await storage.set('test-key', { data: 'test' }); 256 + 257 + // Wait 50ms 258 + await new Promise((resolve) => setTimeout(resolve, 50)); 259 + 260 + // Renew TTL 261 + await storage.touch('test-key', 200); 262 + 263 + // Wait another 100ms (would have expired without touch) 264 + await new Promise((resolve) => setTimeout(resolve, 100)); 265 + 266 + // Should still exist 267 + expect(await storage.get('test-key')).toEqual({ data: 'test' }); 268 + }); 269 + }); 270 + 271 + describe('Prefix Invalidation', () => { 272 + it('should invalidate all keys with prefix', async () => { 273 + const storage = new TieredStorage({ 274 + tiers: { 275 + hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 276 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 277 + }, 278 + }); 279 + 280 + await storage.set('user:123', { name: 'Alice' }); 281 + await storage.set('user:456', { name: 'Bob' }); 282 + await storage.set('post:789', { title: 'Test' }); 283 + 284 + const deleted = await storage.invalidate('user:'); 285 + 286 + expect(deleted).toBe(2); 287 + expect(await storage.exists('user:123')).toBe(false); 288 + expect(await storage.exists('user:456')).toBe(false); 289 + expect(await storage.exists('post:789')).toBe(true); 290 + }); 291 + }); 292 + 293 + describe('Compression', () => { 294 + it('should compress data when enabled', async () => { 295 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 296 + 297 + const storage = new TieredStorage({ 298 + tiers: { cold }, 299 + compression: true, 300 + }); 301 + 302 + const largeData = { data: 'x'.repeat(10000) }; 303 + const result = await storage.set('test-key', largeData); 304 + 305 + // Check that compressed flag is set 306 + expect(result.metadata.compressed).toBe(true); 307 + 308 + // Verify data can be retrieved correctly 309 + const retrieved = await storage.get('test-key'); 310 + expect(retrieved).toEqual(largeData); 311 + }); 312 + }); 313 + 314 + describe('Bootstrap', () => { 315 + it('should bootstrap hot from warm', async () => { 316 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 317 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 318 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 319 + 320 + const storage = new TieredStorage({ 321 + tiers: { hot, warm, cold }, 322 + }); 149 323 150 - // Write only to cold 151 - await cold.set( 152 - 'test-key', 153 - new TextEncoder().encode(JSON.stringify({ data: 'test' })), 154 - { 155 - key: 'test-key', 156 - size: 100, 157 - createdAt: new Date(), 158 - lastAccessed: new Date(), 159 - accessCount: 0, 160 - compressed: false, 161 - checksum: 'abc123', 162 - } 163 - ); 324 + // Write some data 325 + await storage.set('key1', { data: '1' }); 326 + await storage.set('key2', { data: '2' }); 327 + await storage.set('key3', { data: '3' }); 164 328 165 - const result = await storage.getWithMetadata('test-key'); 329 + // Clear hot tier 330 + await hot.clear(); 166 331 167 - expect(result?.source).toBe('cold'); 168 - expect(result?.data).toEqual({ data: 'test' }); 169 - }); 170 - }); 332 + // Bootstrap hot from warm 333 + const loaded = await storage.bootstrapHot(); 171 334 172 - describe('Promotion Strategy', () => { 173 - it('should eagerly promote data to upper tiers', async () => { 174 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 175 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 176 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 335 + expect(loaded).toBe(3); 336 + expect(await hot.exists('key1')).toBe(true); 337 + expect(await hot.exists('key2')).toBe(true); 338 + expect(await hot.exists('key3')).toBe(true); 339 + }); 177 340 178 - const storage = new TieredStorage({ 179 - tiers: { hot, warm, cold }, 180 - promotionStrategy: 'eager', 181 - }); 341 + it('should bootstrap warm from cold', async () => { 342 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 343 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 182 344 183 - // Write only to cold 184 - await cold.set( 185 - 'test-key', 186 - new TextEncoder().encode(JSON.stringify({ data: 'test' })), 187 - { 188 - key: 'test-key', 189 - size: 100, 190 - createdAt: new Date(), 191 - lastAccessed: new Date(), 192 - accessCount: 0, 193 - compressed: false, 194 - checksum: 'abc123', 195 - } 196 - ); 345 + const storage = new TieredStorage({ 346 + tiers: { warm, cold }, 347 + }); 197 348 198 - // Read should promote to hot and warm 199 - await storage.get('test-key'); 349 + // Write directly to cold 350 + await cold.set('key1', new TextEncoder().encode(JSON.stringify({ data: '1' })), { 351 + key: 'key1', 352 + size: 100, 353 + createdAt: new Date(), 354 + lastAccessed: new Date(), 355 + accessCount: 0, 356 + compressed: false, 357 + checksum: 'abc', 358 + }); 200 359 201 - expect(await hot.exists('test-key')).toBe(true); 202 - expect(await warm.exists('test-key')).toBe(true); 203 - }); 360 + // Bootstrap warm from cold 361 + const loaded = await storage.bootstrapWarm({ limit: 10 }); 204 362 205 - it('should lazily promote data (not automatic)', async () => { 206 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 207 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 208 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 363 + expect(loaded).toBe(1); 364 + expect(await warm.exists('key1')).toBe(true); 365 + }); 366 + }); 209 367 210 - const storage = new TieredStorage({ 211 - tiers: { hot, warm, cold }, 212 - promotionStrategy: 'lazy', 213 - }); 368 + describe('Statistics', () => { 369 + it('should return statistics for all tiers', async () => { 370 + const storage = new TieredStorage({ 371 + tiers: { 372 + hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 373 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 374 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 375 + }, 376 + }); 214 377 215 - // Write only to cold 216 - await cold.set( 217 - 'test-key', 218 - new TextEncoder().encode(JSON.stringify({ data: 'test' })), 219 - { 220 - key: 'test-key', 221 - size: 100, 222 - createdAt: new Date(), 223 - lastAccessed: new Date(), 224 - accessCount: 0, 225 - compressed: false, 226 - checksum: 'abc123', 227 - } 228 - ); 378 + await storage.set('key1', { data: 'test1' }); 379 + await storage.set('key2', { data: 'test2' }); 229 380 230 - // Read should NOT promote to hot and warm 231 - await storage.get('test-key'); 381 + const stats = await storage.getStats(); 232 382 233 - expect(await hot.exists('test-key')).toBe(false); 234 - expect(await warm.exists('test-key')).toBe(false); 235 - }); 236 - }); 383 + expect(stats.cold.items).toBe(2); 384 + expect(stats.warm?.items).toBe(2); 385 + expect(stats.hot?.items).toBe(2); 386 + }); 387 + }); 237 388 238 - describe('TTL Management', () => { 239 - it('should expire data after TTL', async () => { 240 - const storage = new TieredStorage({ 241 - tiers: { 242 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 243 - }, 244 - }); 389 + describe('Placement Rules', () => { 390 + it('should place index.html in all tiers based on rule', async () => { 391 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 392 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 393 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 245 394 246 - // Set with 100ms TTL 247 - await storage.set('test-key', { data: 'test' }, { ttl: 100 }); 395 + const storage = new TieredStorage({ 396 + tiers: { hot, warm, cold }, 397 + placementRules: [ 398 + { pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] }, 399 + { pattern: '**', tiers: ['warm', 'cold'] }, 400 + ], 401 + }); 248 402 249 - // Should exist immediately 250 - expect(await storage.get('test-key')).toEqual({ data: 'test' }); 403 + await storage.set('site:abc/index.html', { content: 'hello' }); 251 404 252 - // Wait for expiration 253 - await new Promise((resolve) => setTimeout(resolve, 150)); 405 + expect(await hot.exists('site:abc/index.html')).toBe(true); 406 + expect(await warm.exists('site:abc/index.html')).toBe(true); 407 + expect(await cold.exists('site:abc/index.html')).toBe(true); 408 + }); 254 409 255 - // Should be null after expiration 256 - expect(await storage.get('test-key')).toBeNull(); 257 - }); 410 + it('should skip hot tier for non-matching files', async () => { 411 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 412 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 413 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 258 414 259 - it('should renew TTL with touch', async () => { 260 - const storage = new TieredStorage({ 261 - tiers: { 262 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 263 - }, 264 - defaultTTL: 100, 265 - }); 415 + const storage = new TieredStorage({ 416 + tiers: { hot, warm, cold }, 417 + placementRules: [ 418 + { pattern: '**/index.html', tiers: ['hot', 'warm', 'cold'] }, 419 + { pattern: '**', tiers: ['warm', 'cold'] }, 420 + ], 421 + }); 266 422 267 - await storage.set('test-key', { data: 'test' }); 423 + await storage.set('site:abc/about.html', { content: 'about' }); 268 424 269 - // Wait 50ms 270 - await new Promise((resolve) => setTimeout(resolve, 50)); 425 + expect(await hot.exists('site:abc/about.html')).toBe(false); 426 + expect(await warm.exists('site:abc/about.html')).toBe(true); 427 + expect(await cold.exists('site:abc/about.html')).toBe(true); 428 + }); 271 429 272 - // Renew TTL 273 - await storage.touch('test-key', 200); 430 + it('should match directory patterns', async () => { 431 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 432 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 433 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 274 434 275 - // Wait another 100ms (would have expired without touch) 276 - await new Promise((resolve) => setTimeout(resolve, 100)); 435 + const storage = new TieredStorage({ 436 + tiers: { hot, warm, cold }, 437 + placementRules: [ 438 + { pattern: 'assets/**', tiers: ['warm', 'cold'] }, 439 + { pattern: '**', tiers: ['hot', 'warm', 'cold'] }, 440 + ], 441 + }); 277 442 278 - // Should still exist 279 - expect(await storage.get('test-key')).toEqual({ data: 'test' }); 280 - }); 281 - }); 443 + await storage.set('assets/images/logo.png', { data: 'png' }); 444 + await storage.set('index.html', { data: 'html' }); 282 445 283 - describe('Prefix Invalidation', () => { 284 - it('should invalidate all keys with prefix', async () => { 285 - const storage = new TieredStorage({ 286 - tiers: { 287 - hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 288 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 289 - }, 290 - }); 446 + // assets/** should skip hot 447 + expect(await hot.exists('assets/images/logo.png')).toBe(false); 448 + expect(await warm.exists('assets/images/logo.png')).toBe(true); 291 449 292 - await storage.set('user:123', { name: 'Alice' }); 293 - await storage.set('user:456', { name: 'Bob' }); 294 - await storage.set('post:789', { title: 'Test' }); 450 + // everything else goes to all tiers 451 + expect(await hot.exists('index.html')).toBe(true); 452 + }); 295 453 296 - const deleted = await storage.invalidate('user:'); 454 + it('should match file extension patterns', async () => { 455 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 456 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 457 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 297 458 298 - expect(deleted).toBe(2); 299 - expect(await storage.exists('user:123')).toBe(false); 300 - expect(await storage.exists('user:456')).toBe(false); 301 - expect(await storage.exists('post:789')).toBe(true); 302 - }); 303 - }); 459 + const storage = new TieredStorage({ 460 + tiers: { hot, warm, cold }, 461 + placementRules: [ 462 + { pattern: '**/*.{jpg,png,gif,mp4}', tiers: ['warm', 'cold'] }, 463 + { pattern: '**', tiers: ['hot', 'warm', 'cold'] }, 464 + ], 465 + }); 304 466 305 - describe('Compression', () => { 306 - it('should compress data when enabled', async () => { 307 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 467 + await storage.set('site/hero.png', { data: 'image' }); 468 + await storage.set('site/video.mp4', { data: 'video' }); 469 + await storage.set('site/index.html', { data: 'html' }); 308 470 309 - const storage = new TieredStorage({ 310 - tiers: { cold }, 311 - compression: true, 312 - }); 471 + // Images and video skip hot 472 + expect(await hot.exists('site/hero.png')).toBe(false); 473 + expect(await hot.exists('site/video.mp4')).toBe(false); 313 474 314 - const largeData = { data: 'x'.repeat(10000) }; 315 - const result = await storage.set('test-key', largeData); 475 + // HTML goes everywhere 476 + expect(await hot.exists('site/index.html')).toBe(true); 477 + }); 316 478 317 - // Check that compressed flag is set 318 - expect(result.metadata.compressed).toBe(true); 479 + it('should use first matching rule', async () => { 480 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 481 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 482 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 319 483 320 - // Verify data can be retrieved correctly 321 - const retrieved = await storage.get('test-key'); 322 - expect(retrieved).toEqual(largeData); 323 - }); 324 - }); 484 + const storage = new TieredStorage({ 485 + tiers: { hot, warm, cold }, 486 + placementRules: [ 487 + // Specific rule first 488 + { pattern: 'assets/critical.css', tiers: ['hot', 'warm', 'cold'] }, 489 + // General rule second 490 + { pattern: 'assets/**', tiers: ['warm', 'cold'] }, 491 + { pattern: '**', tiers: ['warm', 'cold'] }, 492 + ], 493 + }); 325 494 326 - describe('Bootstrap', () => { 327 - it('should bootstrap hot from warm', async () => { 328 - const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 329 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 330 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 495 + await storage.set('assets/critical.css', { data: 'css' }); 496 + await storage.set('assets/style.css', { data: 'css' }); 331 497 332 - const storage = new TieredStorage({ 333 - tiers: { hot, warm, cold }, 334 - }); 498 + // critical.css matches first rule -> hot 499 + expect(await hot.exists('assets/critical.css')).toBe(true); 335 500 336 - // Write some data 337 - await storage.set('key1', { data: '1' }); 338 - await storage.set('key2', { data: '2' }); 339 - await storage.set('key3', { data: '3' }); 501 + // style.css matches second rule -> no hot 502 + expect(await hot.exists('assets/style.css')).toBe(false); 503 + }); 340 504 341 - // Clear hot tier 342 - await hot.clear(); 505 + it('should allow skipTiers to override placement rules', async () => { 506 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 507 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 508 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 343 509 344 - // Bootstrap hot from warm 345 - const loaded = await storage.bootstrapHot(); 510 + const storage = new TieredStorage({ 511 + tiers: { hot, warm, cold }, 512 + placementRules: [{ pattern: '**', tiers: ['hot', 'warm', 'cold'] }], 513 + }); 346 514 347 - expect(loaded).toBe(3); 348 - expect(await hot.exists('key1')).toBe(true); 349 - expect(await hot.exists('key2')).toBe(true); 350 - expect(await hot.exists('key3')).toBe(true); 351 - }); 515 + // Explicit skipTiers should override the rule 516 + await storage.set('large-file.bin', { data: 'big' }, { skipTiers: ['hot'] }); 352 517 353 - it('should bootstrap warm from cold', async () => { 354 - const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 355 - const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 518 + expect(await hot.exists('large-file.bin')).toBe(false); 519 + expect(await warm.exists('large-file.bin')).toBe(true); 520 + expect(await cold.exists('large-file.bin')).toBe(true); 521 + }); 356 522 357 - const storage = new TieredStorage({ 358 - tiers: { warm, cold }, 359 - }); 523 + it('should always include cold tier even if not in rule', async () => { 524 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 525 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 526 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 360 527 361 - // Write directly to cold 362 - await cold.set( 363 - 'key1', 364 - new TextEncoder().encode(JSON.stringify({ data: '1' })), 365 - { 366 - key: 'key1', 367 - size: 100, 368 - createdAt: new Date(), 369 - lastAccessed: new Date(), 370 - accessCount: 0, 371 - compressed: false, 372 - checksum: 'abc', 373 - } 374 - ); 528 + const storage = new TieredStorage({ 529 + tiers: { hot, warm, cold }, 530 + placementRules: [ 531 + // Rule doesn't include cold (should be auto-added) 532 + { pattern: '**', tiers: ['hot', 'warm'] }, 533 + ], 534 + }); 375 535 376 - // Bootstrap warm from cold 377 - const loaded = await storage.bootstrapWarm({ limit: 10 }); 536 + await storage.set('test-key', { data: 'test' }); 378 537 379 - expect(loaded).toBe(1); 380 - expect(await warm.exists('key1')).toBe(true); 381 - }); 382 - }); 538 + expect(await cold.exists('test-key')).toBe(true); 539 + }); 383 540 384 - describe('Statistics', () => { 385 - it('should return statistics for all tiers', async () => { 386 - const storage = new TieredStorage({ 387 - tiers: { 388 - hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), 389 - warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 390 - cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 391 - }, 392 - }); 541 + it('should write to all tiers when no rules match', async () => { 542 + const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }); 543 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 544 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 393 545 394 - await storage.set('key1', { data: 'test1' }); 395 - await storage.set('key2', { data: 'test2' }); 546 + const storage = new TieredStorage({ 547 + tiers: { hot, warm, cold }, 548 + placementRules: [{ pattern: 'specific-pattern-only', tiers: ['warm', 'cold'] }], 549 + }); 396 550 397 - const stats = await storage.getStats(); 551 + // This doesn't match any rule 552 + await storage.set('other-key', { data: 'test' }); 398 553 399 - expect(stats.cold.items).toBe(2); 400 - expect(stats.warm?.items).toBe(2); 401 - expect(stats.hot?.items).toBe(2); 402 - }); 403 - }); 554 + expect(await hot.exists('other-key')).toBe(true); 555 + expect(await warm.exists('other-key')).toBe(true); 556 + expect(await cold.exists('other-key')).toBe(true); 557 + }); 558 + }); 404 559 });
+95
test/glob.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { matchGlob } from '../src/utils/glob.js'; 3 + 4 + describe('matchGlob', () => { 5 + describe('exact matches', () => { 6 + it('should match exact strings', () => { 7 + expect(matchGlob('index.html', 'index.html')).toBe(true); 8 + expect(matchGlob('index.html', 'about.html')).toBe(false); 9 + }); 10 + 11 + it('should match paths exactly', () => { 12 + expect(matchGlob('site/index.html', 'site/index.html')).toBe(true); 13 + expect(matchGlob('site/index.html', 'other/index.html')).toBe(false); 14 + }); 15 + }); 16 + 17 + describe('* wildcard', () => { 18 + it('should match any characters except /', () => { 19 + expect(matchGlob('*.html', 'index.html')).toBe(true); 20 + expect(matchGlob('*.html', 'about.html')).toBe(true); 21 + expect(matchGlob('*.html', 'style.css')).toBe(false); 22 + }); 23 + 24 + it('should not match across path separators', () => { 25 + expect(matchGlob('*.html', 'dir/index.html')).toBe(false); 26 + }); 27 + 28 + it('should work with prefix and suffix', () => { 29 + expect(matchGlob('index.*', 'index.html')).toBe(true); 30 + expect(matchGlob('index.*', 'index.css')).toBe(true); 31 + expect(matchGlob('index.*', 'about.html')).toBe(false); 32 + }); 33 + }); 34 + 35 + describe('** wildcard', () => { 36 + it('should match any characters including /', () => { 37 + expect(matchGlob('**', 'anything')).toBe(true); 38 + expect(matchGlob('**', 'path/to/file.txt')).toBe(true); 39 + }); 40 + 41 + it('should match deeply nested paths', () => { 42 + expect(matchGlob('**/index.html', 'index.html')).toBe(true); 43 + expect(matchGlob('**/index.html', 'site/index.html')).toBe(true); 44 + expect(matchGlob('**/index.html', 'a/b/c/index.html')).toBe(true); 45 + expect(matchGlob('**/index.html', 'a/b/c/about.html')).toBe(false); 46 + }); 47 + 48 + it('should match directory prefixes', () => { 49 + expect(matchGlob('assets/**', 'assets/style.css')).toBe(true); 50 + expect(matchGlob('assets/**', 'assets/images/logo.png')).toBe(true); 51 + expect(matchGlob('assets/**', 'other/style.css')).toBe(false); 52 + }); 53 + 54 + it('should match in the middle of a path', () => { 55 + expect(matchGlob('site/**/index.html', 'site/index.html')).toBe(true); 56 + expect(matchGlob('site/**/index.html', 'site/pages/index.html')).toBe(true); 57 + expect(matchGlob('site/**/index.html', 'site/a/b/c/index.html')).toBe(true); 58 + }); 59 + }); 60 + 61 + describe('{a,b,c} alternation', () => { 62 + it('should match any of the alternatives', () => { 63 + expect(matchGlob('*.{html,css,js}', 'index.html')).toBe(true); 64 + expect(matchGlob('*.{html,css,js}', 'style.css')).toBe(true); 65 + expect(matchGlob('*.{html,css,js}', 'app.js')).toBe(true); 66 + expect(matchGlob('*.{html,css,js}', 'image.png')).toBe(false); 67 + }); 68 + 69 + it('should work with ** and alternation', () => { 70 + expect(matchGlob('**/*.{jpg,png,gif}', 'logo.png')).toBe(true); 71 + expect(matchGlob('**/*.{jpg,png,gif}', 'images/logo.png')).toBe(true); 72 + expect(matchGlob('**/*.{jpg,png,gif}', 'a/b/photo.jpg')).toBe(true); 73 + expect(matchGlob('**/*.{jpg,png,gif}', 'style.css')).toBe(false); 74 + }); 75 + }); 76 + 77 + describe('edge cases', () => { 78 + it('should handle empty strings', () => { 79 + expect(matchGlob('', '')).toBe(true); 80 + expect(matchGlob('', 'something')).toBe(false); 81 + expect(matchGlob('**', '')).toBe(true); 82 + }); 83 + 84 + it('should escape regex special characters', () => { 85 + expect(matchGlob('file.txt', 'file.txt')).toBe(true); 86 + expect(matchGlob('file.txt', 'filextxt')).toBe(false); 87 + expect(matchGlob('file[1].txt', 'file[1].txt')).toBe(true); 88 + }); 89 + 90 + it('should handle keys with colons (common in storage)', () => { 91 + expect(matchGlob('site:*/index.html', 'site:abc/index.html')).toBe(true); 92 + expect(matchGlob('site:**/index.html', 'site:abc/pages/index.html')).toBe(true); 93 + }); 94 + }); 95 + });
+598
test/streaming.test.ts
··· 1 + import { describe, it, expect, afterEach } from 'vitest'; 2 + import { TieredStorage } from '../src/TieredStorage.js'; 3 + import { MemoryStorageTier } from '../src/tiers/MemoryStorageTier.js'; 4 + import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js'; 5 + import { rm } from 'node:fs/promises'; 6 + import { Readable } from 'node:stream'; 7 + import { createHash } from 'node:crypto'; 8 + 9 + describe('Streaming Operations', () => { 10 + const testDir = './test-streaming-cache'; 11 + 12 + afterEach(async () => { 13 + await rm(testDir, { recursive: true, force: true }); 14 + }); 15 + 16 + /** 17 + * Helper to create a readable stream from a string or buffer 18 + */ 19 + function createStream(data: string | Buffer): Readable { 20 + return Readable.from([Buffer.from(data)]); 21 + } 22 + 23 + /** 24 + * Helper to consume a stream and return its contents as a buffer 25 + */ 26 + async function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> { 27 + const chunks: Buffer[] = []; 28 + for await (const chunk of stream) { 29 + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); 30 + } 31 + return Buffer.concat(chunks); 32 + } 33 + 34 + /** 35 + * Helper to compute SHA256 checksum of a buffer 36 + */ 37 + function computeChecksum(data: Buffer): string { 38 + return createHash('sha256').update(data).digest('hex'); 39 + } 40 + 41 + describe('DiskStorageTier Streaming', () => { 42 + it('should write and read data using streams', async () => { 43 + const tier = new DiskStorageTier({ directory: testDir }); 44 + 45 + const testData = 'Hello, streaming world! '.repeat(100); 46 + const testBuffer = Buffer.from(testData); 47 + const checksum = computeChecksum(testBuffer); 48 + 49 + const metadata = { 50 + key: 'streaming-test.txt', 51 + size: testBuffer.byteLength, 52 + createdAt: new Date(), 53 + lastAccessed: new Date(), 54 + accessCount: 0, 55 + compressed: false, 56 + checksum, 57 + }; 58 + 59 + // Write using stream 60 + await tier.setStream('streaming-test.txt', createStream(testData), metadata); 61 + 62 + // Verify file exists 63 + expect(await tier.exists('streaming-test.txt')).toBe(true); 64 + 65 + // Read using stream 66 + const result = await tier.getStream('streaming-test.txt'); 67 + expect(result).not.toBeNull(); 68 + 69 + const retrievedData = await streamToBuffer(result!.stream); 70 + expect(retrievedData.toString()).toBe(testData); 71 + expect(result!.metadata.key).toBe('streaming-test.txt'); 72 + }); 73 + 74 + it('should handle large data without memory issues', async () => { 75 + const tier = new DiskStorageTier({ directory: testDir }); 76 + 77 + // Create a 1MB chunk and repeat pattern 78 + const chunkSize = 1024 * 1024; // 1MB 79 + const chunk = Buffer.alloc(chunkSize, 'x'); 80 + 81 + const metadata = { 82 + key: 'large-file.bin', 83 + size: chunkSize, 84 + createdAt: new Date(), 85 + lastAccessed: new Date(), 86 + accessCount: 0, 87 + compressed: false, 88 + checksum: computeChecksum(chunk), 89 + }; 90 + 91 + // Write using stream 92 + await tier.setStream('large-file.bin', Readable.from([chunk]), metadata); 93 + 94 + // Read using stream 95 + const result = await tier.getStream('large-file.bin'); 96 + expect(result).not.toBeNull(); 97 + 98 + const retrievedData = await streamToBuffer(result!.stream); 99 + expect(retrievedData.length).toBe(chunkSize); 100 + expect(retrievedData.equals(chunk)).toBe(true); 101 + }); 102 + 103 + it('should return null for non-existent key', async () => { 104 + const tier = new DiskStorageTier({ directory: testDir }); 105 + 106 + const result = await tier.getStream('non-existent-key'); 107 + expect(result).toBeNull(); 108 + }); 109 + 110 + it('should handle nested directories with streaming', async () => { 111 + const tier = new DiskStorageTier({ directory: testDir }); 112 + 113 + const testData = 'nested streaming data'; 114 + const testBuffer = Buffer.from(testData); 115 + 116 + const metadata = { 117 + key: 'deep/nested/path/file.txt', 118 + size: testBuffer.byteLength, 119 + createdAt: new Date(), 120 + lastAccessed: new Date(), 121 + accessCount: 0, 122 + compressed: false, 123 + checksum: computeChecksum(testBuffer), 124 + }; 125 + 126 + await tier.setStream('deep/nested/path/file.txt', createStream(testData), metadata); 127 + 128 + const result = await tier.getStream('deep/nested/path/file.txt'); 129 + expect(result).not.toBeNull(); 130 + 131 + const retrievedData = await streamToBuffer(result!.stream); 132 + expect(retrievedData.toString()).toBe(testData); 133 + }); 134 + }); 135 + 136 + describe('MemoryStorageTier Streaming', () => { 137 + it('should write and read data using streams', async () => { 138 + const tier = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }); 139 + 140 + const testData = 'Memory tier streaming test'; 141 + const testBuffer = Buffer.from(testData); 142 + 143 + const metadata = { 144 + key: 'memory-test.txt', 145 + size: testBuffer.byteLength, 146 + createdAt: new Date(), 147 + lastAccessed: new Date(), 148 + accessCount: 0, 149 + compressed: false, 150 + checksum: computeChecksum(testBuffer), 151 + }; 152 + 153 + // Write using stream 154 + await tier.setStream('memory-test.txt', createStream(testData), metadata); 155 + 156 + // Read using stream 157 + const result = await tier.getStream('memory-test.txt'); 158 + expect(result).not.toBeNull(); 159 + 160 + const retrievedData = await streamToBuffer(result!.stream); 161 + expect(retrievedData.toString()).toBe(testData); 162 + }); 163 + 164 + it('should return null for non-existent key', async () => { 165 + const tier = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }); 166 + 167 + const result = await tier.getStream('non-existent-key'); 168 + expect(result).toBeNull(); 169 + }); 170 + }); 171 + 172 + describe('TieredStorage Streaming', () => { 173 + it('should store and retrieve data using streams', async () => { 174 + const storage = new TieredStorage({ 175 + tiers: { 176 + hot: new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }), 177 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 178 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 179 + }, 180 + }); 181 + 182 + const testData = 'TieredStorage streaming test data'; 183 + const testBuffer = Buffer.from(testData); 184 + 185 + // Write using stream 186 + const setResult = await storage.setStream('stream-key', createStream(testData), { 187 + size: testBuffer.byteLength, 188 + }); 189 + 190 + expect(setResult.key).toBe('stream-key'); 191 + expect(setResult.metadata.size).toBe(testBuffer.byteLength); 192 + // Hot tier is skipped by default for streaming 193 + expect(setResult.tiersWritten).not.toContain('hot'); 194 + expect(setResult.tiersWritten).toContain('warm'); 195 + expect(setResult.tiersWritten).toContain('cold'); 196 + 197 + // Read using stream 198 + const result = await storage.getStream('stream-key'); 199 + expect(result).not.toBeNull(); 200 + 201 + const retrievedData = await streamToBuffer(result!.stream); 202 + expect(retrievedData.toString()).toBe(testData); 203 + }); 204 + 205 + it('should compute checksum during streaming write', async () => { 206 + const storage = new TieredStorage({ 207 + tiers: { 208 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 209 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 210 + }, 211 + }); 212 + 213 + const testData = 'Data for checksum test'; 214 + const testBuffer = Buffer.from(testData); 215 + const expectedChecksum = computeChecksum(testBuffer); 216 + 217 + const setResult = await storage.setStream('checksum-test', createStream(testData), { 218 + size: testBuffer.byteLength, 219 + }); 220 + 221 + // Checksum should be computed and stored 222 + expect(setResult.metadata.checksum).toBe(expectedChecksum); 223 + }); 224 + 225 + it('should use provided checksum without computing', async () => { 226 + const storage = new TieredStorage({ 227 + tiers: { 228 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 229 + }, 230 + }); 231 + 232 + const testData = 'Data with pre-computed checksum'; 233 + const testBuffer = Buffer.from(testData); 234 + const providedChecksum = 'my-custom-checksum'; 235 + 236 + const setResult = await storage.setStream('custom-checksum', createStream(testData), { 237 + size: testBuffer.byteLength, 238 + checksum: providedChecksum, 239 + }); 240 + 241 + expect(setResult.metadata.checksum).toBe(providedChecksum); 242 + }); 243 + 244 + it('should return null for non-existent key', async () => { 245 + const storage = new TieredStorage({ 246 + tiers: { 247 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 248 + }, 249 + }); 250 + 251 + const result = await storage.getStream('non-existent'); 252 + expect(result).toBeNull(); 253 + }); 254 + 255 + it('should read from appropriate tier (warm before cold)', async () => { 256 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 257 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 258 + 259 + const storage = new TieredStorage({ 260 + tiers: { warm, cold }, 261 + }); 262 + 263 + const testData = 'Tier priority test data'; 264 + const testBuffer = Buffer.from(testData); 265 + 266 + await storage.setStream('tier-test', createStream(testData), { 267 + size: testBuffer.byteLength, 268 + }); 269 + 270 + // Both tiers should have the data 271 + expect(await warm.exists('tier-test')).toBe(true); 272 + expect(await cold.exists('tier-test')).toBe(true); 273 + 274 + // Read should come from warm (first available) 275 + const result = await storage.getStream('tier-test'); 276 + expect(result).not.toBeNull(); 277 + expect(result!.source).toBe('warm'); 278 + }); 279 + 280 + it('should fall back to cold tier when warm has no data', async () => { 281 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 282 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 283 + 284 + const storage = new TieredStorage({ 285 + tiers: { warm, cold }, 286 + }); 287 + 288 + // Write directly to cold only 289 + const testData = 'Cold tier only data'; 290 + const testBuffer = Buffer.from(testData); 291 + const metadata = { 292 + key: 'cold-only', 293 + size: testBuffer.byteLength, 294 + createdAt: new Date(), 295 + lastAccessed: new Date(), 296 + accessCount: 0, 297 + compressed: false, 298 + checksum: computeChecksum(testBuffer), 299 + }; 300 + 301 + await cold.setStream('cold-only', createStream(testData), metadata); 302 + 303 + // Read should come from cold 304 + const result = await storage.getStream('cold-only'); 305 + expect(result).not.toBeNull(); 306 + expect(result!.source).toBe('cold'); 307 + }); 308 + 309 + it('should handle TTL with metadata', async () => { 310 + const storage = new TieredStorage({ 311 + tiers: { 312 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 313 + }, 314 + defaultTTL: 60000, // 1 minute 315 + }); 316 + 317 + const testData = 'TTL test data'; 318 + const testBuffer = Buffer.from(testData); 319 + 320 + const setResult = await storage.setStream('ttl-test', createStream(testData), { 321 + size: testBuffer.byteLength, 322 + ttl: 30000, // 30 seconds 323 + }); 324 + 325 + expect(setResult.metadata.ttl).toBeDefined(); 326 + expect(setResult.metadata.ttl!.getTime()).toBeGreaterThan(Date.now()); 327 + }); 328 + 329 + it('should include mimeType in metadata', async () => { 330 + const storage = new TieredStorage({ 331 + tiers: { 332 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 333 + }, 334 + }); 335 + 336 + const testData = '{"message": "json data"}'; 337 + const testBuffer = Buffer.from(testData); 338 + 339 + const setResult = await storage.setStream('json-file.json', createStream(testData), { 340 + size: testBuffer.byteLength, 341 + mimeType: 'application/json', 342 + }); 343 + 344 + expect(setResult.metadata.mimeType).toBe('application/json'); 345 + }); 346 + 347 + it('should write to multiple tiers simultaneously', async () => { 348 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 349 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 350 + 351 + const storage = new TieredStorage({ 352 + tiers: { warm, cold }, 353 + }); 354 + 355 + const testData = 'Multi-tier streaming data'; 356 + const testBuffer = Buffer.from(testData); 357 + 358 + await storage.setStream('multi-tier', createStream(testData), { 359 + size: testBuffer.byteLength, 360 + }); 361 + 362 + // Verify data in both tiers 363 + const warmResult = await warm.getStream('multi-tier'); 364 + const coldResult = await cold.getStream('multi-tier'); 365 + 366 + expect(warmResult).not.toBeNull(); 367 + expect(coldResult).not.toBeNull(); 368 + 369 + const warmData = await streamToBuffer(warmResult!.stream); 370 + const coldData = await streamToBuffer(coldResult!.stream); 371 + 372 + expect(warmData.toString()).toBe(testData); 373 + expect(coldData.toString()).toBe(testData); 374 + }); 375 + 376 + it('should skip hot tier by default for streaming writes', async () => { 377 + const hot = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }); 378 + const warm = new DiskStorageTier({ directory: `${testDir}/warm` }); 379 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 380 + 381 + const storage = new TieredStorage({ 382 + tiers: { hot, warm, cold }, 383 + }); 384 + 385 + const testData = 'Skip hot tier test'; 386 + const testBuffer = Buffer.from(testData); 387 + 388 + const setResult = await storage.setStream('skip-hot', createStream(testData), { 389 + size: testBuffer.byteLength, 390 + }); 391 + 392 + // Hot should be skipped by default 393 + expect(setResult.tiersWritten).not.toContain('hot'); 394 + expect(await hot.exists('skip-hot')).toBe(false); 395 + 396 + // Warm and cold should have data 397 + expect(setResult.tiersWritten).toContain('warm'); 398 + expect(setResult.tiersWritten).toContain('cold'); 399 + }); 400 + 401 + it('should allow including hot tier explicitly', async () => { 402 + const hot = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }); 403 + const cold = new DiskStorageTier({ directory: `${testDir}/cold` }); 404 + 405 + const storage = new TieredStorage({ 406 + tiers: { hot, cold }, 407 + }); 408 + 409 + const testData = 'Include hot tier test'; 410 + const testBuffer = Buffer.from(testData); 411 + 412 + const setResult = await storage.setStream('include-hot', createStream(testData), { 413 + size: testBuffer.byteLength, 414 + skipTiers: [], // Don't skip any tiers 415 + }); 416 + 417 + // Hot should be included 418 + expect(setResult.tiersWritten).toContain('hot'); 419 + expect(await hot.exists('include-hot')).toBe(true); 420 + }); 421 + }); 422 + 423 + describe('Streaming with Compression', () => { 424 + it('should compress stream data when compression is enabled', async () => { 425 + const storage = new TieredStorage({ 426 + tiers: { 427 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 428 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 429 + }, 430 + compression: true, 431 + }); 432 + 433 + const testData = 'Compressible data '.repeat(100); // Repeating data compresses well 434 + const testBuffer = Buffer.from(testData); 435 + 436 + const setResult = await storage.setStream('compress-test', createStream(testData), { 437 + size: testBuffer.byteLength, 438 + }); 439 + 440 + // Metadata should indicate compression 441 + expect(setResult.metadata.compressed).toBe(true); 442 + // Checksum should be of original uncompressed data 443 + expect(setResult.metadata.checksum).toBe(computeChecksum(testBuffer)); 444 + }); 445 + 446 + it('should decompress stream data automatically on read', async () => { 447 + const storage = new TieredStorage({ 448 + tiers: { 449 + warm: new DiskStorageTier({ directory: `${testDir}/warm` }), 450 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 451 + }, 452 + compression: true, 453 + }); 454 + 455 + const testData = 'Hello, compressed world! '.repeat(50); 456 + const testBuffer = Buffer.from(testData); 457 + 458 + await storage.setStream('decompress-test', createStream(testData), { 459 + size: testBuffer.byteLength, 460 + }); 461 + 462 + // Read back via stream 463 + const result = await storage.getStream('decompress-test'); 464 + expect(result).not.toBeNull(); 465 + expect(result!.metadata.compressed).toBe(true); 466 + 467 + // Stream should be decompressed automatically 468 + const retrievedData = await streamToBuffer(result!.stream); 469 + expect(retrievedData.toString()).toBe(testData); 470 + }); 471 + 472 + it('should not compress when compression is disabled', async () => { 473 + const storage = new TieredStorage({ 474 + tiers: { 475 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 476 + }, 477 + compression: false, 478 + }); 479 + 480 + const testData = 'Uncompressed data '.repeat(50); 481 + const testBuffer = Buffer.from(testData); 482 + 483 + const setResult = await storage.setStream('no-compress-test', createStream(testData), { 484 + size: testBuffer.byteLength, 485 + }); 486 + 487 + expect(setResult.metadata.compressed).toBe(false); 488 + 489 + // Read back - should be exact same data 490 + const result = await storage.getStream('no-compress-test'); 491 + expect(result).not.toBeNull(); 492 + 493 + const retrievedData = await streamToBuffer(result!.stream); 494 + expect(retrievedData.toString()).toBe(testData); 495 + }); 496 + 497 + it('should preserve checksum of original data when compressed', async () => { 498 + const storage = new TieredStorage({ 499 + tiers: { 500 + cold: new DiskStorageTier({ directory: `${testDir}/cold` }), 501 + }, 502 + compression: true, 503 + }); 504 + 505 + const testData = 'Data for checksum verification '.repeat(100); 506 + const testBuffer = Buffer.from(testData); 507 + const expectedChecksum = computeChecksum(testBuffer); 508 + 509 + const setResult = await storage.setStream('checksum-compress', createStream(testData), { 510 + size: testBuffer.byteLength, 511 + }); 512 + 513 + // Checksum should match the ORIGINAL uncompressed data 514 + expect(setResult.metadata.checksum).toBe(expectedChecksum); 515 + 516 + // Read back and verify content matches 517 + const result = await storage.getStream('checksum-compress'); 518 + const retrievedData = await streamToBuffer(result!.stream); 519 + expect(computeChecksum(retrievedData)).toBe(expectedChecksum); 520 + }); 521 + }); 522 + 523 + describe('Edge Cases', () => { 524 + it('should handle empty streams', async () => { 525 + const tier = new DiskStorageTier({ directory: testDir }); 526 + 527 + const metadata = { 528 + key: 'empty-file.txt', 529 + size: 0, 530 + createdAt: new Date(), 531 + lastAccessed: new Date(), 532 + accessCount: 0, 533 + compressed: false, 534 + checksum: computeChecksum(Buffer.from('')), 535 + }; 536 + 537 + await tier.setStream('empty-file.txt', createStream(''), metadata); 538 + 539 + const result = await tier.getStream('empty-file.txt'); 540 + expect(result).not.toBeNull(); 541 + 542 + const data = await streamToBuffer(result!.stream); 543 + expect(data.length).toBe(0); 544 + }); 545 + 546 + it('should preserve binary data integrity', async () => { 547 + const tier = new DiskStorageTier({ directory: testDir }); 548 + 549 + // Create binary data with all possible byte values 550 + const binaryData = Buffer.alloc(256); 551 + for (let i = 0; i < 256; i++) { 552 + binaryData[i] = i; 553 + } 554 + 555 + const metadata = { 556 + key: 'binary-file.bin', 557 + size: binaryData.byteLength, 558 + createdAt: new Date(), 559 + lastAccessed: new Date(), 560 + accessCount: 0, 561 + compressed: false, 562 + checksum: computeChecksum(binaryData), 563 + }; 564 + 565 + await tier.setStream('binary-file.bin', Readable.from([binaryData]), metadata); 566 + 567 + const result = await tier.getStream('binary-file.bin'); 568 + expect(result).not.toBeNull(); 569 + 570 + const retrievedData = await streamToBuffer(result!.stream); 571 + expect(retrievedData.equals(binaryData)).toBe(true); 572 + }); 573 + 574 + it('should handle special characters in keys', async () => { 575 + const tier = new DiskStorageTier({ directory: testDir }); 576 + 577 + const testData = 'special key test'; 578 + const testBuffer = Buffer.from(testData); 579 + 580 + const specialKey = 'user:123/file[1].txt'; 581 + const metadata = { 582 + key: specialKey, 583 + size: testBuffer.byteLength, 584 + createdAt: new Date(), 585 + lastAccessed: new Date(), 586 + accessCount: 0, 587 + compressed: false, 588 + checksum: computeChecksum(testBuffer), 589 + }; 590 + 591 + await tier.setStream(specialKey, createStream(testData), metadata); 592 + 593 + const result = await tier.getStream(specialKey); 594 + expect(result).not.toBeNull(); 595 + expect(result!.metadata.key).toBe(specialKey); 596 + }); 597 + }); 598 + });
+8
tsconfig.eslint.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "compilerOptions": { 4 + "noEmit": true 5 + }, 6 + "include": ["src/**/*", "test/**/*"], 7 + "exclude": ["node_modules", "dist"] 8 + }