Initial push
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using SqrtSpace.SpaceTime.Core;
|
||||
|
||||
namespace SqrtSpace.SpaceTime.AspNetCore;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for streaming large responses with √n memory usage
|
||||
/// </summary>
|
||||
public static class SpaceTimeStreamingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a large enumerable as JSON stream with √n buffering
|
||||
/// </summary>
|
||||
public static async Task WriteAsJsonStreamAsync<T>(
|
||||
this HttpResponse response,
|
||||
IAsyncEnumerable<T> items,
|
||||
JsonSerializerOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
response.ContentType = "application/json";
|
||||
response.Headers.Add("X-SpaceTime-Streaming", "sqrtn");
|
||||
|
||||
await using var writer = new Utf8JsonWriter(response.Body, new JsonWriterOptions
|
||||
{
|
||||
Indented = options?.WriteIndented ?? false
|
||||
});
|
||||
|
||||
writer.WriteStartArray();
|
||||
|
||||
var count = 0;
|
||||
var bufferSize = SpaceTimeCalculator.CalculateSqrtInterval(100_000); // Estimate
|
||||
var buffer = new List<T>(bufferSize);
|
||||
|
||||
await foreach (var item in items.WithCancellation(cancellationToken))
|
||||
{
|
||||
buffer.Add(item);
|
||||
count++;
|
||||
|
||||
if (buffer.Count >= bufferSize)
|
||||
{
|
||||
await FlushBufferAsync(writer, buffer, options, cancellationToken);
|
||||
buffer.Clear();
|
||||
await response.Body.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining items
|
||||
if (buffer.Count > 0)
|
||||
{
|
||||
await FlushBufferAsync(writer, buffer, options, cancellationToken);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
await writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an async enumerable result with √n chunking
|
||||
/// </summary>
|
||||
public static IActionResult StreamWithSqrtNChunking<T>(
|
||||
this ControllerBase controller,
|
||||
IAsyncEnumerable<T> items,
|
||||
int? estimatedCount = null)
|
||||
{
|
||||
return new SpaceTimeStreamResult<T>(items, estimatedCount);
|
||||
}
|
||||
|
||||
private static async Task FlushBufferAsync<T>(
|
||||
Utf8JsonWriter writer,
|
||||
List<T> buffer,
|
||||
JsonSerializerOptions? options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var item in buffer)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, item, options);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action result for streaming with SpaceTime optimizations
|
||||
/// </summary>
|
||||
public class SpaceTimeStreamResult<T> : IActionResult
|
||||
{
|
||||
private readonly IAsyncEnumerable<T> _items;
|
||||
private readonly int? _estimatedCount;
|
||||
|
||||
public SpaceTimeStreamResult(IAsyncEnumerable<T> items, int? estimatedCount = null)
|
||||
{
|
||||
_items = items;
|
||||
_estimatedCount = estimatedCount;
|
||||
}
|
||||
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
var response = context.HttpContext.Response;
|
||||
response.ContentType = "application/json";
|
||||
response.Headers.Add("X-SpaceTime-Streaming", "chunked");
|
||||
|
||||
if (_estimatedCount.HasValue)
|
||||
{
|
||||
response.Headers.Add("X-Total-Count", _estimatedCount.Value.ToString());
|
||||
}
|
||||
|
||||
await response.WriteAsJsonStreamAsync(_items, cancellationToken: context.HttpContext.RequestAborted);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to configure streaming behavior
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class SpaceTimeStreamingAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk size strategy
|
||||
/// </summary>
|
||||
public ChunkStrategy ChunkStrategy { get; set; } = ChunkStrategy.SqrtN;
|
||||
|
||||
/// <summary>
|
||||
/// Custom chunk size (if not using automatic strategies)
|
||||
/// </summary>
|
||||
public int? ChunkSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include progress headers
|
||||
/// </summary>
|
||||
public bool IncludeProgress { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strategies for determining chunk size
|
||||
/// </summary>
|
||||
public enum ChunkStrategy
|
||||
{
|
||||
/// <summary>Use √n of estimated total</summary>
|
||||
SqrtN,
|
||||
/// <summary>Fixed size chunks</summary>
|
||||
Fixed,
|
||||
/// <summary>Adaptive based on response time</summary>
|
||||
Adaptive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for streaming file downloads
|
||||
/// </summary>
|
||||
public static class FileStreamingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams a file with √n buffer size
|
||||
/// </summary>
|
||||
public static async Task StreamFileWithSqrtNBufferAsync(
|
||||
this HttpResponse response,
|
||||
string filePath,
|
||||
string? contentType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
var bufferSize = (int)SpaceTimeCalculator.CalculateOptimalBufferSize(
|
||||
fileInfo.Length,
|
||||
4 * 1024 * 1024); // Max 4MB buffer
|
||||
|
||||
response.ContentType = contentType ?? "application/octet-stream";
|
||||
response.ContentLength = fileInfo.Length;
|
||||
response.Headers.Add("X-SpaceTime-Buffer-Size", bufferSize.ToString());
|
||||
|
||||
await using var fileStream = new FileStream(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
bufferSize,
|
||||
useAsync: true);
|
||||
|
||||
await fileStream.CopyToAsync(response.Body, bufferSize, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Middleware for automatic response streaming optimization
|
||||
/// </summary>
|
||||
public class ResponseStreamingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ResponseStreamingOptions _options;
|
||||
|
||||
public ResponseStreamingMiddleware(RequestDelegate next, ResponseStreamingOptions options)
|
||||
{
|
||||
_next = next;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// Check if response should be streamed
|
||||
if (_options.EnableAutoStreaming && IsLargeResponse(context))
|
||||
{
|
||||
// Replace response body with buffered stream
|
||||
var originalBody = context.Response.Body;
|
||||
using var bufferStream = new SqrtNBufferedStream(originalBody, _options.MaxBufferSize);
|
||||
context.Response.Body = bufferStream;
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Response.Body = originalBody;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLargeResponse(HttpContext context)
|
||||
{
|
||||
// Check endpoint metadata
|
||||
var endpoint = context.GetEndpoint();
|
||||
var streamingAttr = endpoint?.Metadata.GetMetadata<SpaceTimeStreamingAttribute>();
|
||||
return streamingAttr != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for response streaming middleware
|
||||
/// </summary>
|
||||
public class ResponseStreamingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable automatic streaming optimization
|
||||
/// </summary>
|
||||
public bool EnableAutoStreaming { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum buffer size in bytes
|
||||
/// </summary>
|
||||
public int MaxBufferSize { get; set; } = 4 * 1024 * 1024; // 4MB
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stream that buffers using √n strategy
|
||||
/// </summary>
|
||||
internal class SqrtNBufferedStream : Stream
|
||||
{
|
||||
private readonly Stream _innerStream;
|
||||
private readonly int _bufferSize;
|
||||
private readonly byte[] _buffer;
|
||||
private int _bufferPosition;
|
||||
|
||||
public SqrtNBufferedStream(Stream innerStream, int maxBufferSize)
|
||||
{
|
||||
_innerStream = innerStream;
|
||||
_bufferSize = Math.Min(maxBufferSize, SpaceTimeCalculator.CalculateSqrtInterval(1_000_000) * 1024);
|
||||
_buffer = new byte[_bufferSize];
|
||||
}
|
||||
|
||||
public override bool CanRead => _innerStream.CanRead;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => _innerStream.CanWrite;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
if (_bufferPosition > 0)
|
||||
{
|
||||
_innerStream.Write(_buffer, 0, _bufferPosition);
|
||||
_bufferPosition = 0;
|
||||
}
|
||||
_innerStream.Flush();
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_bufferPosition > 0)
|
||||
{
|
||||
await _innerStream.WriteAsync(_buffer.AsMemory(0, _bufferPosition), cancellationToken);
|
||||
_bufferPosition = 0;
|
||||
}
|
||||
await _innerStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
while (count > 0)
|
||||
{
|
||||
var bytesToCopy = Math.Min(count, _bufferSize - _bufferPosition);
|
||||
Buffer.BlockCopy(buffer, offset, _buffer, _bufferPosition, bytesToCopy);
|
||||
|
||||
_bufferPosition += bytesToCopy;
|
||||
offset += bytesToCopy;
|
||||
count -= bytesToCopy;
|
||||
|
||||
if (_bufferPosition >= _bufferSize)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var remaining = buffer;
|
||||
while (remaining.Length > 0)
|
||||
{
|
||||
var bytesToCopy = Math.Min(remaining.Length, _bufferSize - _bufferPosition);
|
||||
remaining.Slice(0, bytesToCopy).CopyTo(_buffer.AsMemory(_bufferPosition));
|
||||
|
||||
_bufferPosition += bytesToCopy;
|
||||
remaining = remaining.Slice(bytesToCopy);
|
||||
|
||||
if (_bufferPosition >= _bufferSize)
|
||||
{
|
||||
await FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user