Change:
The CancelablePromise disposes its CancellationTokenSource inside a onCancellationRequested listener. That means that any other onCancellationRequested listeners that were after that one won't be called, because the EventEmitter has been disposed. In practice that means that any listener that was after an async step inside the callback function is not called. This caused issues in chat, but the function is used in many places and I think it's likely that some of those are affected. My suggested fix is in but I don't think this is safe to check in at the end of the iteration, so I will wait until December. Looks like you guys have touched this code, lmk what you think: Transferred from a private issue. Original issue discussion below <detailsa question such as "Explain recursion in detail to me" hit cancel Watch the copilot chat log, you will see the request continues and even follow ups are computed even though no tokens are shown to the user. This is a bit weird. Order of events is this: Call to get a token Attach some listeners to it, some after async steps Cancel the cancellable promise The cancellable promise disposes the token source The cancellation listeners that were after an async step don't get called because the event emitter is disposed before it can call them The last step is suspicious but I guess that's how EventEmitter works? And the cancellable promise seems like it's doing the right thing. I need to check that the same thing happens with a normal CancellationTokenSource because the logic is the same. Still not sure of the right solution. Essentially has a listener which disposes the . Listeners that were later than that first listener will not be called because the event emitter will be disposed in the middle of emitting, which is by design for EventEmitter. But there is no way to guarantee that all listeners have been called before disposing, besides something like which doesn't seem nice </details
That's a good find and something we shouldn't be doing. Looks like a regression from
Disposables and leaks are tricky... Thanks for finding this! Here is the PR that introduced this: Ironic that I introduced leaks with the goal to reduce leaks...

Discussion:
const promise = new Promise<T>((resolve, reject) => { const subscription = source.token.onCancellationRequested(() => { subscription.dispose(); <del> source.dispose(); </del> reject(new CancellationError()); }); Promise.resolve(thenable).then(value => {

return <CancelablePromise<T>>new class { cancel() { source.cancel(); <ins> source.dispose(); </ins> } then<TResult1 = T, TResult2 = never>(resolve?: ((value: T) => TResult1 | Promise<TResult1>) | undefined | null, reject?: ((reason: any) => TResult2 | Promise<TResult2>) | undefined | null): Promise<TResult1 | TResult2> { return promise.then(resolve, reject);

return promise.then(() => assert.deepStrictEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); }); <ins> test('execution order (async with late listener)', async function () { const order: string[] = []; const cancellablePromise = async.createCancelablePromise(async token => { order.push('in callback'); await async.timeout(0); store.add(token.onCancellationRequested(_ => order.push('cancelled'))); cancellablePromise.cancel(); order.push('afterCancel'); }); order.push('afterCreate'); const promise = cancellablePromise .then(undefined, err => null) .then(() => order.push('finally')); return promise.then(() => assert.deepStrictEqual(order, ['in callback', 'afterCreate', 'cancelled', 'afterCancel', 'finally'])); }); </ins> test('get inner result', async function () { const promise = async.createCancelablePromise(token => { return async.timeout(12).then(_ => 1234);