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 { 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});