Skip to content

Commit 3249128

Browse files
[Repo Assist] Add AsyncSeq.withCancellation (design parity with TaskSeq, #277) (#278)
* Add AsyncSeq.withCancellation for design parity with TaskSeq (#277) Adds AsyncSeq.withCancellation, which returns a new AsyncSeq<'T> that passes the given CancellationToken to GetAsyncEnumerator, overriding whatever token would otherwise be supplied during iteration. This mirrors TaskSeq.withCancellation and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a CancellationToken through GetAsyncEnumerator. Closes #277 (partial - withCancellation addressed; further design parity investigation tracked in the issue). Co-authored-by: Copilot <[email protected]> * ci: trigger checks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]>
1 parent 7c774fe commit 3249128

5 files changed

Lines changed: 77 additions & 1 deletion

File tree

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 4.10.0
2+
3+
* Added `AsyncSeq.withCancellation` — returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277).
4+
15
### 4.9.0
26

37
* Performance: `filterAsync` — replaced `asyncSeq`-builder implementation with a direct optimised enumerator, reducing allocation and generator overhead.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2522,6 +2522,13 @@ module AsyncSeq =
25222522
(emptyAsync fillChannelTask)
25232523
}
25242524

2525+
/// Returns a new AsyncSeq that passes the given CancellationToken to GetAsyncEnumerator,
2526+
/// overriding whatever token would otherwise be used. Useful when consuming sequences from
2527+
/// libraries (such as Entity Framework) that accept a CancellationToken through GetAsyncEnumerator.
2528+
let withCancellation (cancellationToken: CancellationToken) (source: AsyncSeq<'T>) : AsyncSeq<'T> =
2529+
{ new IAsyncEnumerable<'T> with
2530+
member _.GetAsyncEnumerator(_ct) = source.GetAsyncEnumerator(cancellationToken) }
2531+
25252532
#endif
25262533

25272534

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,14 @@ module AsyncSeq =
824824
/// Transforms an async seq to a new one that fetches values ahead of time to improve throughput.
825825
val prefetch<'T> : numberToPrefetch: int -> source: AsyncSeq<'T> -> AsyncSeq<'T>
826826

827+
/// <summary>
828+
/// Returns a new <c>AsyncSeq</c> that passes the given <c>CancellationToken</c> to
829+
/// <c>GetAsyncEnumerator</c>, overriding whatever token would otherwise be used when iterating.
830+
/// This is useful when consuming sequences from libraries such as Entity Framework that
831+
/// accept a <c>CancellationToken</c> through <c>GetAsyncEnumerator</c>.
832+
/// </summary>
833+
val withCancellation<'T> : cancellationToken: System.Threading.CancellationToken -> source: AsyncSeq<'T> -> AsyncSeq<'T>
834+
827835
#endif
828836

829837

tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3662,3 +3662,60 @@ let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` ()
36623662
|> AsyncSeq.toArrayAsync
36633663
|> Async.RunSynchronously |> ignore)
36643664
|> ignore
3665+
3666+
// ===== withCancellation =====
3667+
3668+
[<Test>]
3669+
let ``AsyncSeq.withCancellation passes token to enumerator`` () =
3670+
use cts = new System.Threading.CancellationTokenSource()
3671+
let receivedToken = ref System.Threading.CancellationToken.None
3672+
let source =
3673+
{ new System.Collections.Generic.IAsyncEnumerable<int> with
3674+
member _.GetAsyncEnumerator(ct) =
3675+
receivedToken.Value <- ct
3676+
(AsyncSeq.ofSeq [1; 2; 3]).GetAsyncEnumerator(ct) }
3677+
source
3678+
|> AsyncSeq.withCancellation cts.Token
3679+
|> AsyncSeq.toArrayAsync
3680+
|> Async.RunSynchronously
3681+
|> ignore
3682+
Assert.AreEqual(cts.Token, receivedToken.Value)
3683+
3684+
[<Test>]
3685+
let ``AsyncSeq.withCancellation overrides incoming token`` () =
3686+
use cts1 = new System.Threading.CancellationTokenSource()
3687+
use cts2 = new System.Threading.CancellationTokenSource()
3688+
let receivedToken = ref System.Threading.CancellationToken.None
3689+
let source : System.Collections.Generic.IAsyncEnumerable<int> =
3690+
{ new System.Collections.Generic.IAsyncEnumerable<int> with
3691+
member _.GetAsyncEnumerator(ct) =
3692+
receivedToken.Value <- ct
3693+
(AsyncSeq.ofSeq [1; 2; 3]).GetAsyncEnumerator(ct) }
3694+
let wrapped = source |> AsyncSeq.withCancellation cts1.Token
3695+
// Enumerate with cts2's token - withCancellation should still pass cts1's token
3696+
let e = wrapped.GetAsyncEnumerator(cts2.Token)
3697+
e.MoveNextAsync().AsTask() |> Async.AwaitTask |> Async.RunSynchronously |> ignore
3698+
e.DisposeAsync() |> ignore
3699+
Assert.AreEqual(cts1.Token, receivedToken.Value)
3700+
3701+
[<Test>]
3702+
let ``AsyncSeq.withCancellation preserves sequence values`` () =
3703+
use cts = new System.Threading.CancellationTokenSource()
3704+
let result =
3705+
AsyncSeq.ofSeq [1; 2; 3; 4; 5]
3706+
|> AsyncSeq.withCancellation cts.Token
3707+
|> AsyncSeq.toArrayAsync
3708+
|> Async.RunSynchronously
3709+
Assert.AreEqual([| 1; 2; 3; 4; 5 |], result)
3710+
3711+
[<Test>]
3712+
let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledException`` () =
3713+
use cts = new System.Threading.CancellationTokenSource()
3714+
cts.Cancel()
3715+
Assert.Catch<System.OperationCanceledException>(fun () ->
3716+
AsyncSeq.ofSeq [1; 2; 3]
3717+
|> AsyncSeq.withCancellation cts.Token
3718+
|> AsyncSeq.toArrayAsync
3719+
|> Async.RunSynchronously
3720+
|> ignore)
3721+
|> ignore

version.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>4.8.0</Version>
3+
<Version>4.10.0</Version>
44
</PropertyGroup>
55
</Project>

0 commit comments

Comments
 (0)