wip library to store cold objects in s3, warm objects on disk, and hot objects in memory
nodejs
typescript
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});