mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 19:35:44 -05:00
feat(data): library browse session
Add from Drawing Library needs the same race-safe paging as the Database-mode Open dialog: a generation counter so an older response never replaces a newer one, cancellation after the superseding request is installed, and IsLoading tracking only the latest request. `DrawingLibraryBrowseSession` mirrors `NestBrowseSession` for library queries. It starts filtered to the nest's material, grade and thickness, since that is almost always what the operator wants and a mismatch drops cut pins anyway (D3). "Show all" lifts the filter. A nest with no thickness filters by material only, and the empty-result text tells the operator when the nest filter, rather than an empty library, is why nothing is listed. Archived drawings stay hidden. `IDrawingLibrary` is the read side the session and the add flow use, so both are testable with fakes; `RemoteDrawingLibrary` implements it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4dbb4527e2
commit
175782d8f8
4 files changed
+518
-1
No files matched your search
@@ -0,0 +1,230 @@
|
||||
using System.Globalization;
|
||||
using OpenNest;
|
||||
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Browse state for Add from Drawing Library: the requested search, sort, page and
|
||||
/// nest filter, and the last page applied. By default only drawings with the nest's
|
||||
/// material, grade and thickness are listed; <see cref="SetShowAllAsync"/> lifts that
|
||||
/// filter. Every change issues one bounded server query. A response that a later request
|
||||
/// superseded is discarded (and the earlier request is cancelled), so an older result can
|
||||
/// never replace a newer one. Call from one thread (the UI thread).
|
||||
/// </summary>
|
||||
public sealed class DrawingLibraryBrowseSession : IDisposable
|
||||
{
|
||||
private readonly IDrawingLibrary _library;
|
||||
private CancellationTokenSource? _pending;
|
||||
private int _generation;
|
||||
private bool _disposed;
|
||||
|
||||
public DrawingLibraryBrowseSession(IDrawingLibrary library, Nest nest, int pageSize = DrawingLibraryQuery.DefaultLimit)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(library);
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
if (pageSize < 1 || pageSize > DrawingLibraryQuery.MaxLimit)
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, $"Page size must be 1 to {DrawingLibraryQuery.MaxLimit}.");
|
||||
|
||||
_library = library;
|
||||
PageSize = pageSize;
|
||||
MaterialName = (nest.Material?.Name ?? "").Trim();
|
||||
MaterialGrade = (nest.Material?.Grade ?? "").Trim();
|
||||
// A nest without a thickness has nothing to match; it filters by material only.
|
||||
Thickness = double.IsFinite(nest.Thickness) && nest.Thickness > 0
|
||||
? DrawingLibraryRules.NormalizeThickness(nest.Thickness)
|
||||
: null;
|
||||
}
|
||||
|
||||
public int PageSize { get; }
|
||||
|
||||
/// <summary>The nest's material name, applied unless <see cref="ShowAll"/>; blank means any.</summary>
|
||||
public string MaterialName { get; }
|
||||
|
||||
/// <summary>The nest's grade, applied unless <see cref="ShowAll"/>; blank means any.</summary>
|
||||
public string MaterialGrade { get; }
|
||||
|
||||
/// <summary>The nest's thickness, applied unless <see cref="ShowAll"/>; null means any.</summary>
|
||||
public double? Thickness { get; }
|
||||
|
||||
/// <summary>True when the nest filter is lifted and every active drawing is listed.</summary>
|
||||
public bool ShowAll { get; private set; }
|
||||
|
||||
/// <summary>True when the latest request narrows by the nest's material, grade or thickness.</summary>
|
||||
public bool IsFilteredToNest =>
|
||||
!ShowAll && (MaterialName.Length > 0 || MaterialGrade.Length > 0 || Thickness is not null);
|
||||
|
||||
/// <summary>Trimmed search text of the latest request.</summary>
|
||||
public string Search { get; private set; } = "";
|
||||
|
||||
public DrawingSortField Sort { get; private set; } = DrawingSortField.SavedAt;
|
||||
|
||||
public bool Descending { get; private set; } = true;
|
||||
|
||||
/// <summary>Offset of the latest request.</summary>
|
||||
public int Offset { get; private set; }
|
||||
|
||||
/// <summary>The latest applied page; null before the first result and after a failed request.</summary>
|
||||
public DrawingLibraryPage? Page { get; private set; }
|
||||
|
||||
/// <summary>True while the latest request is outstanding; superseded requests do not count.</summary>
|
||||
public bool IsLoading { get; private set; }
|
||||
|
||||
public bool CanGoPrevious => Offset > 0;
|
||||
|
||||
public bool CanGoNext => Page is { } page && page.Offset + page.Items.Count < page.Total;
|
||||
|
||||
/// <summary>Operator-facing summary of <see cref="Page"/>, such as "Showing 101-200 of 1,234 drawings".</summary>
|
||||
public string Summary
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Page is not { } page)
|
||||
return "";
|
||||
if (page.Total == 0)
|
||||
{
|
||||
if (Search.Length > 0)
|
||||
return "No drawings match the search.";
|
||||
return IsFilteredToNest
|
||||
? "No library drawings match this nest's material and thickness. Choose Show all to see every drawing."
|
||||
: "No drawings in the library.";
|
||||
}
|
||||
if (page.Items.Count == 0)
|
||||
return $"No drawings on this page ({Count(page.Total)} in total).";
|
||||
|
||||
return $"Showing {Count(page.Offset + 1)}-{Count(page.Offset + page.Items.Count)} of {Count(page.Total)} drawings";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Applies new search text and returns to the first page.</summary>
|
||||
public Task<bool> SetSearchAsync(string? search)
|
||||
{
|
||||
Search = (search ?? "").Trim();
|
||||
Offset = 0;
|
||||
return RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>Lifts or restores the nest filter and returns to the first page.</summary>
|
||||
public Task<bool> SetShowAllAsync(bool showAll)
|
||||
{
|
||||
ShowAll = showAll;
|
||||
Offset = 0;
|
||||
return RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>Sorts by a column: a new column starts ascending, the same column reverses.</summary>
|
||||
public Task<bool> SortByAsync(DrawingSortField field)
|
||||
{
|
||||
Descending = field == Sort && !Descending;
|
||||
Sort = field;
|
||||
Offset = 0;
|
||||
return RunAsync();
|
||||
}
|
||||
|
||||
public Task<bool> NextPageAsync()
|
||||
{
|
||||
if (!CanGoNext)
|
||||
return Task.FromResult(false);
|
||||
|
||||
Offset = Page!.Offset + PageSize;
|
||||
return RunAsync();
|
||||
}
|
||||
|
||||
public Task<bool> PreviousPageAsync()
|
||||
{
|
||||
if (!CanGoPrevious)
|
||||
return Task.FromResult(false);
|
||||
|
||||
Offset = System.Math.Max(0, Offset - PageSize);
|
||||
return RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>Re-reads the current page; steps back to the last page if it is now past the end.</summary>
|
||||
public Task<bool> RefreshAsync() => RunAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Sends the current request. Returns true when its page was applied, false when a later
|
||||
/// request superseded it. Failures of the latest request clear <see cref="Page"/> and propagate;
|
||||
/// failures of superseded requests are ignored.
|
||||
/// </summary>
|
||||
private async Task<bool> RunAsync()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
// Supersede before cancelling: a request that completes synchronously when cancelled
|
||||
// must already see itself as superseded. Each source is released by its own request.
|
||||
var previous = _pending;
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
_pending = cancellation;
|
||||
var generation = ++_generation;
|
||||
IsLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
previous?.Cancel();
|
||||
var page = await QueryAsync(cancellation.Token);
|
||||
if (generation != _generation)
|
||||
return false;
|
||||
|
||||
// An archive or another PC can leave the requested page past the end.
|
||||
if (page.Items.Count == 0 && page.Offset > 0 && page.Total > 0)
|
||||
{
|
||||
Offset = (page.Total - 1) / PageSize * PageSize;
|
||||
page = await QueryAsync(cancellation.Token);
|
||||
if (generation != _generation)
|
||||
return false;
|
||||
}
|
||||
|
||||
Page = page;
|
||||
return true;
|
||||
}
|
||||
catch (Exception) when (generation != _generation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Page = null;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_pending, cancellation))
|
||||
_pending = null;
|
||||
if (generation == _generation)
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<DrawingLibraryPage> QueryAsync(CancellationToken cancellationToken) =>
|
||||
_library.QueryAsync(
|
||||
new DrawingLibraryQuery
|
||||
{
|
||||
Search = Search,
|
||||
MaterialName = ShowAll ? "" : MaterialName,
|
||||
MaterialGrade = ShowAll ? "" : MaterialGrade,
|
||||
Thickness = ShowAll ? null : Thickness,
|
||||
Sort = Sort,
|
||||
Descending = Descending,
|
||||
Offset = Offset,
|
||||
Limit = PageSize,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
private static string Count(int value) => value.ToString("N0", CultureInfo.CurrentCulture);
|
||||
|
||||
/// <summary>
|
||||
/// Cancels any request in flight (which releases its own source when it ends);
|
||||
/// the library client is not owned and stays open.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
_generation++;
|
||||
IsLoading = false;
|
||||
var pending = _pending;
|
||||
_pending = null;
|
||||
pending?.Cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The read side of the server drawing library that browsing and adding to a nest use.
|
||||
/// <see cref="RemoteDrawingLibrary"/> is the only production implementation.
|
||||
/// </summary>
|
||||
public interface IDrawingLibrary
|
||||
{
|
||||
/// <summary>One bounded page of drawings matching the query, filtered and ordered by the server.</summary>
|
||||
Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>The exact package of one immutable revision, or null when the drawing or revision is unknown.</summary>
|
||||
Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace OpenNest.Data;
|
||||
/// the library raises <see cref="ServerTooOldException"/>; this client never falls back
|
||||
/// to another store. Other failures surface as <see cref="IOException"/>.
|
||||
/// </summary>
|
||||
public sealed class RemoteDrawingLibrary : IDisposable
|
||||
public sealed class RemoteDrawingLibrary : IDrawingLibrary, IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
using OpenNest.Data;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class DrawingLibraryBrowseSessionTests
|
||||
{
|
||||
private static Nest SyntheticNest() => new("Synthetic job")
|
||||
{
|
||||
Thickness = 0.187504,
|
||||
Material = new Material(" Synthetic Steel ", "Grade A"),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_DefaultsToTheNestsMaterialGradeAndThicknessNewestSavedFirst()
|
||||
{
|
||||
var library = new PagedLibrary(25);
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest(), pageSize: 10);
|
||||
|
||||
Assert.True(await session.RefreshAsync());
|
||||
|
||||
var query = Assert.Single(library.Queries);
|
||||
Assert.Equal(("Synthetic Steel", "Grade A", (double?)0.1875), (query.MaterialName, query.MaterialGrade, query.Thickness));
|
||||
Assert.Equal(("", (CutType?)null, false), (query.Search, query.CutType, query.IncludeArchived));
|
||||
Assert.Equal((DrawingSortField.SavedAt, true, 0, 10), (query.Sort, query.Descending, query.Offset, query.Limit));
|
||||
Assert.True(session.IsFilteredToNest);
|
||||
Assert.Equal("Showing 1-10 of 25 drawings", session.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShowAll_LiftsTheNestFilterAndReturnsToTheFirstPage()
|
||||
{
|
||||
var library = new PagedLibrary(25);
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest(), pageSize: 10);
|
||||
await session.RefreshAsync();
|
||||
await session.NextPageAsync();
|
||||
|
||||
await session.SetShowAllAsync(true);
|
||||
|
||||
var all = library.Queries[^1];
|
||||
Assert.Equal(("", "", (double?)null, 0), (all.MaterialName, all.MaterialGrade, all.Thickness, all.Offset));
|
||||
Assert.False(session.IsFilteredToNest);
|
||||
|
||||
await session.SetShowAllAsync(false);
|
||||
|
||||
var filtered = library.Queries[^1];
|
||||
Assert.Equal(("Synthetic Steel", "Grade A", (double?)0.1875), (filtered.MaterialName, filtered.MaterialGrade, filtered.Thickness));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NestWithoutMaterialOrThickness_IsNotFiltered()
|
||||
{
|
||||
var library = new PagedLibrary(3);
|
||||
using var session = new DrawingLibraryBrowseSession(library, new Nest("Blank job") { Material = null! });
|
||||
|
||||
await session.RefreshAsync();
|
||||
|
||||
var query = Assert.Single(library.Queries);
|
||||
Assert.Equal(("", "", (double?)null), (query.MaterialName, query.MaterialGrade, query.Thickness));
|
||||
Assert.False(session.IsFilteredToNest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SearchSortAndPaging_FollowTheNestBrowseRules()
|
||||
{
|
||||
var library = new PagedLibrary(25);
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest(), pageSize: 10);
|
||||
await session.RefreshAsync();
|
||||
Assert.False(session.CanGoPrevious);
|
||||
|
||||
Assert.True(await session.NextPageAsync());
|
||||
Assert.True(await session.NextPageAsync());
|
||||
Assert.Equal("Showing 21-25 of 25 drawings", session.Summary);
|
||||
Assert.False(session.CanGoNext);
|
||||
var sent = library.Queries.Count;
|
||||
Assert.False(await session.NextPageAsync());
|
||||
Assert.Equal(sent, library.Queries.Count);
|
||||
Assert.True(await session.PreviousPageAsync());
|
||||
Assert.Equal(10, library.Queries[^1].Offset);
|
||||
|
||||
await session.SetSearchAsync(" bracket ");
|
||||
Assert.Equal(("bracket", 0), (library.Queries[^1].Search, library.Queries[^1].Offset));
|
||||
|
||||
await session.SortByAsync(DrawingSortField.Name);
|
||||
Assert.Equal((DrawingSortField.Name, false), (library.Queries[^1].Sort, library.Queries[^1].Descending));
|
||||
await session.SortByAsync(DrawingSortField.Name);
|
||||
Assert.Equal((DrawingSortField.Name, true), (library.Queries[^1].Sort, library.Queries[^1].Descending));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "", "No library drawings match this nest's material and thickness. Choose Show all to see every drawing.")]
|
||||
[InlineData(true, "", "No drawings in the library.")]
|
||||
[InlineData(false, "no such text", "No drawings match the search.")]
|
||||
public async Task Summary_ExplainsAnEmptyResult(bool showAll, string search, string expected)
|
||||
{
|
||||
using var session = new DrawingLibraryBrowseSession(new PagedLibrary(0), SyntheticNest());
|
||||
|
||||
await session.SetShowAllAsync(showAll);
|
||||
await session.SetSearchAsync(search);
|
||||
|
||||
Assert.Equal(expected, session.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OlderResponse_ArrivingLast_NeverReplacesTheNewerResult()
|
||||
{
|
||||
var library = new ControlledLibrary();
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest());
|
||||
var older = session.SetSearchAsync("a");
|
||||
var newer = session.SetShowAllAsync(true);
|
||||
var newerPage = Page("show all result");
|
||||
|
||||
library.Requests[1].Reply.SetResult(newerPage);
|
||||
Assert.True(await newer);
|
||||
library.Requests[0].Reply.SetResult(Page("a result"));
|
||||
Assert.False(await older);
|
||||
|
||||
Assert.Same(newerPage, session.Page);
|
||||
Assert.True(library.Requests[0].Token.IsCancellationRequested);
|
||||
Assert.False(library.Requests[1].Token.IsCancellationRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupersededFailure_IsIgnored_LatestFailure_ClearsThePageAndPropagates()
|
||||
{
|
||||
var library = new ControlledLibrary();
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest());
|
||||
var older = session.SetSearchAsync("a");
|
||||
var newer = session.SetSearchAsync("ab");
|
||||
|
||||
library.Requests[0].Reply.SetException(new IOException("superseded failure"));
|
||||
Assert.False(await older);
|
||||
library.Requests[1].Reply.SetResult(Page("ab result"));
|
||||
Assert.True(await newer);
|
||||
|
||||
var failing = session.RefreshAsync();
|
||||
library.Requests[2].Reply.SetException(new ServerTooOldException());
|
||||
await Assert.ThrowsAsync<ServerTooOldException>(() => failing);
|
||||
|
||||
Assert.Null(session.Page);
|
||||
Assert.Equal("", session.Summary);
|
||||
Assert.False(session.IsLoading);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SupersededRequest_CompletingSynchronouslyOnCancellation_IsNotReportedAsTheLatestFailure()
|
||||
{
|
||||
var library = new ControlledLibrary { CompleteOnCancellation = true };
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest());
|
||||
var older = session.SetSearchAsync("a");
|
||||
var newer = session.SetSearchAsync("ab");
|
||||
|
||||
Assert.False(await older);
|
||||
library.Requests[1].Reply.SetResult(Page("ab result"));
|
||||
Assert.True(await newer);
|
||||
|
||||
Assert.Equal("ab result", Assert.Single(session.Page!.Items).Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsLoading_FollowsOnlyTheLatestRequest()
|
||||
{
|
||||
var library = new ControlledLibrary();
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest());
|
||||
|
||||
var first = session.SetSearchAsync("a");
|
||||
var second = session.SetSearchAsync("ab");
|
||||
library.Requests[0].Reply.SetResult(Page("a result"));
|
||||
Assert.False(await first);
|
||||
Assert.True(session.IsLoading);
|
||||
library.Requests[1].Reply.SetResult(Page("ab result"));
|
||||
Assert.True(await second);
|
||||
Assert.False(session.IsLoading);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Refresh_AfterTheLastPageWasEmptied_StepsBackToTheNewLastPage()
|
||||
{
|
||||
var library = new PagedLibrary(21);
|
||||
using var session = new DrawingLibraryBrowseSession(library, SyntheticNest(), pageSize: 10);
|
||||
await session.RefreshAsync();
|
||||
await session.NextPageAsync();
|
||||
await session.NextPageAsync();
|
||||
|
||||
library.RemoveLast();
|
||||
Assert.True(await session.RefreshAsync());
|
||||
|
||||
Assert.Equal(new[] { 20, 10 }, library.Queries.TakeLast(2).Select(q => q.Offset));
|
||||
Assert.Equal("Showing 11-20 of 20 drawings", session.Summary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Dispose_CancelsTheRequestAndDiscardsItsLateResponse()
|
||||
{
|
||||
var library = new ControlledLibrary();
|
||||
var session = new DrawingLibraryBrowseSession(library, SyntheticNest());
|
||||
var pending = session.SetSearchAsync("a");
|
||||
|
||||
session.Dispose();
|
||||
library.Requests[0].Reply.SetResult(Page("late"));
|
||||
|
||||
Assert.True(library.Requests[0].Token.IsCancellationRequested);
|
||||
Assert.False(await pending);
|
||||
Assert.Null(session.Page);
|
||||
await Assert.ThrowsAsync<ObjectDisposedException>(() => session.RefreshAsync());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(DrawingLibraryQuery.MaxLimit + 1)]
|
||||
public void Constructor_RejectsPageSizesTheServerWouldRefuse(int pageSize)
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new DrawingLibraryBrowseSession(new PagedLibrary(0), SyntheticNest(), pageSize));
|
||||
}
|
||||
|
||||
private static DrawingLibraryPage Page(string name) =>
|
||||
new() { Items = new[] { new DrawingLibraryRecord { Id = Guid.NewGuid(), Revision = 1, Name = name } }, Total = 1, Limit = 100 };
|
||||
|
||||
/// <summary>Serves pages of an in-memory set (ignoring filters) and records each query.</summary>
|
||||
private sealed class PagedLibrary : IDrawingLibrary
|
||||
{
|
||||
private readonly List<DrawingLibraryRecord> _records;
|
||||
|
||||
public PagedLibrary(int count) =>
|
||||
_records = Enumerable.Range(0, count)
|
||||
.Select(i => new DrawingLibraryRecord { Id = Guid.NewGuid(), Revision = 1, Name = $"Drawing {i}" })
|
||||
.ToList();
|
||||
|
||||
public List<DrawingLibraryQuery> Queries { get; } = new();
|
||||
|
||||
public void RemoveLast() => _records.RemoveAt(_records.Count - 1);
|
||||
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Assert.Null(query.GetValidationError());
|
||||
Queries.Add(query);
|
||||
var matches = query.NormalizedSearch.Length == 0 ? _records : new List<DrawingLibraryRecord>();
|
||||
return Task.FromResult(new DrawingLibraryPage
|
||||
{
|
||||
Items = matches.Skip(query.Offset).Take(query.Limit).ToArray(),
|
||||
Total = matches.Count,
|
||||
Offset = query.Offset,
|
||||
Limit = query.Limit,
|
||||
});
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leaves every query pending until the test completes it. With
|
||||
/// <see cref="CompleteOnCancellation"/>, cancellation completes the query synchronously
|
||||
/// inside the canceller's call, as some HTTP handlers do.
|
||||
/// </summary>
|
||||
private sealed class ControlledLibrary : IDrawingLibrary
|
||||
{
|
||||
public bool CompleteOnCancellation { get; init; }
|
||||
|
||||
public List<(DrawingLibraryQuery Query, TaskCompletionSource<DrawingLibraryPage> Reply, CancellationToken Token)> Requests { get; } = new();
|
||||
|
||||
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var reply = new TaskCompletionSource<DrawingLibraryPage>();
|
||||
if (CompleteOnCancellation)
|
||||
cancellationToken.Register(() => reply.TrySetCanceled(cancellationToken));
|
||||
Requests.Add((query, reply, cancellationToken));
|
||||
return reply.Task;
|
||||
}
|
||||
|
||||
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user