namespace Greencore.platform.validators; public enum DataState { Idle, Pending, Ready, Failed, Canceled } public sealed record DataSnapshot(long Version, DataState State, T? Value = default, Exception? Error = null); /// Resultado propio de una operación; la publicación visual puede seguir encolada. public sealed record DataOperationResult(DataSnapshot Snapshot, bool IsCurrentAtCompletion) { /// Estado de la operación sin navegar por la instantánea. public DataState State => Snapshot.State; /// Valor calculado cuando el estado es Ready. public T? Value => Snapshot.Value; /// Error técnico cuando el estado es Failed. public Exception? Error => Snapshot.Error; } /// Publica solamente la versión vigente; el dispatcher permite aplicar estados en el hilo de UI. public sealed class AsyncDataSession : IDisposable { private readonly object _gate = new(); private readonly object _notifications = new(); private readonly Action _dispatch; private CancellationTokenSource? _active; private long _version; private bool _disposed; private DataSnapshot _snapshot = new(0, DataState.Idle); public DataSnapshot Snapshot { get { lock (_gate) return _snapshot; } } public event Action>? Changed; public AsyncDataSession(Action? dispatch = null) { var context = SynchronizationContext.Current; _dispatch = dispatch ?? (context is null ? action => action() : action => context.Post(_ => action(), null)); } public async ValueTask RunAsync(Func> load, CancellationToken cancellationToken = default) => _ = await RunWithResultAsync(load, cancellationToken).ConfigureAwait(false); public ValueTask> RunWithResultAsync(Func> load, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(load); return RunRequestAsync(Begin(cancellationToken), load); } // Registrar una petición no ejecuta callbacks del consumidor bajo bloqueos ajenos. internal Request Begin(CancellationToken cancellationToken) { lock (_gate) { ObjectDisposedException.ThrowIf(_disposed, this); var previous = _active; var source = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _active = source; return new(source, previous, ++_version); } } internal async ValueTask> RunRequestAsync(Request request, Func> load) { var (source, previous, version) = request; try { Cancel(previous); Publish(new(version, DataState.Pending)); DataSnapshot completed; try { source.Token.ThrowIfCancellationRequested(); var value = await load(source.Token).ConfigureAwait(false); source.Token.ThrowIfCancellationRequested(); completed = new(version, DataState.Ready, value); } catch (OperationCanceledException) when (source.IsCancellationRequested) { completed = new(version, DataState.Canceled); } catch (Exception exception) { completed = new(version, DataState.Failed, Error: exception); } Publish(completed); lock (_gate) return new(completed, !_disposed && version == _version); } finally { lock (_gate) { if (ReferenceEquals(_active, source)) _active = null; source.Dispose(); } } } public void Dispose() { CancellationTokenSource? active; lock (_gate) { if (_disposed) return; _disposed = true; ++_version; active = _active; Changed = null; } Cancel(active); } private void Publish(DataSnapshot snapshot) => _dispatch(() => { lock (_notifications) { Action>? changed; lock (_gate) { if (_disposed || snapshot.Version != _version) return; // Un dispatcher personalizado puede entregar callbacks en un orden distinto. if (_snapshot.Version == snapshot.Version && _snapshot.State != DataState.Pending && snapshot.State == DataState.Pending) return; _snapshot = snapshot; changed = Changed; } // Serializar notificaciones sin ejecutar código externo bajo el bloqueo de peticiones. changed?.Invoke(snapshot); } }); private static void Cancel(CancellationTokenSource? source) { try { source?.Cancel(); } catch (ObjectDisposedException) { /* La consulta anterior terminó antes de cancelar. */ } } internal sealed record Request(CancellationTokenSource Source, CancellationTokenSource? Previous, long Version); }