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