Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add a GC to diskstoragetier and also fix tests

authored by

nekomimi.pet and committed by
Tangled
bd8e5367 6cd4dc4c

+360 -100
+3
packages/@wispplace/tiered-storage/.gitignore
··· 12 12 13 13 # Test cache directories 14 14 test-cache/ 15 + test-disk-cache/ 16 + test-disk-gc/ 17 + test-streaming-cache/ 15 18 16 19 # Build artifacts 17 20 *.tsbuildinfo
+54 -1
packages/@wispplace/tiered-storage/src/tiers/DiskStorageTier.ts
··· 73 73 * ``` 74 74 */ 75 75 encodeColons?: boolean 76 + 77 + /** 78 + * Interval in milliseconds for automatic garbage collection of expired entries. 79 + * 80 + * @remarks 81 + * When set, a periodic sweep deletes entries whose TTL has expired, 82 + * reclaiming disk space. Call `dispose()` to stop the interval. 83 + */ 84 + gcIntervalMs?: number 76 85 } 77 86 78 87 /** ··· 110 119 * ``` 111 120 */ 112 121 export class DiskStorageTier implements StorageTier { 113 - private metadataIndex = new Map<string, { size: number; createdAt: Date; lastAccessed: Date }>() 122 + private metadataIndex = new Map<string, { size: number; createdAt: Date; lastAccessed: Date; ttl?: Date }>() 114 123 private currentSize = 0 115 124 private readonly encodeColons: boolean 125 + private gcTimer: ReturnType<typeof setInterval> | null = null 116 126 117 127 constructor(private config: DiskStorageTierConfig) { 118 128 if (!config.directory) { ··· 128 138 129 139 void this.ensureDirectory() 130 140 void this.rebuildIndex() 141 + 142 + if (config.gcIntervalMs) { 143 + this.gcTimer = setInterval(() => void this.gc(), config.gcIntervalMs) 144 + if (this.gcTimer.unref) this.gcTimer.unref() 145 + } 131 146 } 132 147 133 148 private async rebuildIndex(): Promise<void> { ··· 160 175 size: fileStats.size, 161 176 createdAt: new Date(metadata.createdAt), 162 177 lastAccessed: new Date(metadata.lastAccessed), 178 + ...(metadata.ttl && { ttl: new Date(metadata.ttl) }), 163 179 }) 164 180 165 181 this.currentSize += fileStats.size ··· 316 332 size: metadata.size, 317 333 createdAt: metadata.createdAt, 318 334 lastAccessed: metadata.lastAccessed, 335 + ...(metadata.ttl && { ttl: metadata.ttl }), 319 336 }) 320 337 this.currentSize += metadata.size 321 338 } ··· 345 362 size: data.byteLength, 346 363 createdAt: metadata.createdAt, 347 364 lastAccessed: metadata.lastAccessed, 365 + ...(metadata.ttl && { ttl: metadata.ttl }), 348 366 }) 349 367 this.currentSize += data.byteLength 350 368 } ··· 500 518 return { bytes, items } 501 519 } 502 520 521 + /** 522 + * Sweep and delete all entries whose TTL has expired. 523 + * 524 + * @returns Number of expired entries deleted 525 + */ 526 + async gc(): Promise<number> { 527 + const now = Date.now() 528 + const expired: string[] = [] 529 + 530 + for (const [key, entry] of this.metadataIndex) { 531 + if (entry.ttl && now > entry.ttl.getTime()) { 532 + expired.push(key) 533 + } 534 + } 535 + 536 + for (const key of expired) { 537 + await this.delete(key) 538 + } 539 + 540 + return expired.length 541 + } 542 + 543 + /** 544 + * Stop the automatic GC interval. Call this before discarding the tier. 545 + */ 546 + dispose(): void { 547 + if (this.gcTimer) { 548 + clearInterval(this.gcTimer) 549 + this.gcTimer = null 550 + } 551 + } 552 + 503 553 async clear(): Promise<void> { 504 554 if (existsSync(this.config.directory)) { 505 555 await rm(this.config.directory, { recursive: true, force: true }) ··· 547 597 await rename(tempMetaPath, metaPath) 548 598 } catch (error) { 549 599 await unlink(tempMetaPath).catch(() => {}) 600 + // If the directory was removed (e.g. concurrent clear/cleanup), don't propagate 601 + const code = getErrnoCode(error) 602 + if (code === 'ENOENT' || code === 'EINVAL') return 550 603 throw error 551 604 } 552 605 }
+192
packages/@wispplace/tiered-storage/test/DiskStorageTier.gc.test.ts
··· 1 + import { rm } from 'node:fs/promises' 2 + import { afterAll, beforeEach, describe, expect, test } from 'bun:test' 3 + import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js' 4 + import type { StorageMetadata } from '../src/types/index.js' 5 + 6 + const testDir = './test-disk-gc' 7 + 8 + function makeMetadata(key: string, size: number, ttl?: Date): StorageMetadata { 9 + return { 10 + key, 11 + size, 12 + createdAt: new Date(), 13 + lastAccessed: new Date(), 14 + accessCount: 0, 15 + compressed: false, 16 + checksum: 'abc', 17 + ...(ttl && { ttl }), 18 + } 19 + } 20 + 21 + describe('DiskStorageTier - Garbage Collection', () => { 22 + beforeEach(async () => { 23 + await rm(testDir, { recursive: true, force: true }) 24 + }) 25 + 26 + afterAll(async () => { 27 + await rm(testDir, { recursive: true, force: true }) 28 + }) 29 + 30 + describe('gc()', () => { 31 + test('should delete expired entries', async () => { 32 + const tier = new DiskStorageTier({ directory: testDir }) 33 + const data = new TextEncoder().encode('test') 34 + 35 + const expiredTTL = new Date(Date.now() - 1000) 36 + await tier.set('expired1', data, makeMetadata('expired1', data.byteLength, expiredTTL)) 37 + await tier.set('expired2', data, makeMetadata('expired2', data.byteLength, expiredTTL)) 38 + 39 + const futureTTL = new Date(Date.now() + 60_000) 40 + await tier.set('alive', data, makeMetadata('alive', data.byteLength, futureTTL)) 41 + 42 + expect(await tier.exists('expired1')).toBe(true) 43 + expect(await tier.exists('expired2')).toBe(true) 44 + 45 + const deleted = await tier.gc() 46 + 47 + expect(deleted).toBe(2) 48 + expect(await tier.exists('expired1')).toBe(false) 49 + expect(await tier.exists('expired2')).toBe(false) 50 + expect(await tier.exists('alive')).toBe(true) 51 + }) 52 + 53 + test('should not delete entries without TTL', async () => { 54 + const tier = new DiskStorageTier({ directory: testDir }) 55 + const data = new TextEncoder().encode('test') 56 + 57 + await tier.set('no-ttl', data, makeMetadata('no-ttl', data.byteLength)) 58 + 59 + const deleted = await tier.gc() 60 + 61 + expect(deleted).toBe(0) 62 + expect(await tier.exists('no-ttl')).toBe(true) 63 + }) 64 + 65 + test('should return 0 when nothing is expired', async () => { 66 + const tier = new DiskStorageTier({ directory: testDir }) 67 + const data = new TextEncoder().encode('test') 68 + 69 + const futureTTL = new Date(Date.now() + 60_000) 70 + await tier.set('alive', data, makeMetadata('alive', data.byteLength, futureTTL)) 71 + 72 + const deleted = await tier.gc() 73 + expect(deleted).toBe(0) 74 + }) 75 + 76 + test('should return 0 on empty tier', async () => { 77 + const tier = new DiskStorageTier({ directory: testDir }) 78 + const deleted = await tier.gc() 79 + expect(deleted).toBe(0) 80 + }) 81 + 82 + test('should reclaim disk space tracked by currentSize', async () => { 83 + const tier = new DiskStorageTier({ directory: testDir, maxSizeBytes: 1024 }) 84 + const data = new TextEncoder().encode('x'.repeat(100)) 85 + 86 + const expiredTTL = new Date(Date.now() - 1000) 87 + await tier.set('expired', data, makeMetadata('expired', data.byteLength, expiredTTL)) 88 + 89 + const statsBefore = await tier.getStats() 90 + expect(statsBefore.items).toBe(1) 91 + 92 + await tier.gc() 93 + 94 + const statsAfter = await tier.getStats() 95 + expect(statsAfter.items).toBe(0) 96 + expect(statsAfter.bytes).toBe(0) 97 + }) 98 + 99 + test('should handle mixed expired and alive entries in nested dirs', async () => { 100 + const tier = new DiskStorageTier({ directory: testDir }) 101 + const data = new TextEncoder().encode('test') 102 + 103 + const expired = new Date(Date.now() - 1000) 104 + const alive = new Date(Date.now() + 60_000) 105 + 106 + await tier.set('site:a/old.html', data, makeMetadata('site:a/old.html', data.byteLength, expired)) 107 + await tier.set('site:a/new.html', data, makeMetadata('site:a/new.html', data.byteLength, alive)) 108 + await tier.set('site:b/old.html', data, makeMetadata('site:b/old.html', data.byteLength, expired)) 109 + 110 + const deleted = await tier.gc() 111 + 112 + expect(deleted).toBe(2) 113 + expect(await tier.exists('site:a/old.html')).toBe(false) 114 + expect(await tier.exists('site:a/new.html')).toBe(true) 115 + expect(await tier.exists('site:b/old.html')).toBe(false) 116 + }) 117 + }) 118 + 119 + describe('gcIntervalMs auto-sweep', () => { 120 + test('should run gc automatically on interval', async () => { 121 + const tier = new DiskStorageTier({ directory: testDir, gcIntervalMs: 200 }) 122 + const data = new TextEncoder().encode('test') 123 + 124 + const expiredTTL = new Date(Date.now() - 1000) 125 + await tier.set('will-expire', data, makeMetadata('will-expire', data.byteLength, expiredTTL)) 126 + 127 + expect(await tier.exists('will-expire')).toBe(true) 128 + 129 + // Wait for the GC interval to fire 130 + await new Promise((resolve) => setTimeout(resolve, 350)) 131 + 132 + expect(await tier.exists('will-expire')).toBe(false) 133 + 134 + tier.dispose() 135 + }) 136 + 137 + test('should not run gc after dispose', async () => { 138 + const tier = new DiskStorageTier({ directory: testDir, gcIntervalMs: 200 }) 139 + const data = new TextEncoder().encode('test') 140 + 141 + tier.dispose() 142 + 143 + const expiredTTL = new Date(Date.now() - 1000) 144 + await tier.set('still-here', data, makeMetadata('still-here', data.byteLength, expiredTTL)) 145 + 146 + await new Promise((resolve) => setTimeout(resolve, 350)) 147 + 148 + // Should still exist because GC was stopped 149 + expect(await tier.exists('still-here')).toBe(true) 150 + }) 151 + }) 152 + 153 + describe('gc + eviction interaction', () => { 154 + test('should free space so new writes succeed without LRU eviction of live entries', async () => { 155 + const tier = new DiskStorageTier({ directory: testDir, maxSizeBytes: 200 }) 156 + const data = new TextEncoder().encode('x'.repeat(80)) 157 + 158 + const expired = new Date(Date.now() - 1000) 159 + const alive = new Date(Date.now() + 60_000) 160 + 161 + await tier.set('old1', data, makeMetadata('old1', data.byteLength, expired)) 162 + await tier.set('old2', data, makeMetadata('old2', data.byteLength, expired)) 163 + 164 + const deleted = await tier.gc() 165 + expect(deleted).toBe(2) 166 + 167 + await tier.set('new1', data, makeMetadata('new1', data.byteLength, alive)) 168 + await tier.set('new2', data, makeMetadata('new2', data.byteLength, alive)) 169 + 170 + expect(await tier.exists('new1')).toBe(true) 171 + expect(await tier.exists('new2')).toBe(true) 172 + }) 173 + }) 174 + 175 + describe('rebuildIndex preserves TTL', () => { 176 + test('should track TTL through index rebuild so gc works after restart', async () => { 177 + const data = new TextEncoder().encode('test') 178 + const expiredTTL = new Date(Date.now() - 1000) 179 + 180 + const tier1 = new DiskStorageTier({ directory: testDir }) 181 + await tier1.set('stale', data, makeMetadata('stale', data.byteLength, expiredTTL)) 182 + 183 + // Simulate restart - new instance rebuilds index from disk 184 + const tier2 = new DiskStorageTier({ directory: testDir }) 185 + await new Promise((resolve) => setTimeout(resolve, 100)) 186 + 187 + const deleted = await tier2.gc() 188 + expect(deleted).toBe(1) 189 + expect(await tier2.exists('stale')).toBe(false) 190 + }) 191 + }) 192 + })
+22 -18
packages/@wispplace/tiered-storage/test/DiskStorageTier.test.ts
··· 1 1 import { existsSync } from 'node:fs' 2 2 import { readdir, rm } from 'node:fs/promises' 3 3 import { join } from 'node:path' 4 - import { afterEach, describe, expect, it } from 'vitest' 4 + import { afterAll, beforeEach, describe, expect, test } from 'bun:test' 5 5 import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js' 6 6 7 + const testDir = './test-disk-cache' 8 + 7 9 describe('DiskStorageTier - Recursive Directory Support', () => { 8 - const testDir = './test-disk-cache' 10 + beforeEach(async () => { 11 + await rm(testDir, { recursive: true, force: true }) 12 + }) 9 13 10 - afterEach(async () => { 14 + afterAll(async () => { 11 15 await rm(testDir, { recursive: true, force: true }) 12 16 }) 13 17 14 18 describe('Nested Directory Creation', () => { 15 - it('should create nested directories for keys with slashes', async () => { 16 - const tier = new DiskStorageTier({ directory: testDir }) 19 + test('should create nested directories for keys with slashes', async () => { 20 + const tier = new DiskStorageTier({ directory: testDir, encodeColons: true }) 17 21 18 22 const data = new TextEncoder().encode('test data') 19 23 const metadata = { ··· 36 40 expect(existsSync(join(testDir, 'did%3Aplc%3Aabc/site/pages/index.html.meta'))).toBe(true) 37 41 }) 38 42 39 - it('should handle multiple files in different nested directories', async () => { 43 + test('should handle multiple files in different nested directories', async () => { 40 44 const tier = new DiskStorageTier({ directory: testDir }) 41 45 42 46 const data = new TextEncoder().encode('test') ··· 61 65 }) 62 66 63 67 describe('Recursive Listing', () => { 64 - it('should list all keys across nested directories', async () => { 68 + test('should list all keys across nested directories', async () => { 65 69 const tier = new DiskStorageTier({ directory: testDir }) 66 70 67 71 const data = new TextEncoder().encode('test') ··· 95 99 expect(listedKeys.sort()).toEqual(keys.sort()) 96 100 }) 97 101 98 - it('should list keys with prefix filter across directories', async () => { 102 + test('should list keys with prefix filter across directories', async () => { 99 103 const tier = new DiskStorageTier({ directory: testDir }) 100 104 101 105 const data = new TextEncoder().encode('test') ··· 122 126 expect(siteKeys.sort()).toEqual(['site:a/about.html', 'site:a/index.html', 'site:b/index.html']) 123 127 }) 124 128 125 - it('should handle empty directories gracefully', async () => { 129 + test('should handle empty directories gracefully', async () => { 126 130 const tier = new DiskStorageTier({ directory: testDir }) 127 131 128 132 const keys: string[] = [] ··· 135 139 }) 136 140 137 141 describe('Recursive Stats Collection', () => { 138 - it('should calculate stats across all nested directories', async () => { 142 + test('should calculate stats across all nested directories', async () => { 139 143 const tier = new DiskStorageTier({ directory: testDir }) 140 144 141 145 const data1 = new TextEncoder().encode('small') ··· 162 166 expect(stats.bytes).toBe(data1.byteLength + data2.byteLength + data3.byteLength) 163 167 }) 164 168 165 - it('should return zero stats for empty directory', async () => { 169 + test('should return zero stats for empty directory', async () => { 166 170 const tier = new DiskStorageTier({ directory: testDir }) 167 171 168 172 const stats = await tier.getStats() ··· 173 177 }) 174 178 175 179 describe('Index Rebuilding', () => { 176 - it('should rebuild index from nested directory structure on init', async () => { 180 + test('should rebuild index from nested directory structure on init', async () => { 177 181 const data = new TextEncoder().encode('test data') 178 182 const createMetadata = (key: string) => ({ 179 183 key, ··· 207 211 expect(stats.items).toBe(3) 208 212 }) 209 213 210 - it('should handle corrupted metadata files during rebuild', async () => { 214 + test('should handle corrupted metadata files during rebuild', async () => { 211 215 const tier = new DiskStorageTier({ directory: testDir }) 212 216 213 217 const data = new TextEncoder().encode('test') ··· 238 242 }) 239 243 240 244 describe('getWithMetadata Optimization', () => { 241 - it('should retrieve data and metadata from nested directories in parallel', async () => { 245 + test('should retrieve data and metadata from nested directories in parallel', async () => { 242 246 const tier = new DiskStorageTier({ directory: testDir }) 243 247 244 248 const data = new TextEncoder().encode('test data content') ··· 264 268 }) 265 269 266 270 describe('Deletion from Nested Directories', () => { 267 - it('should delete files from nested directories', async () => { 271 + test('should delete files from nested directories', async () => { 268 272 const tier = new DiskStorageTier({ directory: testDir }) 269 273 270 274 const data = new TextEncoder().encode('test') ··· 289 293 expect(await tier.exists('a/b/file2.txt')).toBe(true) 290 294 }) 291 295 292 - it('should delete multiple files across nested directories', async () => { 296 + test('should delete multiple files across nested directories', async () => { 293 297 const tier = new DiskStorageTier({ directory: testDir }) 294 298 295 299 const data = new TextEncoder().encode('test') ··· 318 322 }) 319 323 320 324 describe('Edge Cases', () => { 321 - it('should handle keys with many nested levels', async () => { 325 + test('should handle keys with many nested levels', async () => { 322 326 const tier = new DiskStorageTier({ directory: testDir }) 323 327 324 328 const data = new TextEncoder().encode('deep') ··· 341 345 expect(retrieved).toEqual(data) 342 346 }) 343 347 344 - it('should handle keys with special characters', async () => { 348 + test('should handle keys with special characters', async () => { 345 349 const tier = new DiskStorageTier({ directory: testDir }) 346 350 347 351 const data = new TextEncoder().encode('test')
+6 -6
packages/@wispplace/tiered-storage/test/S3StorageTier.test.ts
··· 1 1 import { Readable } from 'node:stream' 2 2 import { gzipSync } from 'node:zlib' 3 - import { describe, expect, it } from 'vitest' 3 + import { describe, expect, test } from 'bun:test' 4 4 import { S3StorageTier } from '../src/tiers/S3StorageTier.js' 5 5 6 6 describe('S3StorageTier metadata fallback', () => { 7 - it('getWithMetadata should synthesize metadata when S3 metadata headers are missing', async () => { 7 + test('getWithMetadata should synthesize metadata when S3 metadata headers are missing', async () => { 8 8 const tier = new S3StorageTier({ 9 9 bucket: 'test-bucket', 10 10 region: 'us-east-1', ··· 34 34 }) 35 35 }) 36 36 37 - it('getStream should synthesize metadata when S3 metadata headers are missing', async () => { 37 + test('getStream should synthesize metadata when S3 metadata headers are missing', async () => { 38 38 const tier = new S3StorageTier({ 39 39 bucket: 'test-bucket', 40 40 region: 'us-east-1', ··· 57 57 expect(result!.metadata.checksum).toBe('etag-stream') 58 58 }) 59 59 60 - it('getMetadata should synthesize metadata from HeadObject when headers are missing', async () => { 60 + test('getMetadata should synthesize metadata from HeadObject when headers are missing', async () => { 61 61 const tier = new S3StorageTier({ 62 62 bucket: 'test-bucket', 63 63 region: 'us-east-1', ··· 81 81 expect(metadata!.customMetadata).toEqual({ mimeType: 'text/html' }) 82 82 }) 83 83 84 - it('getWithMetadata should infer gzip encoding from magic bytes for text-like content', async () => { 84 + test('getWithMetadata should infer gzip encoding from magic bytes for text-like content', async () => { 85 85 const tier = new S3StorageTier({ 86 86 bucket: 'test-bucket', 87 87 region: 'us-east-1', ··· 109 109 }) 110 110 }) 111 111 112 - it('getWithMetadata should decode base64 payload and infer gzip encoding for text-like content', async () => { 112 + test('getWithMetadata should decode base64 payload and infer gzip encoding for text-like content', async () => { 113 113 const tier = new S3StorageTier({ 114 114 bucket: 'test-bucket', 115 115 region: 'us-east-1',
+37 -33
packages/@wispplace/tiered-storage/test/TieredStorage.test.ts
··· 1 1 import { rm } from 'node:fs/promises' 2 - import { afterEach, describe, expect, it } from 'vitest' 2 + import { afterAll, beforeEach, describe, expect, test } from 'bun:test' 3 3 import { TieredStorage } from '../src/TieredStorage.js' 4 4 import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js' 5 5 import { MemoryStorageTier } from '../src/tiers/MemoryStorageTier.js' 6 6 7 + const testDir = './test-cache' 8 + 7 9 describe('TieredStorage', () => { 8 - const testDir = './test-cache' 10 + beforeEach(async () => { 11 + await rm(testDir, { recursive: true, force: true }) 12 + }) 9 13 10 - afterEach(async () => { 14 + afterAll(async () => { 11 15 await rm(testDir, { recursive: true, force: true }) 12 16 }) 13 17 14 18 describe('Basic Operations', () => { 15 - it('should store and retrieve data', async () => { 19 + test('should store and retrieve data', async () => { 16 20 const storage = new TieredStorage({ 17 21 tiers: { 18 22 hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), ··· 27 31 expect(result).toEqual({ message: 'Hello, world!' }) 28 32 }) 29 33 30 - it('should return null for non-existent key', async () => { 34 + test('should return null for non-existent key', async () => { 31 35 const storage = new TieredStorage({ 32 36 tiers: { 33 37 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 38 42 expect(result).toBeNull() 39 43 }) 40 44 41 - it('should delete data from all tiers', async () => { 45 + test('should delete data from all tiers', async () => { 42 46 const storage = new TieredStorage({ 43 47 tiers: { 44 48 hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), ··· 54 58 expect(result).toBeNull() 55 59 }) 56 60 57 - it('should check if key exists', async () => { 61 + test('should check if key exists', async () => { 58 62 const storage = new TieredStorage({ 59 63 tiers: { 60 64 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 69 73 }) 70 74 71 75 describe('Cascading Write', () => { 72 - it('should write to all configured tiers', async () => { 76 + test('should write to all configured tiers', async () => { 73 77 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 74 78 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 75 79 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 86 90 expect(await cold.exists('test-key')).toBe(true) 87 91 }) 88 92 89 - it('should skip tiers when specified', async () => { 93 + test('should skip tiers when specified', async () => { 90 94 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 91 95 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 92 96 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 103 107 expect(await cold.exists('test-key')).toBe(true) 104 108 }) 105 109 106 - it('should write only to specified tiers with onlyTiers', async () => { 110 + test('should write only to specified tiers with onlyTiers', async () => { 107 111 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 108 112 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 109 113 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 120 124 expect(await cold.exists('test-key')).toBe(true) 121 125 }) 122 126 123 - it('should write only to warm and cold with onlyTiers', async () => { 127 + test('should write only to warm and cold with onlyTiers', async () => { 124 128 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 125 129 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 126 130 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 137 141 expect(await cold.exists('test-key')).toBe(true) 138 142 }) 139 143 140 - it('should allow onlyTiers to override placement rules', async () => { 144 + test('should allow onlyTiers to override placement rules', async () => { 141 145 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 142 146 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 143 147 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 155 159 expect(await cold.exists('test-key')).toBe(true) 156 160 }) 157 161 158 - it('should prioritize onlyTiers over skipTiers if both are provided', async () => { 162 + test('should prioritize onlyTiers over skipTiers if both are provided', async () => { 159 163 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 160 164 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 161 165 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 181 185 }) 182 186 183 187 describe('Bubbling Read', () => { 184 - it('should read from hot tier first', async () => { 188 + test('should read from hot tier first', async () => { 185 189 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 186 190 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 187 191 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 197 201 expect(result?.data).toEqual({ data: 'test' }) 198 202 }) 199 203 200 - it('should fall back to warm tier on hot miss', async () => { 204 + test('should fall back to warm tier on hot miss', async () => { 201 205 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 202 206 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 203 207 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 215 219 expect(result?.data).toEqual({ data: 'test' }) 216 220 }) 217 221 218 - it('should fall back to cold tier on hot and warm miss', async () => { 222 + test('should fall back to cold tier on hot and warm miss', async () => { 219 223 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 220 224 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 221 225 ··· 242 246 }) 243 247 244 248 describe('Promotion Strategy', () => { 245 - it('should eagerly promote data to upper tiers', async () => { 249 + test('should eagerly promote data to upper tiers', async () => { 246 250 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 247 251 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 248 252 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 270 274 expect(await warm.exists('test-key')).toBe(true) 271 275 }) 272 276 273 - it('should lazily promote data (not automatic)', async () => { 277 + test('should lazily promote data (not automatic)', async () => { 274 278 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 275 279 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 276 280 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 300 304 }) 301 305 302 306 describe('TTL Management', () => { 303 - it('should expire data after TTL', async () => { 307 + test('should expire data after TTL', async () => { 304 308 const storage = new TieredStorage({ 305 309 tiers: { 306 310 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 320 324 expect(await storage.get('test-key')).toBeNull() 321 325 }) 322 326 323 - it('should renew TTL with touch', async () => { 327 + test('should renew TTL with touch', async () => { 324 328 const storage = new TieredStorage({ 325 329 tiers: { 326 330 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 345 349 }) 346 350 347 351 describe('Prefix Invalidation', () => { 348 - it('should invalidate all keys with prefix', async () => { 352 + test('should invalidate all keys with prefix', async () => { 349 353 const storage = new TieredStorage({ 350 354 tiers: { 351 355 hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), ··· 367 371 }) 368 372 369 373 describe('Compression', () => { 370 - it('should compress data when enabled', async () => { 374 + test('should compress data when enabled', async () => { 371 375 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 372 376 373 377 const storage = new TieredStorage({ ··· 388 392 }) 389 393 390 394 describe('Bootstrap', () => { 391 - it('should bootstrap hot from warm', async () => { 395 + test('should bootstrap hot from warm', async () => { 392 396 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 393 397 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 394 398 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 414 418 expect(await hot.exists('key3')).toBe(true) 415 419 }) 416 420 417 - it('should bootstrap warm from cold', async () => { 421 + test('should bootstrap warm from cold', async () => { 418 422 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 419 423 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 420 424 ··· 442 446 }) 443 447 444 448 describe('Statistics', () => { 445 - it('should return statistics for all tiers', async () => { 449 + test('should return statistics for all tiers', async () => { 446 450 const storage = new TieredStorage({ 447 451 tiers: { 448 452 hot: new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }), ··· 463 467 }) 464 468 465 469 describe('Placement Rules', () => { 466 - it('should place index.html in all tiers based on rule', async () => { 470 + test('should place index.html in all tiers based on rule', async () => { 467 471 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 468 472 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 469 473 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 483 487 expect(await cold.exists('site:abc/index.html')).toBe(true) 484 488 }) 485 489 486 - it('should skip hot tier for non-matching files', async () => { 490 + test('should skip hot tier for non-matching files', async () => { 487 491 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 488 492 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 489 493 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 503 507 expect(await cold.exists('site:abc/about.html')).toBe(true) 504 508 }) 505 509 506 - it('should match directory patterns', async () => { 510 + test('should match directory patterns', async () => { 507 511 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 508 512 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 509 513 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 527 531 expect(await hot.exists('index.html')).toBe(true) 528 532 }) 529 533 530 - it('should match file extension patterns', async () => { 534 + test('should match file extension patterns', async () => { 531 535 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 532 536 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 533 537 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 552 556 expect(await hot.exists('site/index.html')).toBe(true) 553 557 }) 554 558 555 - it('should use first matching rule', async () => { 559 + test('should use first matching rule', async () => { 556 560 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 557 561 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 558 562 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 578 582 expect(await hot.exists('assets/style.css')).toBe(false) 579 583 }) 580 584 581 - it('should allow skipTiers to override placement rules', async () => { 585 + test('should allow skipTiers to override placement rules', async () => { 582 586 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 583 587 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 584 588 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 596 600 expect(await cold.exists('large-file.bin')).toBe(true) 597 601 }) 598 602 599 - it('should always include cold tier even if not in rule', async () => { 603 + test('should always include cold tier even if not in rule', async () => { 600 604 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 601 605 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 602 606 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 614 618 expect(await cold.exists('test-key')).toBe(true) 615 619 }) 616 620 617 - it('should write to all tiers when no rules match', async () => { 621 + test('should write to all tiers when no rules match', async () => { 618 622 const hot = new MemoryStorageTier({ maxSizeBytes: 1024 * 1024 }) 619 623 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 620 624 const cold = new DiskStorageTier({ directory: `${testDir}/cold` })
+15 -15
packages/@wispplace/tiered-storage/test/glob.test.ts
··· 1 - import { describe, expect, it } from 'vitest' 1 + import { describe, expect, test } from 'bun:test' 2 2 import { matchGlob } from '../src/utils/glob.js' 3 3 4 4 describe('matchGlob', () => { 5 5 describe('exact matches', () => { 6 - it('should match exact strings', () => { 6 + test('should match exact strings', () => { 7 7 expect(matchGlob('index.html', 'index.html')).toBe(true) 8 8 expect(matchGlob('index.html', 'about.html')).toBe(false) 9 9 }) 10 10 11 - it('should match paths exactly', () => { 11 + test('should match paths exactly', () => { 12 12 expect(matchGlob('site/index.html', 'site/index.html')).toBe(true) 13 13 expect(matchGlob('site/index.html', 'other/index.html')).toBe(false) 14 14 }) 15 15 }) 16 16 17 17 describe('* wildcard', () => { 18 - it('should match any characters except /', () => { 18 + test('should match any characters except /', () => { 19 19 expect(matchGlob('*.html', 'index.html')).toBe(true) 20 20 expect(matchGlob('*.html', 'about.html')).toBe(true) 21 21 expect(matchGlob('*.html', 'style.css')).toBe(false) 22 22 }) 23 23 24 - it('should not match across path separators', () => { 24 + test('should not match across path separators', () => { 25 25 expect(matchGlob('*.html', 'dir/index.html')).toBe(false) 26 26 }) 27 27 28 - it('should work with prefix and suffix', () => { 28 + test('should work with prefix and suffix', () => { 29 29 expect(matchGlob('index.*', 'index.html')).toBe(true) 30 30 expect(matchGlob('index.*', 'index.css')).toBe(true) 31 31 expect(matchGlob('index.*', 'about.html')).toBe(false) ··· 33 33 }) 34 34 35 35 describe('** wildcard', () => { 36 - it('should match any characters including /', () => { 36 + test('should match any characters including /', () => { 37 37 expect(matchGlob('**', 'anything')).toBe(true) 38 38 expect(matchGlob('**', 'path/to/file.txt')).toBe(true) 39 39 }) 40 40 41 - it('should match deeply nested paths', () => { 41 + test('should match deeply nested paths', () => { 42 42 expect(matchGlob('**/index.html', 'index.html')).toBe(true) 43 43 expect(matchGlob('**/index.html', 'site/index.html')).toBe(true) 44 44 expect(matchGlob('**/index.html', 'a/b/c/index.html')).toBe(true) 45 45 expect(matchGlob('**/index.html', 'a/b/c/about.html')).toBe(false) 46 46 }) 47 47 48 - it('should match directory prefixes', () => { 48 + test('should match directory prefixes', () => { 49 49 expect(matchGlob('assets/**', 'assets/style.css')).toBe(true) 50 50 expect(matchGlob('assets/**', 'assets/images/logo.png')).toBe(true) 51 51 expect(matchGlob('assets/**', 'other/style.css')).toBe(false) 52 52 }) 53 53 54 - it('should match in the middle of a path', () => { 54 + test('should match in the middle of a path', () => { 55 55 expect(matchGlob('site/**/index.html', 'site/index.html')).toBe(true) 56 56 expect(matchGlob('site/**/index.html', 'site/pages/index.html')).toBe(true) 57 57 expect(matchGlob('site/**/index.html', 'site/a/b/c/index.html')).toBe(true) ··· 59 59 }) 60 60 61 61 describe('{a,b,c} alternation', () => { 62 - it('should match any of the alternatives', () => { 62 + test('should match any of the alternatives', () => { 63 63 expect(matchGlob('*.{html,css,js}', 'index.html')).toBe(true) 64 64 expect(matchGlob('*.{html,css,js}', 'style.css')).toBe(true) 65 65 expect(matchGlob('*.{html,css,js}', 'app.js')).toBe(true) 66 66 expect(matchGlob('*.{html,css,js}', 'image.png')).toBe(false) 67 67 }) 68 68 69 - it('should work with ** and alternation', () => { 69 + test('should work with ** and alternation', () => { 70 70 expect(matchGlob('**/*.{jpg,png,gif}', 'logo.png')).toBe(true) 71 71 expect(matchGlob('**/*.{jpg,png,gif}', 'images/logo.png')).toBe(true) 72 72 expect(matchGlob('**/*.{jpg,png,gif}', 'a/b/photo.jpg')).toBe(true) ··· 75 75 }) 76 76 77 77 describe('edge cases', () => { 78 - it('should handle empty strings', () => { 78 + test('should handle empty strings', () => { 79 79 expect(matchGlob('', '')).toBe(true) 80 80 expect(matchGlob('', 'something')).toBe(false) 81 81 expect(matchGlob('**', '')).toBe(true) 82 82 }) 83 83 84 - it('should escape regex special characters', () => { 84 + test('should escape regex special characters', () => { 85 85 expect(matchGlob('file.txt', 'file.txt')).toBe(true) 86 86 expect(matchGlob('file.txt', 'filextxt')).toBe(false) 87 87 expect(matchGlob('file[1].txt', 'file[1].txt')).toBe(true) 88 88 }) 89 89 90 - it('should handle keys with colons (common in storage)', () => { 90 + test('should handle keys with colons (common in storage)', () => { 91 91 expect(matchGlob('site:*/index.html', 'site:abc/index.html')).toBe(true) 92 92 expect(matchGlob('site:**/index.html', 'site:abc/pages/index.html')).toBe(true) 93 93 })
+31 -27
packages/@wispplace/tiered-storage/test/streaming.test.ts
··· 1 1 import { createHash } from 'node:crypto' 2 2 import { rm } from 'node:fs/promises' 3 3 import { Readable } from 'node:stream' 4 - import { afterEach, describe, expect, it } from 'vitest' 4 + import { afterAll, beforeEach, describe, expect, test } from 'bun:test' 5 5 import { TieredStorage } from '../src/TieredStorage.js' 6 6 import { DiskStorageTier } from '../src/tiers/DiskStorageTier.js' 7 7 import { MemoryStorageTier } from '../src/tiers/MemoryStorageTier.js' 8 + 9 + const testDir = './test-streaming-cache' 8 10 9 11 describe('Streaming Operations', () => { 10 - const testDir = './test-streaming-cache' 12 + beforeEach(async () => { 13 + await rm(testDir, { recursive: true, force: true }) 14 + }) 11 15 12 - afterEach(async () => { 16 + afterAll(async () => { 13 17 await rm(testDir, { recursive: true, force: true }) 14 18 }) 15 19 ··· 39 43 } 40 44 41 45 describe('DiskStorageTier Streaming', () => { 42 - it('should write and read data using streams', async () => { 46 + test('should write and read data using streams', async () => { 43 47 const tier = new DiskStorageTier({ directory: testDir }) 44 48 45 49 const testData = 'Hello, streaming world! '.repeat(100) ··· 71 75 expect(result!.metadata.key).toBe('streaming-test.txt') 72 76 }) 73 77 74 - it('should handle large data without memory issues', async () => { 78 + test('should handle large data without memory issues', async () => { 75 79 const tier = new DiskStorageTier({ directory: testDir }) 76 80 77 81 // Create a 1MB chunk and repeat pattern ··· 100 104 expect(retrievedData.equals(chunk)).toBe(true) 101 105 }) 102 106 103 - it('should return null for non-existent key', async () => { 107 + test('should return null for non-existent key', async () => { 104 108 const tier = new DiskStorageTier({ directory: testDir }) 105 109 106 110 const result = await tier.getStream('non-existent-key') 107 111 expect(result).toBeNull() 108 112 }) 109 113 110 - it('should handle nested directories with streaming', async () => { 114 + test('should handle nested directories with streaming', async () => { 111 115 const tier = new DiskStorageTier({ directory: testDir }) 112 116 113 117 const testData = 'nested streaming data' ··· 134 138 }) 135 139 136 140 describe('MemoryStorageTier Streaming', () => { 137 - it('should write and read data using streams', async () => { 141 + test('should write and read data using streams', async () => { 138 142 const tier = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }) 139 143 140 144 const testData = 'Memory tier streaming test' ··· 161 165 expect(retrievedData.toString()).toBe(testData) 162 166 }) 163 167 164 - it('should return null for non-existent key', async () => { 168 + test('should return null for non-existent key', async () => { 165 169 const tier = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }) 166 170 167 171 const result = await tier.getStream('non-existent-key') ··· 170 174 }) 171 175 172 176 describe('TieredStorage Streaming', () => { 173 - it('should store and retrieve data using streams', async () => { 177 + test('should store and retrieve data using streams', async () => { 174 178 const storage = new TieredStorage({ 175 179 tiers: { 176 180 hot: new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }), ··· 202 206 expect(retrievedData.toString()).toBe(testData) 203 207 }) 204 208 205 - it('should compute checksum during streaming write', async () => { 209 + test('should compute checksum during streaming write', async () => { 206 210 const storage = new TieredStorage({ 207 211 tiers: { 208 212 warm: new DiskStorageTier({ directory: `${testDir}/warm` }), ··· 222 226 expect(setResult.metadata.checksum).toBe(expectedChecksum) 223 227 }) 224 228 225 - it('should use provided checksum without computing', async () => { 229 + test('should use provided checksum without computing', async () => { 226 230 const storage = new TieredStorage({ 227 231 tiers: { 228 232 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 241 245 expect(setResult.metadata.checksum).toBe(providedChecksum) 242 246 }) 243 247 244 - it('should return null for non-existent key', async () => { 248 + test('should return null for non-existent key', async () => { 245 249 const storage = new TieredStorage({ 246 250 tiers: { 247 251 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 252 256 expect(result).toBeNull() 253 257 }) 254 258 255 - it('should read from appropriate tier (warm before cold)', async () => { 259 + test('should read from appropriate tier (warm before cold)', async () => { 256 260 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 257 261 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 258 262 ··· 277 281 expect(result!.source).toBe('warm') 278 282 }) 279 283 280 - it('should fall back to cold tier when warm has no data', async () => { 284 + test('should fall back to cold tier when warm has no data', async () => { 281 285 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 282 286 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 283 287 ··· 306 310 expect(result!.source).toBe('cold') 307 311 }) 308 312 309 - it('should handle TTL with metadata', async () => { 313 + test('should handle TTL with metadata', async () => { 310 314 const storage = new TieredStorage({ 311 315 tiers: { 312 316 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 326 330 expect(setResult.metadata.ttl!.getTime()).toBeGreaterThan(Date.now()) 327 331 }) 328 332 329 - it('should include mimeType in metadata', async () => { 333 + test('should include mimeType in metadata', async () => { 330 334 const storage = new TieredStorage({ 331 335 tiers: { 332 336 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 344 348 expect(setResult.metadata.mimeType).toBe('application/json') 345 349 }) 346 350 347 - it('should write to multiple tiers simultaneously', async () => { 351 + test('should write to multiple tiers simultaneously', async () => { 348 352 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 349 353 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 350 354 ··· 373 377 expect(coldData.toString()).toBe(testData) 374 378 }) 375 379 376 - it('should skip hot tier by default for streaming writes', async () => { 380 + test('should skip hot tier by default for streaming writes', async () => { 377 381 const hot = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }) 378 382 const warm = new DiskStorageTier({ directory: `${testDir}/warm` }) 379 383 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) ··· 398 402 expect(setResult.tiersWritten).toContain('cold') 399 403 }) 400 404 401 - it('should allow including hot tier explicitly', async () => { 405 + test('should allow including hot tier explicitly', async () => { 402 406 const hot = new MemoryStorageTier({ maxSizeBytes: 10 * 1024 * 1024 }) 403 407 const cold = new DiskStorageTier({ directory: `${testDir}/cold` }) 404 408 ··· 421 425 }) 422 426 423 427 describe('Streaming with Compression', () => { 424 - it('should compress stream data when compression is enabled', async () => { 428 + test('should compress stream data when compression is enabled', async () => { 425 429 const storage = new TieredStorage({ 426 430 tiers: { 427 431 warm: new DiskStorageTier({ directory: `${testDir}/warm` }), ··· 443 447 expect(setResult.metadata.checksum).toBe(computeChecksum(testBuffer)) 444 448 }) 445 449 446 - it('should decompress stream data automatically on read', async () => { 450 + test('should decompress stream data automatically on read', async () => { 447 451 const storage = new TieredStorage({ 448 452 tiers: { 449 453 warm: new DiskStorageTier({ directory: `${testDir}/warm` }), ··· 469 473 expect(retrievedData.toString()).toBe(testData) 470 474 }) 471 475 472 - it('should not compress when compression is disabled', async () => { 476 + test('should not compress when compression is disabled', async () => { 473 477 const storage = new TieredStorage({ 474 478 tiers: { 475 479 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 494 498 expect(retrievedData.toString()).toBe(testData) 495 499 }) 496 500 497 - it('should preserve checksum of original data when compressed', async () => { 501 + test('should preserve checksum of original data when compressed', async () => { 498 502 const storage = new TieredStorage({ 499 503 tiers: { 500 504 cold: new DiskStorageTier({ directory: `${testDir}/cold` }), ··· 521 525 }) 522 526 523 527 describe('Edge Cases', () => { 524 - it('should handle empty streams', async () => { 528 + test('should handle empty streams', async () => { 525 529 const tier = new DiskStorageTier({ directory: testDir }) 526 530 527 531 const metadata = { ··· 543 547 expect(data.length).toBe(0) 544 548 }) 545 549 546 - it('should preserve binary data integrity', async () => { 550 + test('should preserve binary data integrity', async () => { 547 551 const tier = new DiskStorageTier({ directory: testDir }) 548 552 549 553 // Create binary data with all possible byte values ··· 571 575 expect(retrievedData.equals(binaryData)).toBe(true) 572 576 }) 573 577 574 - it('should handle special characters in keys', async () => { 578 + test('should handle special characters in keys', async () => { 575 579 const tier = new DiskStorageTier({ directory: testDir }) 576 580 577 581 const testData = 'special key test'