@@ -106,7 +106,7 @@ public ValueTask FullDuplexAsync<T>(
106
106
IAsyncEnumerable < T > source ,
107
107
Func < T , CallContext , ValueTask > consumer )
108
108
{
109
- using var allDone = CancellationTokenSource . CreateLinkedTokenSource ( CancellationToken , default ) ;
109
+ var allDone = CancellationTokenSource . CreateLinkedTokenSource ( CancellationToken , default ) ;
110
110
try
111
111
{
112
112
var context = new CallContext ( this , allDone . Token ) ;
@@ -119,37 +119,60 @@ public ValueTask FullDuplexAsync<T>(
119
119
} , context . CancellationToken ) ;
120
120
var produced = producer ( context ) ;
121
121
if ( produced . IsCompletedSuccessfully ) return new ValueTask ( consumed ) ;
122
- return BothAsync ( produced , consumed ) ;
122
+ return BothAsync ( produced , consumed , SwapOut ( ref allDone ) ) ;
123
123
}
124
124
finally
125
125
{
126
126
// stop the producer, in any exit scenario
127
- allDone . Cancel ( ) ;
127
+ CancelAndDisposeTaskSource ( allDone ) ;
128
128
}
129
129
}
130
130
131
- private static async ValueTask BothAsync ( ValueTask produced , Task consumed )
131
+ static T ? SwapOut < T > ( ref T ? value ) where T : class
132
132
{
133
- try
133
+ var tmp = value ;
134
+ value = default ;
135
+ return tmp ;
136
+ }
137
+
138
+ static void CancelAndDisposeTaskSource ( CancellationTokenSource ? cts )
139
+ {
140
+ if ( cts is object )
134
141
{
135
- await produced . ConfigureAwait ( false ) ;
142
+ cts . Cancel ( ) ;
143
+ cts . Dispose ( ) ;
136
144
}
137
- catch ( Exception producerEx )
145
+ }
146
+
147
+ private static async ValueTask BothAsync ( ValueTask produced , Task consumed , CancellationTokenSource ? allDone )
148
+ {
149
+ try
138
150
{
139
151
try
140
152
{
141
- await consumed . ConfigureAwait ( false ) ; // make sure we try and await both
153
+ await produced . ConfigureAwait ( false ) ;
142
154
}
143
- catch ( Exception consumerEx )
155
+ catch ( Exception producerEx )
144
156
{
145
- // so they *both* failed; talk about embarrassing!
146
- throw new AggregateException ( producerEx , consumerEx ) ;
157
+ try
158
+ {
159
+ await consumed . ConfigureAwait ( false ) ; // make sure we try and await both
160
+ }
161
+ catch ( Exception consumerEx )
162
+ {
163
+ // so they *both* failed; talk about embarrassing!
164
+ throw new AggregateException ( producerEx , consumerEx ) ;
165
+ }
166
+ throw ; // re-throw the exception from the producer
147
167
}
148
- throw ; // re-throw the exception from the producer
168
+ // producer completed cleanly; we can just await the
169
+ // consumer - if it throws, it throws
170
+ await consumed . ConfigureAwait ( false ) ;
171
+ }
172
+ finally
173
+ {
174
+ CancelAndDisposeTaskSource ( allDone ) ;
149
175
}
150
- // producer completed cleanly; we can just await the
151
- // consumer - if it throws, it throws
152
- await consumed . ConfigureAwait ( false ) ;
153
176
}
154
177
155
178
/// <summary>
@@ -160,19 +183,19 @@ public ValueTask FullDuplexAsync<T>(
160
183
IAsyncEnumerable < T > source ,
161
184
Func < IAsyncEnumerable < T > , CallContext , ValueTask > consumer )
162
185
{
163
- using var allDone = CancellationTokenSource . CreateLinkedTokenSource ( CancellationToken , default ) ;
186
+ var allDone = CancellationTokenSource . CreateLinkedTokenSource ( CancellationToken , default ) ;
164
187
try
165
188
{
166
189
var context = new CallContext ( this , allDone . Token ) ;
167
190
var consumed = Task . Run ( ( ) => consumer ( source , context ) . AsTask ( ) , context . CancellationToken ) ; // note this shares a capture scope
168
191
var produced = producer ( context ) ;
169
192
if ( produced . IsCompletedSuccessfully ) return new ValueTask ( consumed ) ;
170
- return BothAsync ( produced , consumed ) ;
193
+ return BothAsync ( produced , consumed , SwapOut ( ref allDone ) ) ;
171
194
}
172
195
finally
173
196
{
174
197
// stop the producer, in any exit scenario
175
- allDone . Cancel ( ) ;
198
+ CancelAndDisposeTaskSource ( allDone ) ;
176
199
}
177
200
}
178
201
0 commit comments