Skip to content

PullFromJSDataStream should be seekable #62169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
1 task done
ivanjx opened this issue May 30, 2025 · 0 comments
Open
1 task done

PullFromJSDataStream should be seekable #62169

ivanjx opened this issue May 30, 2025 · 0 comments
Labels
needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically

Comments

@ivanjx
Copy link

ivanjx commented May 30, 2025

Is there an existing issue for this?

  • 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.

    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<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
            => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken);

        public override async ValueTask<int> ReadAsync(Memory<byte> 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<byte[]> RequestDataFromJSAsync(int numBytesToRead)
        {
            numBytesToRead = (int)Math.Min(
                numBytesToRead,
                _totalLength - _offset);
            var bytesRead = await _runtime.InvokeAsync<byte[]>(
                "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

@github-actions github-actions bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label May 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically
Projects
None yet
Development

No branches or pull requests

1 participant