-28
.eslintrc.cjs
-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
+9
.prettierrc.json
+148
-466
README.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
});