Skip to content

PullFromJSDataStream should be seekable #62169

Open
@ivanjx

Description

@ivanjx

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions