@@ -63,7 +63,7 @@ internal sealed class LiteFileHandle<TFileId> : ILiteFileHandle<TFileId>
6363 // ── Write state ───────────────────────────────────────────────────────────
6464
6565 private MemoryStream _writeBuffer ;
66- private bool _finalized ;
66+ private bool _writeStateDirty ;
6767
6868 // ── Construction ─────────────────────────────────────────────────────────
6969
@@ -134,11 +134,9 @@ public async ValueTask<int> Read(Memory<byte> buffer, CancellationToken cancella
134134 if ( buffer . IsEmpty || _position >= FileInfo . Length )
135135 return 0 ;
136136
137- var targetChunkIndex = EstimateChunkIndex ( _position ) ;
138-
139- if ( _currentChunkIndex != targetChunkIndex || _currentChunkData == null )
137+ if ( _currentChunkData == null )
140138 {
141- await LoadChunk ( targetChunkIndex , cancellationToken ) . ConfigureAwait ( false ) ;
139+ await LoadChunkForPosition ( _position , cancellationToken ) . ConfigureAwait ( false ) ;
142140 }
143141
144142 if ( _currentChunkData == null )
@@ -189,6 +187,7 @@ public async ValueTask Write(ReadOnlyMemory<byte> buffer, CancellationToken canc
189187 if ( buffer . IsEmpty )
190188 return ;
191189
190+ _writeStateDirty = true ;
192191 _position += buffer . Length ;
193192
194193 // MemoryStream.WriteAsync accepts byte[] rather than ReadOnlyMemory<byte> on .NET Standard 2.x,
@@ -252,7 +251,7 @@ public async ValueTask DisposeAsync()
252251
253252 _disposed = true ;
254253
255- if ( CanWrite && ! _finalized )
254+ if ( CanWrite && ( _writeStateDirty || ( _writeBuffer ? . Length ?? 0 ) > 0 ) )
256255 {
257256 // Best-effort flush; if the write session was abandoned without an explicit Flush()
258257 // we still attempt to commit whatever data was buffered.
@@ -296,38 +295,56 @@ private async ValueTask LoadChunk(int chunkIndex, CancellationToken cancellation
296295 }
297296
298297 /// <summary>
299- /// Estimate the chunk index that contains <paramref name="position"/> within the file.
300- /// Uses cached chunk lengths for accuracy; falls back to uniform-chunk-size division for
301- /// chunks not yet loaded.
298+ /// Load the exact chunk that contains <paramref name="position"/> and set
299+ /// <see cref="_positionInChunk"/> to the correct intra-chunk offset.
302300 /// </summary>
303- private int EstimateChunkIndex ( long position )
301+ private async ValueTask LoadChunkForPosition ( long position , CancellationToken cancellationToken )
304302 {
305- if ( position == 0 )
306- return 0 ;
303+ if ( position < 0 || position >= FileInfo . Length )
304+ {
305+ _currentChunkData = null ;
306+ _currentChunkIndex = - 1 ;
307+ _positionInChunk = 0 ;
308+ return ;
309+ }
307310
308- long cumulative = 0 ;
311+ long remaining = position ;
309312
310- for ( int i = 0 ; ; i ++ )
313+ for ( var i = 0 ; i < FileInfo . Chunks ; i ++ )
311314 {
312- if ( _chunkLengths . TryGetValue ( i , out var chunkLen ) )
315+ if ( ! _chunkLengths . TryGetValue ( i , out var chunkLen ) )
313316 {
314- if ( cumulative + chunkLen > position )
315- return i ;
317+ await LoadChunk ( i , cancellationToken ) . ConfigureAwait ( false ) ;
316318
317- cumulative += chunkLen ;
319+ if ( _currentChunkData == null )
320+ return ;
321+
322+ chunkLen = _currentChunkData . Length ;
318323 }
319- else
324+
325+ if ( remaining < chunkLen )
320326 {
321- // Remaining lengths are unknown; use MaxChunkSize as the estimate.
322- return i + ( int ) ( ( position - cumulative ) / MaxChunkSize ) ;
327+ if ( _currentChunkIndex != i || _currentChunkData == null )
328+ {
329+ await LoadChunk ( i , cancellationToken ) . ConfigureAwait ( false ) ;
330+ }
331+
332+ _positionInChunk = ( int ) remaining ;
333+ return ;
323334 }
335+
336+ remaining -= chunkLen ;
324337 }
338+
339+ _currentChunkData = null ;
340+ _currentChunkIndex = - 1 ;
341+ _positionInChunk = 0 ;
325342 }
326343
327344 /// <summary>
328345 /// Drain <see cref="_writeBuffer"/> into the chunks collection.
329346 /// When <paramref name="finalize"/> is <c>true</c>, updates file metadata in <c>_files</c>
330- /// and sets <see cref="_finalized"/> so subsequent calls are no-ops .
347+ /// if there have been writes since the last finalize .
331348 /// </summary>
332349 private async ValueTask PersistBufferedChunks ( bool finalize , CancellationToken cancellationToken )
333350 {
@@ -364,12 +381,12 @@ private async ValueTask PersistBufferedChunks(bool finalize, CancellationToken c
364381 await _chunks . Insert ( chunkDoc , cancellationToken ) . ConfigureAwait ( false ) ;
365382 }
366383
367- if ( finalize && ! _finalized )
384+ if ( finalize && _writeStateDirty )
368385 {
369- _finalized = true ;
370386 FileInfo . UploadDate = DateTime . UtcNow ;
371387 FileInfo . Length = _position ;
372388 await _files . Upsert ( FileInfo , cancellationToken ) . ConfigureAwait ( false ) ;
389+ _writeStateDirty = false ;
373390 }
374391
375392 // Reset the write buffer for subsequent Write() calls before a final Flush().
0 commit comments