Skip to content

Commit

Permalink
Added AsyncEnumerable.Repeat API.
Browse files Browse the repository at this point in the history
  • Loading branch information
timcassell committed Dec 3, 2023
1 parent c1d663c commit 45b1c75
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 0 deletions.
1 change: 1 addition & 0 deletions Package/Core/Linq/Generators/Range.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ internal override Promise DisposeAsync(int id)
protected override void DisposeAndReturnToPool()
{
Dispose();
_disposed = true;
ObjectPool.MaybeRepool(this);
}

Expand Down
132 changes: 132 additions & 0 deletions Package/Core/Linq/Generators/Repeat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#if PROTO_PROMISE_DEBUG_ENABLE || (!PROTO_PROMISE_DEBUG_DISABLE && DEBUG)
#define PROMISE_DEBUG
#else
#undef PROMISE_DEBUG
#endif

#pragma warning disable IDE0090 // Use 'new(...)'

using System.Runtime.CompilerServices;
using System.Threading;

namespace Proto.Promises.Linq
{
#if CSHARP_7_3_OR_NEWER // We only expose AsyncEnumerable where custom async method builders are supported.
public static partial class AsyncEnumerable
{
/// <summary>
/// Generates an async-enumerable sequence that contains one repeated value.
/// </summary>
/// <typeparam name="T">The type of the value to be repeated in the result async-enumerable sequence.</typeparam>
/// <param name="element">The value to be repeated.</param>
/// <param name="count">The number of sequential integers to generate.</param>
/// <returns>An async-enumerable sequence that contains a range of sequential integral numbers.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is less than zero.</exception>
public static AsyncEnumerable<T> Repeat<T>(T element, int count)
=> AsyncEnumerable<T>.Repeat(element, count);
}

partial struct AsyncEnumerable<T>
{
/// <summary>
/// Generates an async-enumerable sequence that contains one repeated value.
/// </summary>
/// <param name="element">The value to be repeated.</param>
/// <param name="count">The number of sequential integers to generate.</param>
/// <returns>An async-enumerable sequence that contains a range of sequential integral numbers.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is less than zero.</exception>
public static AsyncEnumerable<T> Repeat(T element, int count)
{
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "count is less than zero", Internal.GetFormattedStacktrace(1));
}

return count == 0
? Empty()
#if PROMISE_DEBUG
// In DEBUG mode we use the Create function so its proper use will be verified.
: Create((element, count), async (cv, writer, cancelationToken) =>
{
unchecked
{
while (--cv.count > 0)
{
await writer.YieldAsync(cv.element);
}
}
});
#else
: new AsyncEnumerable<T>(Internal.AsyncEnumerableRepeat<T>.GetOrCreate(element, count));
#endif
}
}
#endif // CSHARP_7_3_OR_NEWER
}

namespace Proto.Promises
{
#if CSHARP_7_3_OR_NEWER && !PROMISE_DEBUG
partial class Internal
{
internal sealed class AsyncEnumerableRepeat<T> : PromiseRefBase.AsyncEnumerableBase<T>
{
private int _count;

private AsyncEnumerableRepeat() { }

[MethodImpl(InlineOption)]
private static AsyncEnumerableRepeat<T> GetOrCreate()
{
var obj = ObjectPool.TryTakeOrInvalid<AsyncEnumerableRepeat<T>>();
return obj == InvalidAwaitSentinel.s_instance
? new AsyncEnumerableRepeat<T>()
: obj.UnsafeAs<AsyncEnumerableRepeat<T>>();
}

[MethodImpl(InlineOption)]
internal static AsyncEnumerableRepeat<T> GetOrCreate(T element, int count)
{
var enumerable = GetOrCreate();
enumerable.Reset();
enumerable._count = count;
enumerable._current = element;
return enumerable;
}

internal override Promise<bool> MoveNextAsync(int id)
{
unchecked
{
return Promise.Resolved(--_count >= 0);
}
}

internal override Promise DisposeAsync(int id)
{
if (Interlocked.CompareExchange(ref _enumerableId, id + 1, id) == id)
{
// This was not already disposed.
#if PROMISE_DEBUG || PROTO_PROMISE_DEVELOPER_MODE
SetCompletionState(null, Promise.State.Resolved);
#endif
DisposeAndReturnToPool();
}
// IAsyncDisposable.DisposeAsync must not throw if it's called multiple times, according to MSDN documentation.
return Promise.Resolved();
}

protected override void DisposeAndReturnToPool()
{
Dispose();
_current = default;
_disposed = true;
ObjectPool.MaybeRepool(this);
}

protected override void Start(int enumerableId) { throw new System.InvalidOperationException(); }
internal override void MaybeDispose() { throw new System.InvalidOperationException(); }
}
}
#endif // CSHARP_7_3_OR_NEWER && !PROMISE_DEBUG
}
11 changes: 11 additions & 0 deletions Package/Core/Linq/Generators/Repeat.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions Package/Tests/CoreTests/APIs/Linq/AsyncEnumerableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,38 @@ public void AsyncEnumerableRange_Empty()

Assert.AreEqual(0, count);
}

[Test]
public void AsyncEnumerableRepeat_BadArgs()
{
Assert.Catch<System.ArgumentOutOfRangeException>(() => AsyncEnumerable.Repeat(0, -1));
}

[Test]
public void AsyncEnumerableRepeat_Empty()
{
int count = 0;
AsyncEnumerable.Repeat(2, 0)
.ForEachAsync(num => ++count)
.WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1));

Assert.AreEqual(0, count);
}

[Test]
public void AsyncEnumerableRepeat_Count()
{
int count = 0;
AsyncEnumerable.Repeat(2, 5)
.ForEachAsync(num =>
{
Assert.AreEqual(2, num);
++count;
})
.WaitWithTimeoutWhileExecutingForegroundContext(TimeSpan.FromSeconds(1));

Assert.AreEqual(5, count);
}
}
}

Expand Down

0 comments on commit 45b1c75

Please sign in to comment.