dotnet / dotnet/aspnetcore

PullFromJSDataStream should be seekable

Open
#62,169 3 comments 0 reactions 0 assignees View on GitHub
area-blazor
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Is your feature request related to a problem? Please describe the problem.

the `PullFromJSDataStream` can be marked as seekable because the underlying js interop uses blob slicing.

### Describe the solution you'd like

the class below is just a copy-paste from the existing `PullFromJSDataStream` code. i tested this with a `File` from `input` of type `file` on blazor wasm.
```cs
class WebStream : Stream
{
private readonly IJSRuntime _runtime;
private readonly IJSStreamReference _jsStreamReference;
private readonly long _totalLength;
private readonly CancellationToken _streamCancellationToken;
private long _offset;

public static WebStream CreateWebStream(
IJSRuntime runtime,
IJSStreamReference jsStreamReference,
long totalLength,
CancellationToken cancellationToken = default)
{
return new WebStream(
runtime,
jsStreamReference,
totalLength,
cancellationToken);
}

private WebStream(
IJSRuntime runtime,
IJSStreamReference jsStreamReference,
long totalLength,
CancellationToken cancellationToken)
{
_runtime = runtime;
_jsStreamReference = jsStreamReference;
_totalLength = totalLength;
_streamCancellationToken = cancellationToken;
_offset = 0;
}

public override bool CanRead => true;

public override bool CanSeek => true;

public override bool CanWrite => false;

public override long Length => _totalLength;

public override long Position
{
get => _offset;
set => Seek(value, SeekOrigin.Begin);
}

public override void Flush()
{
// No-op
}

public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;

public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException("Synchronous reads are not supported.");

public override long Seek(long offset, SeekOrigin origin)
{
var newOffset = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _offset + offset,
SeekOrigin.End => _totalLength + offset,
_ => throw new ArgumentOutOfRangeException(nameof(origin), origin, null),
};

if (newOffset < 0 || newOffset > _totalLength)
{
throw new ArgumentOutOfRangeException(nameof(offset), "Seek offset is out of bounds.");
}

_offset = newOffset;
return _offset;
}

public override void SetLength(long value)
=> throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();

public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken);

public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
{
var bytesRead = await RequestDataFromJSAsync(buffer.Length);
ThrowIfCancellationRequested(cancellationToken);
bytesRead.CopyTo(buffer);

return bytesRead.Length;
}

private void ThrowIfCancellationRequested(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested ||
_streamCancellationToken.IsCancellationRequested)
{
throw new TaskCanceledException();
}
}

private async ValueTask RequestDataFromJSAsync(int numBytesToRead)
{
numBytesToRead = (int)Math.Min(
numBytesToRead,
_totalLength - _offset);
var bytesRead = await _runtime.InvokeAsync(
"Blazor._internal.getJSDataStreamChunk",
_jsStreamReference,
_offset,
numBytesToRead);

if (bytesRead.Length != numBytesToRead)
{
throw new EndOfStreamException("Failed to read the requested number of bytes from the stream.");
}

_offset += bytesRead.Length;

if (_offset == _totalLength)
{
Dispose(true);
}

return bytesRead;
}
}
```

### Additional context

_No response_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.