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:
parent
0e0661d05d
commit
9fc89a7258
1 changed files with 19 additions and 1 deletions
|
|
@ -105,6 +105,7 @@ export class BlobStorage {
|
||||||
// Compression (lazily loaded)
|
// Compression (lazily loaded)
|
||||||
private zstdCompress?: (data: Buffer) => Promise<Buffer>
|
private zstdCompress?: (data: Buffer) => Promise<Buffer>
|
||||||
private zstdDecompress?: (data: Buffer) => Promise<Buffer>
|
private zstdDecompress?: (data: Buffer) => Promise<Buffer>
|
||||||
|
private compressionReady = false
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
private readonly CACHE_MAX_SIZE = 100 * 1024 * 1024 // 100MB default
|
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
|
* Compute SHA-256 hash of data
|
||||||
*
|
*
|
||||||
|
|
@ -194,6 +205,10 @@ export class BlobStorage {
|
||||||
return hash
|
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
|
// Determine compression strategy
|
||||||
const compression = this.selectCompression(data, options)
|
const compression = this.selectCompression(data, options)
|
||||||
|
|
||||||
|
|
@ -207,11 +222,14 @@ export class BlobStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create metadata
|
// 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 = {
|
const metadata: BlobMetadata = {
|
||||||
hash,
|
hash,
|
||||||
size: data.length,
|
size: data.length,
|
||||||
compressedSize,
|
compressedSize,
|
||||||
compression,
|
compression: actualCompression,
|
||||||
type: options.type || 'blob', // CRITICAL FIX: Use 'blob' default to match storage prefix
|
type: options.type || 'blob', // CRITICAL FIX: Use 'blob' default to match storage prefix
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
refCount: 1
|
refCount: 1
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue