wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs typescript
at main 12 kB view raw
1import { describe, it, expect, afterEach } from 'vitest'; 2import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js'; 3import { rm, readdir } from 'node:fs/promises'; 4import { existsSync } from 'node:fs'; 5import { join } from 'node:path'; 6 7describe('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});