Skip to content
Merged
  •  
  •  
  •  
9 changes: 4 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,11 @@ dotnet test "tests/ReactiveUI.Primitives.Async.Tests/ReactiveUI.Primitives.Async
--treenode-filter "/*/*/*/Async"
```

### API Approval Notes
### Public API Checks

- API approval baselines live under `src/tests/**/ApiApprovalTests.*.verified.txt`
- New TFMs usually require corresponding new `DotNet11_0.verified.txt` files
- If approval tests fail with `.received.txt` output, inspect the generated snapshot and promote it intentionally if the
API change is expected
- `PublicApiSharp.Analyzers` checks each package's `src/<Project>/PublicAPI/<tfm>/PublicAPI.txt` baseline.
- New target frameworks require a corresponding baseline directory.
- For an intentional API change, review the affected signatures and update the corresponding framework baselines.

---

Expand Down
1 change: 0 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="$(AvaloniaVersion)"/>
<PackageVersion Include="Avalonia.Headless" Version="$(AvaloniaVersion)"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="8.0.31" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0'))"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.20" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0')) AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="10.0.12" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0')) AND !$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net11.0'))"/>
Expand Down
6 changes: 1 addition & 5 deletions src/Polyfills/ArgumentExceptionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@

namespace ReactiveUI.Primitives.Internal;

/// <summary>
/// Polyfill for <c>ArgumentNullException.ThrowIfNull</c> on target frameworks (net462-net481) that predate it.
/// On net8.0 and later this type is not compiled; consuming projects alias the <c>ArgumentExceptionHelper</c>
/// identifier directly to <see cref="ArgumentNullException"/> so the call sites bind to the BCL method.
/// </summary>
/// <summary>Polyfill for <c>ArgumentNullException.ThrowIfNull</c> on target frameworks that predate it.</summary>
[ExcludeFromCodeCoverage]
internal static class ArgumentExceptionHelper
{
Expand Down
4 changes: 1 addition & 3 deletions src/Polyfills/ArgumentOutOfRangeExceptionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ internal static void ThrowIfLessThan(
"Design",
"SST2318:Members should not have identical bodies",
Justification =
"A type-specialized polyfill overload. The bounds-check body is identical to the int overload only "
+ "because the guard shape is the same; the two operate on different value types (int vs TimeSpan) and "
+ "cannot forward to one another. This mirrors the BCL's per-type ThrowIfLessThan overloads.")]
"The int and TimeSpan overloads guard different value types and cannot forward to one another.")]
internal static void ThrowIfLessThan(
TimeSpan value,
TimeSpan other,
Expand Down
6 changes: 1 addition & 5 deletions src/Polyfills/ObjectDisposedExceptionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@

namespace ReactiveUI.Primitives.Internal;

/// <summary>
/// Polyfill for <c>ObjectDisposedException.ThrowIf</c> on target frameworks (net462-net481) that predate it.
/// On net8.0 and later this type is not compiled; consuming projects alias the <c>ObjectDisposedExceptionHelper</c>
/// identifier directly to <see cref="ObjectDisposedException"/> so the call sites bind to the BCL method.
/// </summary>
/// <summary>Polyfill for <c>ObjectDisposedException.ThrowIf</c> on target frameworks that predate it.</summary>
[ExcludeFromCodeCoverage]
internal static class ObjectDisposedExceptionHelper
{
Expand Down
6 changes: 2 additions & 4 deletions src/Polyfills/TaskCompletionSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal sealed class TaskCompletionSource

/// <summary>Initializes a new instance of the <see cref="TaskCompletionSource"/> class.</summary>
[SuppressMessage("Concurrency", "PSH1302", Justification =
"BCL-parity polyfill; must match the framework ctor's TaskCreationOptions.None default, not force async continuations.")]
"Matches the framework constructor's TaskCreationOptions.None default.")]
public TaskCompletionSource() => _inner = new();

/// <summary>Transitions the underlying task to the <see cref="TaskStatus.RanToCompletion"/> state.</summary>
Expand Down Expand Up @@ -47,9 +47,7 @@ internal sealed class TaskCompletionSource
"Design",
"SST2318:Members should not have identical bodies",
Justification =
"Distinct BCL-parity surface members with different contracts and return types: SetCanceled is void and "
+ "TrySetCanceled returns bool. They share the same backing call here but must track the framework surface "
+ "independently, so they are deliberately kept as separate members rather than one forwarding to the other.")]
"SetCanceled returns void and TrySetCanceled returns bool; each tracks its own framework contract.")]
internal bool TrySetCanceled() => _inner.TrySetCanceled();

/// <summary>Attempts to transition the underlying task to the <see cref="TaskStatus.Canceled"/> state for the specified token.</summary>
Expand Down
9 changes: 8 additions & 1 deletion src/Polyfills/TaskPolyfillExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private static async Task WaitForCompletionAsync(Task task, TimeSpan timeout, Ca
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
if (timeout != Timeout.InfiniteTimeSpan)
{
linked.CancelAfter(timeout);
ScheduleTimeout(linked, timeout);
}

using (linked.Token.Register(static state => ((TaskCompletionSource<bool>)state).TrySetResult(true), signal))
Expand All @@ -71,5 +71,12 @@ private static async Task WaitForCompletionAsync(Task task, TimeSpan timeout, Ca
}
}
}

/// <summary>Schedules cancellation when the timeout elapses.</summary>
/// <param name="source">The cancellation source.</param>
/// <param name="timeout">The timeout duration.</param>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private static void ScheduleTimeout(CancellationTokenSource source, TimeSpan timeout) => source.CancelAfter(timeout);
}
#endif
20 changes: 16 additions & 4 deletions src/Primitives.Async.Shared/Advanced/StartSubscription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,21 @@ protected override async ValueTask ExecuteAsyncCore(
return;
}

await Task.Factory.StartNew(
await ExecuteOnSchedulerAsync(observer, taskScheduler, cancellationToken).ConfigureAwait(false);
}

/// <summary>Starts the action through the supplied scheduler.</summary>
/// <param name="observer">The observer receiving the signal notification.</param>
/// <param name="taskScheduler">The scheduler that starts the action.</param>
/// <param name="cancellationToken">Cancellation for the scheduled task and notifications.</param>
/// <returns>The scheduled action and notification operation.</returns>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
private Task ExecuteOnSchedulerAsync(
IObserverAsync<RxVoid> observer,
TaskScheduler taskScheduler,
CancellationToken cancellationToken) =>
Task.Factory.StartNew(
static s =>
{
var (self, observer, cancellationToken) =
Expand All @@ -57,9 +71,7 @@ await Task.Factory.StartNew(
cancellationToken,
TaskCreationOptions.DenyChildAttach,
taskScheduler)
.Unwrap()
.ConfigureAwait(false);
}
.Unwrap();

/// <summary>Runs the action and forwards the completion signal.</summary>
/// <param name="observer">The observer receiving the signal notification.</param>
Expand Down
Loading
Loading