v3.0.0 - Source Generators & Ref-Counted Signals
Source Generator (new)
Declaring signals by hand gets repetitive. Mark a partial class with [GenerateSignals], declare partial auto properties, and the generator backs each one with a Signal<T>:
[GenerateSignals]
public partial class Person
{
public partial string Name { get; set; }
public partial int Age { get; set; }
}Name and Age read and write like ordinary properties but are signals underneath, so assigning to them notifies every dependent. Each property also gets a {Name}Signal member exposing the underlying IReadOnlySignal<T>.
Derived properties come from methods named Compute<PropertyName>:
[GenerateSignals]
[GenerateNotifyPropertyChanged]
public partial class Person
{
public partial string Name { get; set; }
public partial int Age { get; set; }
[Computed]
string ComputeFullName() => $"{Name} {Age}";
[AsyncComputed(ConcurrentChangeStrategy = ConcurrentChangeStrategy.CancelCurrent)]
async ValueTask<bool> ComputeIsAdult(CancellationToken token)
{
var age = Age;
await Task.Delay(100, token);
return age >= 18;
}
}[AsyncComputed] additionally generates an Is{PropertyName}Computing signal.
Also included:
[GenerateNotifyPropertyChanged]: optionalINotifyPropertyChangedimplementation for XAML binding.ModelChanged: a generatedIReadOnlySignal<T>that re-emits the model whenever any writable signal property changes.[Signal]/[SignalIgnore]: back a single property without annotating the class, or exclude one.partial void OnInitialized(): runs after the generated signals are ready.- Records keep their value semantics:
PrintMembers,Equals,GetHashCode, and the copy constructor use only the data properties, so signals stay out ofToString()and equality, andwithproduces an independent copy. - Diagnostics (
SIG001toSIG013) for non-partial types, positional records, bad[Computed]signatures, name conflicts, and other misuse.
Ref-Counted Subscriptions (new, opt-in)
SubscriptionStrategy.RefCount keeps a signal subscribed only while something is observing it, and unsubscribes when the last observer goes away. This propagates: when a ref-counted computed goes idle it releases its dependencies, so a whole derived graph can wind down behind a closed view.
var signal = Signal.Computed(() => a.Value + b.Value,
config => config with { SubscriptionStrategy = SubscriptionStrategy.RefCount });Or globally:
ReadonlySignalConfiguration.Default = ReadonlySignalConfiguration.Default with
{
SubscriptionStrategy = SubscriptionStrategy.RefCount
};Persistent remains the default, so existing behaviour is unchanged unless you opt in.
With
RefCount, a signal is inert until something observes it, and while idle its value is whatever it last saw. Subscribe toValues(not onlyFutureValues) to activate it and read the current value. In XAML and Blazor this is automatic: a binding or aTrackedScopeis itself an observer.
Other Additions
Signal.WaitForChangeAsync: await the next change to any signal read by a function.ToSignal/FromObservableoverloads taking an initial value.
Performance
Computed round trips got faster and allocate substantially less than v2.3.1 in common cases.