diff --git a/README.md b/README.md index 5022f76..0436a96 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,9 @@ dotnet add package ReactiveUI.Primitives ``` The library is split into a layered set of packages, so you can pull only the surface that matches your -integration point. Every package below ships at the same version and targets the same `net8.0` to `net11.0` plus -`net462` to `net481` matrix. The platform packages add their OS-specific TFMs. +integration point. Every package below is produced by a packable project in the current solution and ships at the same +version. Target frameworks vary by package; the exact matrices are documented under +[Target frameworks and dependencies](#target-frameworks-and-dependencies). | Package | NuGet | Use when | |-------------------------------------------------------|------------------------------|-------------------------------------------------------------------------------------------------------------------| @@ -124,7 +125,6 @@ integration point. Every package below ships at the same version and targets the | [ReactiveUI.Primitives.Async][Async] | [![AsyncB]][Async] | Native `IObservableAsync` / `IObserverAsync` signals. | | [ReactiveUI.Primitives.R3Bridge.Generator][R3Bridge] | [![R3BridgeB]][R3Bridge] | Optional analyzer package that generates R3 and R3Async bridge adapters. | | [ReactiveUI.Primitives.Async.Reactive][AsyncRx] | [![AsyncRxB]][AsyncRx] | Async Primitives compiled against System.Reactive `Unit` and `IScheduler`. | -| [ReactiveUI.Primitives.Extensions.Core][ExtCore] | [![ExtCoreB]][ExtCore] | The type-agnostic core for the migrated `ReactiveUI.Extensions` helpers. | | [ReactiveUI.Primitives.Wpf][Wpf] | [![WpfB]][Wpf] | WPF dispatcher sequencer integration. | | [ReactiveUI.Primitives.Wpf.Reactive][WpfRx] | [![WpfRxB]][WpfRx] | WPF dispatcher scheduler integration for System.Reactive-first projects. | | [ReactiveUI.Primitives.WinForms][WinForms] | [![WinFormsB]][WinForms] | Windows Forms control sequencer integration. | @@ -133,6 +133,8 @@ integration point. Every package below ships at the same version and targets the | [ReactiveUI.Primitives.WinUI.Reactive][WinUIRx] | [![WinUIRxB]][WinUIRx] | WinUI dispatcher-queue scheduler integration for System.Reactive-first projects. | | [ReactiveUI.Primitives.Blazor][Blazor] | [![BlazorB]][Blazor] | Blazor renderer sequencer integration. | | [ReactiveUI.Primitives.Blazor.Reactive][BlazorRx] | [![BlazorRxB]][BlazorRx] | Blazor renderer scheduler integration for System.Reactive-first projects. | +| [ReactiveUI.Primitives.Avalonia][Avalonia] | [![AvaloniaB]][Avalonia] | Avalonia UI-thread sequencer integration. | +| [ReactiveUI.Primitives.Avalonia.Reactive][AvaloniaRx] | [![AvaloniaRxB]][AvaloniaRx] | Avalonia UI-thread scheduler integration for System.Reactive-first projects. | | [ReactiveUI.Primitives.Maui][Maui] | [![MauiB]][Maui] | MAUI dispatcher sequencer integration. | | [ReactiveUI.Primitives.Maui.Reactive][MauiRx] | [![MauiRxB]][MauiRx] | MAUI dispatcher scheduler integration for System.Reactive-first projects. | @@ -168,10 +170,6 @@ integration point. Every package below ships at the same version and targets the [AsyncRxB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Async.Reactive.svg -[ExtCore]: https://www.nuget.org/packages/ReactiveUI.Primitives.Extensions.Core/ - -[ExtCoreB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Extensions.Core.svg - [Wpf]: https://www.nuget.org/packages/ReactiveUI.Primitives.Wpf/ [WpfB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Wpf.svg @@ -204,6 +202,14 @@ integration point. Every package below ships at the same version and targets the [BlazorRxB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Blazor.Reactive.svg +[Avalonia]: https://www.nuget.org/packages/ReactiveUI.Primitives.Avalonia/ + +[AvaloniaB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Avalonia.svg + +[AvaloniaRx]: https://www.nuget.org/packages/ReactiveUI.Primitives.Avalonia.Reactive/ + +[AvaloniaRxB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Avalonia.Reactive.svg + [Maui]: https://www.nuget.org/packages/ReactiveUI.Primitives.Maui/ [MauiB]: https://img.shields.io/nuget/v/ReactiveUI.Primitives.Maui.svg @@ -216,10 +222,11 @@ integration point. Every package below ships at the same version and targets the The base and async families use type-agnostic `.Core` projects, with a **lean** leaf binding the shared `RxVoid`/`ISequencer` source to lightweight implementations and a `.Reactive` leaf recompiling it against -System.Reactive's `Unit`/`IScheduler`. `ReactiveUI.Primitives.Extensions.Core` remains the shared implementation layer -for the migrated extension helpers, but those helpers now ship directly from `ReactiveUI.Primitives` and -`ReactiveUI.Primitives.Reactive`. The platform packages also come in lean and `.Reactive` leaves. (Arrows point from a -package to what it depends on.) +System.Reactive's `Unit`/`IScheduler`. Type-agnostic extension-helper sources are compiled into +`ReactiveUI.Primitives.Core`, while the lean and System.Reactive helper surfaces ship from `ReactiveUI.Primitives` and +`ReactiveUI.Primitives.Reactive`. The `src/ReactiveUI.Primitives.Extensions.Core` directory is source only; it is not a +project or NuGet package. The platform packages also come in lean and `.Reactive` leaves. (Arrows point from a package +to what it depends on.) ```mermaid graph TD @@ -231,23 +238,19 @@ graph TD AsyncCore["...Async.Core"] Async["...Async (lean)"] AsyncRx["...Async.Reactive"] - ExtCore["...Extensions.Core"] - Plat["Wpf / WinForms / WinUI
Blazor / Maui"] - PlatRx["Wpf.Reactive / WinForms.Reactive
WinUI.Reactive / Blazor.Reactive / Maui.Reactive"] + Plat["Wpf / WinForms / WinUI / Blazor
Avalonia / Maui"] + PlatRx["Wpf.Reactive / WinForms.Reactive / WinUI.Reactive
Blazor.Reactive / Avalonia.Reactive / Maui.Reactive"] Core --> Disp Prim --> Core Prim --> Disp - Prim --> ExtCore Rx --> Core - Rx --> ExtCore Rx --> SR AsyncCore --> Core Async --> Prim Async --> AsyncCore AsyncRx --> Rx AsyncRx --> AsyncCore - ExtCore --> Core Plat --> Prim PlatRx --> Rx ``` @@ -338,11 +341,11 @@ Most shared library packages use `$(LibraryTargetFrameworks)` from `src/Director Package TFM groups are: - `ReactiveUI.Disposables`, `ReactiveUI.Primitives.Core`, `ReactiveUI.Primitives.Async.Core`, - `ReactiveUI.Primitives.Async`, `ReactiveUI.Primitives.Async.Reactive`, and - `ReactiveUI.Primitives.Extensions.Core`: `$(LibraryTargetFrameworks)`. + `ReactiveUI.Primitives.Async`, and `ReactiveUI.Primitives.Async.Reactive`: `$(LibraryTargetFrameworks)`. +- `ReactiveUI.Primitives.R3Bridge.Generator`: `netstandard2.0`. - `ReactiveUI.Primitives`: `$(LibraryTargetFrameworks)` plus `net10.0-android`, `net11.0-android`, and Apple platform TFMs (`net10.0-ios`, `net11.0-ios`, `net10.0-tvos`, `net11.0-tvos`, `net10.0-macos`, `net11.0-macos`, - `net10.0-maccatalyst`, `net11.0-maccatalyst`) when the build OS supports restoring those workloads. + `net10.0-maccatalyst`, `net11.0-maccatalyst`) when building on Windows or macOS. - `ReactiveUI.Primitives.Reactive`: the same matrix as `ReactiveUI.Primitives`, compiled with System.Reactive `Unit` and `IScheduler` aliases. - `ReactiveUI.Primitives.Wpf` and `ReactiveUI.Primitives.Wpf.Reactive`: `net8.0-windows`, `net9.0-windows`, @@ -353,26 +356,29 @@ Package TFM groups are: `net9.0-windows10.0.19041.0`, `net10.0-windows10.0.19041.0`, `net11.0-windows10.0.19041.0`. - `ReactiveUI.Primitives.Blazor` and `ReactiveUI.Primitives.Blazor.Reactive`: `net8.0`, `net9.0`, `net10.0`, `net11.0`. -- `ReactiveUI.Primitives.Maui` and `ReactiveUI.Primitives.Maui.Reactive`: `net9.0`, `net10.0`, `net11.0`. +- `ReactiveUI.Primitives.Avalonia` and `ReactiveUI.Primitives.Avalonia.Reactive`: `net8.0`, `net9.0`, `net10.0`, + `net11.0`. +- `ReactiveUI.Primitives.Maui` and `ReactiveUI.Primitives.Maui.Reactive`: `net10.0`, `net11.0`. Runtime package dependencies are intentionally small. The default production packages do not depend on System.Reactive, R3, R3Async, or the optional R3 bridge generator. `ReactiveUI.Primitives` references `ReactiveUI.Disposables`, -`ReactiveUI.Primitives.Core`, and `ReactiveUI.Primitives.Extensions.Core`. `ReactiveUI.Disposables` references -`System.ValueTuple` only for `net462`. +and `ReactiveUI.Primitives.Core`. `ReactiveUI.Primitives.Core` contains the type-agnostic implementation used by the +extension-helper surfaces. `ReactiveUI.Disposables` references `System.ValueTuple` only for `net462`. The `.Reactive` leaf packages intentionally reference `System.Reactive` through `src/Directory.Build.props`. They recompile the shared Primitives source with `RxVoid` aliased to `System.Reactive.Unit`, `ISequencer` aliased to `System.Reactive.Concurrency.IScheduler`, and the shared source shifted into `.Reactive` namespaces. `ReactiveUI.Primitives`, `ReactiveUI.Primitives.Reactive`, `ReactiveUI.Primitives.Async.Core`, -`ReactiveUI.Primitives.Async`, `ReactiveUI.Primitives.Async.Reactive`, and `ReactiveUI.Primitives.Extensions.Core` add -.NET Framework compatibility/support packages where required, such as +`ReactiveUI.Primitives.Async`, and `ReactiveUI.Primitives.Async.Reactive` add .NET Framework compatibility/support +packages where required, such as `System.ValueTuple`, Microsoft.Bcl.TimeProvider, System.Threading.Channels, System.Runtime.CompilerServices.Unsafe, System.ComponentModel.Annotations, System.Buffers, System.Memory, and System.Collections.Immutable. Add the standalone `ReactiveUI.Primitives.R3Bridge.Generator` analyzer package to generate R3/R3Async bridge methods in consuming projects that already reference those external libraries. `ReactiveUI.Primitives.Blazor` and `ReactiveUI.Primitives.Blazor.Reactive` reference `Microsoft.AspNetCore.Components`. +`ReactiveUI.Primitives.Avalonia` and `ReactiveUI.Primitives.Avalonia.Reactive` reference `Avalonia`. `ReactiveUI.Primitives.Maui` and `ReactiveUI.Primitives.Maui.Reactive` reference `Microsoft.Maui.Core` and Microsoft.Extensions infrastructure packages. `ReactiveUI.Primitives.WinUI` and `ReactiveUI.Primitives.WinUI.Reactive` reference `Microsoft.WindowsAppSDK`. The remaining shared package references are analyzer, SourceLink, versioning, @@ -1049,7 +1055,7 @@ using var subscription = failed.Subscribe( A sequencer decides when and on which thread scheduled work runs. Rx calls this a scheduler. Sequencers live in `ReactiveUI.Primitives.Concurrency` and implement `ISequencer`. The core `ReactiveUI.Primitives` package does not -reference WPF, Windows Forms, WinUI, Blazor, or MAUI. The optional integration packages supply the UI-thread +reference WPF, Windows Forms, WinUI, Blazor, Avalonia, or MAUI. The optional integration packages supply the UI-thread sequencers. | Sequencer | Purpose | @@ -1063,13 +1069,23 @@ sequencers. | `ControlSequencer` | Schedule onto a Windows Forms control from `ReactiveUI.Primitives.WinForms`. | | `DispatcherQueueSequencer` | Schedule onto a WinUI dispatcher queue from `ReactiveUI.Primitives.WinUI`. | | `BlazorRendererSequencer` | Schedule component work through Blazor's renderer from `ReactiveUI.Primitives.Blazor`. | +| `AvaloniaScheduler` | Schedule onto an Avalonia dispatcher from `ReactiveUI.Primitives.Avalonia`. | | `MauiDispatcherSequencer` | Schedule onto an MAUI dispatcher from `ReactiveUI.Primitives.Maui`. | | `VirtualClock` | Virtual-time scheduling for deterministic tests. | WPF, Windows Forms, WinUI, Blazor, and MAUI sequencers derive from `DispatchSequencerBase`. That shared base batches ready work into a single posted dispatcher drain, preserves FIFO order, skips cancelled work lazily, and routes delayed UI work through the shared `ThreadPoolSequencer` timing queue before marshaling back to the UI thread. Platform packages -only provide the final dispatcher-specific post primitive. +only provide the final dispatcher-specific post primitive. `AvaloniaScheduler` provides the same coalesced dispatcher +drain behavior and uses dispatcher-bound timers for delayed work, so both posted and delayed callbacks stay associated +with the selected Avalonia dispatcher and priority. + +`AvaloniaScheduler.Instance` uses `Dispatcher.UIThread` at `DispatcherPriority.Background`. To bind scheduling to a +specific dispatcher or priority, construct `new AvaloniaScheduler(dispatcher)` or +`new AvaloniaScheduler(dispatcher, priority)`. The lean type is +`ReactiveUI.Primitives.Concurrency.AvaloniaScheduler`; the System.Reactive-compatible type is +`ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler` from +`ReactiveUI.Primitives.Avalonia.Reactive`. Scheduling APIs include absolute, relative, recursive, and action-based overloads: @@ -1263,6 +1279,7 @@ dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Wpf dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.WinForms dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.WinUI dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Blazor +dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Avalonia dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Maui ``` @@ -1313,6 +1330,7 @@ dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Wpf.Re dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.WinForms.Reactive dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.WinUI.Reactive dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Blazor.Reactive +dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Avalonia.Reactive dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Maui.Reactive ``` @@ -1433,6 +1451,7 @@ dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Maui.R | Windows Forms control scheduling | `ControlSequencer` from `ReactiveUI.Primitives.WinForms` | | WinUI dispatcher queue scheduling | `DispatcherQueueSequencer` from `ReactiveUI.Primitives.WinUI` | | Blazor renderer scheduling | `BlazorRendererSequencer` from `ReactiveUI.Primitives.Blazor` | +| Avalonia dispatcher scheduling | `AvaloniaScheduler` from `ReactiveUI.Primitives.Avalonia` | | MAUI dispatcher scheduling | `MauiDispatcherSequencer` from `ReactiveUI.Primitives.Maui` | | `TestScheduler` / virtual time | `VirtualClock` | @@ -1802,12 +1821,12 @@ duration is indistinguishable from empty method overhead; the benchmark run stil | `src/ReactiveUI.Primitives.slnx` | Current solution entrypoint. | | `src/ReactiveUI.Disposables` | Disposable primitives shared by the package family. | | `src/ReactiveUI.Primitives.Core` | Type-agnostic core shared by lean and System.Reactive-flavoured Primitives leaves. | -| `src/ReactiveUI.Primitives` | Default lean signal/operator/sequencer package, extension helpers, platform sequencers, and R3 analyzer. | +| `src/ReactiveUI.Primitives` | Default lean signal/operator/sequencer package, extension helpers, and platform sequencers. | | `src/ReactiveUI.Primitives.Reactive` | System.Reactive-flavoured Primitives leaf including the Reactive extension helpers. | | `src/ReactiveUI.Primitives.Async.Core` | Type-agnostic async core shared by async leaves. | | `src/ReactiveUI.Primitives.Async` | Lean async observable/signal package built on `IObservableAsync` and `IObserverAsync`. | | `src/ReactiveUI.Primitives.Async.Reactive` | System.Reactive-flavoured async Primitives leaf. | -| `src/ReactiveUI.Primitives.Extensions.Core` | Type-agnostic core for migrated ReactiveUI.Extensions helpers. | +| `src/ReactiveUI.Primitives.Extensions.Core` | Source-only extension-helper implementation linked into `ReactiveUI.Primitives.Core`; not a project or package. | | `src/ReactiveUI.Primitives.Wpf` | Optional WPF dispatcher integration library. | | `src/ReactiveUI.Primitives.Wpf.Reactive` | Optional WPF dispatcher scheduler integration library for System.Reactive consumers. | | `src/ReactiveUI.Primitives.WinForms` | Optional Windows Forms control integration library. | @@ -1816,6 +1835,8 @@ duration is indistinguishable from empty method overhead; the benchmark run stil | `src/ReactiveUI.Primitives.WinUI.Reactive` | Optional WinUI dispatcher queue scheduler integration library for System.Reactive consumers. | | `src/ReactiveUI.Primitives.Blazor` | Optional Blazor renderer integration library. | | `src/ReactiveUI.Primitives.Blazor.Reactive` | Optional Blazor renderer scheduler integration library for System.Reactive consumers. | +| `src/ReactiveUI.Primitives.Avalonia` | Optional Avalonia dispatcher sequencer integration library. | +| `src/ReactiveUI.Primitives.Avalonia.Reactive` | Optional Avalonia dispatcher scheduler integration library for System.Reactive consumers. | | `src/ReactiveUI.Primitives.Maui` | Optional MAUI dispatcher integration library. | | `src/ReactiveUI.Primitives.Maui.Reactive` | Optional MAUI dispatcher scheduler integration library for System.Reactive consumers. | | `src/ReactiveUI.Primitives.R3Bridge.Generator` | Standalone analyzer package for optional R3 and R3Async bridge generation. | diff --git a/Skill.md b/Skill.md index 179677b..60d05a0 100644 --- a/Skill.md +++ b/Skill.md @@ -1,6 +1,6 @@ --- name: reactiveui-primitives -description: Use when working with ReactiveUI.Primitives NuGet packages in .NET projects, including ReactiveUI.Disposables, ReactiveUI.Primitives.Core, ReactiveUI.Primitives, ReactiveUI.Primitives.Reactive, ReactiveUI.Primitives.Async.Core, ReactiveUI.Primitives.Async, ReactiveUI.Primitives.Async.Reactive, ReactiveUI.Primitives.Extensions.Core, ReactiveUI.Primitives.Wpf, ReactiveUI.Primitives.Wpf.Reactive, ReactiveUI.Primitives.WinForms, ReactiveUI.Primitives.WinForms.Reactive, ReactiveUI.Primitives.WinUI, ReactiveUI.Primitives.WinUI.Reactive, ReactiveUI.Primitives.Blazor, ReactiveUI.Primitives.Blazor.Reactive, ReactiveUI.Primitives.Maui, or ReactiveUI.Primitives.Maui.Reactive; choosing Core vs lean vs System.Reactive package variants; using IObservable, IObservableAsync, signals, extension helpers, sequencers, disposable helpers, UI dispatch adapters, R3/R3Async generated bridges, or migration guidance from System.Reactive/R3/R3Async repositories to Primitives or .Reactive package variants. +description: Use when working with ReactiveUI.Primitives NuGet packages in .NET projects, including ReactiveUI.Disposables, ReactiveUI.Primitives.Core, ReactiveUI.Primitives, ReactiveUI.Primitives.Reactive, ReactiveUI.Primitives.Async.Core, ReactiveUI.Primitives.Async, ReactiveUI.Primitives.Async.Reactive, ReactiveUI.Primitives.R3Bridge.Generator, ReactiveUI.Primitives.Wpf, ReactiveUI.Primitives.Wpf.Reactive, ReactiveUI.Primitives.WinForms, ReactiveUI.Primitives.WinForms.Reactive, ReactiveUI.Primitives.WinUI, ReactiveUI.Primitives.WinUI.Reactive, ReactiveUI.Primitives.Blazor, ReactiveUI.Primitives.Blazor.Reactive, ReactiveUI.Primitives.Avalonia, ReactiveUI.Primitives.Avalonia.Reactive, ReactiveUI.Primitives.Maui, or ReactiveUI.Primitives.Maui.Reactive; choosing Core vs lean vs System.Reactive package variants; using IObservable, IObservableAsync, signals, extension helpers, sequencers, disposable helpers, UI dispatch adapters, R3/R3Async generated bridges, or migration guidance from System.Reactive/R3/R3Async repositories to Primitives or .Reactive package variants. --- # ReactiveUI.Primitives @@ -16,13 +16,13 @@ composition work that needs the shared type layer without the full leaf surface. | Package | Use when | Key APIs and notes | |---------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `ReactiveUI.Disposables` | The project only needs disposable lifetime helpers. | `ReactiveUI.Primitives.Disposables`; `Scope`, `MultipleDisposable`, `DisposableBag`, `Pocket`, `Slot`, `AssignmentSlot`, `SingleDisposable`, `SingleReplaceableDisposable`, `MutableDisposable`, `SwapDisposable`, `OnceDisposable`, `BooleanDisposable`, `CancellationDisposable`, `EmptyDisposable`. | -| `ReactiveUI.Primitives.Core` | Building a low-level library that needs the shared Primitives type layer without the full leaf package. | Root namespace remains `ReactiveUI.Primitives`. Includes core signal/state contracts and shared types such as `Result`, `Optional`, `ISignal`, `Signal`, `BehaviorSignal`, `StateSignal`, `ReadOnlyState`, `ConnectableSignal`, `CommandSignal`, concurrency contracts, and advanced witnesses. Depends on `ReactiveUI.Disposables`. | -| `ReactiveUI.Primitives` | Most BCL `IObservable` usage, including migrated `ReactiveUI.Extensions` helpers. | Lean package using Primitives `RxVoid` and `ISequencer`. Includes core signal factories/operators, sequencers, virtual time, extension helpers under `ReactiveUI.Primitives.Extensions`, and R3 bridge analyzer packaging. Depends on `ReactiveUI.Primitives.Extensions.Core`; no separate Extensions leaf package is required. | -| `ReactiveUI.Primitives.Reactive` | The project is System.Reactive-first and wants Primitives operators and extension helpers compiled with `System.Reactive.Unit` and `IScheduler`. | Uses `.Reactive` namespaces plus `ReactiveUI.Primitives.Extensions.Reactive`. Adds `System.Reactive` and depends on `ReactiveUI.Primitives.Extensions.Core`. This is a package variant, not a source-generator bridge; no separate Extensions Reactive leaf package is required. | +| `ReactiveUI.Primitives.Core` | Building a low-level library that needs the shared Primitives type layer without the full leaf package. | Root namespace remains `ReactiveUI.Primitives`. Includes core signal/state contracts, concurrency contracts, advanced witnesses, and type-agnostic extension-helper support under `ReactiveUI.Primitives.Extensions`. Depends on `ReactiveUI.Disposables`. | +| `ReactiveUI.Primitives` | Most BCL `IObservable` usage, including migrated `ReactiveUI.Extensions` helpers. | Lean package using Primitives `RxVoid` and `ISequencer`. Includes signal factories/operators, sequencers, virtual time, and extension helpers under `ReactiveUI.Primitives.Extensions`. Depends on `ReactiveUI.Primitives.Core` and `ReactiveUI.Disposables`; no separate Extensions package is required. | +| `ReactiveUI.Primitives.Reactive` | The project is System.Reactive-first and wants Primitives operators and extension helpers compiled with `System.Reactive.Unit` and `IScheduler`. | Uses `.Reactive` namespaces plus `ReactiveUI.Primitives.Extensions.Reactive`. Adds `System.Reactive` and depends on `ReactiveUI.Primitives.Core`. This is a package variant, not a source-generator bridge; no separate Extensions Reactive package is required. | | `ReactiveUI.Primitives.Async.Core` | Building a low-level async library around Primitives async contracts. | `ReactiveUI.Primitives.Async`; `IObservableAsync`, `IObserverAsync`, `SignalAsync`, `SignalAsync`, async signal factories, async operators, async disposables, helpers, and async signal implementations. Depends on `ReactiveUI.Primitives.Core`. | | `ReactiveUI.Primitives.Async` | The app needs async-native observable pipelines where observer calls can await or observe cancellation. | Lean async leaf using Primitives `RxVoid` and `ISequencer`. Adds `AsyncContext`, `ContextSwitchSignalAsync`, `SignalAsyncReactiveExtensions`, `Yield`, `WitnessOn`, `ObserveOnSafe`, and `ObserveOnIf`. Depends on `ReactiveUI.Primitives` and `ReactiveUI.Primitives.Async.Core`. | | `ReactiveUI.Primitives.Async.Reactive` | Async-native pipelines in a System.Reactive-first project. | System.Reactive-flavoured async leaf with `System.Reactive.Unit` and `IScheduler` conventions. Depends on `ReactiveUI.Primitives.Reactive` and `ReactiveUI.Primitives.Async.Core`. | -| `ReactiveUI.Primitives.Extensions.Core` | Building a library that needs shared extension implementations without choosing lean or System.Reactive scheduler/unit conventions. | Root namespace remains `ReactiveUI.Primitives.Extensions`. Includes type-agnostic extension support such as `CurrentValueSubject`, `Continuation`, `ConcurrencyLimiter`, `Heartbeat`, `Stale`, `IHeartbeat`, `IStale`, and shared operator implementation types. | +| `ReactiveUI.Primitives.R3Bridge.Generator` | The project needs generated adapters at R3 or R3Async boundaries. | Standalone `netstandard2.0` analyzer package. It emits bridges only when the consuming compilation already references the required R3/R3Async symbols; it is not embedded in or required by the runtime packages. | | `ReactiveUI.Primitives.Wpf` | WPF UI code needs dispatcher marshalling. | `ReactiveUI.Primitives.Concurrency.DispatcherSequencer`. Depends on `ReactiveUI.Primitives`. | | `ReactiveUI.Primitives.Wpf.Reactive` | WPF UI code is System.Reactive-first and needs dispatcher scheduling. | `ReactiveUI.Primitives.Reactive.Concurrency.DispatcherSequencer` implements System.Reactive scheduling conventions. Depends on `ReactiveUI.Primitives.Reactive`. | | `ReactiveUI.Primitives.WinForms` | Windows Forms UI code needs control-thread marshalling. | `ReactiveUI.Primitives.Concurrency.ControlSequencer`. Depends on `ReactiveUI.Primitives`. | @@ -31,6 +31,8 @@ composition work that needs the shared type layer without the full leaf surface. | `ReactiveUI.Primitives.WinUI.Reactive` | WinUI code is System.Reactive-first and needs `DispatcherQueue` scheduling. | `ReactiveUI.Primitives.Reactive.Concurrency.DispatcherQueueSequencer` and `DispatcherQueueSequencerExtensions.ToSequencer()`. Depends on `ReactiveUI.Primitives.Reactive` and `Microsoft.WindowsAppSDK`. | | `ReactiveUI.Primitives.Blazor` | Blazor components need render-thread sequencing and component-bound subscriptions. | `ReactiveUI.Primitives.Blazor.Components.ReactiveComponentBase`, `Observe`, `Track`, `InvalidateAsync`, `ReactiveUI.Primitives.Blazor.Concurrency.BlazorRendererSequencer`. Depends on `ReactiveUI.Primitives` and `Microsoft.AspNetCore.Components`. | | `ReactiveUI.Primitives.Blazor.Reactive` | Blazor components are System.Reactive-first and need render-thread scheduling. | `ReactiveUI.Primitives.Blazor.Reactive.Components.ReactiveComponentBase` and `ReactiveUI.Primitives.Blazor.Reactive.Concurrency.BlazorRendererSequencer`. Depends on `ReactiveUI.Primitives.Reactive` and `Microsoft.AspNetCore.Components`. | +| `ReactiveUI.Primitives.Avalonia` | Avalonia UI code needs dispatcher marshalling. | `ReactiveUI.Primitives.Concurrency.AvaloniaScheduler`; `Instance` uses `Dispatcher.UIThread` at background priority, or construct with a `Dispatcher` and optional `DispatcherPriority`. Depends on `ReactiveUI.Primitives` and `Avalonia`. | +| `ReactiveUI.Primitives.Avalonia.Reactive` | Avalonia UI code is System.Reactive-first and needs dispatcher scheduling. | `ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler`; `Instance` uses `Dispatcher.UIThread` at background priority, or construct with a `Dispatcher` and optional `DispatcherPriority`. Depends on `ReactiveUI.Primitives.Reactive` and `Avalonia`. | | `ReactiveUI.Primitives.Maui` | .NET MAUI code needs dispatcher marshalling. | `ReactiveUI.Primitives.Concurrency.MauiDispatcherSequencer` and `MauiDispatcherSequencerExtensions.ToSequencer()`. Depends on `ReactiveUI.Primitives` and `Microsoft.Maui.Core`. | | `ReactiveUI.Primitives.Maui.Reactive` | .NET MAUI code is System.Reactive-first and needs dispatcher scheduling. | `ReactiveUI.Primitives.Reactive.Concurrency.MauiDispatcherSequencer` and `MauiDispatcherSequencerExtensions.ToSequencer()`. Depends on `ReactiveUI.Primitives.Reactive` and `Microsoft.Maui.Core`. | @@ -40,6 +42,7 @@ Install examples: dotnet add package ReactiveUI.Primitives dotnet add package ReactiveUI.Primitives.Async dotnet add package ReactiveUI.Primitives.Wpf +dotnet add package ReactiveUI.Primitives.Avalonia ``` Use `.Core` packages deliberately: @@ -47,7 +50,6 @@ Use `.Core` packages deliberately: ```bash dotnet add package ReactiveUI.Primitives.Core dotnet add package ReactiveUI.Primitives.Async.Core -dotnet add package ReactiveUI.Primitives.Extensions.Core ``` Use `.Reactive` packages when the project already uses System.Reactive idioms: @@ -56,6 +58,7 @@ Use `.Reactive` packages when the project already uses System.Reactive idioms: dotnet add package ReactiveUI.Primitives.Reactive dotnet add package ReactiveUI.Primitives.Async.Reactive dotnet add package ReactiveUI.Primitives.Wpf.Reactive +dotnet add package ReactiveUI.Primitives.Avalonia.Reactive ``` Add `ReactiveUI.Primitives.R3Bridge.Generator` only when the project needs generated R3 or R3Async bridge methods. It is @@ -73,6 +76,8 @@ dotnet add package ReactiveUI.Primitives.R3Bridge.Generator `ReactiveUI.Primitives.Extensions` namespace is retained. - Prefer UI packages only in the matching UI framework; use the lean UI package for `ISequencer` and the `.Reactive` UI package for `IScheduler`. +- For Avalonia UI dispatch, choose `ReactiveUI.Primitives.Avalonia`; choose + `ReactiveUI.Primitives.Avalonia.Reactive` only when the project is System.Reactive-first. - Prefer `.Reactive` variants when public APIs should expose `System.Reactive.Unit`, `IScheduler`, `.Reactive` namespaces, or existing Rx source should compile with minimal code changes. - Prefer `.Core` variants only when composing packages or minimizing a library dependency layer. Most apps should @@ -121,8 +126,10 @@ using ReactiveUI.Primitives.Reactive.Signals; using ReactiveUI.Primitives.Extensions.Reactive; ``` -Reactive UI packages mostly expose sequencers under `ReactiveUI.Primitives.Reactive.Concurrency`. Blazor also exposes -component helpers under `ReactiveUI.Primitives.Blazor.Reactive.Components`. +Reactive UI packages expose lean sequencers under `ReactiveUI.Primitives.Concurrency` and System.Reactive schedulers +under `ReactiveUI.Primitives.Reactive.Concurrency`. Blazor also exposes component helpers under +`ReactiveUI.Primitives.Blazor.Reactive.Components`. Avalonia exposes `AvaloniaScheduler` in both concurrency namespaces; +its ready work is coalesced into one dispatcher drain and delayed work uses dispatcher-bound timers. R3 generated bridges: @@ -184,6 +191,7 @@ dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Wpf dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.WinForms dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.WinUI dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Blazor +dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Avalonia dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Maui ``` @@ -237,6 +245,7 @@ dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Wpf.Re dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.WinForms.Reactive dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.WinUI.Reactive dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Blazor.Reactive +dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Avalonia.Reactive dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Maui.Reactive ``` @@ -286,13 +295,21 @@ Use bridge methods only at boundaries. Keep internal pipelines in one model afte ## Framework And Platform Notes -- General libraries target `net8.0`, `net9.0`, `net10.0`, `net11.0`, `net462`, `net472`, `net48`, and `net481`. -- `ReactiveUI.Primitives` and `ReactiveUI.Primitives.Reactive` also build Android TFMs and Apple TFMs for platform - sequencers in the base package family. -- WPF and WinForms packages, including `.Reactive` variants, target Windows TFMs plus .NET Framework. -- WinUI packages, including `.Reactive` variants, target `net*-windows10.0.19041.0`. -- MAUI packages, including `.Reactive` variants, target `net9.0`, `net10.0`, and `net11.0`. -- Blazor packages, including `.Reactive` variants, target the modern .NET TFMs. +- `ReactiveUI.Disposables`, `ReactiveUI.Primitives.Core`, `ReactiveUI.Primitives.Async.Core`, + `ReactiveUI.Primitives.Async`, and `ReactiveUI.Primitives.Async.Reactive` target `net8.0`, `net9.0`, `net10.0`, + `net11.0`, `net462`, `net472`, `net48`, and `net481`. +- `ReactiveUI.Primitives.R3Bridge.Generator` targets `netstandard2.0`. +- `ReactiveUI.Primitives` and `ReactiveUI.Primitives.Reactive` target the same eight general-library TFMs plus + `net10.0-android` and `net11.0-android`; on Windows or macOS they also target net10/net11 iOS, tvOS, macOS, and Mac + Catalyst for platform sequencers. +- WPF and WinForms packages, including `.Reactive` variants, target `net8.0-windows` through `net11.0-windows` plus + `net462`, `net472`, `net48`, and `net481`. +- WinUI packages, including `.Reactive` variants, target `net8.0-windows10.0.19041.0` through + `net11.0-windows10.0.19041.0`. +- Avalonia packages, including `.Reactive` variants, target `net8.0`, `net9.0`, `net10.0`, and `net11.0` and reference + `Avalonia`. +- MAUI packages, including `.Reactive` variants, target `net10.0` and `net11.0`. +- Blazor packages, including `.Reactive` variants, target `net8.0`, `net9.0`, `net10.0`, and `net11.0`. ## Repository Maintenance diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c9e2cb0..43962af 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -45,6 +45,7 @@ $(NetTargetFrameworks);$(NetFrameworkTargetFrameworks) net8.0-windows;net9.0-windows;net10.0-windows;net11.0-windows $(WindowsNetTargetFrameworks);$(NetFrameworkTargetFrameworks) + $(NetTargetFrameworks) $(NetTargetFrameworks) net10.0;net11.0 diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index df0c0d4..3aeb329 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,6 +6,7 @@ 3.38.1 + 12.1.0 @@ -31,6 +32,8 @@ + + diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/Concurrency/AvaloniaScheduler.cs b/src/ReactiveUI.Primitives.Avalonia.Reactive/Concurrency/AvaloniaScheduler.cs new file mode 100644 index 0000000..71fc700 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/Concurrency/AvaloniaScheduler.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace ReactiveUI.Primitives.Reactive.Concurrency; + +/// Avalonia UI-thread scheduler that coalesces scheduled work onto a dispatcher drain. +/// +public sealed class AvaloniaScheduler : CoalescingDispatchScheduler +{ + /// Gets the shared scheduler for . + public static readonly AvaloniaScheduler Instance = + new(Dispatcher.UIThread, DispatcherPriority.Background); + + /// Initializes a new instance of the class. + /// The dispatcher used to marshal work to the UI thread. + /// is . + public AvaloniaScheduler(Dispatcher dispatcher) + : this(dispatcher, DispatcherPriority.Background) + { + } + + /// Initializes a new instance of the class. + /// The dispatcher used to marshal work to the UI thread. + /// Dispatcher priority used for posted drains and delayed work. + /// is . + public AvaloniaScheduler(Dispatcher dispatcher, DispatcherPriority priority) + { + Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + Priority = priority; + } + + /// Gets the dispatcher used to marshal work to the UI thread. + public Dispatcher Dispatcher { get; } + + /// Gets the dispatcher priority used for posted drains and delayed work. + public DispatcherPriority Priority { get; } + + /// + protected override bool Post(Action drain) + { + Dispatcher.Post(drain, Priority); + return true; + } + + /// + protected override IDisposable ScheduleOnDispatcher(Action work, TimeSpan dueTime) + { + DispatcherTimer timer = new(Priority, Dispatcher) { Interval = dueTime }; + timer.Tick += (_, _) => + { + timer.Stop(); + work(); + }; + timer.Start(); + return Disposable.Create(timer, static value => value.Stop()); + } +} diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net10.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..f8b5a16 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -0,0 +1,14 @@ +#nullable enable +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void +abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.Func! action) -> System.IDisposable! +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.TimeSpan dueTime, System.Func! action) -> System.IDisposable! +static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler! +virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable! diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net10.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net11.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net11.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..f8b5a16 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net11.0/PublicAPI.Shipped.txt @@ -0,0 +1,14 @@ +#nullable enable +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void +abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.Func! action) -> System.IDisposable! +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.TimeSpan dueTime, System.Func! action) -> System.IDisposable! +static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler! +virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable! diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net11.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net11.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net11.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..f8b5a16 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1,14 @@ +#nullable enable +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void +abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.Func! action) -> System.IDisposable! +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.TimeSpan dueTime, System.Func! action) -> System.IDisposable! +static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler! +virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable! diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net9.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..f8b5a16 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net9.0/PublicAPI.Shipped.txt @@ -0,0 +1,14 @@ +#nullable enable +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.CoalescingDispatchScheduler() -> void +ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.RequestDrain() -> void +abstract ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Post(System.Action! drain) -> bool +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.Func! action) -> System.IDisposable! +override ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.Schedule(TState state, System.TimeSpan dueTime, System.Func! action) -> System.IDisposable! +static readonly ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler! +virtual ReactiveUI.Primitives.Reactive.Concurrency.CoalescingDispatchScheduler.ScheduleOnDispatcher(System.Action! work, System.TimeSpan dueTime) -> System.IDisposable! diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net9.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia.Reactive/ReactiveUI.Primitives.Avalonia.Reactive.csproj b/src/ReactiveUI.Primitives.Avalonia.Reactive/ReactiveUI.Primitives.Avalonia.Reactive.csproj new file mode 100644 index 0000000..17215d5 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia.Reactive/ReactiveUI.Primitives.Avalonia.Reactive.csproj @@ -0,0 +1,24 @@ + + + + + $(AvaloniaTargetFrameworks) + Avalonia UI-thread scheduler for ReactiveUI.Primitives, recompiled against System.Reactive's IScheduler. + system.reactive;rx;reactive;primitives;avalonia;dispatcher;scheduler + + + + + + + + + + + + + + + + diff --git a/src/ReactiveUI.Primitives.Avalonia/Concurrency/AvaloniaScheduler.cs b/src/ReactiveUI.Primitives.Avalonia/Concurrency/AvaloniaScheduler.cs new file mode 100644 index 0000000..57e1d73 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/Concurrency/AvaloniaScheduler.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia.Threading; +using ReactiveUI.Primitives.Advanced; + +namespace ReactiveUI.Primitives.Concurrency; + +/// Avalonia UI-thread scheduler that coalesces scheduled work onto a dispatcher drain. +/// +[System.Diagnostics.DebuggerDisplay("{DebuggerDisplay,nq}")] +public sealed class AvaloniaScheduler : ISequencer +{ + /// Gets the shared scheduler for . + public static readonly AvaloniaScheduler Instance = + new(Dispatcher.UIThread, DispatcherPriority.Background); + + /// Coalescing dispatch engine. + private DispatchSequencerState _state; + + /// Initializes a new instance of the class. + /// The dispatcher used to marshal work to the UI thread. + /// is . + public AvaloniaScheduler(Dispatcher dispatcher) + : this(dispatcher, DispatcherPriority.Background) + { + } + + /// Initializes a new instance of the class. + /// The dispatcher used to marshal work to the UI thread. + /// Dispatcher priority used for posted drains and delayed work. + /// is . + public AvaloniaScheduler(Dispatcher dispatcher, DispatcherPriority priority) + { + Dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + Priority = priority; + _state = new(this, Post, RunDrain, ScheduleDelayed); + } + + /// Gets the dispatcher used to marshal work to the UI thread. + public Dispatcher Dispatcher { get; } + + /// Gets the dispatcher priority used for posted drains and delayed work. + public DispatcherPriority Priority { get; } + + /// + public DateTimeOffset Now => DispatchSequencerState.Now; + + /// + public long Timestamp => DispatchSequencerState.Timestamp; + + /// Gets the debugger display text. + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private string DebuggerDisplay => ToString() ?? string.Empty; + + /// + public void Schedule(IWorkItem item) => _state.Schedule(item); + + /// + public void Schedule(IWorkItem item, long dueTimestamp) => _state.Schedule(item, dueTimestamp); + + /// Marshals the cached drain callback onto the dispatcher. + /// The drain callback. + /// , since the dispatcher accepts the work. + private bool Post(Action drain) + { + Dispatcher.Post(drain, Priority); + return true; + } + + /// Runs delayed work on a dispatcher timer bound to the selected dispatcher. + /// Work item to execute at the due time. + /// Absolute monotonic timestamp at which to execute the item. + private void ScheduleDelayed(IWorkItem item, long dueTimestamp) + { + DispatcherTimer timer = + new(Priority, Dispatcher) { Interval = DispatchSequencerState.DelayUntil(dueTimestamp) }; + timer.Tick += (_, _) => + { + timer.Stop(); + DispatchSequencerState.RunIfActive(item); + }; + timer.Start(); + } + + /// Forwards the cached drain callback to the engine. + private void RunDrain() => _state.RunDrain(); +} diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net10.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..240c74a --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -0,0 +1,11 @@ +#nullable enable +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Now.get -> System.DateTimeOffset +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item, long dueTimestamp) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Timestamp.get -> long +static readonly ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Concurrency.AvaloniaScheduler! diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net10.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net11.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net11.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..240c74a --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net11.0/PublicAPI.Shipped.txt @@ -0,0 +1,11 @@ +#nullable enable +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Now.get -> System.DateTimeOffset +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item, long dueTimestamp) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Timestamp.get -> long +static readonly ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Concurrency.AvaloniaScheduler! diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net11.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net11.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net11.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..240c74a --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1,11 @@ +#nullable enable +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Now.get -> System.DateTimeOffset +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item, long dueTimestamp) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Timestamp.get -> long +static readonly ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Concurrency.AvaloniaScheduler! diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net9.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000..240c74a --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net9.0/PublicAPI.Shipped.txt @@ -0,0 +1,11 @@ +#nullable enable +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.AvaloniaScheduler(Avalonia.Threading.Dispatcher! dispatcher, Avalonia.Threading.DispatcherPriority priority) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Dispatcher.get -> Avalonia.Threading.Dispatcher! +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Now.get -> System.DateTimeOffset +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Priority.get -> Avalonia.Threading.DispatcherPriority +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Schedule(ReactiveUI.Primitives.Concurrency.IWorkItem! item, long dueTimestamp) -> void +ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Timestamp.get -> long +static readonly ReactiveUI.Primitives.Concurrency.AvaloniaScheduler.Instance -> ReactiveUI.Primitives.Concurrency.AvaloniaScheduler! diff --git a/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net9.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/ReactiveUI.Primitives.Avalonia/ReactiveUI.Primitives.Avalonia.csproj b/src/ReactiveUI.Primitives.Avalonia/ReactiveUI.Primitives.Avalonia.csproj new file mode 100644 index 0000000..5b57621 --- /dev/null +++ b/src/ReactiveUI.Primitives.Avalonia/ReactiveUI.Primitives.Avalonia.csproj @@ -0,0 +1,14 @@ + + + + $(AvaloniaTargetFrameworks) + Avalonia UI-thread integration sequencers for ReactiveUI.Primitives. + system.reactive;rx;reactive;primitives;avalonia;dispatcher;scheduler;sequencer + + + + + + + + diff --git a/src/ReactiveUI.Primitives.slnx b/src/ReactiveUI.Primitives.slnx index 8e672ab..11519d4 100644 --- a/src/ReactiveUI.Primitives.slnx +++ b/src/ReactiveUI.Primitives.slnx @@ -20,6 +20,7 @@ + @@ -29,6 +30,7 @@ + @@ -41,6 +43,7 @@ + @@ -51,6 +54,7 @@ + diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AssemblyHooks.cs new file mode 100644 index 0000000..529c626 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AssemblyHooks.cs @@ -0,0 +1,5 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +[assembly: NotInParallel] diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AvaloniaSchedulerTests.cs b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AvaloniaSchedulerTests.cs new file mode 100644 index 0000000..686a3ba --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AvaloniaSchedulerTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive.Concurrency; +using Avalonia.Threading; +using ReactiveUI.Primitives.Reactive.Concurrency; + +namespace ReactiveUI.Primitives.Avalonia.Reactive.Tests; + +/// Tests for against a pumped Avalonia headless dispatcher. +public sealed class AvaloniaSchedulerTests +{ + /// Maximum time to wait for dispatcher work. + private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(5); + + /// Delay used to exercise the native dispatcher-timer path. + private static readonly TimeSpan DelayedDueTime = TimeSpan.FromMilliseconds(50); + + /// Delay used for work that is cancelled before its timer fires. + private static readonly TimeSpan CancellationDueTime = TimeSpan.FromMilliseconds(100); + + /// Time allowed to prove cancelled dispatcher work remains inactive. + private static readonly TimeSpan CancellationWaitTime = TimeSpan.FromMilliseconds(150); + + /// Verifies constructor validation. + /// A task representing the asynchronous operation. + [Test] + public async Task ConstructorRejectsNullDispatcher() => + await Assert.That(static () => new AvaloniaScheduler(null!)).ThrowsExactly(); + + /// Verifies the singleton uses Avalonia's UI dispatcher and legacy background priority. + /// A task representing the asynchronous operation. + [Test] + public async Task InstanceUsesUiDispatcherAndBackgroundPriority() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + await Assert.That(AvaloniaScheduler.Instance.Dispatcher).IsSameReferenceAs(Dispatcher.UIThread); + await Assert.That(AvaloniaScheduler.Instance.Priority).IsEqualTo(DispatcherPriority.Background); + await Assert.That(AvaloniaScheduler.Instance).IsSameReferenceAs(AvaloniaScheduler.Instance); + }, + CancellationToken.None); + + /// Verifies immediate scheduler work is posted to and executed on the selected dispatcher thread. + /// A task representing the asynchronous operation. + [Test] + public async Task ImmediateScheduleExecutesOnDispatcherThread() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + var dispatcherThreadId = Environment.CurrentManagedThreadId; + AvaloniaScheduler scheduler = new(Dispatcher.UIThread); + TaskCompletionSource completion = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + _ = scheduler.Schedule( + () => completion.TrySetResult(Environment.CurrentManagedThreadId)); + + var executionThreadId = await completion.Task.WaitAsync(WaitTimeout); + await Assert.That(executionThreadId).IsEqualTo(dispatcherThreadId); + }, + CancellationToken.None); + + /// Verifies delayed scheduler work runs on a timer bound to the selected dispatcher. + /// A task representing the asynchronous operation. + [Test] + public async Task DelayedScheduleExecutesOnDispatcherThread() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + var dispatcherThreadId = Environment.CurrentManagedThreadId; + AvaloniaScheduler scheduler = new(Dispatcher.UIThread, DispatcherPriority.Normal); + TaskCompletionSource completion = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + _ = scheduler.Schedule( + DelayedDueTime, + () => completion.TrySetResult(Environment.CurrentManagedThreadId)); + + var executionThreadId = await completion.Task.WaitAsync(WaitTimeout); + await Assert.That(executionThreadId).IsEqualTo(dispatcherThreadId); + await Assert.That(scheduler.Priority).IsEqualTo(DispatcherPriority.Normal); + }, + CancellationToken.None); + + /// Verifies disposing delayed work stops its dispatcher timer before execution. + /// A task representing the asynchronous operation. + [Test] + public async Task DelayedScheduleCanBeCancelled() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + AvaloniaScheduler scheduler = new(Dispatcher.UIThread); + var executed = false; + + var disposable = scheduler.Schedule( + CancellationDueTime, + () => executed = true); + disposable.Dispose(); + + await Task.Delay(CancellationWaitTime); + await Assert.That(executed).IsFalse(); + }, + CancellationToken.None); +} diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AvaloniaTestSession.cs b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AvaloniaTestSession.cs new file mode 100644 index 0000000..87e318b --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/AvaloniaTestSession.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia; +using Avalonia.Headless; + +namespace ReactiveUI.Primitives.Avalonia.Reactive.Tests; + +/// Provides one process-wide Avalonia headless session with a continuously pumped UI thread. +internal static class AvaloniaTestSession +{ + /// Process-wide headless session. + private static readonly Lazy Session = + new( + static () => HeadlessUnitTestSession.StartNew(typeof(Application)), + LazyThreadSafetyMode.ExecutionAndPublication); + + /// Gets the shared headless test session. + internal static HeadlessUnitTestSession Instance => Session.Value; +} diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests.csproj b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests.csproj new file mode 100644 index 0000000..1f1ac57 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests/ReactiveUI.Primitives.Avalonia.Reactive.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(AvaloniaTargetFrameworks) + false + Exe + + + + + + + + + diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AssemblyHooks.cs b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AssemblyHooks.cs new file mode 100644 index 0000000..529c626 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AssemblyHooks.cs @@ -0,0 +1,5 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +[assembly: NotInParallel] diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AvaloniaSchedulerTests.cs b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AvaloniaSchedulerTests.cs new file mode 100644 index 0000000..0e1eee2 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AvaloniaSchedulerTests.cs @@ -0,0 +1,105 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Diagnostics; +using Avalonia.Threading; +using ReactiveUI.Primitives.Concurrency; + +namespace ReactiveUI.Primitives.Avalonia.Tests; + +/// Tests for against a pumped Avalonia headless dispatcher. +public sealed class AvaloniaSchedulerTests +{ + /// Maximum time to wait for dispatcher work. + private static readonly TimeSpan WaitTimeout = TimeSpan.FromSeconds(5); + + /// Future scheduling delay in stopwatch ticks. + private static readonly long ScheduleDelayTicks = Stopwatch.Frequency / 20; + + /// Verifies constructor validation. + /// A task representing the asynchronous operation. + [Test] + public async Task ConstructorRejectsNullDispatcher() => + await Assert.That(static () => new AvaloniaScheduler(null!)).ThrowsExactly(); + + /// Verifies the singleton uses Avalonia's UI dispatcher and legacy background priority. + /// A task representing the asynchronous operation. + [Test] + public async Task InstanceUsesUiDispatcherAndBackgroundPriority() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + await Assert.That(AvaloniaScheduler.Instance.Dispatcher).IsSameReferenceAs(Dispatcher.UIThread); + await Assert.That(AvaloniaScheduler.Instance.Priority).IsEqualTo(DispatcherPriority.Background); + await Assert.That(AvaloniaScheduler.Instance).IsSameReferenceAs(AvaloniaScheduler.Instance); + await Assert.That(AvaloniaScheduler.Instance.Now).IsGreaterThan(DateTimeOffset.MinValue); + }, + CancellationToken.None); + + /// Verifies immediate work is posted to and executed on the selected dispatcher thread. + /// A task representing the asynchronous operation. + [Test] + public async Task ImmediateScheduleExecutesOnDispatcherThread() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + var dispatcherThreadId = Environment.CurrentManagedThreadId; + AvaloniaScheduler scheduler = new(Dispatcher.UIThread); + TaskCompletionSource completion = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + scheduler.Schedule( + new DelegateWorkItem( + () => completion.TrySetResult(Environment.CurrentManagedThreadId))); + + var executionThreadId = await completion.Task.WaitAsync(WaitTimeout); + await Assert.That(executionThreadId).IsEqualTo(dispatcherThreadId); + }, + CancellationToken.None); + + /// Verifies future work runs through a timer bound to the selected dispatcher. + /// A task representing the asynchronous operation. + [Test] + public async Task DelayedScheduleExecutesOnDispatcherThread() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + var dispatcherThreadId = Environment.CurrentManagedThreadId; + AvaloniaScheduler scheduler = new(Dispatcher.UIThread, DispatcherPriority.Normal); + TaskCompletionSource completion = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + scheduler.Schedule( + new DelegateWorkItem( + () => completion.TrySetResult(Environment.CurrentManagedThreadId)), + scheduler.Timestamp + ScheduleDelayTicks); + + var executionThreadId = await completion.Task.WaitAsync(WaitTimeout); + await Assert.That(executionThreadId).IsEqualTo(dispatcherThreadId); + await Assert.That(scheduler.Priority).IsEqualTo(DispatcherPriority.Normal); + }, + CancellationToken.None); + + /// Verifies the sequencer validates both work-item overloads. + /// A task representing the asynchronous operation. + [Test] + public async Task ScheduleRejectsNullWorkItems() => + await AvaloniaTestSession.Instance.Dispatch( + static async () => + { + AvaloniaScheduler scheduler = new(Dispatcher.UIThread); + + await Assert.That(() => scheduler.Schedule(null!)).ThrowsExactly(); + await Assert.That(() => scheduler.Schedule(null!, scheduler.Timestamp)).ThrowsExactly(); + }, + CancellationToken.None); + + /// Work item backed by an action. + /// Action to invoke. + private sealed class DelegateWorkItem(Action action) : IWorkItem + { + /// + public void Execute() => action(); + } +} diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AvaloniaTestSession.cs b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AvaloniaTestSession.cs new file mode 100644 index 0000000..8600646 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/AvaloniaTestSession.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia; +using Avalonia.Headless; + +namespace ReactiveUI.Primitives.Avalonia.Tests; + +/// Provides one process-wide Avalonia headless session with a continuously pumped UI thread. +internal static class AvaloniaTestSession +{ + /// Process-wide headless session. + private static readonly Lazy Session = + new( + static () => HeadlessUnitTestSession.StartNew(typeof(Application)), + LazyThreadSafetyMode.ExecutionAndPublication); + + /// Gets the shared headless test session. + internal static HeadlessUnitTestSession Instance => Session.Value; +} diff --git a/src/tests/ReactiveUI.Primitives.Avalonia.Tests/ReactiveUI.Primitives.Avalonia.Tests.csproj b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/ReactiveUI.Primitives.Avalonia.Tests.csproj new file mode 100644 index 0000000..e980cd3 --- /dev/null +++ b/src/tests/ReactiveUI.Primitives.Avalonia.Tests/ReactiveUI.Primitives.Avalonia.Tests.csproj @@ -0,0 +1,14 @@ + + + + $(AvaloniaTargetFrameworks) + false + Exe + + + + + + + +