fix: resolve BlobStorage compression race condition causing data corruption (v5.7.5)

CRITICAL BUG FIX - VFS file corruption since v5.0.0

Root cause: BlobStorage.initCompression() is async but called without await in
constructor, creating race window where write() happens before zstd loads.

Result: Data written uncompressed, metadata says "compressed" → read attempts
decompression → garbage → hash mismatch → "Blob integrity check failed" error.

Impact: VFS files (Workshop) written in first 100-500ms after BlobStorage creation
are permanently corrupted and unreadable. Data loss for users.

Fix (3 changes in BlobStorage.ts):

1. Added compressionReady flag to track init state (line 108)
2. Added ensureCompressionReady() to await init before write (lines 162-170)
3. Store ACTUAL compression state in metadata, not intended (line 227)

This ensures:
- Compression fully initialized before first write
- Metadata accurately reflects what actually happened
- No corruption even if zstd fails to load

Tests: All 30 BlobStorage unit tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-11-12 15:58:51 -08:00
parent 0e0661d05d
commit 9fc89a7258

View file

@ -105,6 +105,7 @@ export class BlobStorage {
// Compression (lazily loaded)
private zstdCompress?: (data: Buffer) => Promise<Buffer>
private zstdDecompress?: (data: Buffer) => Promise<Buffer>
private compressionReady = false
// Configuration
private readonly CACHE_MAX_SIZE = 100 * 1024 * 1024 // 100MB default
@ -158,6 +159,16 @@ export class BlobStorage {
}
}
/**
* v5.7.5: Ensure compression is ready before write operations
* Fixes race condition where write happens before async compression init completes
*/
private async ensureCompressionReady(): Promise<void> {
if (this.compressionReady) return
await this.initCompression()
this.compressionReady = true
}
/**
* Compute SHA-256 hash of data
*
@ -194,6 +205,10 @@ export class BlobStorage {
return hash
}
// v5.7.5: Ensure compression is initialized before writing
// Fixes race condition where write happens before async init completes
await this.ensureCompressionReady()
// Determine compression strategy
const compression = this.selectCompression(data, options)
@ -207,11 +222,14 @@ export class BlobStorage {
}
// Create metadata
// v5.7.5: Store ACTUAL compression state, not intended
// Prevents corruption if compression failed to initialize
const actualCompression = finalData === data ? 'none' : compression
const metadata: BlobMetadata = {
hash,
size: data.length,
compressedSize,
compression,
compression: actualCompression,
type: options.type || 'blob', // CRITICAL FIX: Use 'blob' default to match storage prefix
createdAt: Date.now(),
refCount: 1