/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.Client.Subscriptions.Streaming { /// /// Extensions on useful for /// short-lived subscriptions tracking state-machine transitions /// or bounded observation windows. /// /// /// These helpers exist alongside (but do not replace) the /// equivalents in System.Linq.Async because their semantics /// are tuned for OPC UA subscribe-and-wait scenarios and differ /// from the standard LINQ operators: /// /// is inclusive of /// the matching item (yields it last), unlike /// System.Linq.Async.TakeWhile which stops just before /// the matching item. /// completes the /// enumeration silently on timeout instead of /// throwing, so callers can use it as a bounded observation /// window without needing a try/catch around the await foreach. /// yields fixed-size /// chunks; the trailing partial chunk (if the source ends with /// fewer items) is yielded too. /// /// Adding a dependency on System.Linq.Async just to swap /// these out would force a single-implementation choice; keeping /// these here lets the stack own the semantics callers depend on. /// public static class StreamingSubscriptionExtensions { /// /// Yields items until the predicate returns true. The /// matching item is yielded last, then enumeration stops. /// Useful for waiting on a specific state transition. /// /// The type of items in the source sequence. public static IAsyncEnumerable TakeUntilAsync( this IAsyncEnumerable source, Func predicate, CancellationToken ct = default) { if (source == null) { throw new ArgumentNullException(nameof(source)); } if (predicate == null) { throw new ArgumentNullException(nameof(predicate)); } return TakeUntilImpl(source, predicate, ct); } private static async IAsyncEnumerable TakeUntilImpl( IAsyncEnumerable source, Func predicate, [EnumeratorCancellation] CancellationToken ct) { await foreach (T item in source.ConfigureAwait(false)) { ct.ThrowIfCancellationRequested(); yield return item; if (predicate(item)) { yield break; } } } /// /// Yields items until the timeout elapses. The enumeration /// completes silently when the timeout expires. /// /// The type of items in the source sequence. /// The source sequence. /// The timeout duration. /// /// Optional used for the timeout /// scheduler; defaults to . /// /// Cancellation token. public static IAsyncEnumerable WithTimeoutAsync( this IAsyncEnumerable source, TimeSpan timeout, TimeProvider? timeProvider = null, CancellationToken ct = default) { if (source == null) { throw new ArgumentNullException(nameof(source)); } return WithTimeoutImpl(source, timeout, timeProvider ?? TimeProvider.System, ct); } private static async IAsyncEnumerable WithTimeoutImpl( IAsyncEnumerable source, TimeSpan timeout, TimeProvider timeProvider, [EnumeratorCancellation] CancellationToken ct) { using CancellationTokenSource timeoutCts = timeProvider .CreateCancellationTokenSource(timeout); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); IAsyncEnumerator enumerator = source.GetAsyncEnumerator(cts.Token); try { while (true) { bool hasNext; try { hasNext = await enumerator.MoveNextAsync().ConfigureAwait(false); } catch (OperationCanceledException) when (cts.IsCancellationRequested && !ct.IsCancellationRequested) { yield break; } if (!hasNext) { yield break; } yield return enumerator.Current; } } finally { await enumerator.DisposeAsync().ConfigureAwait(false); } } /// /// Yields exactly items then completes. /// /// The type of items in the source sequence. public static IAsyncEnumerable TakeAsync( this IAsyncEnumerable source, int count, CancellationToken ct = default) { if (source == null) { throw new ArgumentNullException(nameof(source)); } if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); } return TakeImpl(source, count, ct); } private static async IAsyncEnumerable TakeImpl( IAsyncEnumerable source, int count, [EnumeratorCancellation] CancellationToken ct) { int taken = 0; await foreach (T item in source.ConfigureAwait(false)) { ct.ThrowIfCancellationRequested(); yield return item; if (++taken >= count) { yield break; } } } /// /// Buffers the first items into a /// list and returns it. /// /// The type of items in the source sequence. public static async ValueTask> BufferedAsync( this IAsyncEnumerable source, int count, CancellationToken ct = default) { if (source == null) { throw new ArgumentNullException(nameof(source)); } if (count <= 0) { throw new ArgumentOutOfRangeException(nameof(count)); } var buffer = new List(count); await foreach (T item in source.ConfigureAwait(false)) { ct.ThrowIfCancellationRequested(); buffer.Add(item); if (buffer.Count >= count) { break; } } return buffer; } } }