Skip to content

Commit

Permalink
Requests Cancellation (Resolves #6)
Browse files Browse the repository at this point in the history
  • Loading branch information
tpeczek authored Sep 13, 2023
1 parent 71cf974 commit 9599e24
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 92 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ BenchmarkDotNet.Artifacts/
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json

# StyleCop
StyleCopReport.xml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.Extensions.Logging;
using Ndjson.AsyncStreams.AspNetCore.Mvc;
using Demo.WeatherForecasts;
using System.Threading;
using System.Runtime.CompilerServices;

namespace Demo.Ndjson.AsyncStreams.AspNetCore.Mvc
{
Expand All @@ -22,30 +24,30 @@ public WeatherForecastsController(IWeatherForecaster weatherForecaster, ILogger<
}

[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
public async Task<IEnumerable<WeatherForecast>> Get(CancellationToken cancellationToken)
{
List<WeatherForecast> weatherForecasts = new();

for (int daysFromToday = 1; daysFromToday <= 10; daysFromToday++)
{
weatherForecasts.Add(await _weatherForecaster.GetWeatherForecastAsync(daysFromToday));
weatherForecasts.Add(await _weatherForecaster.GetWeatherForecastAsync(daysFromToday, cancellationToken));
};

return weatherForecasts;
}

[HttpGet("stream")]
// This action always returns NDJSON.
public NdjsonAsyncEnumerableResult<WeatherForecast> GetStream()
public NdjsonAsyncEnumerableResult<WeatherForecast> GetStream(CancellationToken cancellationToken)
{
return new NdjsonAsyncEnumerableResult<WeatherForecast>(StreamWeatherForecastsAsync());
return new NdjsonAsyncEnumerableResult<WeatherForecast>(StreamWeatherForecastsAsync(cancellationToken));
}

[HttpGet("negotiate-stream")]
// This action returns JSON or NDJSON depending on Accept request header.
public IAsyncEnumerable<WeatherForecast> NegotiateStream()
public IAsyncEnumerable<WeatherForecast> NegotiateStream(CancellationToken cancellationToken)
{
return StreamWeatherForecastsAsync();
return StreamWeatherForecastsAsync(cancellationToken);
}

[HttpPost("stream")]
Expand All @@ -60,11 +62,11 @@ public async Task<IActionResult> PostStream(IAsyncEnumerable<WeatherForecast> we
return Ok();
}

private async IAsyncEnumerable<WeatherForecast> StreamWeatherForecastsAsync()
private async IAsyncEnumerable<WeatherForecast> StreamWeatherForecastsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int daysFromToday = 1; daysFromToday <= 10; daysFromToday++)
{
WeatherForecast weatherForecast = await _weatherForecaster.GetWeatherForecastAsync(daysFromToday);
WeatherForecast weatherForecast = await _weatherForecaster.GetWeatherForecastAsync(daysFromToday, cancellationToken);

yield return weatherForecast;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ndjson.AsyncStreams.AspNetCore.Mvc" Version="1.2.0" />
<PackageReference Include="Ndjson.AsyncStreams.AspNetCore.Mvc" Version="1.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Demo.WeatherForecasts\Demo.WeatherForecasts.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"Demo.Ndjson.AsyncStreams.AspNetCore.Mvc": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}
8 changes: 3 additions & 5 deletions Demo.Ndjson.AsyncStreams.AspNetCore.Mvc/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
Expand All @@ -13,8 +12,7 @@ public class Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNdjson()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
.AddNdjson();

services.AddSingleton<IWeatherForecaster, WeatherForecaster>();
}
Expand All @@ -30,7 +28,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
defaultFilesOptions.DefaultFileNames.Clear();
defaultFilesOptions.DefaultFileNames.Add("fetch-streaming.html");

app.UseCors(policy => policy.WithOrigins("http://localhost:5011", "https://localhost:5011")
app.UseCors(policy => policy.WithOrigins("http://localhost:8080", "https://localhost:8081")
.AllowAnyMethod()
.AllowAnyHeader());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<button id="fetch-weather-forecasts-json-stream">Fetch Weather Forecast Stream (JSON)</button>
<button id="fetch-weather-forecasts-ndjson-stream">Fetch Weather Forecast Stream (NDJSON)</button>
<button id="post-weather-forecasts-ndjson-stream">Post Weather Forecast Stream (NDJSON)</button>
<button id="abort" disabled>Abort Operation</button>
<hr />
<table id="weather-forecasts">
<thead>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const FetchStreaming = (function () {

let abortController;
let weatherForecastsTable;
let fetchWeatherForecastsJsonButton, fetchWeatherForecastsJsonStreamButton, fetchWeatherForecastsNdjsonStreamButton, postWeatherForecastsNdjsonStreamButton;

let fetchWeatherForecastsJsonButton, fetchWeatherForecastsJsonStreamButton, fetchWeatherForecastsNdjsonStreamButton, postWeatherForecastsNdjsonStreamButton, abortButton;

function initializeUI() {
fetchWeatherForecastsJsonButton = document.getElementById('fetch-weather-forecasts-json');
Expand All @@ -15,62 +17,110 @@

postWeatherForecastsNdjsonStreamButton = document.getElementById('post-weather-forecasts-ndjson-stream');
postWeatherForecastsNdjsonStreamButton.addEventListener('click', postWeatherForecastsNdjsonStream);


abortButton = document.getElementById('abort');
abortButton.addEventListener('click', triggerAbortSignal);

weatherForecastsTable = document.getElementById('weather-forecasts');
};

function fetchWeatherForecastsJson() {
abortController = new AbortController();

switchButtonsState(true);
clearWeatherForecasts();

fetch('api/WeatherForecasts')
fetch('api/WeatherForecasts', { signal: abortController.signal })
.then(function (response) {
return response.json();
})
.then(function (weatherForecasts) {
weatherForecasts.forEach(appendWeatherForecast);
switchButtonsState(false);
});
};

function fetchWeatherForecastsJsonStream() {
abortController = new AbortController();
const abortSignal = abortController.signal;

switchButtonsState(true);
clearWeatherForecasts();

oboe('api/WeatherForecasts/negotiate-stream')
const oboeInstance = oboe('api/WeatherForecasts/negotiate-stream')
.node('!.*', function (weatherForecast) {
appendWeatherForecast(weatherForecast);
})
.done(function () {
switchButtonsState(false);
});

abortSignal.onabort = function () {
oboeInstance.abort();
};
}

function fetchWeatherForecastsNdjsonStream() {
abortController = new AbortController();

switchButtonsState(true);
clearWeatherForecasts();

fetch('api/WeatherForecasts/stream')
fetch('api/WeatherForecasts/negotiate-stream', { headers: { 'Accept': 'application/x-ndjson' }, signal: abortController.signal })
.then(function (response) {
const weatherForecasts = response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(parseNDJSON());
.pipeThrough(transformNdjsonStream());

readWeatherForecastsStream(weatherForecasts.getReader());
readWeatherForecastsNdjsonStream(weatherForecasts.getReader());
});
};

function postWeatherForecastsNdjsonStream() {
const weatherForecastsStream = WeatherForecaster.getWeatherForecastsStream().pipeThrough(new TextEncoderStream());
abortController = new AbortController();

fetch('api/WeatherForecasts/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/x-ndjson' },
body: weatherForecastsStream,
duplex: 'half'
});
switchButtonsState(true);
clearWeatherForecasts();

const weatherForecastsStream = WeatherForecaster.getWeatherForecastsStream().pipeThrough(new TextEncoderStream());
fetch('api/WeatherForecasts/stream', { method: 'POST', headers: { 'Content-Type': 'application/x-ndjson' }, body: weatherForecastsStream, duplex: 'half', signal: abortController.signal })
.then(function (response) {
switchButtonsState(false);
});
};

function triggerAbortSignal() {
if (abortController) {
abortController.abort();
switchButtonsState(false);
}
}

function switchButtonsState(operationInProgress) {
fetchWeatherForecastsJsonButton.disabled = operationInProgress;
fetchWeatherForecastsJsonStreamButton.disabled = operationInProgress;
fetchWeatherForecastsNdjsonStreamButton.disabled = operationInProgress;
postWeatherForecastsNdjsonStreamButton = operationInProgress;

abortButton.disabled = !operationInProgress;
}

function clearWeatherForecasts() {
for (let rowIndex = 1; rowIndex < weatherForecastsTable.rows.length;) {
weatherForecastsTable.deleteRow(rowIndex );
}
};

function parseNDJSON() {
function appendWeatherForecast(weatherForecast) {
let weatherForecastRow = weatherForecastsTable.insertRow(-1);

weatherForecastRow.insertCell(0).appendChild(document.createTextNode(weatherForecast.dateFormatted));
weatherForecastRow.insertCell(1).appendChild(document.createTextNode(weatherForecast.temperatureC));
weatherForecastRow.insertCell(2).appendChild(document.createTextNode(weatherForecast.temperatureF));
weatherForecastRow.insertCell(3).appendChild(document.createTextNode(weatherForecast.summary));
};

function transformNdjsonStream() {
let ndjsonBuffer = '';

return new TransformStream({
Expand All @@ -90,26 +140,19 @@
});
};

function readWeatherForecastsStream(weatherForecastsStreamReader) {
function readWeatherForecastsNdjsonStream(weatherForecastsStreamReader) {
weatherForecastsStreamReader.read()
.then(function (result) {
if (!result.done) {
appendWeatherForecast(result.value);

readWeatherForecastsStream(weatherForecastsStreamReader);
readWeatherForecastsNdjsonStream(weatherForecastsStreamReader);
} else {
switchButtonsState(false);
}
});
};

function appendWeatherForecast(weatherForecast) {
let weatherForecastRow = weatherForecastsTable.insertRow(-1);

weatherForecastRow.insertCell(0).appendChild(document.createTextNode(weatherForecast.dateFormatted));
weatherForecastRow.insertCell(1).appendChild(document.createTextNode(weatherForecast.temperatureC));
weatherForecastRow.insertCell(2).appendChild(document.createTextNode(weatherForecast.temperatureF));
weatherForecastRow.insertCell(3).appendChild(document.createTextNode(weatherForecast.summary));
};

return {
initialize: function () {
initializeUI();
Expand Down
Loading

0 comments on commit 9599e24

Please sign in to comment.