From 86a748f89520191b0f4c0e3d519609f498183a9e Mon Sep 17 00:00:00 2001 From: Burak Keskin Date: Wed, 19 Aug 2026 23:07:52 +0300 Subject: [PATCH] fix(scheduler): do not clearInterval Immediate ids in AsapAction AsapAction.recycleAsyncId forwarded setImmediate handles to AsyncAction whenever the next delay was greater than zero. clearInterval then ran against those handles, which share the timer ID number space, and could cancel unrelated intervals. Only recycle through AsyncAction when the action was itself scheduled with a delay. timer(0, period, asapScheduler) is the reported case. --- spec/schedulers/AsapScheduler-spec.ts | 22 ++++++++++++++++++++++ src/internal/scheduler/AsapAction.ts | 9 +++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/spec/schedulers/AsapScheduler-spec.ts b/spec/schedulers/AsapScheduler-spec.ts index 56a2ff7609..5f28c81d75 100644 --- a/spec/schedulers/AsapScheduler-spec.ts +++ b/spec/schedulers/AsapScheduler-spec.ts @@ -115,6 +115,28 @@ describe('Scheduler.asap', () => { sandbox.restore(); }); + it('should not pass setImmediate ids to clearInterval when rescheduling with a delay', (done) => { + const sandbox = sinon.createSandbox(); + const setImmediateSpy = sandbox.spy(immediateProvider, 'setImmediate'); + const clearIntervalSpy = sandbox.spy(intervalProvider, 'clearInterval'); + const setIntervalSpy = sandbox.spy(intervalProvider, 'setInterval'); + + asap.schedule(function () { + try { + const immediateId = setImmediateSpy.getCall(0).returnValue; + this.schedule(undefined, 20); + expect(clearIntervalSpy).to.have.not.been.calledWith(immediateId); + expect(setIntervalSpy).to.have.been.calledOnce; + this.unsubscribe(); + sandbox.restore(); + done(); + } catch (error) { + sandbox.restore(); + done(error); + } + }, 0); + }); + it('should schedule an action to happen later', (done) => { let actionHappened = false; asap.schedule(() => { diff --git a/src/internal/scheduler/AsapAction.ts b/src/internal/scheduler/AsapAction.ts index 178f677e90..eb9164ec21 100644 --- a/src/internal/scheduler/AsapAction.ts +++ b/src/internal/scheduler/AsapAction.ts @@ -23,10 +23,11 @@ export class AsapAction extends AsyncAction { } protected recycleAsyncId(scheduler: AsapScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined { - // If delay exists and is greater than 0, or if the delay is null (the - // action wasn't rescheduled) but was originally scheduled as an async - // action, then recycle as an async action. - if (delay != null ? delay > 0 : this.delay > 0) { + // Only recycle as an interval action when this action was itself scheduled + // with setInterval. Immediate handles share the number space of timer IDs, + // so passing them to clearInterval can cancel unrelated intervals + // (for example timer(0, period, asapScheduler) rescheduling after the first tick). + if (this.delay > 0) { return super.recycleAsyncId(scheduler, id, delay); } // If the scheduler queue has no remaining actions with the same async id,